After reversing the binary it turned out the structure of the challenge is the usual menu system based pwn challenge.
We had the following menus:
- “1 :) Fill your information”
- It reads your team name to a 20 byte buffer and prints it out. It can be used to leak data from stack if you don’t fill the 20 byte buffer. For example it can be used to leak the heap base address. But we never used this vulnerability in the final exploit.
- “2 :) Upload and parse File”
- This is where the main functionality lies: you can upload a zlib-compressed PNG image, it will parse its headers, allocate a width*height-sized buffer with mmap, and copies the content into it (but limits the size to the buffer length, so it does not cause overflow). It also puts various metadata into a linked list about these uploaded files, including the decompressed content’s size.
- “3 :) Show File info”
- Prints out the width, height and “pixels” (bit depth) properties of the chosen file. Not used in the exploit.
- “4 :) Delete File”
- Deletes a selected file. Munmaps its content (based on the decompressed content’s length), frees it’s metadata and unlinks the item from the linked list. We will use the munmap in the exploit.
- “5 :) Commit task”
- Creates a thread which prints out every file’s width, height, bit depth. You can only call this method six times. Not used in the exploit, probably just a red herring :)
- “6 :) Monitor File”
- Create a thread which notifies you if a new file was uploaded. It uses a conditional variable (and mutex) to block until the main thread notifies it.
- 8 - a hidden “backdoor”
- It’s a gets-based stack overflow, but exits immediately after it overwrites the stack. Although the deinitialization logic runs in the exit call, the stack overflow is still not exploitable this way. It’s obviously there just for misleading us.
There are a few misleading / non-exploitable functionality (including the whole PNG parsing), but there is something fishy going on with the content length calculation when it allocates the buffer based on the width and height instead of the decompressed content length.
Also despite that the PNG is a compressed file format, there is an additional layer of compression which is a hint about that we may have to send a lot of data. As we will see later, our hunch was not unfounded.
If we take a closer look at the buffer allocation, it turns out fast that the allocation size and the length used for munmap can differ.
This is exactly the same situation that my teammate tukan described in one of his ptmalloc fanzine back in 2016, so he could point us into the correct direction right away.
Although the next mapped memory segment after our mmap’d allocation is either the read-only data section of the libc or the guard page before the stack of a thread created by either the 5th or 6th menu item, the munmap can remove them if we use a bigger length than the original allocation was.
So our plan is the following:
- create a long-running thread (with 6th menu item), which will mmap a thread stack (the size is architecture dependent, but was 8MB on x64).
- the thread waits for the condition variable until we upload a new file
- upload a file whose decompressed length is bigger than the allocation size, so munmap will remove the following mmap’d segment too
- I used the following values: width = 1024, height = 1024, width * height = 1MB, decompressed length = 9MB (1MB my content + 8MB overflow into the thread’s stack)
- delete this file with the 4th menu item
- although the thread’s stack is unmapped, the thread won’t wake up, so this won’t cause any problem (segmentation fault for example)
- upload a new, fake thread stack (8MB), and putting a ROP chain in the place of the original stack where the execution returns
- we also have to restore a few variables (like mutex and cond) so the waking up thread won’t crash immediately or behave incorrectly
- the ROP chain does the following:
- leaks libc address (puts in our case)
- unlocks the mutex, so the main thread won’t block anymore
- make the thread sleep forever, so its broken state won’t cause any problems for us (for example race condition)
- we calculate the address of one of the one-gadget-RCEs (thanks to david942j for his tool)
- calling system did not work for me, probably something broke meanwhile
- we repeat everything from the beginning, but this time we call the one-gadget-RCE in the ROP chain
Executing our plan actually worked and give us a shell.
We found the flag in the /home/uploadcenter/flag file:
Below the whole exploit: