阅读 119

格式化字符串漏洞

漏洞原理

格式化字符串是一种很常见的漏洞,其产生根源是printf函数设计的缺陷,即printf()函数并不能确定数据参数arg1,arg2…究竟在什么地方结束,也就是说,它不知道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数format后面地址的内容
格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配

 

如图所示,执行的命令为printf("%s %d %d %d %x\n",buf, 1, 2, 3),紧随格式化字串后压入栈上的参数为4个,但格式化字串有五个参数,printf在解析第五个参数%x时,会继续往栈上读取,造成了信息泄露:

常见格式化字符串漏洞函数

格式化字符串符号说明:

  • 转换指示符

字符类型使用
d4-byteInteger
u4-byteUnsigned Integer
x4-byteHex
s4-byteptr String
c1-byteCharacter
  • 长度

字符类型使用
hh1-bytechar
h2-byteshort int
l4-bytelong int
ll8-bytelong long int
  • %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而hn表示写入的地址空间为2字节,%hhn表示写入的地址空间为1字节,lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%hn或hhn来适时调整。
    %n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据

#include <stdio.h>
int main(void)
{
    int a;
    printf("aaaaaaa%n\n",&a);
    printf("%d\n",a);
    return 0;
}复制代码

判断是否存在漏洞

拿到一个程序之后可以通过输入若干个%s来进行判断是否存在漏洞

%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s复制代码

因为如果存在格式化字符串漏洞。在输入一串%s后,就会把对应地址的内容当做指针 来打印该指针指向的地址空间的字符串。然而并不是所有空间都具有可读权限,这样的话,就会导致程序崩溃。

泄露内存

由于格式化字符串变长参数的特性,在实际运行中,如果Format String的符号说明个数超过待匹配的参数个数,即有更多的符号说明需要被匹配时,printf会根据解析结果和调用约定去取栈上(reg)相应的值并输出。

//leakmem
#include <stdio.h>
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}复制代码

我们输入:%08x.%08x.%08x
得到的结果如下图:

我们来调试一下:
首先第一个printf的参数如下

%08x.%08x.%08x是作为%s相对应的参数的,所以他的打印结果如下:

第二个printf的参数对应如下:

很奇怪的是 当程序结束后才会打印出来

我们不只是可以%x%x%x来泄露数据,还可以用%p

如果还想输出特定位置的内容还可以用%n$x,这里的n为一个自然数,代表第n位。
输入%3$x,得到:

任意地址泄露

任意地址读需要用到printf的另外一个特性,操作符.这个操作符可以输出指定位置的参数.利用操作符.这个操作符可以输出指定位置的参数.利用x这样的字符串就可以获得对应的第n+1个参数的数值(因为格式化参数里边的n指的是格式化字符串对应的第n个输出参数,那么相对于输出函数来说就成了第n+1个).
代码:

//gcc test.c -o test -m32
#include <stdio.h>
 
int main(void)
{
    char str[100];
    scanf("%s",str);
    printf(str);
    return 0;
}复制代码

首先测一下字符串开头的偏移量:

➜  / ./test
AAAA%1$x
AAAAffdb0848#                                                                                                                          ➜  / ./test
AAAA%2$x
AAAAc2#                                                                                                                                ➜  / ./test
AAAA%3$x
AAAAf7e998fb#                                                                                                                          ➜  / ./test
AAAA%4$x
AAAAffdefece#                                                                                                                          ➜  / ./test
AAAA%5$x
AAAAffa838dc#                                                                                                                          ➜  / ./test
AAAA%6$x
AAAA41414141#复制代码

point : 实际偏移量
注意64位算偏移时要先将调用约定中寄存器的数量加进去,并且payload里地址之前的其他格式化字符串也会在栈上占去位置,会导致实际偏移量增加,需要进行新的计算,假如无法确定%K$n中的K到底是多少,可以多输出几个%p来确定。

 

如果将AAAA替换成某个函数的got地址,那么程序就会打印这个函数的真实地址

内存覆盖

内存覆盖的思路和任意地址泄露的思路相似,一种可行的方法是同上面一样,构造payload,在格式化字符串中包含想要写入的地址,此时该地址会随格式化字符串放在栈上,然后用格式化字符串的'%K$n'来实现写功能。

 

利用%n改写
函数:

#include <stdio.h>
int main()
{
    int flag = 30;# 0x1e 有很多文章都将flag设成了0,但是系统中有很多为0的内存 容易混淆
    int *p = &flag;
    char a[100];
    scanf("%s",a);
    printf(a);
    if(flag == 2000)
       {
        printf("good!!\n");
       }
 
    return 0;
}
#编译后程序是附件test32复制代码

在图中 明显的参数0x1e为第5个
所以我们输入的是:%.2000x%5$n
结果如下:

%.2000x是以2000对其的方式输出某地址
%n是写入,本身也是记录输出个数;%5$n是写入第5个常数。
所以%2000x%5$n是将栈中第5个刚好修改为2000-参考文献


作者:HBhan
链接:https://juejin.cn/post/7015846184522776613

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐