Windows下堆保护机制原理及其绕过

前面我们介绍了很多Windows下的保护机制,我们知道Windows对于栈,做出了很多保护机制,那是因为栈上的溢出是很容易的,而对于存在于堆上的漏洞,通常利用起来都比较困难,那我们就来详细看看Windows对于堆做出了哪些保护:

一.堆保护机制详解

微软在堆中也增加了很多的安全校验操作,其中包括:

  1. PEB random:这也就是我们说的PEB机制随机化,我们在介绍ASLR保护机制的时候也提到过,PEB随机化后,就可以在一定程度上抵御攻击存储在PEB中的函数指针,回想一下,在我们DWORD SHOOT的时候,修改PEB中的函数指针,是不是相对来说比较简单?
  2. 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{
		//如果进入到了这里,说明链表不完整,进入异常
	}
}

这样,我们就可以在链表操作之前,验证是否发生溢出。

  1. heap cookie:
    还记得在栈上的Security Cookie吗?我们在堆中也可以使用Security Cookie,用来检测是否发生了堆溢出。
    我们知道在内存中的堆区,有堆首的存在,heap cookie就存储在堆首。
  2. 元数据加密
    微软在操作系统中,将块首中的一些重要数据保存的时候,会与一个4字节的随机数进行异或运算,在使用的时候再还原回来,这样我们就不可以直接破坏这些数据了。

二.堆保护机制绕过方式分析

即使Windows的保护机制有多么完善,总是会有人提出绕过方式的,我们就来看看对于堆的保护,我们如何进行突破:

  1. 不知道大家有没有发现一个问题,就是在堆保护机制中,仅仅是对堆块中的重要数据进行了保护,并没有对堆中存储的内容进行保护,那我们就可以去修改堆中保护的数据了。
  2. 利用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。

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shad0w-2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值