1. 漏洞样式
通常情况下漏洞程序样式:
1 | char buffer[1024]; |
字符串 buffer 可控且作为 printf 的第一个参数。当其中包含 格式化字符(例如%s, %d, %p等) 时,栈上的内容就会被当做printf的第2个、第3个参数等被输出。
2. 利用方法
2.1 确定偏移
输入字符串(即Table 1中buffer)在栈上的偏移,即buffer被printf当做参数时,作为第几个参数。参数序号从0开始: printf(arg0, arg1, arg2, …, argn); 第10参数即表示arg10
- 步骤1.
break printf;即在printf下断点 - 步骤2. 输入
%p%p%p%p等特殊字符 - 步骤3. 在
printf函数断点,使用stack命令查看栈。找到%p%p%p%p特殊字符串在栈上的位置。如Figure 1所示。
需注意图中①断点在printf入口,已跳转到printf 但尚未执行printf中指令(尤其是栈指令,否则栈布局会改变);注意图中② 0b 即字符串 %p%p%p%p 距离栈顶( esp )的偏移为 11;由于在 esp + 0 的位置存放函数返回地址。因此 %p%p%p%p 字符串实际上位于 栈上 第 10 个参数。
- 步骤4. 根据不同架构确定
buffer在printf函数参数的序号。参考函数调用约定。- a). x86架构
x86架构的函数参数全部通过栈传递, 因此buffer是printf的第10个参数。 - b). x64架构
x64传参顺序为rdi, rsi, rdx, rcx, r8, r9; 之后才使用栈传参。因此若Figure 1在x64架构中,buffer对应的是printf函数的第(0xb + 6 - 1)= 16 个参数(参数序号从0开始,0,1,2,…, 16)。
- a). x86架构
2.2 实现任意地址写
往 任意目标地址(记为target_address) 中写入 任意值(记为target_value) 。
- 1). 任意值的控制:通过格式化字符串
%Mc其中M=target_value来操控 - 2). 任意地址的控制:将目标地址(
target_address)写入buffer字符串中;并通过格式化字符串%N$n来指定将*当前printf已经输字符个数* 写入到第N个参数指定的地址中。其中N即为’使用2.1中方法确定的‘’在buffer字符串中的‘’target_address在栈上的位置对应的printf的参数序号‘。(该处使用’'分句停顿帮助阅读)
例如:
2.2.1 当target_value 较小时,直接写入
假如buffer字符串位于printf第10个参数的位置(即arg10、即printf栈0xb(10+1)参数位)
1 | // 写4到target_address |
其中:
payload1实现写4(p32为4byte)到target_address
1 | payload2 = b'%100c' + b'%13$n' |
payload2实现写100到target_address, 此处由于target_adress没有写在字符串的开头,因此需要重新计算在栈上的偏移:字符串开头位于arg10处,字符串中target_address之前有12个字符即占3个参数位,因此target_address对应的参数位为13
2.2.2 当target_value太大时,分字节写入
假如buffer字符串位于printf第10个参数的位置(即arg10、即printf栈0xb(10+1)参数位);且需要向target_address 中写入的target_value为*0xbaedbeef*。
实际上就是令:
*(int8*)target_address = 0xef = 239
*(int8*)(target_address + 1) = 0xbe = 190
*(int8*)(target_address + 2) = 0xed = 237
*(int8*)(target_address + 3) = 0xba = 186
这种情况下使用 %N$hhn 向目标地址写入int8宽度值 和 使用 %N$hn 向目标地址写入int16宽度值,将会非常有用。
那么可以使用如下payload实现:
1 | payload = p32(target_address) //arg10 |
2.2.3 使用pwnlib的fmtstr_payload函数自动构造payload
示例如下:
1 | from pwn import * |
