内容简介:在本篇文章和本系列的其他文章中,我会把一些内部构建解压到动态堆数据结构和相关的beast中。这篇文章是专门为没有堆背景知识的人准备的,不过可能会涉及到一点ELF内部构建和调试的知识。本文还会详细讲解一些实验,通过完成这些实验,你可以学到堆的工作原理是什么。堆其实就是执行程序时用来存储数据的内存区域列表。存储在堆内存区域中的数据在运行时进行请求调用。它允许像glibc这样的运行时环境为程序提供动态内存来分配数据。由于内存区域作为一种服务(它的作用就是如此),也就意味着在整个混乱的内存区域中,肯定需要关于内存区
在本篇文章和本系列的其他文章中,我会把一些内部构建解压到动态堆数据结构和相关的beast中。这篇文章是专门为没有堆背景知识的人准备的,不过可能会涉及到一点ELF内部构建和调试的知识。本文还会详细讲解一些实验,通过完成这些实验,你可以学到堆的工作原理是什么。
介绍
堆其实就是执行程序时用来存储数据的内存区域列表。存储在堆内存区域中的数据在运行时进行请求调用。它允许像glibc这样的运行时环境为程序提供动态内存来分配数据。由于内存区域作为一种服务(它的作用就是如此),也就意味着在整个混乱的内存区域中,肯定需要关于内存区域的计算信息。为了实现这一点,堆使用“chunk”这种内部结构来描述或修饰用户数据区域。根据其属性对块进行分类和分组。基本属性如下:
·是否可用
· 块的大小
· 在列表中,块的前后都有哪些块等
内存管理中最重要的一点是,它的本质就是围绕块搜索函数来定位块,然后执行释放或重新分配。
本文我将重点关注的堆分配器是glibc版本的ptmalloc,在glibc版本2.23-2.28中实现。当然,这并不是说只有glibc才能理解;堆分配存在多种方法。关于如何实现各种操作,每一种方法都是独特的。比如合并空闲的块,搜索和分类空闲块,并进行快速分组。当然可能还有更多其他功能—比如提升安全性。所以很多位置因为其复杂性会滋生并恶化为安全问题。但这些问题的根源在于用户是如何请求数据的,还有管理数据的分配器是如何响应内存区域中的元数据的。
最后再介绍一点,堆通常看起来都非常密集和复杂,并且内部构件非常粗糙,但是大部分构件,如aid memorization(帮助备忘),还有其他计算机技术,都有助于加快链表搜索速度。你也可以这样说,它们只不过是存储“cheat”元数据的巧妙的方法,这些元数据不需要在每次搜索时都搜索整个堆内存区域。但是这些元数据对我们来说很重要,因为在某些情况下,我们想要改变链表的搜索和解释方式。
Heap speak
你可能已经猜到堆的基本单位是chunk。你可能也想知道这些块在glibc代码中是什么形式,请看下面的代码:
这里我会对每一个字段做一个通俗易懂的解释(尽可能通俗易懂)。
· INTERNAL_SIZE_T–这是一个大小类型,用于在堆管理中定义“bookeeping”函数的字段 – 诸如指针(地址)和位字段之类的东西。这个大小由具体的实现来定义。我们可以猜想glibc想要在不同的硬件和运行时的实现中具有可移植性和灵活性 – 因此映射到INTERNAL_SIZE_T的地址大小可能会有所不同。无论如何,INTERNAL_SIZE_T被定义为size_t – 它可以追溯到C运行时最初解决问题的方式。
· mchunk_prev_size– 是块格式的第一部分,无论是空闲块还是已用块,都会有这个部分。该字段指示当前块的前一个块的大小,并且如果所引用的块是空闲的,则其最低有效位被设置为0x1。因此,如果你正在查看一个块,并且其prev_size的最小sig位为0x1,那么就在此之前仍然是“使用”的块。
· mchunk_size– 非常标准,实际上只保存当前大小,以字节为单位lol。
· struct malloc_chunk * fd– 这是struct块结构中的一个字段,用于定义另一个块的地址空间。这是因为它形成了一个链表。这里定义的链表是“空闲列表”,它将堆上空闲的所有块拼接在一起。这里我们在链表中定义“正向指针”。
· struct malloc_chunk * bk– 这个字段与上面提到的字段类型相同,只不过这个是“反向指针”。
· struct malloc_chunk * fd_nextsize– 这个字段来自堆中的另一层空闲链表技术。如果指针高于特定大小阈值(我们将在稍后介绍),则将此指针添加到空闲块中 – 以便堆管理器可以在它们出现时跟踪大的块。它有点像在赌场中的高级玩家,当你出场时,他们跟踪你的行为动作,因为你很大程度上能够影响当晚的盈利能力。
我们再来看一下这些在执行过程中是怎么样的,看看不同类型的块看起来有什么区别(空闲块和已分配的块)。我们现在通过gdb来运行一个C程序,然后解压缩堆来显示它在内部是如何响应的。C程序如下:
#include <string.h> #include <stdio.h> #include <stdlib.h> char * make_string(size_t length){ char *arr = (char *)malloc(length); asm("int $3"); return arr; } void free_string(char *arr){ free(arr); asm("int $3"); } int main(int argc, char **argv){ int _len = 128; int index = 0; char *array,*array_1,*array_2,*array_3,*array_4; //char *array_; //char *array__; //find a way to show the chosen candiate for each round int inner_array = 0; int _char = 1; for(index = 0;index <= _len;index++){ array = make_string(_len); memset(array,0xAA,_len); printf("[*] array @[%p]\n",array); /* 4 more allocations*/ array_1 = make_string(_len+80*1); printf("[*] array @[%p]\n",array_1); memset(array_1,0xBB,_len+80*1); array_2 = make_string(_len+80*2); printf("[*] array @[%p]\n",array_2); memset(array_2,0xCC,_len+80*2); array_3 = make_string(_len+80*3); printf("[*] array @[%p]\n",array_3); memset(array_3,0xDD,_len+80*3); array_4 = make_string(_len+80*4); printf("[*] array @[%p]\n",array_4); memset(array_4,0xEE,_len+80*4); /*free each array and clear it */ memset(array,0xFF,_len); free_string(array); memset(array_1,0xFF,_len+10); free_string(array_1); memset(array_2,0xFF,_len+20); free_string(array_2); memset(array_3,0xFF,_len+30); free_string(array_3); memset(array_4,0xFF,_len+40); free_string(array_4); _char++; printf("\n\n"); } //printf("[*] done"); return 0; }
我知道这段代码可能有点长,你可能不想一行一行的看,所以你可以完全忽略其他的mallocs和空闲块。这里我添加它们是为了举例说明,然后逆向一些更有意思的数据。
在上面的代码中,我添加了一个简单的wrapper函数,而且在最后一个return之前下了一个断点。这样我就可以隔离我们正在研究的内存区域上的空闲块和malloc调用效果。
现在让我们看看堆分配内存时会发生什么。我们需要先找到一个指向堆的指针。这很简单,因为malloc会在返回主函数后将其保存在rax中,我在前几个gdb命令中显示:
所以当我设置好hook-stop时,它会输出$rax-0x10附近的所有内容,这是保存块头信息的地址。我这样做是因为当我们下这个断点时,malloc刚好返回并将寄存器设置为其返回值 – 这就是分配的内存区域的地址。我们可以直接看到这些宏指令如何在glibc / malloc/malloc.c中对堆元数据数据进行操作:
你可以看到它只是简单地增加或减少2个地址以获取mem(用户数据启动的原始内存指针)或两个地址之前的块信息。还有许多其他操作可以提取和设置其他元数据。
好的,这就是基本格式,接下来我们来看看它是如何运作的。
不断增长的堆
在下了第一个断点后你应该看到gdb显示分配的第一个堆块,下图是一个带有注释版本的堆分配图,显示了堆的格式:
当在你的屏幕中出现这段内容时,尝试执行“c”gdb命令跳到下一个断点。你会看到更多已分配块的示例,然后在你的屏幕上会显示如下:
这就表明我们不能像以前在hook-stop中使用$ rax中的值那样。可能你会想,这是因为$ rax不再保留内存指针,它现在转到了一个空闲调用,因此它保留了一些其他值。无论如何,我们可以使用传递给free_string函数的地址来转储块,因为在这里我们可以非常方便地看到它显示出来。这是块被释放后的样子:
除了空闲块之外,上面的图片中显示的是第一个空闲块fd(正向空闲链表)和bk(反向空闲链表)指针。这里我们可以看到,如果我们使用gdb的内存检查器函数来跟踪它们,它们最终会在0x602a00位置结束,这是顶部块的地址; 指向当前分配的堆地址顶部的指针。
好的,这就是当块被分配和释放时的样子,我们能够查看块是如何合并成更大的空闲块呢?当然可以,这是下一篇文章的内容。
空闲块合并
在分配了块之后,我们的程序将按照它们分配的顺序释放每个块。这意味着我们可以认为彼此相邻的被分配的两个块,也是相继被释放并且相邻的-所以,我们就有了两个空闲块合并成了一个大的块。
看起来是下图这样的:
上图的左边我们能看到的是地址0x602580和0x6024a0处的两个块(名为块1和块2)。在右边0x6024a0地址处我们可以看到一个新的块,但是我们可以看到合并后的大小字段是0x211(如图所示 也就是0xe1 + 0x130)。
这差不多就是块合并的整个行为。这也是我这篇文章想要讲解的东西。在这个系列文章中,我还会讲到fast-bins,大块管理,也可能讲一些堆重定向技巧。
敬请期待。
以上所述就是小编给大家介绍的《Glibc堆漏洞利用基础-深入理解ptmalloc2 part1》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 漏洞分析:对CVE-2018-8587(Microsoft Outlook)漏洞的深入分析
- MacOS/iOS CVE-2019-6231 漏洞深入分析
- 由Typecho 深入理解PHP反序列化漏洞
- 深入分析Windows系统DHCP漏洞(CVE-2019-0726)
- 对CVE-2018-8587(Microsoft Outlook)漏洞的深入分析
- Apache Tomcat从文件包含到RCE漏洞原理深入分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。