http://blog.oxff.net/#snql4p42vu2h2vautajq

2011-06-06 18:09

DefCon 19 CTF Qualifications: pp500

During this years qualficiations, there was again a lot of broken challenges. Therefore I choose to go for the hard stuff instead of stupidly guessing XOR keys trivia style. pp500 is a 32bit Linux ELF binary written in C++, so most of the time for this challenge went to actually reversing the binary. Initially, it does the same as most of other challenges and drops privilegues to user "pwn400" (probably copy-pasta). Interestingly, the forking server also invokes execve on itself and passes the client file-descriptor as argv[1], this enables ASLR fully for each new connection.

Each client then needs to provide a static password -- std::string::compare(pwd_string, "haveFun!n") -- and can then repeatedly select options in a menu. For our unconvenience, the menu is implemented using a std::list and after each action taken, the list order is randomized (seeded with /dev/urandom). The main handling code then lets you choose an option and processes it in a endless loop:

Outer menu implementation

Since the actual implementation of the menu is irrelevant, I will spare you the details here.

In this program, we deal with Records, binary blobs of data that we can specify, that additionally have an identifier to reference them, a global unique counter value (incremented with each new record) and a type field. We can:

To understand how records are created, I had a look at the implementation of uploading:

Record uploading

As is visible from this code, the first four bytes of the record data determine the type of the record. There is actually one super class Record and two derived classes Record1 and Record2 in this binary. Also, there is code to generate the identifier for the blob based on the content. It basically derives the identifier from the MD5 sum over the first 64 bytes of the actual data. Looking at the different constructors, we can find the vtables for them:

Record vtable

So after finding that, I spent some time analyzing the whole codebase and rebuilding the classes as structures:

Record class layout

After looking at the different functions, I noticed that record1_update (invoked from edit record) honors the length of the existing record but record2_update does not. Both have a statically sized array for the data anyway, but the Record2 storage is only 0xd8 bytes long. The code however always writes 0x3d * 4 = 0xf4 bytes. That is an overflow of exactly 0Ch bytes, or in other words the length of a glibc heap header and the vtable pointer:

Record2 Update function

This theoretically allows us to redirect code execution upon invocation of any virtual function. The route we choose to go down is to just use the view summary functionality, as it iterates over all objects and invokes a virtual function pointer on them. However, where do we point our vtable? We cannot just write a function pointer, but need to write the pointer to a function pointer. Also, the binary runs with full ASLR on each connection.

Thankfully, we could also construct an information leak (props to kaliman who found this out). When editing a record, it would always receive a buffer of 100h bytes and pass the whole buffer. The code would then copy as many bytes as the original record was big from that buffer. If we send a buffer of smaller size in the edit, we have a classical unitialized variable scenario and can leak a heap address relative to our latest chunk:

Record Update function

At this point, I was too tired to cook up a working exploit. Thankfully, kaliman did that and we got the key!