堆溢出

Posted on 2019-11-07 19:31  Volcano3511  阅读(1053)  评论(0编辑  收藏  举报

堆溢出

堆栈区别

堆:顺序随意
栈:后进先出(Last-In/First-Out)
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由[编译器]需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。

堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的[应用程序]去[控制],一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,[操作系统]会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的[C语言]中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

明确区分堆与栈

在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。

首先,我们举一个例子:

void f() { int* p=new int[5]; }

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax
  1. 管理方式不同;
    对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
  2. 空间大小不同;
    一般来讲在32位系统下,堆内存可以达到[4G]的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的
  3. 能否产生碎片不同;
    对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列
  4. 生长方向不同;
    对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

  1. 分配方式不同;
    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的[机制]是很复杂的,例如为了分配一块内存,库函数会按照一定的[算法](具体的算法可以参考数据结构/[操作系统])在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。
  2. 分配[效率]不同;
    显然,堆的效率比栈要低得多。

定义

堆是内存的一个区域,它被应用程序利用并在运行时被动态分配。堆内存空间发生缓冲区溢出是很常见的,对这些缺陷的利用与基于堆栈的缓冲区溢出是不同的。
静态全局变量是位于数据段并且在程序开始运行的时候被加载。而程序的动态的局部变量则分配在堆栈里面。


堆内存与堆栈内存的不同在于它在函数之间更持久稳固。这意味着分配给一个函数的内存会持续保持分配直到完全被释放为止。这说明一个堆溢出可能发生了但却没被注意到,直到该内存段在后面被使用。这里没有涉及堆和被保存的EIP的概念,但是堆中存储了其他重要的东西,而且它们会通过溢出动态缓冲区被破坏。


堆是用于数据动态分配的内存区域。在这个过程中,地址空间通常被分配到堆栈的同一个段,并且从高端地址到低端地址增长。


堆内存可以通过_malloc_类型函数分配,_malloc_类型函数在结构程序设计语言中是常见的,例如,HeapAlloc() (Windows)、malloc() (ANSI C)和new() (C++)。相对地,内存可以被作用相反的函数HeapFree()、free()和delete()释放。在后台有一个OS或者C语言库的部分,就是所知的堆管理器,它给进程分配堆,并允许堆的增长以使一个进程得到更多的动态空间。


每个程序分配的内存(这里指的是malloc函数)在内部被一个叫做”堆块”的所替代。一个堆块是由元数据和程序返回的内存组成的(实际上内存是malloc的返回值)。所有的这些堆块都是保存在堆上,这块内存区域在申请新的内存时会不断的扩大。同样,当一定数量的内存释放时,堆可以收缩。

Meta-data of chunk created by malloc(256)

The 256 bytes of memory return by malloc

—————————————–

Meta-data of chunk created by malloc(512)

The 512 bytes of memory return by malloc

—————————————–

Meta-data of chunk created by malloc(1024)

The 1024 bytes of memory return by malloc

—————————————–

Meta-data of the top chunk

在堆块之间的”—”是虚拟的边界,实际当中他们是彼此相邻的。你可能会问,为何我要在布局当中包含一个”顶块”元数据。顶级块表示堆中可利用的内存,而且是唯一的可以大小可以生长的堆块。当申请新的内存时,顶块分成两个部分:第一个部分变成所申请的堆块,第二个部分变为新的顶块(因此顶块大小可以收缩)。如果顶块不能够满足申请的内存区域大小,程序就会要求操作系统扩大顶块大侠(让堆继续生长)。

## 0×02 堆块结构解析

解析堆块需要依赖于当前堆块的状态。例如,在分配的堆块中只有presize和size字段,程序分配的缓冲区开始于fd字段。这就表示分配的堆块总是有8字节的元数据,在这之后才是缓冲区。而且奇怪的是prev_size字段不是被分配的堆块所使用!下面我们将会看到未分配的(释放)的堆块使用其他的字段。

另一个重点是,在glibc堆块中是8字节对齐的。为了简化内存的管理,堆块的大小总是字节的倍数。这就表示size的最后3位可以是其他用途(正常情况下总是置0)。只有第一位对我们很重要。如果这位置1,就表示前面的堆块在使用。如果没有置1,表示前面的堆块没有被使用。如果相关的内存被释放了,堆块就不会被使用(通过调用函数去释放)。

当堆块释放后,在下一个堆块的size字段关键位一定会被清零。此外,prev_size字段将会被设置成堆块释放状态。

已经释放的堆块同样使用fd和bk字段。这两个字段可以作为漏洞利用字段。fd字段指向之前的已释放堆块,bk字段指向之后的已释放堆块。这就表示释放的堆块存储在一个双向链表中。然而所有的堆块不是保存在一个链表中的。实际上有很多链表保存释放的堆块。每个链表包含的是特定大小的堆块。这样的话搜索指定大小的堆块将会很快,因为只需要在指定大小的链表中搜索相应堆块。现在我们来矫正一下刚开始的陈述:当申请内存时,首先从具有相同大小的已经释放的堆块(或者大一点的堆块)中查找并重新使用这段内存。仅仅当没有找到合适的堆块时才会使用顶块。

最后,我们可以知道如何去利用了:释放堆块。当一个堆块释放了(通过调用free函数),它会检查之前的堆块是否被释放了。如果之前的堆块没有在使用,那么就会和当前的堆块合并。这样就会增加堆块的大小。结果就是这个堆块需要被放置在不同的链表中。这样的话,之前释放的堆块就需要首先从链表中删除,接着再和当前堆块合并到另一个合适的链表中。从一个链表中删除一个堆块的代码如下:
图片9.png

使用堆

malloc()、calloc()和realloc()
动态内存分配相对于静态变量和自动变量(考虑到函数参数或局部变量)的分配,必须由可执行程序显式地完成。在C语言中,程序需要调用一些函数以利用内存块。


realloc函数可以将地址为_ptr_块的大小改变为_newsize_。用于此种工作的相应算法是相当复杂的。例如,当块后面的空间正在被使用时,realloc通常会把块复制到一个新的地址,在那有更多的空间可利用。realloc调用的值是块的新地址。如果块需要被移动,realloc就会把旧的内容复制到新的文件中。


free函数释放由_ptr_所指向的内存块。大多数情况下,内存停留在堆池中,但是在特定情况下,它可以返回到OS,这会导致一个更小的进程映像。

C++中使用new()和delete()函数来产生差不多相同的效果。在运行Microsoft Windows时,内部调用包含HeapAlloc()和HeapFree()这样的函数。


在不同的系统之间,对堆管理的实现没有一个统一标准,甚至在UNIX系统中也使用了几个不同的标准。在本章中,将关注最普遍的两个:Linux中的堆管理器和Solaris系统中的堆管理器。


如果没有特殊规定,本章假定使用堆管理的Linux算法。参考下一部分“高级堆腐烂(heap corruption)—— Doug Lea malloc ”

简单的堆和BSS溢出

堆由很多内存块组成,其中一些已经分配给程序,一些是空闲的,但是通常已分配的块在内存中是相邻的。

1. /*heap1.c – 最简单的堆溢出*/
2. #include
3. #include
4.
5. int main(int argc, char *argv[])
6. {
7.
8. char *input = malloc (20);
9. char *output = malloc (20);
10.
11. strcpy (output, "normal output");
12. strcpy (input, argv[1]);
13.
14. printf ("input at %p: %s\n", input, input);
15. printf ("output at %p: %s\n", output, output);
16.
17. printf("\n\n%s\n", output);
18.
19. }
[root@localhost]# ./heap1 hackshacksuselessdata
input at 0x8049728: hackshacksuselessdata
output at 0x8049740: normal output
normal output
[root@localhost]# ./heap1
hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
input     at 0x8049728:
hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks
output    at 0x8049740: hackshackshackshacks5hacks6hacks7
hackshacks5hackshacks6hackshacks7
[root@localhost]# ./heap1
"hackshacks1hackshacks2hackshacks3hackshacks4what have I done?"
input at 0x8049728: hackshacks1hackshacks2hackshacks3hackshacks4what
have I done?
output at 0x8049740: what have I done?
what have I done?
[root@localhost]#

因此,覆盖堆上的变量是非常简单的,也并不总会产生冲突。
类似的情况也可以在位于BSS段中的静态变量上发生。

腐烂C++中的函数指针

攻击这种类型的堆溢出的技巧是腐烂(corrupt)一个函数指针。存在大量的腐烂指针的方法。首先,可以试着以类似于前面例子的方式从另一个相邻的内存块中覆盖一个堆对象。类对象和结构通常存储在堆中,这样利用这种类型的机会就会增加。

在本例中,堆上实例化了两个类对象。类对象中的一个静态缓冲区被溢出,侵入到另一个相邻的类对象中。这种侵入覆盖了第二个类对象的虚函数列表指针(vtable指针) 。地址被覆盖,这样vtable地址指向缓冲区。然后可以把值放入自己的为类函数指示新地址的Trojan列表中。其中一个是破坏对象(destructor),用于覆盖,这样当类对象被删除时,新的破坏对象就可以被调用了。因此可以简单地通过将破坏对象指向负载(payload)而执行任何代码。其缺点是堆对象地址可能包含一个空字符,从而限制了其功能。要么必须把负载放在不需要空地址的地方,要么参考使EIP回到地址的技巧来弹出旧的堆栈。下面的例子示范了这种方法。

 // class_tres1.cpp : 定义了控制台应用程序的入口点

#include
#include
class test1
{
      public:
      char name[10];
      virtual ~test1();
      virtual void run();
};

16. class test2
17. {
18. public:
19.      char name[10];
20.      virtual ~test2();
21.      virtual void run();
22. };
23.
24.
25. int main(int argc, char* argv[])
26. {
27.      class test1 *t1 = new class test1;
28.      class test1 *t5 = new class test1;
29.      class test2 *t2 = new class test2;
30.      class test2 *t3 = new class test2;
31.
32.      //////////////////////////////////////
33.      // 覆盖t2的虚函数指针w/堆地址0x00301E54
34.      // 使破坏对象出现在0x77777777
35.      // 并且使run()函数出现在0x88888888
36.      // ////////////////////////////////////
37.
38.
39.
40.      strcpy(t3->name, "\x77\x77\x77\x77\x88\x88\x88\x88XX XXXXXXXXXX"\
41.      "XXXXXXXXXX XXXXXXXXXX XXXXXXXXXX XXXX\x54\x1E\x30\x00");
42.


43.      delete t1;
44.      delete t2; // 导致破坏对象0x77777777被调用
45.      delete t3;
46.
47.      return 0;
48. }
49.
50. void test1::run()
51. {
52. }
53.
54. test1::~test1()
55. {
56. }
57.
58.
59. void test2::run()
60. {
61.     puts("hey");
62. }
63.
64. test2::~test2()
65. {
66. }

堆对象间的近似允许覆盖一个相邻堆对象的虚函数指针。一旦被覆盖,攻击者就可以插入一个指向被控缓冲区的值,在那个缓冲区里攻击者可以建立一个新的虚函数表。新的列表可以使攻击者提供的代码在其中一个类函数被执行时运行。破坏对象是一个用于替代的有用函数,因为在对象从内存中被删除时,破坏对象就被执行了。

攻击

指针改写

从攻击者的角度来看,重要的事情就是这两个写内存的操作可以做些手脚。现在的目标就是操纵这些元数据,这样的话就可以控制写入的值,还可以控制在哪里写入。我们可以通过这个向任意内存写入任意值。重写函数的指针,最后让指针指向我们的代码。
不幸的是,上述介绍的方法在新版的glibc中不再适用。删除堆块的函数变得更健壮并且在运行时做了检查:前块的前驱指针必须指向当前块。类似的,后块的后驱指针必须指向当前块

更多技术

The House of Prime:在调用函数malloc后,要求两个释放的堆块包含攻击者可控的size字段。

The House of Mind:要求能够操作程序反复分配新的内存。

The House of Force:要求能够重写顶块,并且存在一个可控size的malloc 函数,最后还需要再调用另一个malloc函数。

The House of Lore:不适用于我们的例程。

The House of Spirit:假设攻击者可以控制一个释放块的指针,但是这个技术还不能使用。

The House of Chaos:实际上这不是一个技术,仅仅是文章的一部分而已。