背景
格式字符串函数可以接受可变数量的参数,并使用第一个参数作为格式字符串,并根据该格式字符串解析后续参数。 几乎所有的C/C++程序都使用格式化字符串函数来输出信息、调试程序或处理字符串。 一般来说,格式字符串在使用时主要分为三部分:
(1)格式化字符串函数
(2) 格式字符串
(3) 后续参数
随心所欲地阅读
利用格式化字符串函数漏洞读取程序运行空间中的数据。
#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. 两种方法的调试结果如下图所示:
我们发现,使用b设置断点时,指令地址变为,左图中的地址是程序加载后的虚拟地址。 群友说:“f7开头的是libc链接库的,是函数指令实际存在的地方,是动态链接的,就是plt表和got表的内容。” 嗯,我不知道。 稍后我会填补这些漏洞。
这里我们还是关注调用函数时栈上的内容。 输入数据为aaaa%x%x:
当断点打开时,在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,需要先打开它:
开启后,程序每次加载时基址都会改变。 但当我们利用格式字符串漏洞时,我们关注的是变量在堆栈上的相对位置,因此PIE和ASLR对此类漏洞的利用影响不大。
接下来我们分析关闭饼图的程序,输入aaaa%x%x,观察堆栈内容:
我们可以看到,在32位环境下,如果传入一个没有格式字符串的变量,那么传入的变量会在栈中存储两次。 继续运行并查看程序输出:
根据正常程序的分析,程序读取到的格式字符%x会在堆栈上进行匹配,并按照对应的格式输出。 输出顺序如下:
(1)正常输出aaaa
(2)扫描到第一个%x,在栈中向下匹配,以十六进制输出栈地址处的内容(如果要输出字符串内容,需要%s间接寻址)
(3) 扫描到第二个%x,在栈中继续向下匹配,将栈地址处的内容0以十六进制输出
理论上我们可以在格式化字符串后读取每个地址的数据。 不过,如果要读取第100个地址的内容,就相当麻烦了。 更不用说我们必须输入 100 %x。 如果字符串变量的长度不够,栈就会被破坏,程序就会崩溃。 在这里,我们使用 %{n}$x 快速打印格式化字符串中第 n 个地址的数据。
我们来测试一下。 例如,at数据位的地址是格式字符串(esp)后的第六个变量(前面有标签),所以我们输入%6$x来验证输出内容是否为:
此时,我们可以通过分析堆栈上的内容,使用%{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进行分析。 我们先看一下栈:
此时除了我们输入的局部变量字符串之外,并没有相关的内容。 我们看一下相关的寄存器:
由于只有一个参数,因此它存储在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中; 读取并输出它。
存在格式字符串漏洞,因此只需计算偏移量并构造合适的读取buf即可。 在断点下载地址处输入aaaa,并在本地创建flag文件:
发现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