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

2024-04-09 16:04

本文主要是介绍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。

这篇关于Windows下堆保护机制原理及其绕过的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/888597

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

Go语言并发之通知退出机制的实现

《Go语言并发之通知退出机制的实现》本文主要介绍了Go语言并发之通知退出机制的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、通知退出机制1.1 进程/main函数退出1.2 通过channel退出1.3 通过cont

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理

在MySQL中实现冷热数据分离的方法及使用场景底层原理解析

《在MySQL中实现冷热数据分离的方法及使用场景底层原理解析》MySQL冷热数据分离通过分表/分区策略、数据归档和索引优化,将频繁访问的热数据与冷数据分开存储,提升查询效率并降低存储成本,适用于高并发... 目录实现冷热数据分离1. 分表策略2. 使用分区表3. 数据归档与迁移在mysql中实现冷热数据分

Java中的xxl-job调度器线程池工作机制

《Java中的xxl-job调度器线程池工作机制》xxl-job通过快慢线程池分离短时与长时任务,动态降级超时任务至慢池,结合异步触发和资源隔离机制,提升高频调度的性能与稳定性,支撑高并发场景下的可靠... 目录⚙️ 一、调度器线程池的核心设计 二、线程池的工作流程 三、线程池配置参数与优化 四、总结:线程

Windows环境下解决Matplotlib中文字体显示问题的详细教程

《Windows环境下解决Matplotlib中文字体显示问题的详细教程》本文详细介绍了在Windows下解决Matplotlib中文显示问题的方法,包括安装字体、更新缓存、配置文件设置及编码調整,并... 目录引言问题分析解决方案详解1. 检查系统已安装字体2. 手动添加中文字体(以SimHei为例)步骤

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语