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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit

【Tools】大模型中的自注意力机制

摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样                      🎵 方芳《摇太阳》 自注意力机制(Self-Attention)是一种在Transformer等大模型中经常使用的注意力机制。该机制通过对输入序列中的每个元素计算与其他元素之间的相似性,

如何通俗理解注意力机制?

1、注意力机制(Attention Mechanism)是机器学习和深度学习中一种模拟人类注意力的方法,用于提高模型在处理大量信息时的效率和效果。通俗地理解,它就像是在一堆信息中找到最重要的部分,把注意力集中在这些关键点上,从而更好地完成任务。以下是几个简单的比喻来帮助理解注意力机制: 2、寻找重点:想象一下,你在阅读一篇文章的时候,有些段落特别重要,你会特别注意这些段落,反复阅读,而对其他部分

在 Windows 上部署 gitblit

在 Windows 上部署 gitblit 在 Windows 上部署 gitblit 缘起gitblit 是什么安装JDK部署 gitblit 下载 gitblit 并解压配置登录注册为 windows 服务 修改 installService.cmd 文件运行 installService.cmd运行 gitblitw.exe查看 services.msc 缘起