This was one of the more interesting challenges I’ve done in a while. We were one of 4 teams who solved this binary exploitation challenge in DawgCTF.
This was the only challenge where we were given a libc. Looks like it’s going to be a heap pwn.
As usual, the first thing we do is look at the protections on the binary.
$ checksec tiktok [*] '/home/robert/writeups/binexp/umbccd/tiktok/tiktok' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
All protections are enabled except for PIE, implying that we’ll probably have to use either FSOP or write to one of the malloc hooks to win.
Dumping the binary into Ghidra, I immediately noted something suspicious. The
fd is stored but never validated again after creation.
Null byte overwrites are really common, especially when dealing with off by one errors or C string functions. If we could somehow overwrite the
fd to null, we could control the heap!
Unfortunately, the actual vulnerability was significantly harder to find. Only after many hours of staring at the binary did I think of reading the documentation for the
In particular, this line.
This end of the token is automatically replaced by a null-character, and the beginning of the token is returned by the function.
fd is located directly below the file path in memory, and the file path is not properly null terminated, we can potentially generate a string that overwrites the fd when tokenized.
sVar3 = read(0, songs[song_count].file_path, 0x18);
Also note that we are able to open folders, passing the check for
(songs[song_count].fd != -1).
My final payload looked as such.
"Warrior".ljust(0x18, "/") + [fd]
fd is equal to
ord('.') or 0x2e, the path in memory looks like
Warrior/////////////////.. Thus, the call to strtok would overwrite the
fd with a null byte.
pcVar4 = strtok((char *)0x0,".");
This was one of the more well-hidden vulnerabilities I’ve encountered. The takeaway here is probably, most C string functions can write null bytes to sneaky places.
fd being set to 0, we now have full control of the heap.
memset(songs[choice].lyrics,0,(ulong)(size + 1)); read(songs[choice].fd,songs[choice].lyrics,(ulong)size);
Note that because
size is now read in from stdin, we can set size to -1. This would memset 0, while at the same time reading
(ulong) -1 which ends up being
Unfortunately, we are only able to read once as it checks if the buffer is null, which makes exploitation a bit tricky.
The heap took a bit of massaging to get right. In the end, I settled on the following configuration.
For the initial heap state before any exploitation, I malloced chunks to get it as such.
I then freed the first 0x20 chunk and malloced into it with my one heap read. This allowed me to overwrite the entire heap. I then modified the
fd of the second 0x20 chunk to point to the .bss section. I also extended the unsorted bin chunk to overlap with the next 0x20 sized chunk, which will be used to get a leak.
After mallocing for 0x670 again, a main_arena address is written into the
fd pointer of the 3rd 0x20 chunk.
We can then print this address out with the
play_song function, giving us a libc leak.
With the modified
fd of the second chunk on the heap, I pointed it to a fake chunk that I created previously in the .bss section.
Note that when opening a folder,
size will be set to 0, allowing us to clear one null byte at the address returned by malloc.
size = atoi((char *)int_buffer); iVar1 = choice; buffer = (char *)malloc((ulong)(size + 1)); songs[iVar1].lyrics = buffer; memset(songs[choice].lyrics,0,(ulong)(size + 1));
Thus, the fake chunk setup below would clear the
More importantly, by creating a valid fake chunk in the
.bss section, we can then free this chunk immediately. This allows us to malloc again with the just zeroed
fd, gaining full control of the
From here, with unlimited reads from stdin, it’s simply a matter of overwriting a chunk’s
fd pointer to
__free_hook and writing system for
One cool trick for this final stage of exploitation is setting the
__free_hook - 8. We can then write
"/bin/sh\x00" + p64(system). This has the advantage of saving a malloc for the
"/bin/sh" string, as we can immediately free the just malloced chunk for a shell.