Category: pwn
Points: 100
Challenge Binary: saas
You've heard of software as a service, but have you heard of syscall as a service?
The program lets us make arbitrary amd64 system calls except:
The return value of the system call is echoed back to the user. Also the binary has all protections enabled:
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 77 Symbols Yes 0 1 saas
If the binary wouldn't be position independent, executing shellcode would be trivial: Just mark the text-segment as writable with mprotect() and overwrite .text with our shellcode. Unfortunately we are completely blind. As the other writeups show, we can use mmap() to get our data into the process. Since mmap() can create executable regions we can use this to store shellcode that pops a shell (see shellcode.asm). The only question that remains is: How do we get control over the instruction pointer with system calls? And that's where signals come in handy! If we manage to recrate the following code snippet we can get full code execution:
void* shellcode = mmap(NULL, 0x1337, PROT_READ | PROT_WRITE | PROT_EXECUTE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
read(0, shellcode, 0x1337);
signal(SIGHUP, shellcode);
raise(SIGHUP);
Note that SIGHUP is chosen arbitrarily. We can use any other signal.
Let's recreate
signal(SIGHUP, shellcode)
with some system calls.
The system call that installs a signal handler is sys_rt_sigaction
(number 13). It takes the following arguments:
The struct sigaction
looks like this:
struct kernel_sigaction
{
__sighandler_t k_sa_handler;
unsigned long sa_flags;
void (*sa_restorer) (void);
sigset_t sa_mask;
};
k_sa_handler
is the handler function for the signal. This will be the address of our shellcode. sa_flags
and sa_mask
will be 0 since we don't want any special behaviour. sa_restorer
normally contains a pointer to a function that restores the state of the process right before the signal handler was executed. This is where sigreturn normally comes into play. In our case this field doesn't matter because execve will replace the process anyway but in the exploit script I set this to the address of our shellcode. oact
also doesn't matter and can be NULL and sigsetsize
is 8.
I chose to deliver a signal with the sys_tgkill
system call (number 234). This system call just takes a process id and a signal and delivers that signal to the process. Nothing special.
We can now combine everything into following exploit:
struct sigaction
k_sa_handler
= shellcodesa_flags
= 0sa_restorer
= shellcode (but doesn't matter really)sa_mask
= 0See exploit.py for a concrete implementation.