格式化字符串漏洞

 2024-03-13 03:04:15  阅读 0

背景

格式字符串函数可以接受可变数量的参数,并使用第一个参数作为格式字符串,并根据该格式字符串解析后续参数。 几乎所有的C/C++程序都使用格式化字符串函数来输出信息、调试程序或处理字符串。 一般来说,格式字符串在使用时主要分为三部分:

(1)格式化字符串函数

(2) 格式字符串

(3) 后续参数

格式化输出pi值_格式化输出函数是_printf格式化输出没有参数

随心所欲地阅读

利用格式化字符串函数漏洞读取程序运行空间中的数据。

#include 

void main(){
    char a[10];
    int b = 1024;
    double c = 40;
    scanf("%s", a);
    printf("%s, %d, %f\n", a, b, c);
}
// gcc -m32 -o normal_stringpwn normal_stringpwn.c -no-pie

#include 

void main(){
    char a[10];
    scanf("%s", a);
    printf(a);
}
// gcc -m32 -o stringpwn stringpwn.c
// gcc -m32 -o stringpwn_nopie stringpwn.c -no-pie

32位正常程序分析

首先,gdb调试32位正常程序,了解正常调用时的堆栈情况。 我们想在一个地方设置一个断点。 这里我使用了两种方式设置断点:b *() 和 b. 两种方法的调试结果如下图所示:

格式化输出pi值_printf格式化输出没有参数_格式化输出函数是

我们发现,使用b设置断点时,指令地址变为,左图中的地址是程序加载后的虚拟地址。 群友说:“f7开头的是libc链接库的,是函数指令实际存在的地方,是动态链接的,就是plt表和got表的内容。” 嗯,我不知道。 稍后我会填补这些漏洞。

这里我们还是关注调用函数时栈上的内容。 输入数据为aaaa%x%x:

printf格式化输出没有参数_格式化输出函数是_格式化输出pi值

当断点打开时,在32位情况下,esp指针指向内容格式字符串,然后对应变量a(%s)、b(%d)、c(%f)。 这里我们要注意几点。 观点:

(1)比较("%s,%d,%f\n",a,b,c); 可以发现参数是从右向左压入栈的;

(2)输出时,会从栈顶从右向左取出格式化后的字符串,并按指定格式输出;

(3)栈中存储的变量中,b和c将它们的值存储在栈上,而a则不会将字符串压入栈中,而是存储在实际值存储的地址中。 字符串内容。 换句话说,%s的作用是间接寻址,并将数据以字符串形式输出。

32位漏洞程序分析

由于我们之前在编译32位有漏洞程序时区分了pie选项,所以这里我们看一下PIE的影响。 gdb默认关闭ASLR,需要先打开它:

格式化输出函数是_格式化输出pi值_printf格式化输出没有参数

格式化输出pi值_printf格式化输出没有参数_格式化输出函数是

开启后,程序每次加载时基址都会改变。 但当我们利用格式字符串漏洞时,我们关注的是变量在堆栈上的相对位置,因此PIE和ASLR对此类漏洞的利用影响不大。

接下来我们分析关闭饼图的程序,输入aaaa%x%x,观察堆栈内容:

格式化输出函数是_格式化输出pi值_printf格式化输出没有参数

我们可以看到,在32位环境下,如果传入一个没有格式字符串的变量,那么传入的变量会在栈中存储两次。 继续运行并查看程序输出:

根据正常程序的分析,程序读取到的格式字符%x会在堆栈上进行匹配,并按照对应的格式输出。 输出顺序如下:

(1)正常输出aaaa

(2)扫描到第一个%x,在栈中向下匹配,以十六进制输出栈地址处的内容(如果要输出字符串内容,需要%s间接寻址)

(3) 扫描到第二个%x,在栈中继续向下匹配,将栈地址处的内容0以十六进制输出

理论上我们可以在格式化字符串后读取每个地址的数据。 不过,如果要读取第100个地址的内容,就相当麻烦了。 更不用说我们必须输入 100 %x。 如果字符串变量的长度不够,栈就会被破坏,程序就会崩溃。 在这里,我们使用 %{n}$x 快速打印格式化字符串中第 n 个地址的数据。

我们来测试一下。 例如,at数据位的地址是格式字符串(esp)后的第六个变量(前面有标签),所以我们输入%6$x来验证输出内容是否为:

格式化输出函数是_格式化输出pi值_printf格式化输出没有参数

此时,我们可以通过分析堆栈上的内容,使用%{n}$x来读取任意内容。 注意这里的$x或$p以十六进制输出数据。 $s 通常用于读取间接寻址的字符串。

64位漏洞程序分析

继续上面的漏洞程序分析,并将其编译成64位可执行程序。 编译指令为:

gcc -o stringpwn_64 stringpwn.c -no-pie

这里需要注意的是,64位程序调用函数时的参数设置与32位程序不同:

(1) 当参数个数小于7时,参数从左到右放入寄存器:rdi、rsi、rdx、rcx、r8、r9。 (2)当参数个数大于等于7时,前6个参数设置同(1),后面的参数从右向左入栈,与32个相同位程序。

喜欢:

H(a, b, c, d, e, f, g, h);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->8(%esp)
g->(%esp)
call H

同样,我们在处设置断点,输入aaaa%x%x进行分析。 我们先看一下栈:

此时除了我们输入的局部变量字符串之外,并没有相关的内容。 我们看一下相关的寄存器:

格式化输出pi值_格式化输出函数是_printf格式化输出没有参数

由于只有一个参数,因此它存储在rdi寄存器中。 让我们看一下输出:

输出序列与32位程序类似:

(1)正常输出aaaa

(2)扫描到第一个%x,输出寄存器rsi 0xa的内容

(3)扫描到第二个%x,输出寄存器rdx 0x0的内容

因此,在64位环境下,我们也可以使用%{n}$x来读取任意内容,但是这里当n≤5时,将会从rdi寄存器之后的第n个寄存器(格式字符串所在的位置)开始按rdi、rsi、rdx、rcx、r8、r9的顺序读取内容; 当n>5时,将从栈顶读取内容到栈上。

因此,%5$x 应该输出 r9 寄存器的内容,%6$x 应该输出栈顶(rsp)的内容。 读者可以自行验证。

一个示例问题

9.1K

·

百度云盘

IDA分析其逻辑:读取flag并存储到局部变量buf中; 读取并输出它。

格式化输出pi值_printf格式化输出没有参数_格式化输出函数是

存在格式字符串漏洞,因此只需计算偏移量并构造合适的读取buf即可。 在断点下载地址处输入aaaa,并在本地创建flag文件:

格式化输出函数是_格式化输出pi值_printf格式化输出没有参数

发现r8寄存器和堆栈中存在flag。 调试发现r8中的内容是read(fd, buf, );时生成的被称为。

根据前面提到的偏移规则,我们可以在本地调试中输入%4$s和%7%s来读取该标志。 然而,从远端读取时,只能读取%7%s。 经验如下:

from pwn import *
p = remote('****', 9338)
payload = '%7$s'
p.sendline(payload)
flag = p.recvall()
print(flag)

这让我再次思考,为什么该标志同时存在于 r8 和堆栈上? 为什么服务器读取时r8中的标志消失了? buf是一个栈地址,为什么我们需要将内容存储在栈上呢?

至于寄存器如何变化,我们就不深究了。 这里我们只关注第三个问题。 观察汇编发现,buf变量作为局部变量,存储的是分配空间的堆地址(),但并不存储数据本身。 因此,并不是flag存在于栈中,而是flag的堆地址存在于栈中,因此可以使用%s间接寻址的方式读出。

关于r8寄存器的问题,看来是Linux内核引起的。 有些人也有类似的问题,这超出了我的知识范围。 我们以后再讨论:

///when-linux-x86-64---r8-r9-and-r10

//linux/blob//工具///.h#L268

printf格式化输出没有参数_格式化输出pi值_格式化输出函数是

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码