help (pwn)
An interesting pwn challenge that gives us really powerful primitives from the get-go
On doing a checksec on the binary we find that it has all protections enabled:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabledFurthermore, the libc version is 2.39 which is important to consider when exploiting.
The pseudocode of the main function is as follows:
There are a few key observations to be made here:
Data of chunks which are malloc'd are not zeroed out, giving us free heap and libc leaks (will be elaborated on later)
We can only call
scanfandputson the last chunk we allocated since the program useslocal_1a4to track our most recently malloc'd chunkWe can overflow
local_198with our malloc'd chunk pointers; This is becauselocal_1a4can increment until it is โฅ 15, allowing us to malloc a pointer intolocal_198[16]and beyond.
This allows us to overflow into the buffer (local_118) used for writing our commands!
Freed pointers are not cleared from memory. However, this is actually a small detail which we won't need much for our exploit.
Notice that if our malloc pointers overflow into the command buffer, when we are writing the command we can modify those pointers as well! For example, if I have already malloc'd 18 times, then local_118[0] would have a malloc pointer and so would local_118[1]. Then, if I input a command like b'puts\x00\x00\x00\x00\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA', then my command would be correctly interpreted to be puts (since my command ends with a null terminator) and the program would attempt to read whatever is at address 0xAAAAAAAAAAAAAAAA. This means that we have an arbitrary read.
If we use a command like b'scanf\x00\x00\x00\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA' then we could also write data into 0xAAAAAAAAAAAAAAAA . This means we also have an arbitrary write!
So what should we overwrite in order to get a shell?
Some of the popular options would include:
Overwriting a global offset table entry, e.g. overwrite GOT entry for
freetosystemto potentially callsystem('/bin/sh')when we free a chunk. This won't work since both libc and the binary have full RELRO enabled.Overwriting
__malloc_hookor__free_hook. These won't work since it has been patched in this libc version.Overwriting the return address of
main. This won't work sincemaindoesn't return. Rather,maincallsexitto exit the program.
Note: The challenge author's solution was actually to get a stack leak, then overwrite the return address of scanf. However, I adopted a different solution.
It turns out that when a program exits, it calls a list of exit functions. Furthermore, the structs which contain these exit functions are in the DATA region of libc, which is readable and writeable. Hence, if we can overwrite these exit functions correctly to functions that can give us a shell, when the program exits we could get a shell!
Some useful resources to learn how this exploit technique works can be found here and here.
TLDR:
When
exit()is called, it calls__run_exit_handlers
__run_exit_handlerstakes in astruct exit_function_list **, then for each entry it demangles the function pointer and calls the function:
The struct
exit_function_listand the other relevant structs look like this:
There are 5 different types of exit handlers: ef_free, ef_us, ef_on, ef_at and ef_cxa.
During demangling, the following assembly in
__run_exit_handlersis applied:
Essentially, the
fsregister is a special register which points to the per thread data, also known as the Thread Control Block (TCB).At
fs:0x30is the pointer guard, which is a "secret value" used to mangle and demangle the function pointers of the exit handlers.Some Python code for
rol,roland encrypting is given below:
Usually, the
struct exit_function_list **passed to__run_exit_handlershas at least one entry in it, and the first entry is usually the_dl_finifunction which is inldcode. Therefore to find the pointer guard value, there are 2 ways to do so:
Leak the value from
fs:0x30Calculate the value from the address of
_dl_finiand the mangled pointer
For my solution I adopted the second approach.
First I needed to find out the location of
__exit_funcs(which is thestruct exit_function_list **) so in GDB I can do something like this:

So we can see that it contains two pointers, the first of which is a pointer to the exit_function_list struct for _dl_fini.

Now that I know the address of
exit_function_list *and I know that it is in a fixed position within the libc data section, I can calculate its offset.
Alternatively if I wanted to find the position of fs:0x30 I could also have done something like this:

Notes:
TCB region is always located directly above libc
The
0x6b66c574a13346dewe see is the pointer guard value
Note that the TCB is readable and writeable, and the exit function structs are also readable and writeable. Therefore for exploitation, we can either:
Rewrite pointer guard value to a value of our choosing and change the
exit_function_liststruct for_dl_fini(arb write + arb write), orRead pointer guard value, then change
exit_function_liststruct for_dl_fini(arb read + arb write)
I also went with the second approach for my solution.
A brief outline of my solution is written below, including all the leaks I got, how I got them, and how I finally exploited the program:
Malloc, free, malloc then read a chunk to get a heap leak (this is because the heap metadata is not cleared when mallocing a chunk)
Malloc a bunch of times so our malloc pointers overflow into the command buffer
At this point the program has malloc'd 21 times, the last 5 of which overflow into the command buffer.
Since the program forces all chunks malloc'd to be of size
0x40, we cannot directly free a chunk into the unsorted bin and get a libc leak. However, I used arbitrary write to change thesizefield of a chunk to be a large value such as0x800then freed it. The freed chunk would go into the unsorted bin. I then used the arbitrary read to read its metadata, which points to main arena in libc which is at a constant offset, thereby getting the libc leak.
Note:
To prevent coalescing of this big chunk with the wilderness, and to prevent double free or corruptions from being detected, I had 1 fake
0x800size chunk followed by 2 fake0x30size chunks. I free the first0x30chunk to act as a guard chunk to prevent the0x800chunk from coalescing, then free the0x800chunk. The third chunk is necessary to prevent corruption errors.
Since I have a libc leak, and
ldis adjacent tolibc, and_dl_finiis at a constant offset inld, I know the address of_dl_fini. Furthermore, since theexit_function_liststruct of_dl_finiis at a constant offset in libc, I also know its address. Thus I know both the unmangled and mangled values of the_dl_finifunction pointer and can calculate the pointer guard value:
Note: For some reason, on remote the offset between ld and libc is 0x2000 more than the local offset. I got this value by bruteforcing.
Now that I have the
key(aka pointer guard) value, I can mangle a function pointer tosystemin libc. I then overwrite theexit_function_liststruct for_dl_finiwith my own. In order for it to runsystem("/bin/sh")on exit, the exit function list has to be of typeex_cfa(which is represented by 4 in the enum). We keep theidfield the same (which is 1), and we add a pointer to/bin/shin libc as*arg.
Finally, we can exit the program. On exiting, it will run
system("/bin/sh")and give us shell!
Below is my full exploit code:
Last updated