0%

【pdkx培训】pwn format

pwn format

漏洞成因

格式化字符串(Format String)漏洞是 pwn 题中的经典类型。它的核心成因通常是程序中直接将用户输入作为格式化字符串参数传给了 printf、sprintf、fprintf 等函数,例如:

1
2
3
4
5
// 漏洞代码
printf(user_input);

// 正确代码
printf("%s", user_input);

当程序这样写时,如果我们输入格式化占位符(如 %p, %x, %n),printf 就会误把栈上的数据当作参数来解析,从而导致任意内存读取或任意内存写入。

解题技巧

确定偏移量(Offset)

做题的第一步,是找出你的输入数据在 printf 函数栈上的第几个参数位置

  • 测试方法: 输入一串标志性字符(如 AAAAA 的 ASCII 码 0x41414141),后面接一串 %p
  • 示例输入: AAAA.%p.%p.%p.%p.%p.%p.%p.%p
  • 观察输出: 寻找输出中哪一个位置出现了 0x41414141(64位系统下是 0x4141414141414141,或者是指向它的指针)。
  • 快捷定位: 如果算出来是第 6 个位置,后续可以用 %6$p 直接打印该位置的值,不用写一长串 %p

任意内存读取(Leak 信息)

有了偏移量后,我们可以利用 %s%p 来泄露敏感信息,从而绕过安全防护。

  1. 泄露 Canary 或 libc 地址

    • 绕过 Canary: Canary 通常存在于栈上固定位置。找到对应的偏移量后,用 %X$p(X为偏移量)就可以直接读出 Canary 的值,防止本地溢出时触发报错。

    • 泄露 libc 基地址(绕过 ASLR): 栈上通常会残留 __libc_start_main 或其他 libc 函数的返回地址。通过 %X$p 读出这个地址,减去它在 libc 中的固定偏移,就能算出 libc 的基地址,进而拿到 system 函数和 /bin/sh 的地址。

  2. 任意地址读取

    如果你想读取一个已知明文地址 0xabcdef00 的内容:

    • 构造 Payload: [目标地址] + %Offset$s
    • printf 解析到 %Offset$s 时,会把前面的 目标地址 当作指针,并打印出该指针指向的字符串内容。

任意内存写入(Modify 数据)

这是格式化字符串威力最大的地方,主要利用 %n 占位符。

关键原理: %n 不打印任何东西,而是把前面已经打印的字符总数,写入到对应的指针变量中。

由于 %n 一次性写入 4 字节,数值太大(比如要写一个地址 0x08049000,需要打印几千万个字符,会导致网络超时或程序崩溃),所以实际打 payload 时通常用:

  • %hn:写入 2 字节(short)
  • %hhn:写入 1 字节(char)

常用修改目标

  1. 修改 GOT 表(最常见): 将某个常用函数(如 putsprintf 自身)的 GOT 表地址,修改为 system 函数的地址。下一次程序调用 puts(input) 时,实际上执行的就是 system(input)
  2. 修改返回地址: 修改栈上的返回地址(Return Address),直接劫持控制流到后门函数或 ROP 链。
  3. 修改全局变量: 有些题目会检查一个 flag 变量是否等于某个特定值,直接用 %n 把它改成目标值即可。

自动化原理解题:使用 pwntools

手动去算 %hn 需要多少个字符非常痛苦,好在 pwntools 提供了强大的自动化工具 fmtstr_payload

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

# p = process('./pwn_me')
p = remote('remote_ip', remote_port)

# 假设通过第一步测试,确定格式化字符串的偏移量是 6
offset = 6

# 假设我们泄露了 libc,算出了 system 的地址和 printf 的 GOT 地址
printf_got = 0x804a010 # 目标修改地址
system_addr = 0x8048400 # 想要写入的值

# 使用 pwntools 自动生成修改 GOT 表的 payload
# 作用:将 printf_got 的内容修改为 system_addr
payload = fmtstr_payload(offset, {printf_got: system_addr})

p.sendline(payload)
p.interactive()

总结做题思维导图

  1. Checksec: 看程序开启了什么保护(如果是 Full RELRO,则无法修改 GOT 表,需要考虑修改栈上的返回地址或 __malloc_hook)。
  2. 找偏移: 传入 AAAA 确定输入点在栈上的位置。
  3. Leak 地址: 打印出 Canary 和 libc 地址,计算出真实函数的绝对地址。
  4. 改写劫持: 利用 fmtstr_payload 篡改 GOT 表或返回地址,打出 system('/bin/sh') 拿到 shell。