Glibc堆漏洞利用基础-深入理解ptmalloc2 part1

SoftNight 系统安全 2019年1月18日发布
Favorite收藏

导语:这篇文章是专门为没有堆背景知识的人准备的,不过可能会涉及到一点ELF内部构建和调试的知识。本文还会详细讲解一些实验,通过完成这些实验,你可以学到堆的工作原理是什么。

在本篇文章和本系列的其他文章中,我会把一些内部构建解压到动态堆数据结构和相关的beast中。这篇文章是专门为没有堆背景知识的人准备的,不过可能会涉及到一点ELF内部构建和调试的知识。本文还会详细讲解一些实验,通过完成这些实验,你可以学到堆的工作原理是什么。

介绍

堆其实就是执行程序时用来存储数据的内存区域列表。存储在堆内存区域中的数据在运行时进行请求调用。它允许像glibc这样的运行时环境为程序提供动态内存来分配数据。由于内存区域作为一种服务(它的作用就是如此),也就意味着在整个混乱的内存区域中,肯定需要关于内存区域的计算信息。为了实现这一点,堆使用“chunk”这种内部结构来描述或修饰用户数据区域。根据其属性对块进行分类和分组。基本属性如下:

· 是否可用

· 块的大小

· 在列表中,块的前后都有哪些块等

内存管理中最重要的一点是,它的本质就是围绕块搜索函数来定位块,然后执行释放或重新分配。

本文我将重点关注的堆分配器是glibc版本的ptmalloc,在glibc版本2.23-2.28中实现。当然,这并不是说只有glibc才能理解;堆分配存在多种方法。关于如何实现各种操作,每一种方法都是独特的。比如合并空闲的块,搜索和分类空闲块,并进行快速分组。当然可能还有更多其他功能—比如提升安全性。所以很多位置因为其复杂性会滋生并恶化为安全问题。但这些问题的根源在于用户是如何请求数据的,还有管理数据的分配器是如何响应内存区域中的元数据的。

最后再介绍一点,堆通常看起来都非常密集和复杂,并且内部构件非常粗糙,但是大部分构件,如aid memorization(帮助备忘),还有其他计算机技术,都有助于加快链表搜索速度。你也可以这样说,它们只不过是存储“cheat”元数据的巧妙的方法,这些元数据不需要在每次搜索时都搜索整个堆内存区域。但是这些元数据对我们来说很重要,因为在某些情况下,我们想要改变链表的搜索和解释方式。

Heap speak

你可能已经猜到堆的基本单位是chunk。你可能也想知道这些块在glibc代码中是什么形式,请看下面的代码:

1.png

这里我会对每一个字段做一个通俗易懂的解释(尽可能通俗易懂)。

· 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命令中显示:

 2.png

所以当我设置好hook-stop时,它会输出$rax-0x10附近的所有内容,这是保存块头信息的地址。我这样做是因为当我们下这个断点时,malloc刚好返回并将寄存器设置为其返回值 – 这就是分配的内存区域的地址。我们可以直接看到这些宏指令如何在glibc / malloc/malloc.c中对堆元数据数据进行操作:

 3.png

你可以看到它只是简单地增加或减少2个地址以获取mem(用户数据启动的原始内存指针)或两个地址之前的块信息。还有许多其他操作可以提取和设置其他元数据。

好的,这就是基本格式,接下来我们来看看它是如何运作的。

不断增长的堆

在下了第一个断点后你应该看到gdb显示分配的第一个堆块,下图是一个带有注释版本的堆分配图,显示了堆的格式:

 4.png

当在你的屏幕中出现这段内容时,尝试执行“c”gdb命令跳到下一个断点。你会看到更多已分配块的示例,然后在你的屏幕上会显示如下:

 5.png

这就表明我们不能像以前在hook-stop中使用$ rax中的值那样。可能你会想,这是因为$ rax不再保留内存指针,它现在转到了一个空闲调用,因此它保留了一些其他值。无论如何,我们可以使用传递给free_string函数的地址来转储块,因为在这里我们可以非常方便地看到它显示出来。这是块被释放后的样子:

       6.png

除了空闲块之外,上面的图片中显示的是第一个空闲块fd(正向空闲链表)和bk(反向空闲链表)指针。这里我们可以看到,如果我们使用gdb的内存检查器函数来跟踪它们,它们最终会在0x602a00位置结束,这是顶部块的地址; 指向当前分配的堆地址顶部的指针。

好的,这就是当块被分配和释放时的样子,我们能够查看块是如何合并成更大的空闲块呢?当然可以,这是下一篇文章的内容。

空闲块合并

在分配了块之后,我们的程序将按照它们分配的顺序释放每个块。这意味着我们可以认为彼此相邻的被分配的两个块,也是相继被释放并且相邻的-所以,我们就有了两个空闲块合并成了一个大的块。

看起来是下图这样的:

 7.png

上图的左边我们能看到的是地址0x602580和0x6024a0处的两个块(名为块1和块2)。在右边0x6024a0地址处我们可以看到一个新的块,但是我们可以看到合并后的大小字段是0x211(如图所示 也就是0xe1 + 0x130)。

这差不多就是块合并的整个行为。这也是我这篇文章想要讲解的东西。在这个系列文章中,我还会讲到fast-bins,大块管理,也可能讲一些堆重定向技巧。

敬请期待。

本文翻译自:https://blog.k3170makan.com/2018/11/glibc-heap-exploitation-basics.html如若转载,请注明原文地址: https://www.4hou.com/system/15484.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论