Rating: 4.0

# PlaidCTF 2020 - Sandybox writeup

Written by @oranav on behalf of @pastenctf.
Visit the [original writeup](https://github.com/oranav/ctf-writeups/tree/master/plaid20/sandybox) for the solution code.

## Overview

We are presented with a single binary sandybox. After some setting up, it forks [1], waits for a ptrace tracer to attach [2], stops [3], and finally calls an inner function [4]:

c
child_pid = fork(); // [1]
...
if ( !child_pid )
{
prctl(PR_SET_PDEATHSIG, SIGKILL);
if ( getppid() != 1 )
{
if ( ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL) ) // [2]
{
...
}
myself = getpid();
kill(myself, SIGSTOP); // [3]
child(); // [4]
_exit(0);
}
...
}


Inside the inner function, an RWX space of size 10 bytes is allocated, then a shellcode (of exactly 10 bytes) is read, and finally executed:

c
char *buf, *ptr;
ptr = buf = (char *)mmap(0LL, 10uLL, 7, 34, -1, 0LL);
...
do
{
if ( read(0, ptr, 1uLL) != 1 )
_exit(0);
++ptr;
}
while ( ptr != buf + 10 );
((void (*)(void))buf)();


Note that at the time our shellcode is run, the tracer is already attached to us. Now what does it do? Basically, it sandboxes our shellcode. In short, it sanitizes the syscalls we are making by repeatedly using PTRACE_SYSCALL; what follows is essentially what the sanitizer does (by inspecting the registers, especially the syscall number - rax):

1. read, write, close, fstat, lseek, getpid exit, exit_group (0, 1, 3, 5, 8, 39, 60, 231 respectively) are unconditionally allowed.
2. alarm (37) is allowed only if rdi is at most 20.
3. mmap, mprotect, munmap (9, 10, 11 respectively) are allowed only if rsi (len) is at most 0x1000.
4. open (2) is allowed only if rsi is 0, rdi points to a valid address with a string of size at most 15, that does not contain the substrings "flag", "proc" or "sys".

## 32-bit syscalls to the rescue!

There are multiple solutions possible. However, the easiest I could come up with is just using 32-bit syscalls.

You see, Linux syscall numbers differ among different architectures. x86 and amd64 are no exceptions; they have different syscall tables. However, the interesting situation is that Linux on amd64 (usually) supports running x86 binaries. This means you can issue 32-bit syscalls from a 64-bit process, using **32-bit syscall numbers** (by hitting int 0x80). However, you are limited to 32-bit registers, so it's highly unlikely that you'll be able to pass pointers unless you mapped specific addresses.

However, note that while rax=2 represents open under 64-bit, it represents fork under 32-bit. By setting up the correct register structure, we are able to issue syscall number 2! By using int 0x80 instead of syscall, Linux runs fork().

After we forked, the newly created child is not traced by anyone, and it is free to do whatever it wants. Now we can just open, read, and write the flag.

## All this in 10 bytes?!

Huh? No way.

Well, Linux mmaps with a page granularity. Even though 10 bytes are requested for our RWX section, the generous kernel provides us no less than 4096 bytes!

Hence, we split the shellcode into two parts. The first (small) shellcode just reads the second (larger) shellcode into the RWX section, and right after read returns - the second shellcode runs.

## Wrap up

The flag is PCTF{bonus_round:_did_you_spot_the_other_2_solutions?}.

Well, can you?

Original writeup (https://github.com/oranav/ctf-writeups/tree/master/plaid20/sandybox).