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 strtok function.

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.

Because the 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]

When 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.


With fd being set to 0, we now have full control of the heap.

      memset(songs[choice].lyrics,0,(ulong)(size + 1));

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 0xffffffff.

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 fd.

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 songs array.

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 system("/bin/sh").

One cool trick for this final stage of exploitation is setting the fd to __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.