本文介绍如下内容
- C/C++中动态内存语句使用如malloc等
- 介绍dlmalloc 内存管理的实现
- 利用dlmalloc的数据结构进行堆溢出攻击的原理
- 堆溢出攻击实验
C/C++语言中动态内存语句的使用
内存分配
malloc (size_t size);
- 分配size个字节的内存,并返回指向该内存的指针
- 没有初始化所分配的内存
realloc (void *p, size_t size);
将p指向的内存块大小改为size个字节
新内配的内存没有初始化
p必须是以前调用malloc(),calloc()或者realloc()返回的结果,或者为空
- p = NULL时,等价于malloc(size);
calloc (size_t nmemb, size_t size);
- 为具有nmemb个元素的,元素大小为size的数组分配内存,返回指向分配数组的指针
- 新分配的内存初始化为0
C++中的new的用法比较多,举个例子把
1 | int *pi = new int; //没有初始化 |
内存释放
free (void *p);
释放p指向的内存空间,p必须是以前调用malloc(),calloc()或者realloc()返回的结果,或者为NULL
- p = NULL时,不执行操作
对已释放过的内存进行释放会导致危险的结果。所以一个好的编程习惯是把free后的指针设为NULL
delete和delete[]
- 需要和new配对使用,之前的用new,则delete,之前的new [] ,则用delete[]
常见的坑
初始化问题
- malloc不对分配的内存进行初始化,如果需要初始化,可以用calloc来分配,或者用memset()来初始化
- 初始化错误可能导致信息泄露(Information Leak)
检查返回值错误
内存分配可能会失败,需要对失败的情况进行处理
1 | int *p = (int *)malloc(sizeof(int) * 5); |
C++中new可以用try catch:
1 | try { |
其它
- 多次释放内存 如double free
1 | int *x = (int *) malloc(n * sizeof(int)); |
引用已经释放的内存
- for (p = head; p != NULL; p = p->next) free(p);
- 正确的应该为:
1 | for (p = head; p != NULL; p = q) { |
内存管理函数需要匹配
- malloc、calloc、realloc <-->free
- new <--> delete
- new[] <--> delete[]
malloc(0)
- 与平台有关,有的返回长度为0的缓冲区(MSVC),有的返回NULL,
- 应该避免这种,以及malloc(-1) 是malloc(2^32 - 1);
内存泄漏(Memory Leak)
- 已分配的没有被释放,最后可用的会使得可用内存越来越小,造成服务器宕机
dlmalloc 内存管理的实现
GNU C类库及大多数Linux版本将Doug Lea的malloc实现(dlmalloc)作为默认内存分配器,下面介绍dlmalloc中的内存管理
内存块分类
在dlmalloc中,内存块有2类,已分配块和空闲块。
空闲块通过双向链表形式组织起来
在2类块中,都用一个PREV_INUSE位来标识上一个块是否已被分配
- 1表示有分配,0表示没有分配
- 因为malloc一定为偶数,所以拿最后一个位来标记
它们的结构可以参考如下图:
空闲的双向链表如下图
空闲块合并
调用free时,空闲块可能被合并:
- 若该被释放的块上一块位空闲块,该会被空闲链表中解开并与被释放的块合并
- 如果所释放的块的下一块为空闲块,也要被解开和合并
其所用的是Unlink宏操作,从双向链表中移除一个块
1 | #define unlink(P,BK,FD){\ |
如下图
堆缓冲区溢出攻击
- 堆缓冲区溢出攻击比栈缓冲区溢出要难一些。堆溢出攻击常见的是通过破坏动态内存管理器所使用的数据结构,使得内存管理器在进行内存块操作时发生异常,最终导致执行攻击者提供的shellcode,如破坏数据结构来欺骗unlink宏。
- 如下面的的代码中存在漏洞:
1 |
|
在第9行free(first);时,如果下一块(程序中的第二块)内存没有被分配,那么free操作将会试图将其与第1块内存块合并。为此,需要检查第3块内存的PREV_INUSE标识。而当前块的下一块内存将其块大小作为偏移量使用
于是攻击者输入668字节长度的以上数据,第1块内存将会堆溢出,使得第2块内存中的块管理数据被覆盖
被溢出数据后覆盖的 第二个内存块构如下:
由于大小为-4,于是系统认为其下一块内存从当前内存块前4个字节开始,就是even int处所对应的4个字节,而由于其为偶数,最低位(PREV_INUSE)为0,因此系统认为第2块内存块为空闲块,因此调用unlink宏进行移除空闲链表,并进行合并。
重点来了,此时unlink操作为
- FD = FUNCTION_POINTER-12; //这里的12为BK的偏移
- BK = CODE_ADDRESS
- FD->bk = (FD + 12) = (FUNCTION_POINTER) = BK = CODE_ADDRESS
- 就是说*(FUNCTION_POINTER) = CODE_ADDRESS 就是 Write Anything to Anywhere!
- 这里CODE_ADDRESS为shellcode地址,而FUNCTION_POINTER为要覆盖的函数指针地址! 比如free函数!
堆溢出攻击实验
实验环境
使用的系统为Read Hat Enterprise Linux 4在终端中输入cat /proc/version显示如下内核信息:
- Linux version 2.6.9-5.EL ([email protected]) (gcc version 3.4.3 20041212 (Red Hat 3.4.3-9.EL4)) #1 Wed Jan 5 19:22:18 EST 2005
为了完成这个实验首先关闭内存随机化
- sysctl -w kernel.exec-shield-randomize=0
寻找地址
gcc -ggdb -o bugcode.o bugcode.c
然后输入objdump -R bugcode.o得到如图:
我们将free作为Function_pointer,根据上面的原理
Fd = Function_pointer -12 = 0X08049644 -12 = 0x08049638 (这里是减去十进制的12)
Bk = codeAddress = first address + 8 (+8 的原因是因为first前8个字节在free过程中会被覆盖)
代码构建
我们构建680的字节:
- 前8个字节随意填充
- shellcode(长度设置为X,这里的shellcode是创建用户名和密码都为ALI的用户)
- 然后填充664-8-X个字节(随意)
- 偶数整数
- -4 补码为0xfffffffc
- Fd 0x08049638
- BK 0x0804a010
最终代码
根据上述的步骤,构建出如下代码:
1 |
|
几点说明:
- 这里为了方便实验,直接把shellcode设为数组,而不是原来的argv[1],然后使用的是memcpy函数
- 最后那个even int为了方便gdb查看,填充的是
查看代码效果
- gcc -ggdb -z execstack -g -o a test.c
- gdb a
用gdb设置断点,然后查看second附近的内存
覆盖前
覆盖后
由于上面代码的shellcode是创建用户名和密码都为ALI的用户,我们可以用如下的命令查看效果:
- tail /etc/passwd
运行前
- ./a
运行后
实际上,上面的代码中malloc(668)同样能执行^ ^
参考资料
- RUC 《程序设计安全》 - 梁彬