this is a zer0pts2020 writeup for the pwn task syscallkit.
we are provided with binary called chall and its source code main.cpp. It's basically a system-call emulator, which lets you execute some system calls directly. But there are two limitations about it. Firstly, you can only execute system calls that it allows. juicy system calls like read, write...or execve is not allowed. Secondly, you can only execute 10 system calls.
It's pwn problem. Besides getting a shell, what are you trying to do?
First things first, let's take a look at their blacklist.
int Emulator::check() {
if (this->rax >= 0x40000000) return 1; // x32 ABI is dangerous!
if (this->rax == SYS_open) return 1; // never open files
if (this->rax == SYS_openat) return 1;
if (this->rax == SYS_write) return 1; // no more leak
if (this->rax == SYS_read) return 1; // no more overwrite
if (this->rax == SYS_sendfile) return 1;
if (this->rax == SYS_execve) return 1; // of course not!
if (this->rax == SYS_execveat) return 1;
if (this->rax == SYS_ptrace) return 1; // may ruine the program
if (this->rax == SYS_fork) return 1;
if (this->rax == SYS_vfork) return 1;
if (this->rax == SYS_clone) return 1;
return 0;
}eww. Now please take a look at it's security measures. btw look at "may ruine the program". It makes me feel bad.
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
eww again. Let's take a look at the actual syscalling part.
void Emulator::syscall() {
asm volatile ("movq %0, %%rdi":: "a"(this->rdi));
asm volatile ("movq %0, %%rsi":: "a"(this->rsi));
asm volatile ("movq %0, %%rdx":: "a"(this->rdx));
asm volatile ("movq %0, %%rax":: "a"(this->rax));
asm volatile ("syscall");
asm volatile ("movq %%rax, %0": "=a"(this->rax));
}As you can see, it's going to be tricky to call syscalls with complex arguments, such as mmap().
- brk(0) => heap leak
- writev(1,vtableptr, 1) => code leak
- mprotect(.text, size, RWX)
- mprotect(vtable, size, RWX)
- readv(0,vtableptr, 1) => overwrite vtable to bypass check
- read(0, Emulate::syscall, sizeof(shellcode))
- Calling any syscall grants a shell. :D
- We cannot utilize most of the syscalls if we don't know any address. So we used the
brk()system call to get the program limit. Then we can calculate the program's heap address. As theEmulatorclass is created via thenewkeyword, there, we can find theEmulatorClass' vtable pointer. - Then we can use this heap address to get a code leak. By locating the vtable for the
Emulatorfrom the vtable pointer on the heap, we can get a heap leak! - But reading and writing can be tricky. Thankfully, there are vector I/O system calls like
readv()andwritev(). We can utilize these alternatives in some special cases. - After all, we need to alter the code execution. So, we decided to use
mprotect()to allow the overwriting of the dangerous sections such as GOT or vtable. - Then, we overwritten vtable entry of
Emulate::checktoEmulate::syscall, so that we can bypassEmulate::check. - We can do anything from there. How about writing a shellcode at
Emulate::syscall?
Please Refer to the solver.py for the detailed payload.