Thursday, May 3, 2012

Plaid CTF 2012 - Secure FS

The binary file is stripped 64 bit PIE. If looking in .comment section, we can guess the executable was compiled on Debian 6. With the given libc, we can confirm the guess. The given libc matches the debian libc package version must be 2.11.3-2 (luckily I do not update this vm). So my solution (exploit) is only for Debian 6 with given libc.

Reversing

From strings command result, we see that the executable uses C++ STL. So many unknown functions are C++ STL code. Because executable was not compiled with optimization. We can write the code that use C++ STL and compile it. The assembly should be the same. From known C++ STL function, we know that the executable uses string, list, map. I write simple C++ code to use all these STL (template.cpp). I cannot make IDA FLAIR to build the signature on Linux x64 binary. So I write a script to map the C++ STL (mapstl.py).

$ g++ -o template template.cpp
$ objdump -C -d template > template.objdump.txt
$ objdump -C -d sfs > sfs.objdump.txt
$ python mapstr.py > map.txt

The result (map.txt) is not completed. Some function cannot be mapped. But with this result, reversing is much easier. Here is the reversing code (sfs.cpp sfs2.cpp).

Vulnerability

From code, I found only one bug in "ln" function. The FileData object is deleted if "linkname" is invalid path but File object is still point to it. So this is user-afer-free bug.

user@/$ create file xor a
Data: AAAABBBB
user@/$ cat file
    ####
user@/$ ln file /x/x
Invalid path.
user@/$ cat file
▒▒▒0
user@/$ ln file /x/x
Invalid path.
*** glibc detected *** ./sfs: double free or corruption (!prev): 0x00007f4432e8e1f0 ***
======= Backtrace: =========
...

From above output, this bug can be used for information disclosure. We can "cat" after deleting a file to read data from freed memory.

Exploitation

Because of ASLR and PIE, let start with information disclosure. If we "cat" a file after trigger a bug first time, we get the address in libc .bss section because it is the first large free chunk. So we get libc loaded address. Next, std::list is used in function. Its object is allocated on stack. If a next list is allocated on freed memory, we will see the stack address because there is a pointer that points back to object on stack.

Time to controlling rip. The obvious method is overwriting vftable in freed FileData. But there is no suitable object to replace the freed memory for controlling vftable. I thought about string buffer in std::string. It is possible but I do not want to dig into C++ STL. So I come up with allocating 2 consecutive FileData objects in heap like below.

+--------------------+--------------------+
|         b3         |         b4         |
+--------------------+--------------------+

I trigger a bug to delete "b3" and "b4". Next, doing some task to allocate some data on "b3". Then, creating a new file "evil" to allocate FileData on "b3" and "b4". So I can get heap layout on freed "b3" and "b4" like below.

+--------------------+--------------------+
|  any  |        evil        |    free    |
+--------------------+--------------------+

After getting above heap layout, I can use "edit" command on "evil" to modify vftable pointer of b4 FileData easily. To get this, just do following commands.

do_create(sk, "b1", "xor", "AAAA", "a"*4000)
do_create(sk, "b2", "xor", "BBBB", "a"*4000)
do_ln(sk, "b1", "/xx/yy") # create a hole to hold small data
do_create(sk, "b3", "xor", "CCCC"*100, "a"*4000)
do_create(sk, "b4", "xor", "DDDD"*100, "a"*4000)
do_create(sk, "b5", "xor", "EEEE"*100, "a"*4000)
do_ln(sk, "b3", "/xx/yy")
do_ln(sk, "b4", "/xx/yy")
# ... fill memory here ...
do_create(sk, "evil", "xor", "E", "a"*4000)

I also create one more file after "b4" (below) in order to get the address of controlled data (FileData object). If I create a file after creating "evil", a FileData object is not fit to "free" chunk but a File object does. So I can use "cat" command on "b4" to get the address of FileData object from File object in "free" chunk.

+--------------------+--------------------+--------------------+
|  any  |        evil        |    free    |         b5         |
+--------------------+--------------------+--------------------+

At this point, I can control rip and have controlled data. But I cannot find any gadget for pivoting rsp to controlled data.

We can modify FileData pointer in File object by "edit"ing "b4". So we can read data from any valid memory address. After modify "b5" File object, we might not be able to edit "b5" because vftable pointer might point to invalid vftable.

do_edit() receives input with getline() then calls encrypt() with vftable. Because I know the stack address, I can compute the saved rip address of getline(). I modify "b5" FileData pointer in File object to point to saved rip address minus 8. So getline() fills the input to its saved rip address and after. Now I control rip and rsp points to controlled data.

Then, I do a small ROP to call execv(). Finally, Pwned!. Here is my exploit (sfs_sol.py).

UPDATE:

I got reply from "@riczho"

@sleepya_ Nice writeup! You found a bug that I put in by mistake - it was supposed to be integer wraparound on the hard link count :-)

I forgot to reversing one block in do_ln(). So the bug was supposed to be triggered by doing hard link 256 times, then removing a file. It is still user-after-free on freed FileData object. The idea to exploit is still the same. Here is a updated reversed code (sfs2.cpp).