This is a medium binary exploitation challenge from the Wargames CTF 2024. It involves the usage of a heap overflow to exploit file structures, granting an arbitrary read and write primitive which was used to overwrite the exit handlers.
Screenwriter
Starting off this challenge, we’ll check out what we’re working with and then get some decompilation going.
1 | anthonyjs@frosty:~/screenwriter$ file chall && checksec --file chall && strings libc.so.6 | grep GNU |
All protections besides PIE are enabled. We are also working with libc 2.35 so we know that the common targets within this libc are things such as exit handlers and file structures. Let’s get reversing…
Reverse Engineering
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
Here is the complete decompilation of the main function of this binary pulled from IDA. Couple of important things to pickout here. Let’s start with the heap overflow.
Here we can see a chunk of size 0x28
be allocated and used as “buff
“.
1 | buf = malloc(0x28uLL); |
Now further down in the code we can see the program reading 0x280
bytes into said buffer..
1 | read(0, buf, 0x280uLL); |
Other than that, this could be a totally legit program. Something that you could see as a homework used in a computer science class at a university. Now this heap overflow because very useful to us for this reason:
1 | buf = malloc(0x28uLL); |
Adjacent to our buffer chunk, we have these file structures. When you open a file with fopen, a file structure for said file is then placed onto the heap. So given that we have the ability to overwrite even a little bit (we can overwrite the entire thing) of the file structure, we can do some damage.
Let me do some file structure explanation before we move forward.
1 | struct _IO_FILE |
Here is the metadata associated with the file structure in libc. Normally a popular target for file structure programming is to take advantage of the vtable pointer inside the structure and modify it to get a call into _IO_wdoallocbuf
to get _IO_wfile_overflow
to get called. This involves modify wide data and even creating some fake structures within the correct memory range. Luckily for us we don’t need to do something that complex.
We can abuse the file structures on the heap by doing some cool things. As you can see in the structure we have a tracker for reading:
1 | char *_IO_read_ptr; /* Current read pointer */ |
Now if we were to overwrite the values here to pointers in memory that we want to read from, we totally could and we will!
We could even use the structure to write to arbitrary memory as well!
1 | char *_IO_buf_base; /* Start of reserve area. */ |
Here is the metadata associated with that, and I included the _fileno
which would even allow us to change the file descriptor that gets used. This will be important for later on when we want to write to memory, using this we can give the data we want to write from stdin.
Exploitation
So first step that we want to take is getting our arbitrary read primitive. For this I loaded the binary into gdb and sent in a cyclic pattern to find the distance on the heap from our input buffer to the beginning of the first file structure. Doing so, I found the offset to be 48
bytes.
From there, all I have to do is overwrite enough of the file structure to change the reading pointers to point to where I want to read. And given that PIE is disabled, I chose to read libc addresses from the GOT:
1 | def leak(addr, pad=0): |
In this chunk above, we are writing up to the first file structure, writing the correct flags that will allow us to read and more, then inputting the address we want to read, offseting the end pointer by an arbitrary amount of bytes (here I chose 0x50
), then putting the current ptr at the address.
This makes it so that when we use the read call, it will then print out bytes from that address. The padding ability I added here was due to sometimes we are leaking something that is 6 bytes without null bytes or 8 bytes without null bytes. This padding feature allows us to use this leaking function for both.
Now we are halfway there, we can leak things. I intended to attack the exit handlers when I got up to this part of the challenges, so I walked through the handle_exit_funcs
function to see what functions were lined up, and after the exit handler pointer inside of the libc memory space was demangled, it was discovered that the function that was being called resides in the linkers memory space.
To be able to bypass the checks needed to overwrite a exit handler, we need to mangle the function pointer we want to right with the correct key that the program uses. The easy way I found to leak this key is to leak an already mangled pointer, the pointer unmangled, and do the calculations from there to find the key.
Knowing that, we will need to leak libc memory base, the mangled pointer, and linker memory base:
1 | # rebase libc |
After doing so we can now calculate that mangling key. I’m going to include some assembly that shows exactly how a exit handler pointer gets demangled:
1 | 0x0000000000039fc1 <+177>: ror rdx,0x11 |
Note that this is not from this exact binary but is the same calculations done. I pulled this from here for ease of access.
So to calculate that mangling key it comes down to ror
the leaked address by 0x11
then xoring that with the unmangled address pointer:
1 | # calculate mangle key |
So awesome. From there we can simply just write the address of the function we want to execute into the exit handlers memory space. And typically this will want to be system
. To achieve the write we will use pwntools handy file structure exploitation tools:
1 | # use file structure to write to exit handler |
Now you might be questioning what this is doing. Again we are overwriting the file structure, but under the hood, this fp.read() call does something very simple. I will show you exactly what it does. Say we wanted to read from address 0xdeadbeef
. We call fp.read(0xdeadbeef, 0x20)
:
1 | { |
Here you can see that this is the entirety of the file structure. We see that the _IO_buf_base
and _IO_buf_end
were set to 0xdeadbeef
and 0xdeadbf0f
(base+0x20
) respectively. We can also notice that fileno
was set to 0x0
, indicating that the file descriptor to read from will be stdin. This permits us to trigger a read from stdin to that given address for the given amount of bytes.
So given that ability, we will first mangle the pointer we want to write, then write it to the exit handlers. And luckily for us the following address in memory actually turns out to be stored into the register rdi
at the time the handler is called. This means we have the ability to call a function AND control rdi
. This easily lets us call system('/bin/sh')
:
1 | # writing system with /bin/sh to exit handler |
Note that the rdi
value did NOT need to be mangled.
After submiting the overwrite, the next thing to do is call exit()
and boom we have an interactive shell!
Full Exploit Script
1 | from pwn import * |