通常来说,最常见的是printf,那么我也就根据printf来解析。
首先来看该函数的调用格式:
printf(“<格式化字符串>”, <参量表>)
很多的时候,问题其实出现在参量表,因为在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况
当前字符不是 %,直接输出到相应标准输出。
当前字符是 %, 继续读取下一个字符
如果没有字符,报错
如果下一个字符是 %, 输出 %
否则根据相应的字符,获取相应的参数,对其进行解析并输出
泄露内存
泄露栈内存
利用%n$p来获取栈中的数据,利用%n$s获取栈变量对应的字符串
泄露任意地址内存
利用addr%n$s。
函数会先将addr存入栈中,然后遇到%n$s对栈中存放addr的地址内容进行解析,因为%s是将对应的栈中变量视为字符串变量,输出其数值对应的地址处的字符串。
所以addr%n$s的运作原理:
1.addr入栈
2.%n$s找到栈中第n个参数,也就是addr,对其所指向的地址处的内容进行输出。
覆盖内存
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
也和上面一样,addr%k$n
覆盖小数字
printf(“[addr]%Dd %N$n”)这种格式输出addr就占4个字节,执行覆盖时最小值是4,可不可以小于4?
那么我们就可以将addr放在后面,这是可以的。
此时printf变为printf(“xx%k$n[**] [addr]”) 其中的xx, * *可以为任意的字符。
xx赋值几就几个字符,例如打印2就xx%N$n,由于又需要按字节对齐(否则程序会崩溃)那么我们就需要在$n后面补齐两个字符。
这个时候,xx%N成为了第k个参数,$nxx成为了第k+1个参数,所以我们此时对于printf的利用中N就需要变成N+2
注:其实,这里我们需要掌握的小技巧就是,我们没有必要必须把地址放在最前面,放在那里都可以,只要我们可以找到其对应的偏移即可。
[BJDCTF 2nd]r2t4
一个标准的覆盖内存例子。
格式化字符串漏洞,更改__stack_chk_fail
的got表为后门函数地址。
payload=’%64c%9$hn%1510c%10$hn’+p64(0x601018+2)+p64(0x601018)
%64c%9$hn此处是以两个字节为单位,写入64个
%1510c%10$hn这里写入的可不是1510,而是需要加上前面%64c%9$hn写入的64个字节。
覆盖大数字
先对Printf作一点补充:
printf (“%n”,a):当遇到%n时,程序会检查已经输入了多少字符串,然后将其写入到a中。可以通过这一点来改写栈中内存。
%n写入的内存最大为4字节。
%hn写入的内存最大为2字节。
%hhn写入的内存最大为1字节(用于将大字节拆开写)。
%xd依然会被算入这个空间。
利用fmtstr!
fmtstr_payload(offset, {address: value})
第一个参数 offset 用 autofmt.offset 算好的即可。
然后,我们需要声明 {address: value} 来覆盖address的内容成对应的value。我们还可以同时改写多个地址:
{address1:value1,address2:value2,…,address:valueN}