I could not solve this challenge in time. It is definitely a good challenge (and hard).
The binary is compiled from C++ code and stripped. Even the binary is dynamically linked, we will see many functions in assembly because some code of C++ STL is included in binary. From objdump output, the binary is compiled on Fedora 14. So I tested my exploit only on Fedora 14. Here are some checking output.
$ file pp500_e98c4e1c448e706a94e pp500_e98c4e1c448e706a94e: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, stripped $ strings pp500_e98c4e1c448e706a94e ... _ZSt29_Rb_tree_insert_and_rebalancebPSt18_Rb_tree_node_baseS0_RS_ ... vector::_M_insert_aux ... $ objdump -s -j .comment pp500_e98c4e1c448e706a94e pp500_e98c4e1c448e706a94e: file format elf32-i386 Contents of section .comment: 0000 4743433a 2028474e 55292034 2e352e31 GCC: (GNU) 4.5.1 0010 20323031 30303932 34202852 65642048 20100924 (Red H 0020 61742034 2e352e31 2d342900 at 4.5.1-4). $ checksec.sh --file pp500_e98c4e1c448e706a94e RELRO STACK CANARY NX PIE FILE No RELRO No canary found NX enabled No PIE pp500_e98c4e1c448e706a94e
The assembly from C++ code is very difficult to understand at first sight. The good way to understand (for me) is writing simple C++ code that uses std::string, then compile it and view the assembly (without stripping). Here is the my C++ code for learning std::string teststring.cpp.
After understanding std::string in assembly, reading the assembly code is a lot easier. There are still many unknown functions. But I could at least understand the program flow.
In upload_new_record function, there is a call to "new" after receiving the data from client. I can guess the call functions after "new" are Class constructors. In both constructor, they call the same function. It is a parent constructor. I also can see the vtable in constructor.
I named the class as RecordOdd and RecordEven (from the random value before creating the Record object). Inside RecordEven::setBuffer, there is a overflow bug. The buffer is allocated 224 bytes but the function always write 248 bytes. I thought to use it to overwrite the vtable of adjacent record. So just need to find a way to trigger it. I could guess almost immediately, the setBuffer is called when we edit a record. But that's not enough because it causes a heap corruption.
There are some messy functions. I could guess only they are STL functions and its object is in .bss section. I saw the "vector" and "Rb_tree". So the program must use std::vector and some kind of STL container that use Red-Black tree as internal data structure. Again, I wrote simple C++ code for each container. The binary use std::map. Here is my C++ code testmap.cpp
After I knew the program use std::map to store the Record and use a object id as a key, it is not difficult to reverse the left code. Here is my the reversed C++ code pp500_code.cpp
I tried to look at memory, when doing "upload new record". I found there is a some data of Red-black tree after a new record. If I use the overflow bug, I would overwrite the Red-black tree data, not vtable of next record.
It might be possible to overwrite the Red-black tree data in order to modify the pointer to Record, then make it points to fake vtable area. I think it is difficult to do. IMHO, playing with heap chunk is easier.
To make the Record allocated after another Record without any data in the middle, I upload a new record and delete it to make a hole (free chunk). Then, upload another record to lock the hole. The heap layout after above steps should look like this (omit heap metadata).
---+------------+--------------+----------- | free chunk | Record 1 | ---+------------+--------------+-----------
The "free chunk" should be big enough for many Red-black tree data but too small for a Record. So a new Record will be allocated next to Record1.
I need RecordEven object to be able to overwrite vtable of next Record. I do looping the "upload new record", "view record" and "delete record" (if it is not RecordEven) until got RecordEven object. Now, I could modify the vtable pointer :). The left problem is the address of Record that contained fake vtable.
In "edit_record" function, the received data is copied to Record buffer. The received length is ignored. The RecordOdd::setBuffer() use length that defined since "upload new record" to determine how many data to be copied. So if we can make the received buffer in "edit record" contains the address of heap, the program will copy the address to RecordOdd buffer. Then, we can get the address from "view record". (Not use RecordEven::setBuffer() because it causes a heap corruption.)
In "upload_new_record" function, there is a pointer to new Record. It is in buffer space of "edit_record" function, so we can copied a new uploaded Record to RecordOdd data.
When overwriting the vtable, we also need to put correct heap metadata (only chunk size) to prevent heap corruption. The last, I use "edit_record" function to trigger the exploit because it receives data from client to buffer before calling the function from fake data, so I put ROP data in stack.
The left part is doing ROP. Here is my exploit to get shell pp500.py (Note: you need to copy "/bin/sh" and needed libraries in pwn400 home directory because the program does chroot.)
I also write the exploit to just read the "key" file. It does not need to know the function offset in libc. pp500_readkey.py
$ python pp500_readkey.py creating a hole ... !!! Got odd record, it might be failed !!! creating a record to lock the hole ... creating third record... getting the Record address... 08fb50e4,08fb5024,00000001,d06a93c2,08fb52d8,43434344 Record address: 08fb5100 overwriting vtable... trigger the exploit sending key waiting for key dummy key for testing áP8PÇO♥ XáN N√