安全与风控
当 printf 系列函数把用户输入当格式串时…
在 printf/fprintf/sprintf 等函数直接接收用户可控格式串时,利用 %p/%s 泄露栈、canary、PIE、libc 地址,利用 %n/%hn/%hhn 实现任意写并覆盖 GOT/hook 达成 RCE。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
当 printf 系列函数把用户输入当格通用
解决思路
- 识别漏洞模式:printf(user_input)、fprintf(fp, user_input)、sprintf(buf, user_input)、snprintf(buf, sz, user_input) 均为漏洞;printf("%s", user_input) 安全。
- 快速验证:输入 AAAA%p%p%p%p%p%p%p%p,若输出出现十六进制栈值即确认漏洞,并在输出中寻找 0x4141414141414141 定位输入偏移。
- 栈读取:用 %p 顺序打印栈值,用 %N$p 按位置直接读取第 N 个参数,用 %N$lx 显式十六进制读取(64 位更可移植),用 %N$s 把第 N 个参数当作指针解引用读字符串。
- 定位输入偏移:发送 AAAAAAAA.%p.%p... 观察 0x41414141 出现位置;或循环发送 AAAA%{i}$p(i 从 1 到 30),命中 0x41414141 时记录偏移。
- 泄露关键值:canary 用 %N$p(N 为 canary 距格式串的栈偏移,通常为 buf_size/8 加几);保存的 RBP 用 %N$p 泄露栈地址;返回地址用 %N$p 泄露 .text 地址(PIE base = leak & ~0xfff - offset);libc 地址用 %N$p 读取栈上 __libc_start_main+XX 的返回地址(libc base = leak - offset)。
- 任意地址读取(%s):32 位把地址放在格式串开头,payload = p32(target_addr) + b'%N$s'(N 为该地址在栈上的偏移);64 位因地址含空字节,把地址放在格式说明符之后,payload = b'%8$sAAAA' + p64(target_addr)。
- 写内存说明符:%n 写 4 字节 int,%hn 写 2 字节 short,%hhn 写 1 字节 char,%ln 写 8 字节 long,写入值为已打印字符数(按宽度取模)。
- 32 位任意写:把目标地址直接放在格式串中(会落在栈上),用 %hn 每次写 2 字节,payload = p32(target_addr) + p32(target_addr+2) + f'%{low-8}c%{offset}$hn' + f'%{(high-low)&0xffff}c%{offset+1}$hn',其中 low=value&0xffff、high=(value>>16)&0xffff。
- 64 位任意写:因地址含空字节会截断字符串,把地址放在格式说明符之后——先写格式串部分(无空字节),再补齐到 8 字节对齐,最后追加 p64(target_addr) 和 p64(target_addr+2)。
- 逐字节写(%hhn):对 64 位 48 位地址分 6 次写,每次写一个字节 writes[target_addr+i] = (value>>(i*8))&0xff,可用 pwntools 的 fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') 自动计算。
- 使用 pwntools fmtstr_payload:payload = fmtstr_payload(offset, {elf.got['printf']: libc.symbols['system']}, numbwritten=0, write_size='short'),write_size 可选 'byte'/'short'/'int',64 位地址后置会自动处理。
- 使用 pwntools FmtStr 类交互式利用:定义 send_payload(payload) 发送并返回输出,fmt = FmtStr(execute_fmt=send_payload) 自动探测 offset,再 fmt.write(elf.got['printf'], libc.symbols['system']) 并 fmt.execute_writes()。
- GOT 覆盖目标:printf@GOT→system(下次 printf(user_input) 变 system(user_input),发送 /bin/sh);strlen@GOT→system;puts@GOT→system;atoi@GOT→system(发送 sh 作为“数字”);__stack_chk_fail@GOT→可控地址以绕过 canary 检查;exit@GOT→main 制造循环以便多次利用。
- Hook 覆盖(glibc < 2.34):__malloc_hook 写 one_gadget 地址(任何带大格式串的 printf 会触发内部 malloc);__free_hook 写 system(触发 free("/bin/sh"))。
- 栈指针链利用(格式串不在栈上、如存于堆缓冲区时):寻找栈上已有指针链(如 rbp→prev_rbp→prev_prev_rbp),第一阶段用 %A$hn 修改 ptr_X 低字节使其指向 target_addr,第二阶段用 %B$n 通过被修改的 ptr_X 写入 target_addr。
- 寻找指针链:用 %p 泄露栈,找偏移 N 处指向另一栈地址 B 的栈地址 A,以及偏移 M 处的 B;用 %N$hn 改 A 的值改变 B 的指向,再用 %M$hn 通过 B 写到目标。
- 盲打格式化字符串(无二进制无源码):先发 50 个 %p 转储栈并识别地址模式(libc 0x7f...、栈 0x7ff...、代码地址);再对 N=1..50 发 AAAA%N$p 找 0x41414141 定位输入偏移;由代码地址推 PIE base(或无 PIE 时固定基址);已知基址后用 %N$s 读 GOT;用 GOT 值减 libc 符号偏移算 libc base;最后用 %n 把 GOT 项改写为 system 地址。
- 绕过 FORTIFY_SOURCE(__printf_chk 禁止 %N$n 位置写):改用非位置化的顺序 %hn(打印精确字节数、%hn、调整、再 %hn,脆弱但可行);若格式串在栈上则用非位置化 %n 配合栈位置控制;或改用堆溢出(FORTIFY 不保护堆);或 ROP 调用未加固的 printf。
- 64 位注意事项:地址含 \x00 会截断格式串,须把地址放在格式说明符之后并补齐对齐;地址有效宽度 6 字节,用 3 次 %hn 或 6 次 %hhn 写;因 6 个寄存器参数被保存,输入偏移常在 6 以上;只用 64 位地址的低 48 位。
- 64 位布局模板:[格式说明符][补齐到 8 字节对齐][addr1][addr2][addr3]...,格式串部分不含空字节,地址部分允许含空字节。
- 决策流程:先判断 FORTIFY_SOURCE 是否启用(__printf_chk 则位置化 %n 被禁);再确定需要泄露什么(canary 用 %N$p、PIE base 用返回地址偏移、libc base 用 __libc_start_main 返回地址、heap base 用栈上堆指针、特定地址用 %N$s);再按架构选择地址位置(32 位前置、64 位后置);再按保护选写目标(Partial RELRO 用 GOT,Full RELRO 用 __malloc_hook/__free_hook(<2.34),Full RELRO 且 glibc≥2.34 用 _IO_FILE、exit_funcs、TLS_dtor_list,或直接覆盖栈返回地址);再按单次/多次选择策略(有循环则增量覆盖 GOT 并用指针链,单次则用 fmtstr_payload 一次写入全部);若输入不在栈上(堆缓冲区)则用栈指针链间接写。
适用边界 · 注意事项
- 目标使用 printf("%s", user_input) 等固定格式串写法时不存在该漏洞。
- 目标启用 FORTIFY_SOURCE 且无法使用非位置化 %n、也无法结合堆溢出或 ROP 时,位置化写被禁止。
- glibc ≥ 2.34 时 __malloc_hook/__free_hook 已被移除,不能再作为覆盖目标。