本文主要是介绍Windows下堆保护机制原理及其绕过,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前面我们介绍了很多Windows下的保护机制,我们知道Windows对于栈,做出了很多保护机制,那是因为栈上的溢出是很容易的,而对于存在于堆上的漏洞,通常利用起来都比较困难,那我们就来详细看看Windows对于堆做出了哪些保护:
一.堆保护机制详解
微软在堆中也增加了很多的安全校验操作,其中包括:
- PEB random:这也就是我们说的PEB机制随机化,我们在介绍ASLR保护机制的时候也提到过,PEB随机化后,就可以在一定程度上抵御攻击存储在PEB中的函数指针,回想一下,在我们DWORD SHOOT的时候,修改PEB中的函数指针,是不是相对来说比较简单?
- Safe Unlink:我们知道堆溢出的原理,是因为在双向链表进行拆卸或者合并的时候,不安全的指针修改,具体就像下面这样:
int remove(ListNode* node){node->blink->flink = node->flink;node->flink->blink = node->blink;
}
如果大家学过数据结构,并且自己写过代码,相信这段代码对你来说很熟悉吧?因为我们也经常写出这样的代码。
那我们要如何修改这段代码,让其变得安全呢?我们可以在链表操作之前,验证前向指针和后向指针的完整性,这样可以方式发生DWORD SHOOT,就像这样:
int safe_remove(ListNode* Node){if((node->blink->flink == node) && (node->flink->blink == node)){//在这里完成链表的操作,和上面的差不多}else{//如果进入到了这里,说明链表不完整,进入异常}
}
这样,我们就可以在链表操作之前,验证是否发生溢出。
- heap cookie:
还记得在栈上的Security Cookie吗?我们在堆中也可以使用Security Cookie,用来检测是否发生了堆溢出。
我们知道在内存中的堆区,有堆首的存在,heap cookie就存储在堆首。 - 元数据加密
微软在操作系统中,将块首中的一些重要数据保存的时候,会与一个4字节的随机数进行异或运算,在使用的时候再还原回来,这样我们就不可以直接破坏这些数据了。
二.堆保护机制绕过方式分析
即使Windows的保护机制有多么完善,总是会有人提出绕过方式的,我们就来看看对于堆的保护,我们如何进行突破:
- 不知道大家有没有发现一个问题,就是在堆保护机制中,仅仅是对堆块中的重要数据进行了保护,并没有对堆中存储的内容进行保护,那我们就可以去修改堆中保护的数据了。
- 利用chunk重新设置堆块大小:
我们前面介绍过了,Safe Unlink的精髓就在于:从链表中拆卸的时候,对指针进行完整性验证,但是这里有一个很大的问题,就是在链表拆卸的时候,它会检测,在链表插入的时候,它又不检测了,那就给我们了很好的攻击机会。
使用这种方式攻击的话,我们就要知道在什么时候,链表才会发生插入操作:
<1>内存释放后,chunk不再被使用的时候会重新链入链表。
<2>当chunk的内存空间大于申请的空间的时候,剩余的空间,就会被拆分,建立成一个新的chunk,链入表中。
这里的第二种方法,给了我们攻击机会。
三.堆保护机制绕过详解
1.利用chunk重设大小进行攻击
我们大家都知道从FreeList[0]上申请空间的过程:
<1>.将FreeList[0]上最后一个chunk的大小与申请的大小进行比较,如果大,则继续分配,如果小,就会拓展空间
<2>.从FreeList[0]的第一个chunk开始,进行检测,知道找到符合要求的chunk,然后将这个chunk拆卸下来
<3>.分配好空间后,如果chunk还有剩余空间,剩余的空间就会被建立成一个新的chunk,并且插入到链表中
我们来细细分析一下这三个步骤:第一步,我们貌似没有攻击机会、第二步,如果我们覆盖掉Safe Unlink,第二步就会被检测出来,第三步就更不用说了
这样看来,我们似乎真的没有攻击机会了。但是Safe Unlink中存在一个让人疑惑的问题:就算Safe Unlink检测到chunk被破坏了,但是它还允许后续的一些操作执行,例如重设chunk的大小。
我们写一段程序,来看看重设chunk的具体过程:
#include <stdio.h>
#include <windows.h>int main(){HLOCAL h1;HANDLE hp;hp = HeapCreate(0,0x1000,0x10000);__asm{int 3;}h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,0x10);return 0;
}
我们将我们的调试器设置为默认调试器:
然后直接运行程序,程序发生异常的时候,会自动被附加到调试器。
然后我们来看看插入chunk的精髓汇编代码:
lea eax,dword ptr ds:[edi+8] ;获取新chunk的Flink位置
mov dword ptr ss:[ebp - f0]
mov edx,dword ptr ds:[ecx + 4] ;获取下一个chunk中的Blink的值
mov dword ptr ss:[ebp - f8],edx
mov dword ptr ds:[eax],ecx ;保存新的chunk的Flink
mov dword ptr ds:[eax + 4],edx ;保存新的chunk的Blink
mov dword ptr ds:[edx],eax ;保存下一chunk中的Blink->Flink的Flink
mov dword ptr ds:[ecx + 4],eax ;保存下一chunk中的Blink
将这一过程,使用伪代码形式,就是这样:
新chunk -> Flink = 旧chunk -> Flink
新chunk -> Blink = 旧chunk -> Flink -> Blink
旧chunk -> Flink -> Blink = 新chunk
旧chunk -> Flink -> Blink = 新chunk
执行完上面的汇编代码之后,看看内存中FreeList[0]的链表结构,就会发现已经改变了。
这样,了解了重设chunk插入链表之后,我们该如何攻击呢?大家考虑一下,如果说,我们将旧chunk的Flink和Blink都覆盖掉了,那么会发生什么情况???
实际上,相信大家已经发现了,这实际上就是DWORD SHOOT,而Safe Unlik的验证不严密为这样的攻击打开了一扇大门。
这里写出一个带有漏洞的程序:
#include <stdio.h>
#include <windows.h>int main(){char ShellCode[500]{0};HANDLE hFile = CreateFileA("111.txt",GENERIC_READ,NULL, NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);DWORD dwReadSize = 0;ReadFile(hFile, ShellCode, 300, &dwReadSize, NULL);HLOACL h1 ,h2;HANDLE Handle;Handle = HeapCreate(0,0x1000,0x10000);__asm{int 3;}h1 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);memcpy(h1,ShellCode,300);h2 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);int zero = 0;zero = 5/zero;return 0;
}
我们来观察一下这个程序:主函数中,存在明显的堆溢出,我们可以利用这个漏洞来完成攻击。
这里给出攻击思路:
<1>.堆刚初始化,所以我们申请的堆是从FreeList[0]中申请的,从FreeList[0]中拆卸下来的chunk在分配好后将剩余的空间新建一个chunk插入到FreeList[0]中,这样的话,h1后面就会有一大段空闲的空间
<2>.向h1中复制数据之后,超过16字节空间就会覆盖后面的chunk块首
<3>.后面的chunk块首被覆盖了,当h2再申请空间的时候,程序就会从破坏了的chunk中申请空间,并将剩余的空间新建为一个chunk并插入到FreeList[0]中
那我们将chunk的Flink和Blink伪造一下,在新的chunk中插入FreeList[0]的时候,,就可以实现任意地址写了
最后,程序制造出了除零异常,我们使用跳板,劫持流程,让程序转入ShellCode执行。
我们的chunk这样布置:
| 0x90填充 | chunk块首前8个字节 | 覆盖用的Flink和Blink | 0x90填充 | 跳板指令 | 伪造的块首 | 伪造的Flink和Blink | ShellCode |
再次打开程序,发现程序执行流程已经被我们成功劫持。
如果这里的介绍大家看的不是很懂的话,可以参考一下这篇文章:利用Chunk重设大小攻击堆
2.利用Lookaside表进行攻击
我们知道Safe Unlink只对堆表中的空表进行了双向链表的验证,但是没有对块表中的单链表进行验证,那我们就可以去攻击单链表。
借鉴前面的任意地址写固定地址的思路,如果控制单链表操作中的node->next
,我们就可以控制Lookaside[n] - > next
了,当用户再次申请空间的时候,系统就会将这个伪造的地址当作申请空间的地址返回,我们只要向该内存空间写入数据,就会留下溢出隐患。
我们来写一个带有漏洞的程序:
#include <stdio.h>
#include <windows.h>char ShellCode[300];int main(){HLOCAL h1,h2,h3;HANDLE Handle;Handle = HeapCreate(0,0,0);__asm{int 3;}h1 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);h2 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);h3 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);HeapFree(Handle,0,h3);HeapFree(Handle,0,h2);memcpy(h1,ShellCode,300);h2 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);h3 = HeapAlloc(Handle,HEAP_ZERO_MEMORY,16);memcpy(h3,"\x90\x1E\x39\X00",4);int zero = 0;zero = 5/zero;return 0;
}
我们来看看这个程序,程序中存在明显的堆溢出,首先申请3块16字节的空间,然后将其释放,这样它就存在于块表中了,这样我们下一次申请的时候就可以从块表中分配了。通过向h1中复制内存,就可以完成溢出,覆盖掉h2块首中,下一块的指针。我们申请空间的时候,下一块地址就会被赋值给Lookaside[2] -> next
,当我们再次申请空间的时候,系统就会将我们伪造的地址作为内存首地址返回。
这里给出我的Payload布置方法:
| 短跳转指令 | \x90填充 | 模拟块首 | 默认异常处理指针 | \x90填充 | ShellCode |
这里由于在堆上,不同计算机的地址差异较大,这里就不再做演示了。如果这里看不懂的话,可以参考这篇文章:重重保护下的堆
然后我们就会发现我们成功绕过了保护,并且成功执行了ShellCode。
这篇关于Windows下堆保护机制原理及其绕过的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!