TryPwnMe One
A collection of Exploit Development challenges to practice the basic techniques of binary exploitation. - by rePl4stic
Last updated
A collection of Exploit Development challenges to practice the basic techniques of binary exploitation. - by rePl4stic
Last updated
The following post by 0xb0b is licensed under CC BY 4.0
In this room, we are confronted with seven challenges from different categories of binary exploitation. The difficulty will gradually increase with each challenge.
We use the tools GEF and pwntools to solve this series of challenges.
GEF (GDB Enhanced Features) is a set of advanced features and extensions for the GNU Debugger (GDB) that improves the debugging experience, offering better visualizations, shortcuts, and additional commands for reverse engineering and binary exploitation.
Pwntools is a Python library designed for ease of use in developing exploits. It offers tools for binary exploitation, networking and interacting with processes.
In the first challenge, we face a classic buffer overflow vulnerability, where we just need to overwrite the value of the admin variable.
This program takes user input via an insecure gets()
function, and if the admin
variable is set to 1 (likely through a buffer overflow), it opens and prints the contents flag.txt
; otherwise, it exits. It is vulnerable because of the use of the gets()
function, which does not perform bounds checking. This allows us to perform a buffer overflow, to overwrite the contents of the admin variable, and to read the flag.
The next thing we do is to run overflowme1
, to see how it behaves. We are able to cause a segmentation fault by providing a comment exceeding the buffer.
With the following script using Pwntools we are able to connect to the target challenge machine. It crafts a payload with 16 bytes of padding followed by 32 repetitions of the value 1
, aiming to overwrite the admin
variable. Finally, it sends the payload and enters interactive mode to receive any output from the server.
After we execute our script, we receive the flag for the first challenge.
In the second challenge, we also face a classic buffer overflow vulnerability, where we just need to overwrite the value of the admin variable, but this time with a specific value.
Like in the challenge, before, the program takes user input, it checks if a specific value is set in the admin variable, and if so, it reads and prints the content of flag.txt
; otherwise, it exits with a message. This time the buffer is a bit bigger and the value has to be 0x59595959
.
The next thing we do is to run overflowme2
, to see how it behaves. We are able to cause a segmentation fault by providing a comment exceeding the buffer.
With the following script using Pwntools we are able to connect to the target challenge machine. It crafts a payload that consists of 64 'A' bytes to overflow the buffer, followed by the value 0x59595959
repeated to overwrite the admin
variable. After sending the payload, it interacts with the server to display the flag.
After we execute our script, we receive the flag for the second challenge.
This challenge does not require us to overflow to exploit the vulnerability. This type of binary exploitation involves injecting shellcode.
The program of this challenge reads input into a buffer and attempts to execute it as a function. By providing some shellcode we are able to get a shell.
In the following line lies the vulnerability of the program, it's directly accepting user input and attempting to execute it as code.
The next thing we do is to run tryexecme
, to see how it behaves. It prompts us to give it our shell, and it will execute it.
We use the following script to solve the challenge. We need to set the architecture to amd64
for proper shellcode generation and connects to the target challenge machine. It creates shellcode to spawn a shell using shellcraft.sh()
, a feature provided by pwntools. Then it sends this shellcode as the payload to the remote service. Finally, we switch to the interactive mode to interact with the shell.
After we execute our script, we receive a shell on the target machine and are able to read the flag of the third challenge.
In the fourth challenge, we face a ret2win vulnerability. A ret2win is a binary where there is a win()
function (or equivalent) which we want to redirect execution there.
The program prompts the user with "Return to where?" and reads up to 512 bytes of input. After reading the input, it prints "ok, let's go!" and ends. The win() function is defined but not called in the normal execution flow. The win()
function in the program, if called, would execute the /bin/sh
command, spawning a shell. However, this function is not invoked.
The next thing we do is to run tryretme
, to see how it behaves.
We use the following article by ir0nstone to help us:
Ir0nstone presents numerous binary exploitation methods in his series of articles. It is a very valuable resource, which I recommend to everyone when it comes to binary exploitation.
As mentioned in the introduction, this is a ret2win vulnerability. Our aim is now to write the address of the win()
function to the $rsp
register using a buffer overflow in order to execute it. Since the system call system("/bin/sh");
is executed in this function, we thus obtain a shell.
The $rsp
register holds the return address of the current function, which can be overwritten during a buffer overflow to redirect execution to the desired function, such as win()
. By controlling $rsp
, we can manipulate the program's execution flow and trigger the win()
function to spawn a shell.
For this, we need to know the offset from when we reach the $rsp
register, as well as the address of the win()
function.
First of all, we determine the offset.
We use GEF for this. We debug the program tryretme
using gdb. Before we execute it in gdb, we create a cyclic pattern. With an overflow, we can determine in which register what was written, or how far the offset is to a certain register.
After generation, we run the program using run and paste the cyclic pattern as input. Since it has a length of over 512 bytes, we cause an overflow.
Next, we search for the $rsp
register using the pattern and determine an offset of 264
using GEF. This means that we are in the $rsp
register after 264
characters. We would then only have to write the address of the win()
function in this register. We can do all this quite easily using pwntools.
The following pwntools script is then used to exploit the buffer overflow vulnerability by sending a crafted payload to trigger the win()
function. The payload includes a buffer overflow to reach the return address, a ret
gadget to fix stack alignment, and the address of the win()
function to spawn a shell. After sending the payload to the remote server, the script opens an interactive session to interact with the shell.
The following line finds the address of a ret
instruction inside the binary, which will ensure stack alignment is correct when returning to the win()
function. Addressing the mention issued in the challenge instruction.
Addressing the issue of The challenges in this room are running Ubuntu, so there will be stack alignment issues. Make sure to add a ret gadget to solve it if needed.
The ret
gadget is added to the payload right before the win()
function's address. This ensures the program can "return" to the correct location and avoid crashing due to misalignment when switching control flow to the win()
function.
This extracts the address of the win()
function from the binary's symbol table.
After we execute our script, we receive a shell on the target machine and are able to read the flag of the fourth challenge.
This is a continuation of the ret2win vulnerability from before. However, this time we are probably dealing with ALSR. We again have a win()
function whose address we want to overwrite in $rsp
.
The program displays the address of the vuln()
function, then prompts the user for input, reading up to 0x200 (512) bytes into a buffer that is only 0x20 (32) bytes in size, leading to a potential buffer overflow vulnerability. The win function is not directly called.
Address Space Layout Randomization (ASLR) is a security technique that randomizes the memory addresses used by system and application processes, making it more difficult for attackers to predict the location of critical functions or exploit vulnerabilities like buffer overflows. By changing the memory layout on each program execution, ASLR increases the complexity of crafting reliable exploits.
This mechanism can be circumvented if we get leaked addresses, and we can determine the relative offset to the base from this leak.
The next thing we do is to run random
, to see how it behaves. It gives us a secret, the address of the vuln()
function.
We use the following article by ir0nstone to help us:
For this, we need to know the offset from when we reach the $rsp
register, as well as the address of the win()
function. For a detailed explanation of this, see the TryReMe Challenge solution:
Furthermore, we also need the offset of the functions that we know and that we want to utilize.
We first determine the offset again to reach the $rsp
register during overflow. We run the application in gdb, create a sufficient size cyclic pattern, and run the application.
We pass the pattern.
And determine the offset to the $rsp
register, which is also 264
.
Next, we determine the offset of the vuln and win functions. We can do this simply by using objdump
. This will disassemble us the binary and search for any lines containing vuln
in the disassembly output.
Unlike before, we cannot use the symbol table to get the address of the win function. Instead, we calculate the base address using vuln - offset vuln. And add the offset of the win function to the base address to get the actual address of the win function used at runtime. Otherwise, everything is quite similar to the previous challenge.
The base address in a binary is the starting memory address where the binary's code is loaded into memory during execution. In position-independent executables, the actual memory address of functions and data is calculated relative to this base address. By knowing the base address, offsets of functions or gadgets (like vuln
or win
) from the start of the binary can be used to compute their actual addresses in memory, allowing exploitation or interaction with the binary at runtime.
With our pwntools script, we'll leak the address of the vuln
function, calculates the base address of the binary, and determines the addresses of the win
function and a ret
gadget to align the stack. The payload overflows the buffer, uses the ret
gadget for stack alignment, and jumps to the win
function, providing shell interaction after sending the payload.
After we execute our script, we receive a shell on the target machine and are able to read the flag of the fourth challenge.
This is another continuation of the last two challenges, unlike ret2win, this time we don't have a win function that we want to exploit, but have to make do with what is possible in the included shared libraries. This is a combination of ret2libc and ret2plt challenge.
A ret2libc challenge involves exploiting a buffer overflow vulnerability to overwrite the return address on the stack, redirecting execution to a function in the C standard library (libc), such as system(). Here we provide a payload that includes the address of the system()
function, an address of the /bin/sh
string, and a return address, allowing execution of a shell. This technique bypasses restrictions like non-executable stacks by reusing existing code in the binary or libraries.
A ret2plt (return-to-Procedure Linkage Table) is an exploitation technique in which an attacker redirects the program's execution flow to a function in the PLT (Procedure Linkage Table), which serves as an intermediary for dynamic function calls in programs using shared libraries. By doing so, the attacker can execute functions like system()
by leveraging the function pointers stored in the GOT (Global Offset Table).
At first glance, nothing seems possible here. But we have received two shared libraries for the challenge, which point to a ret2libc attack.
We got the shared libraries ld-linux-x86-64.so.2
and libc.so.6
.
The program prompts the user for input using puts
and reads up to 512 bytes into a buffer of only 32 bytes, creating a potential buffer overflow vulnerability. After reading the input, it prints a confirmation message, but no further action is taken.
The next thing we do is to run thelibrarian
, to see how it behaves.
Here, too, we use a resource from Ir0nStone to help us:
Only this time we don't have a leak of the base address, we have to find it ourselves. We make use of PLT and GOT to leak those addresses needed.
The PLT (Procedure Linkage Table) and GOT (Global Offset Table) are sections in an ELF file that handle dynamic linking, allowing the binary to rely on external system libraries for functions like "puts." This keeps the binary smaller and makes it easier to update system libraries without needing to change the binary itself.
Calling the PLT address of a function is equivalent to calling the function itself
The GOT address contains addresses of functions in
libc
, and the GOT is within the binary.
I used the following sources for the solution, including my first writeup, for the Obscure challenge.
There we had the special case that we had no access to the libc and therefore determined the offsets to the libc at runtime by means of address leaks. However, the tool used for this https://libc.blukat.me/ is currently not available.
Aquinas had used the library directly in his solution approach of obscure at that time, as we do here, and had the influence for me to be able to solve this with his solution.
Also, a shoutout to Jaxafed, who helped me with a sanity check. Don't miss his awesome content at https://jaxafed.github.io/.
I have listed further helpful sources and additional resources below:
As before, we determine the offset to the $rsp
register. Then we determine the offsets within the libc, similar to the previous challenge, only this time we look at the shared library.
We use ret2plt first stage to leak the address of puts
by calling it via the Procedure Linkage Table (PLT). The PLT entry for puts
calls the real function and allows you to leak its actual address from the Global Offset Table (GOT).
Then, we use ret2libc with the calculated base address of libc using the leaked puts
address we call system("/bin/sh")
from libc, exploiting the known offsets.
We run the application in gdb, create a sufficient size cyclic pattern, and run the application.
We pass the pattern.
And determine the offset to the $rsp
register, which is also 264
.
With the following script, we have the first half of our exploit, leaking the addresses.
So, what is happening here?
Padding (b"A" * 264
):
This creates a buffer of 264 "A" characters, which is used to fill up the space up to the return address on the stack. This value (264) is likely determined by the size of the buffer in the binary and the offset needed to overwrite the return address.
Adding a ret
gadget (payload += p64(rop.find_gadget(['ret'])[0])
):
The ret
gadget is used here to align the stack. Since some systems (like Ubuntu) require stack alignment to 16 bytes, the addition of a ret
gadget ensures that the stack is aligned properly before executing the next gadgets. This is often used to bypass issues like stack misalignment that would otherwise cause the program to crash.
Adding a pop rdi
gadget (payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
):
This ROP gadget pops a value off the stack into the rdi
register, which is the first argument to a function in the System. This prepares the rdi
register to hold the argument that will be passed to a function, which in this case is the address of binary.got.puts
.
Loading the address of puts@got
(payload += p64(binary.got.puts)
):
The address of the puts
entry in the Global Offset Table (GOT) is loaded into the rdi
register. The GOT holds the addresses of dynamically linked functions, and this allows to print the actual address of puts
during runtime (which will help in later stages to leak memory addresses for bypassing ASLR).
Calling puts@plt
(payload += p64(binary.plt.puts)
):
The address of puts
in the Procedure Linkage Table (PLT) is added to the payload. This makes the program call puts
, which will print the value in rdi
(which, as set earlier, is the address of puts
in the GOT). This effectively leaks the runtime address of puts
.
Returning to main
(payload += p64(binary.symbols.main)
):
After the puts
call, the program will return to the main
function, allowing the program to restart. This is common in ROP attacks to give the attacker another chance to send a new payload, using the leaked address of puts
to calculate the base address of the libc library and eventually execute a system call.
Running the script, we can see ALSR is present, and the addresses are changing.
Next, we extract the offsets of the system function and the /bin/sh string. Like in the following resource depicted:
The following script combines the information gathered to exploit the binary. We leak the puts
function address from the GOT, calculate the libc base, and then executing a second payload to spawn a shell.
The instruction payload += p64(binary.symbols.main)
allows the program to return to main
after leaking the puts address, giving us another opportunity to send a second payload for spawning the shell.
libc.address = leak - libc.symbols.puts
calculates the base address of libc by subtracting the offset of puts
within libc from the leaked address, thus aligning the attack with the correct library version. See Random Memories
After we execute our script, we receive a shell on the target machine and are able to read the flag of the sixth challenge.
In this challenge, we are dealing with a format string vulnerability. This reminded me of the format string2 challenge from picoctf. But we don't have any variables to overwrite with content here.
This program takes a username input from the user and prints it without format string protection, making it vulnerable to a format string attack. Additionally, the win()
function contains a call to system("/bin/sh")
, which could potentially allow for shell access if exploited properly.
The next thing we do is to run notspecified
, to see how it behaves. After entering a username, it gets printed.
We use the following two resources for assistance:
Referring to the source code, printf(username)
allows a user to provide format specifiers in the input (such as %s
, %x
, %n
), which could lead to arbitrary memory reads or writes. Let's see if we can leak the stack. I find the following payload to be useful to format the output of the leak.
We see, in sixth place, the content of our input.
What can we do with it now?
Well, since we can not only read but also write, we could theoretically replace the address of a subsequent function in the stack with the address of the win function.
This can be quite unpleasant and time-consuming with regard to the note in the challenge: The challenges in this room are running Ubuntu, so there will be stack alignment issues. Make sure to add a ret gadget to solve it if needed.
How this is done manually can be seen in the writeup of cajac:
We can make use of the fmtstr_payload
function of pwntools. But it is important to set the architecture and endianess first.
With the following script, we abuse the format string vulnerability to get a shell. It overwrites the GOT entry for the exit
function with the address of the win
function, so when the program tries to call exit
, it instead executes the win
function. It generates a payload using fmtstr_payload
, which specifies the overwrite.
In a format string vulnerability, we need to know where on the stack our input is, so we can use it to modify memory in a controlled way. In this case, index 6 is where the format string begins, so we would pass 6 as the offset to functions like fmtstr_payload
in pwntools, allowing it to know where to inject the payload.
Don't miss out on Jaxafed's approach, with a manual approach. There you can see the magic happen behind fmtstr_payload
.
https://jaxafed.github.io/posts/tryhackme-trypwnme_one/#creating-the-payload-manually
After we execute our script, we receive a shell on the target machine and are able to read the flag of the seventh challenge.