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的用法比较多,举个例子把

内存释放

  • 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)

检查返回值错误

内存分配可能会失败,需要对失败的情况进行处理

C++中new可以用try catch:

其它

  • 多次释放内存 如double free

  • 引用已经释放的内存
    • for (p = head; p != NULL; p = 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宏操作,从双向链表中移除一个块

如下图

 

 

堆缓冲区溢出攻击

  • 堆缓冲区溢出攻击比栈缓冲区溢出要难一些。堆溢出攻击常见的是通过破坏动态内存管理器所使用的数据结构,使得内存管理器在进行内存块操作时发生异常,最终导致执行攻击者提供的shellcode,如破坏数据结构来欺骗unlink宏。
  • 如下面的的代码中存在漏洞:

  • 在第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 (bhcompile@decompose.build.redhat.com) (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

 

最终代码

根据上述的步骤,构建出如下代码:

几点说明:

  • 这里为了方便实验,直接把shellcode设为数组,而不是原来的argv[1],然后使用的是memcpy函数
  • 最后那个even int为了方便gdb查看,填充的是\x22\x22\x22\x22

查看代码效果

  • gcc -ggdb -z execstack -g -o a test.c
  • gdb a

用gdb设置断点,然后查看second附近的内存

覆盖前

覆盖后

 

由于上面代码的shellcode是创建用户名和密码都为ALI的用户,我们可以用如下的命令查看效果:

  • tail /etc/passwd

运行前

  • ./a

运行后

实际上,上面的代码中malloc(668)同样能执行^ ^

 

参考资料

  • RUC 《程序设计安全》 – 梁彬
本博客若无特殊说明则由 hrwhisper 原创发布
转载请点名出处:细语呢喃 > C动态内存管理 | Malloc 的原理 | 堆溢出攻击
本文地址:https://www.hrwhisper.me/c-dynamic-memory-allocation-and-the-data-struct-of-malloc-and-heap-overflow-attack/

打赏一杯咖啡钱呗

信息安全, 计算机基础 , . permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *