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 * |