Wednesday, April 27, 2011

pCTF 2011 #19 Another small bug

Challenge #19 "Another small bug" was a stack-based buffer overflow.

Djo (@shell_storm) has already published a write-up (english) on Nibbles' blog, as well as @hellman on Leet More's blog, and Agix (@Agixid) on shell-storm. Just like hashcalc1 or hashcalc2, there was no NX. However, ASLR was enabled. Djo and hellman both used a big nopsled + brute-force to circumvent ASLR, Agix used a nice ret2ret, while I chose ROP to mmap rwx. Good thing is that it would also have worked if NX had been effectively enabled.

Not familiar with ROP? Have a look at the references posted on this exploit writing tutorial by @corelanc0d3r.


19 - Another small bug - 250 pts

Category: pwnables

This time, let's attack /opt/pctf/z2/exploitme.

ssh # Q7044oQfwTHFIf8x92VtcQ75


Reversing the binary tells you that:
  • it takes a number as first argument, converted using strtoul
  • it reads input on stdin with fgets_unlocked, size being the previous number
  • input is stored in a stack buffer

decompilation by IDA Pro with Hex-Rays

Let's try quickly with gdb:
gdb$ r 2048 < <(python -c 'from struct import pack;
    print "A"*532+pack("<I",0xdeadbeef)')
Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()

But there was a check?

Well, you may argue it should not work because there is a check on size >= 512. However, if you read attentively, it only exits if log_error succeeds.

log_error fails if it cannot open (or create since it's "a" mode) the log file. But hold on:
z2_16@a5:~$ ls -l /home/z2/logs/
ls: cannot access /home/z2/logs/: No such file or directory
Not sure if it was intended :) but it explains why the previous check on user-supplied size parameter does not stop the program.

It the file existed and was readable, then fopen call would not have failed. To make it fail, we could have used setrlimit with RLIMIT_NOFILE (or simply bash's ulimit -n) to restrict the maximum number of opened file descriptors. For the notice, it also explains why the binary is statically compiled (see just below). Indeed, if dynamically linked, the loader would fail loading libraries, and exploit writer surely did not want that. ;)


Interestingly, the binary is statically compiled:
$ file exploitme
exploitme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
It means that libc functions are present in the binary, like... mmap!

How good is that? Well, we can simply return to mmap asking for an rwx area, copy a shellcode in it, and return to it! Straightforward ROP, similar to my Shmoocon's CTF warm-up alternative solution (exploit).

I used a bunch of gadgets to copy the shellcode byte per byte with a wrapper since we were not limited in payload size. One could use alternatively use recv to directly receive a shellcode from stdin or else.

Full exploit here: colored syntax or plain .py

Just run it with cat to keep stdin opened:
$ { python; cat; } | /opt/pctf/z2/exploitme 1300
uid=2015(z2_16) gid=1001(z2users) egid=1003(z2key) groups=1001(z2users)
ROP is fun!


  1. Wow nice!
    I think there also another way, to use int 0x80 gadget and do execve or smth

  2. W00t !
    Nice sploit!

    it is a masterpiece :)

    Agix (@Agixid), has published another method for "Another Small Bug" ( (ret2ret)

    But, Stalker you're right, the ROP is very fun. good game :)

  3. Thanks for the link, added! Indeed, nice ret2ret by Agix :)

  4. Awesome +) We made it harder...