返回目录

简单格式化字符串漏洞实践

格式说明符 %n、%c

%n是一个格式说明符,它的作用是将到目前为止已经写入输出缓冲区的字符数写入到对应的参数中

例如:

printf("Hello%n, World!\n", &count);

由于在%n之前已经输出了Hello五个字符,所以count的值将会是5。

%c是一个格式说明符,它的作用是将对应的参数解释为一个字符,并将其输出到标准输出流中。

%Nc表示输出N个字符,这也增加了输出字符数,可供利用%n来写入任意值。

例如:

printf("%4660c%n", &count);

由于在%n之前已经输出了4660个字符,所以count的值将会是0x1234(4660的十六进制)。

此外,%n将写入的值限制为32位(4字节),%hn是%n的短整型版本,它将写入的值限制为16位(2字节),%hhn是%n的字符型版本,它将写入的值限制为8位(1字节)。

偏移 $

$符号表示当前参数的索引位置,索引从1开始。例如:

printf("%2$s,%1$s", str1, str2);

%2$s会先取出偏移为2的参数str2,然后将其作为字符串%s输出,接着%1$s会取出偏移为1的参数str1,然后将其作为字符串%s输出。

当不存在参数时,$符号可以用来指定任意偏移位置。

组合得到任意内存写入

通过组合使用格式化字符串漏洞和参数偏移,可以实现任意内存写入。

例如:

from pwn import *

GET_FLAG = 0x00401296 # 函数地址。0x0040 = 64 ; 0x1296 = 4758 ; 4758 - 64 = 4694
ADDR = 0x00404098 # 要修改的内存地址

payload = b"%64c%15$hhn%4694c%14$hn"
payload = payload.ljust(32, b"A") # 补齐至32字节
payload += p64(ADDR)
payload += p64(ADDR + 2)

首先,%64c将计数器增加至64,然后%15$hhn将64(0x40)一个字节写入到栈上偏移为15的地址中(即ADDR + 2,小端地址高位)

然后,%4694c将计数器从64增加至4758,然后%14$hn将4758(0x1296)两个字节写入到栈上偏移为14的地址中(即ADDR,小端地址低位)。

于是,最终ADDR的值被修改为0x00401296,即GET_FLAG函数的地址。

之所以不使用%n一次性写入,是因为%c会产生大量的输出(4MB),使用%hn和%hhn可以避免输出过多字符。

参数偏移

参数偏移 14 和 15 是通过探针探测得来的。它们分别是(10 + 4) 和 (10 + 5)

向程序输入格式化字符串探针:

AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p...以下略

程序输出:

AAAAAAAA.0x1.0x1.0xb.0x402020.(nil).(nil).0x7fff5e327b10.0x7fff5e327d10.0x40149b.0x4141414141414141...以下略

AAAAAAAA的十六进制表示为0x4141414141414141,它出现在输出的第 10 个位置参数中。也就是说,输入缓冲区开头的内容从第 10 个位置参数开始出现。

同时,每个%p输出8个字节的内容,payload已补齐至32字节,所以偏移为10 + 32/8 = 14 和 10 + 40/8 = 15

这里的“第 10 个”不是 x86-64 程序的固定常数,而是本题当前二进制、libc 和调用栈共同决定的结果。

在 Linux x86-64 的 System V ABI 中,整数和指针类型的函数参数前 6 个通常通过 RDI、RSI、RDX、RCX、R8、R9 传递。调用 printf(s) 时,格式串 s 占用了 RDI,没有显式提供其他可变参数。printf 处理位置参数时,会把可变参数分成两部分:前面的参数槽对应寄存器保存区,后面的参数槽对应调用者栈上的溢出参数区。

这个编号最好通过探针确认;如果编译选项、libc 版本、函数调用方式或栈布局改变,输入也可能从其他编号开始出现。

留言