x86 内存管理(一)虚拟地址空间布局-ASLR-VAD
本节开始研究 X86 内存管理,按以下路线进行研究:
- 内存的页式管理、段式管理。
- 进程内存管理(内存管理-用户空间)
- 系统内存管理(内存管理-系统空间)
- 内存页面交换(物理内存-磁盘文件)
- 物理内存管理
- 页面异常处理
- 共享内存区
- 工作集管理
主要参考:《Windows内核原理与实现》、《Windows内核情景分析》、《深入解析Windows操作系统第6,7版-上册》。
《深入解析windows操作系统第6版下册》第10章:内存管理(第二部分)。
x64研究:
- x64汇编基础、调用约定等。
- 段页机制。
- 内存管理。
- 对象管理、I/O管理。
- 进程管理、句柄表、ju’bi线程管理。
- 系统调用、Wow64。
- 异常处理。
《深入解析Windows Kenrel》、Windows 7 x64 虚拟内存管理、《深入解析windows操作系统第6版下册》。
1 内存基本概念
计算机的指令可以直接操纵内存单元中的数据,所以,内存管理是操作系统中除了进程和线程管理以外最为重要的一部分。
内存(memory)是指处理器可以直接访问,但位于处理器之外的存储器。在硬件上,处理器通过一组地址线连接到这些存储器上,这组地址线构成了内存总线。处理器最终需要的是一个物理地址。
- 虚拟地址(有时候称为线性地址)。在 32 位系统上,虚拟地址空间可以达到 4 GB 大小,也就是说,整个空间可以有 $2^{32}=4 294 967 296$ 个字节单元。Intel x86 芯片内有专门的电路(MMU)负责把一个虚拟地址转译成物理地址。页式管理使用线性地址。
- 逻辑地址。逻辑地址包含两部分:段(scgment)和偏移(offset)。段部分指定了在整个地址空间中的一个基地址以及段空间的大小,当然还有段的一些其他属性。与寻址相关的是段的基址和大小。偏移部分指定了一个逻辑地址相对于段基址的偏移量。此偏移量不能超过段的边界。因此,逻辑地址的实际地址是段基址加上偏移量。Intel x86 芯片也有专门的电路把逻辑地址转译成一个虚拟地址或物理地址。段式管理使用逻辑地址。
- 物理地址。即内存存储器的索引,处理器操纵内存芯片时,通过地址线管脚加上电信号来读或写相应的内存单元。在 Intel x86 体系结构上,物理地址是一个32 位或 36位的无符号整数。
如下图,除了 FS
寄存器指向的段基址不为 0
,其他段的 虚拟地址 == 逻辑地址,这种相当于把段机制屏蔽了,成为地址空间平坦化,FS
指向寄存器控制区 KPCR
。
1.1 MMU
将虚拟地址、逻辑地址映射为物理地址的电路叫做存储管理单元(MMU,Memory Management Unit),集成在 CPU 中,X86 的保护模式实际上需要有 MMU 才能实现。
将虚拟地址、逻辑地址映射为物理地址是以页大小为单位的,不是以字节为单位。X86 包括 X64 页大小一般都是 4KB(PTE低12位)。
Windows 和 Linux 都选择了页式内存管理作为主要的内存管理手段,但同时也不可避免地涉及了段机制,x64 使用平坦模式后段式管理就不再使用了。
如下图可以看到,段式管理的逻辑地址需要转换成线性地址,最后才转换成物理地址:
Windows采用页式内存管理方案,在Intel x86处理器上,Windows不适用段来管理虚拟内存,但是,Intel x86处理器在访问内存时必须要通过段描述符,这意味着Windows将所有的段描述符都构造成了从基地址0开始,且段的大小根据段的用户和系统设置的不同,会设置为0x80000000,0xC0000000或0xFFFFFFFF。所以,Windows系统中的代码,包括操作系统本身的代码和应用程序的代码,所面对的地址空间都是线性地址空间。这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。
使用 MMU 的两个重要意义:
- 解决不同进程相同虚拟地址被映射到不同物理页的问题,使每个进程空间得到隔离。
- 解决保护内核的问题。对应于 CPU 内核态与用户态,将地址空间划分为用户空间和内核空间,使得 CPU 运行于用户态时不能访问内核态地址。
TLB
MMU 实际上就是根据 PDE、PTE对应的索引映射到物理内存页。当程序访问一个线性地址,需要先查PDPT,然后查PDT,然后查页表PTT,最后才是访问物理页。这期间多次访问内存,效率非常低。于是TLB就被设计出来了,当然一个内存页有没有 TLB 缓存,由 PDE_G | PTE_G
决定,可看《Windows XP 页保护(三)-TLB》。(64位CPU分页及TLB请点这儿)。
每当需要使用一个页面映射表项时,MMU 首先在 TLB中寻找,找到就不需要访问物理内存了,找不到才从物理内存装入所需的表项。一般而言,一个进程在运行了一会儿以后,其TLB 的命中率还是很高的。关于 TLB 进一步信息可看《Windows内核原理与实现4.1.1页式内存管理》。
综上所述,在 CPU 的页面映射机制中,MMU 的作用是:
- 根据虚拟地址计算出该地址所属的页面。
- 再根据页面映射表的起始地址计算出该页面映射表项所在的物理地址。
- 根据物理地址在高速缓存的 TLB 中寻找该表项的内容。
- 如果该表项不在 TLB 中,就从内存将其内容装载到 TLB中。
- 检查该表项的
PA_PRESENT
标志位(PDE、PTE最低位),如果为 1 就表示映射的目标为某个物理页面,因而可
以访问这个页面,但是需要进一步检查、比较 CPU 当前对此页面是否具有所要求的访问权限,如果权限不够就使当前指令的执行失败并产生一次页面异常。 - 如果
PA_PRESENT
标志位为 0,则说明该虚存页面的映像不在内存中,当前指令的执行因此而失败,CPU 为此产生一次页面异常此时相应的异常处理程序应采取相应的措施。
《TLB内存隐藏》
1.2 虚拟内存与物理内存交换
同一个物理内存页面在不同的时间可被用于不同进程的不同虚存页面。而暂时不会被用到的虚存页面的内容,则可以被存储在磁盘上(就 Windows 而言,实际上是“倒换文件”中),磁盘介质的存储成本比物理内存要小得多。
在现代操作系统中,一般把不紧急的进程中的数据或代码先存放到外存(通常是硬盘)中,从而把它们占用的物理内存腾出来给紧念的进程使用,或者交给系统使用。以后,当内存紫缺的状况缓解时,系统再把外存中的进程数据或代码装回到已经空闲下来的内存单元中,从而使这些进程有机会继续运行。这两个过程称为内存换出和换入。几乎所有的多进程操作系统都支持这种内存管理。
这样,在不同的时间内,根据不同的需要,虚存页面的内容可以在物理内存和磁盘之间倒换(Swap)需要时就把一个虚存页面的内容从磁盘倒入物理内存,暂时不需要的时候就将其倒出到磁盘上。当然,页面的倒入、倒出需要耗费时间,但是适度地以时间换空间在经济上是有利的。
关于 X86 段页管理:
- Windows XP 段保护(一)
- Windows XP 段保护(二)
- Windows XP 段保护(三)
- Windows XP 页保护(一)
- Windows XP 页保护(二)
- Windows XP 页保护(三)
2 x86 地址空间布局
Windows 采用了按需分配的策略,也就是说,只有当一段虚拟地址空间真正被使用的时候,系统才会为它分配页表和物理页面。每个进程的虚拟地址空间的分配情况通过一组虚拟地址描述符(VAD)记录下来,这些描述符构成了有一颗平衡二叉树,以便于快速地定位到一个指定虚拟地址地描述符上。
在Windows中,有三种类型的数据被映射到虚拟地址空间中(0~0xFFFFFFFF
):
- 每个进程私有的代码和数据(
0~0x7FFFeFFF
,0x7FFF0000~ 0x7FFFFFFF
为用户与内核模式都不可访问的 64KB 区域):进程隔离的地址范围,每个进程都有它自己的页表集合,这些页表被保存在只有从内核模式才能访问的页面中,因此一个进程的用户模式线程不能修改它们自己的地址空间布局。 - 会话范围内的代码和数据(
0x80000000~0xC0000000
):在系统地址空间中,有些部分提供一些特殊模块使用,比如会话空间是由会话管理器和Windows子系统使用的。 - 以及系统范围内的代码和数据(
0x80000000~0xFFFFFFFF
):高 2 G 的代码和数据。
在Windows的 32 位版本上,每个用户进程默认可以拥有多达2GB私有地址空间,操作系统使用高 2GB 空间。然而,系统可以被配置使用 BCD 启动选项 increaseuserva
,以便允许用户地址空间多达3GB(PE 文件头必须包含 IMAGE_FILE_LARGE_ADDRESS_AWARE
)。这两种可能的地址空间布局结构如图10.8所示。
Windows 7 32 位:
Windows 10 32 位:
对于用户空间 3 GB的程序,PE 文件头必须包含 IMAGE_FILE_LARGE_ADDRESS_AWARE
。构建相应可执行文件时,指定 /LARGEADDRESSAWARE
链接器标志(在VS中:项目属性-链接器-系统-启用大地址),配置应用程序使用 3 GB 的地址空间。当在一个配置成使用 2 GB 用户地址空间的系统上运行该应用程序时,此链接器标志将不起作用。
可以参考《深入解析Windows操作系统第7版5.5》x86地址空间布局或《《深入解析windows操作系统第6版下册》第10章:内存管理(第二部分)》。
2.1 系统地址空间布局
在 Intel x86 处理器的 Windows 系统中,0x80000000-0xFFFFFFFF
是所有进程共享的系统地址空间。在这段地址空间中,其布局结构是在内核初始化阶段完成的。
32 位 Windows 的各个版本都利用一个虚拟地址分配器,实现了动态的系统地址空间布局结构。仍然有一些指定的区域被保留下来,然而,很多内核模式结构使用了动态的地址空间分配方式。因此,这些结构的虚拟地址互相之间不一定是连续的。每个结构都可以存在于系统地址空间的各个区域中,多个不连续的内存碎片里。使用这种动态方式来分配系统地址空间的场合包括以下这些:
- 非换页内存池
- 换页内存池
- 特殊内存池
- 系统页表项(PTE)
- 系统映射的视图
- 文件系统缓存
- PFN 数据库
- 会话空间
Windows XP 32 位系统地址空间布局:
📢⚠️注意:以下列出的固定地址仅针对 Windows XP,在 Windows Vista 以后的系统中,地址都是动态映射的,每次系统启动时映射的位置都不相同。
一、会话空间
对于有多个会话的系统而言,每个会话(Session0、Session1...
)独有的代码与数据被映射到系统地址空间,但是被属于该会话中的所有进程共享。
在图中,会话映像文件区包括 win32k.sys
、视频驱动程序以及一些打印驱动程序的映像文件;会话内存池是指属于会话空间的换页内存池。
Windows XP 会话空间:
Win10 32 位:
二、PDE、PTE
第一项 PTE 从 0xC0000000
开始, PDE
属于 PTE
中的一部分。
- 10-10-12:页表
0xC0000000~0xC0400000
,PDE = 0xC0300000
。 - 2-9-9-12:页表
0xC0000000~0xC0800000
,PDE = 0xC0600000
。
三、系统缓存
1 | (MmSystemCacheStart)0xC1000000~0xE1000000(MM_SYSTEM_CACHE_END) |
超空间:Windows 内核有时候需要将某些物理页面临时映射到内核的虚存区间,用做“草稿”或其他临时的用途。为此,Windows内核在系统空间划出了一块(虚存)区间专门用于这样的临时映射,称 为“超级空间(Hyperspace)”。《Windows内核情景分析3.1.4》
2.2 用户地址空间布局
内核的地址空间是动态的,类似地,用户地址空间也是动态地被构建的——线程栈、进程堆和已加载镜像的地址(例如DLL和应用程序的可执行文件)都是通过一种称为地址空间布局随机化(Address Space Layout Randomization, ASLR) 的机制,动态地计算出来的(如果应用程序和它的各个映像支持这一特性的话)。
在操作系统层面上,用户地址空间被划分为几个有明确定义的内存区域。可执行文件和DLL都为内存映射文件,随后跟着的是进程的堆和其中各个线程的栈。除了这些区域(和一些保留的系统结构,如TEB和PEB)之外,所有其它的内存分配都依赖于运行时(动态)生成。ASLR 会处理所有这些依赖于运行时的区域的位置,它与DEP相结合,一起提供了一种机制,让通过远程操纵内存来挖掘系统的企图变得更难成功。由于 Windows 的代码和数据都被放在动态的位置了,一个攻击者通常就无法在一个程序或系统提供的DLL中硬编码出一个有意义的偏移量。
这里指的就是远程执行代码的缓冲区溢出攻击,例如使用 metasploit 通过网络发送 shellcode 来取得一个从受害机器到攻击主机的反弹 shell,而且还具备管理员,或者 root 权限。这是由于,被攻击的有缺陷的应用程序同样是以这些特权级别运行的。
实际上系统将所有进程加载的相同 DLL(非第三方)都映射到相同的物理内存页,也就是仅维护一个 DLL 副本,然后在所有进程之间共享——通过共享文件映射的方式。
本质上 VMMap 的Type 中 image
类型段中的所有系统 DLL 与下文提及的 shareable
类型段,Mapped File
类型段一样,都是基于共享文件映射来实现共享。
软件 VMMap 中将用户空间的内存分为很多类型:
- Shareable:可共享的,显示了标记为可共享的内存分配,通常包含共享的内存(但不包括内存映射文件,它们属于“映像”或“映射文件”)
以下主要解释一下重要的几个列:
- Total WS:驻留在物理内存中的字节数,如 $38138KB/4KB = 9534$个页面($4KB = 4*2^{10}Bytes = 2^{12}Bytes= 4096Bytes$)。
- Commited:实际已使用的内存大小,已提交的。
Commited - Total WS = 被换出到磁盘的页面大小
,会在必要的时刻由缺页异常处理程序,或者其它的内核例程将其换入物理内存。 - Shareable WS:可共享的。
- Shared WS:已共享的。“可共享”和“已共享”的页面通常包含程序运行时依赖的动态链接库——多数属于 Windows 子系统 DLL 。“私有”的页面通常包含程序的可执行 PE 文件映射的内存。
具体举例可参考《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分)。
2.3 ASLR
ASLR(Address space layout randomization),地址空间布局随机化。ASLR的概念在WindowsXP时代提出,到Windows Vista才完整实现。
ASLR 包含了映像随机化、堆栈随机化、PEB 与 TEB 随机化。
2.3.1 映像随机化
EXE 和 DLL 随机化映射方式不一样,参见《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分。
映像随机化机制需要编译器及系统同时支持(SafeSEH也是)。
注意:比如在 OllyDbg 中,每次重启电脑后,随机化的地址才和上一次不一样,且地址最低16位不会被随机化,保持不变。
现象:
- 每次重启电脑,EXE都会被加载到不同的基地址。【实测在Windows 10 21H2上,PE和系统都开启强制ASLR情况下。EXE加载的DLL几乎每次重新运行EXE时都会重定位,但是32位EXE倒是看到几次重定位,64位EXE不关机的话一直没看到重定位发生。】
- 32位程序在关闭
/dynamicbase链接
后还有.reloc
区段,64位相同条件下没有该区段(只有在开启随机地址后编译的64位程序才有该区段)。 - Windows 10虽然有强制ASLR功能,但是仅对PE文件中含有
.reloc
区段的程序有用。
- 如果EXE中的两个函数位于地址0x401000和0x401100,即使在镜像重新定位后,它们也将保持0x100字节的距离。显然,由于x86代码中相对调用和jmp指令的普遍性,这一点很重要。同样,0x401000的函数将从镜像的基本地址保持0x1000字节,无论它在哪里。
- 同样,如果镜像中相邻两个静态或全局变量,则在应用ASLR后,它们将保持相邻。
- 相反,堆栈和堆栈变量以及内存映射文件不是镜像的一部分,可以随意随机化,而无需考虑选择的基本地址。
编译时开启 ALSR 的 PE 文件有以下特征:
- 开启 ASLR 后的 PE 文件
Optional Header DllCharacteristics
多一个IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(0x40)
属性。 - 开启 ASLR 后的 PE 文件后 Release版本 PE 文件
Section Headers
多一个.reloc
节(Debug PE自带)。 - 未开启 ASLR 的
File Header Characteristics
会多一个IMAGE_FILE_RELOCS_STRIPPED(0x1)
的属性,开启 ASLR 后不会有这个属性。(该标志表示重定位信息已从文件中删除,该文件必须以其首选的基地址加载。如果基址不可用,则加载程序报告错误。)
以下提供两种方法来去掉 PE 文件的 ASLR 属性(使用010edit工具):
- 在 PE 文件头的
Characteristics
属性中加上IMAGE_FILE_RELOCS_STRIPPED(0x1)
(原属性使用异或组装的),同时将可选文件头DllCharacteristics
的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(0x40)
属性抹掉。 - 抹掉
.reloc
数据:- 将
Section Headers
的.reloc
数据全部清零。(不是删除,否则数据会发生偏移,PE文件就错乱了) - 根据节表头信息(位置和大小),找到
.reloc
数据所在位置,将数据全部清零。 - 修改
_IMAGE_FILE_HEADER.NumberOfSections -= 1
。 - 修改
_IMAGE_OPTIONAL_HEADER.SizeOfImage -= 对齐的页大小
。(如果.reloc大小是600h,而该PE文件SectionAlignment节在内存中的最小单位 (对齐单位) 一般为: 1000h, 1000h > 600h。以1000h对齐,所以我们直接将SizeOfImage减去1000h即可)
- 将
参考ASLR学习总结。
开启 ASLR:
VS2019编译器设置(默认开启):项目属性-链接器-高级-随机地址。
Windows 10 两种设置方法:
方法一:在注册表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session ManagerMemory\
下新建一个DWORD
键值对MoveImages = 0
(该键值默认不存在)。需重启后生效。MoveImages == 0
:禁用映像随机化,总是按照 PE 文件头中指定的基址来加载。MoveImages == 0xFFFFFFFF(-1)
:总是强制随机化有.reloc
节的PE文件。其他值:随机化开启ASLR并且有
.reloc
的PE文件。
方法二:在 Windows 10 的Windows安全中心-应用和浏览器控制-Exploit Protection-强制映像随机化(强制性 ASLR),默认是关闭的。
对于可执行文件 EXE,通过计算它每次加载时的 Delta 值(差值或增量)来得出加载偏移量(load Offset),它是介于 0x10000 到 0xFE0000 之间的 8 位的伪随机数(Delta取值都在 16 MB 范围内),加载偏移量具体的计算方法是(Windows 7):
- 取得当前处理器的时间戳计数器(TSC),将其右移 4 位;
- 执行一次除以 254 的求模运算后再加 1;
- 将得出的数字乘以 64 KB 的分配粒度。
说明:
- 这个8位伪随机数取值范围是
1~0xFE
,共256个数值,地址随机化的分配粒度以64KB(0x10000)为单位。 - 随机化后的加载基址 = PEFileHeader.ImageBase ± Delta。
对于32位系统,事实上,Windows只尝试随机化32位地址的8位。这些是第16位至第23位,仅影响地址的页面目录条目和页表条目部分(PTE)。因此,在暴力的情况下,攻击者枚举256个值会猜测中EXE的基本地址。
当将ASLR应用于64位二进制文件时,Windows能够随机化17-19位的地址(取决于它是DLL还是EXE)。图2显示了64位代码的可能基本地址数量,以及相应地所需的蛮力猜测数量如何大幅增加。这可以允许端点保护软件或系统管理员在攻击成功之前检测到攻击。
在暴力攻击中,ASLR使攻击64位程序的强度至少是攻击完全相同程序的32位版本的512倍。
对于系统DLL,使用另外的计算方法来求加载基址偏移量。每次系统启动引导过程中,会产生一个系统宽度的大小的值,称为
image bias
。该值由函数MiInitializeRelocations
计算并返回得到,保存在内核全局变量MiImageBias
中(Windows 7-8.x)–MiState.Sections.ImageBias
成员中(Windows 10 1507-1809,从1903开始扩充了非常多成员ImageBiasNative
)这里的MiState
是_MI_SYSTEM_INFORMATION
结构参考Windows 10 | 2016 1809 Redstone 5 (October Update)。该值在系统启动期间调用MiInitializeRelocations
返回的当前 CPU 的时间戳计数器。然后同样的将其移位,屏蔽成一个 8 位的值,这就提供了 256 种可能的取值。( 加载偏移量MiImageBias
的取值从0x1~0xFF
)
与可执行文件不同的是,每次启动时只计算一次这个加载偏移量,并且被整个系统共享,从而允许系统 DLL 在物理内存中保持共享,或者仅重定位一次。对于后者,假设系统 DLL 被重新映射到不同进程内部的不同区域,那么系统 DLL 中的代码就无法被共享。映像加载器将不得不为每个进程修正地址引用的差别,从而将原本可共享的只读代码转变为进程私有的数据。在这种情况下,每个进程加载的这个系统 DLL 都会在物理内存中有多份。(这里指的是系统DLL,用户DLL的重定位方法同EXE)一旦偏移量Delta被计算出来后,内存管理器会初始化一个叫作
MilmageBitMap
的位图。该位图被用来表示从0x50000000
到0x78000000
的范围,每个位代表一个分配单位(64KB)。每当内存管理器加载一个DLL时,相应的位就会被设置,以便标记出它在系统中的位置;当同一个系统DLL被再次加载时,内存管理器会共享它的内存区(Section)对象,此对象包含有经过重定位的信息。简单地讲,位图MiImageBitMap
就是用来描述0x78000000~0x50000000
这片地址空间使用情况的内核变量。当每个DLL被加载时,系统将
MilmageBias
作为索引,会自顶部往底部扫描位图来查找空闲位。由于在首个 DLL (Ntdll.dll
)加载时,MiImageBitMap
将完全是空的,Ntdll.dll
的加载地址可以容易地计算得出:0x78000000 – (MiImageBias * 0x10000) 。可以每个后续DLL将会在下一个64KB段被加载。正因为如此,如果ntdll
的地址被获知,其他DLL的地址就容易被计算出来。
为了缓解这种可能性,当Smss加载时,会话管理器在初始化过程中对己知DLL的映射顺序也会随机化。(Smss.exe 是在执行体初始化过程的最后阶段,由 System 进程创建第一个用户模式进程,smss.exe 本身仅使用 Windows 原生系统调用服务——仅导入了 ntdll.dll 中的本机 API 用户模式代理,因此它加载的 DLL 只有少数几个)。最后,假设
MiImageBitMap
位图中没有可用的空闲空间(这意味着为 ASLR 而定义的绝大多数区域都被使用了),系统DLL 重定位将会使用EXE重定位的方法。
这样一来,每个 DLL 就不一定在地址空间中都间隔 64 KB,但会是 64 KB 的整数倍。
Windows7-Windows8.x:MilmageBitMap。
Windows 10 1507-1809:_MI_SECTION_STATE.ImageBitMap
Windows 10 1903-21H2:_MI_SECTION_STATE.ImageBitMapNative
在Windows 10中的计算方法:
- 32 bit:$0x78000000 - (ImageBias + NtDllSizein64KBChunks) * 0x10000$
- 64 bit:$0x7FFF’FFFF0000-(ImageBias64High + NtDllSizein64KBChunks) * 0x10000$
这里NtDllSizein64KBChunks
计算方法:假设 ntdll.dll
文件总大小为1547KB,则1547/64=24.17
,向上取整得25 = 0x19
。则NtDllSizein64KBChunks = 0x19
(表示25个64KB大小)。Windbg双机调试时?nt!mistate
可查看ImageBias大小。0x78000000-(0x6E + 0x19) * 0x10000 = 0x77790000
。
以下是Windows 10 1809 x64结构:
1 | //0x3180 bytes (sizeof) |
以下列出几个关键点:
随机数
ImageBias
在启动阶段,在MiInitializeRelocations
函数获取。取值从 0x1~0xFF。Windows 8以前,获取CPU时间戳使用
rdtsc
指令,在 x86 体系结构上,rdtsc
指令会把一个64位的值写入EDX : EAX
寄存器组。由于该值可预测,在 Windows 8 及以后系统中,使用了更安全的 Intel CPU 指令rdrand
。关于EXE获取Delta:
Windows 7:
- 取得当前处理器的时间戳计数器(TSC),将其右移 4 位;
- 执行一次除以 254 的求模运算后再加 1;
- 将得出的数字乘以 64 KB 的分配粒度。
Windows Vista:
1
2
3
4
5
6TSCStart = ReadTimeStampCounter(); //rdtsc把一个64位的值写入EDX:EAX寄存器组。
Delta = (ULONG)((TSCStart & 0xFF)*0x10000); //随机的时间戳值和255按位与运算
if (Delta == 0)
{
Delta = 0x10000; //对应于Windows 7的模254再加1,让Delta != 0
}
获取随机化地址的函数是
MiSelectImageBase
。MiImageBitMap 的大小为 0x2800 字节(10240 字节),因此它能够表示的部分地址空间大小为 0x28000000 字节(0x2800 * 0x10000),也就是从 0x78000000 到 0x50000000 这一部分。
由于64位暂时还没好好研究,这部分内容先暂时放一下,以后将会单独研究一个模块从loadLibray
开始一直到选择加载基址MiSelectImageBase
进行分析。
这部分内容可以参考:
Windows 7、Vista:《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分)
Windows 8:Windows 8 ASLR Internals(Windows 8 地址空间布局随机化揭秘)
Windows 10:Windows Internals 7 Part1
2.3.2 栈、堆随机化
这项措施是在程序运行时随机的选择堆栈的基址,与映像基址随机化不同的是堆栈的基址不是在系统启动的时候确定的,而是在打开程序的时候确定的,也就是说同一个程序任意两次运行时的堆栈基址都是不同的,进而各变量在内存中的位置也就是不确定的。
一、线程栈的随机化计算方法:
ASLR的下一步是随机化初始线程(和后续的每个新线程)的栈。这种随机化是默认启用的,除非在进程上启用了StackRandomizationDisabled
标志。
操作系统给每个线程预留4MB(32个64KB)或8MB(32个256KB)的空间,线程堆栈随机化的方法是从栈的32个64KB或256KB分隔的可能位置中选择一个。
基地址的选择方法是:
- 根据
x
计算初始基址:找到第一个合适的空闲内存区域, 然后选择第x
个可用区域。其中x
是一个根据当前处理器的TSC移位并掩码为5
位来生成的数 (有32个可能位置)。 - 计算偏移量:这个基地址被选择后,将再次从TSC出发计算出一个值,这个值的长度是9位。然后,这个值被乘以4来保持对齐(左移2位,最低2位为
0
即为4字节对齐),这意味着它最大可以是2048字节(半个页)。 - 栈的基址 = 初始基址+偏移量:最后,把基地址加上它,得到最终的栈的基地址。
二、堆的随机化
Windows internals 7中简单提了:当初始的进程堆(和后续的堆)在用户模式下被创建出来时,ASLR会随机化它们 的位置。RtlCreateHleap
函数使用另一个TSC导出的伪随机值来确定堆的基地址。这个值是5位的,它会被乘以64KB来生成最终的基地址,从0开始,从而,对于初始化堆来说,可能的范 围将是0x00000000
到0x001F0000
。此外,堆的基地址前面的范围被特意取消分配(0-64KB NULL区),以使得当攻击者以暴力扫描整个可能的堆地址范围时,强迫它产生访问违例。
Windows有关堆的代码是操作系统中最重要的一部分代码,在Window7中,它用LFH(Low Fragmentation Heap)取代了之前Windows XP版本中的lookaside。关于介绍存在于Windows8之后Windows中的一个已知堆分配问题:《关于Windows漏洞利用堆风水的思考》–Deterministic_LFH。
2.3.3 TEB 和 PEB 随机化
微软在 XP SP2 之后不再使用固定的 PEB 基址 0x7FFDF000
和 TEB 基址 0x7FFDE000
,而是使用具有一定随机性的基址,这就增加了攻击 PEB 中的函数指针的难度。
获取当前进程的 TEB 和 PEB很简单,TEB 存放在FS:0
和 FS:[0x18]
处,PEB 存放在 TEB偏移 0x30 的位置。
注意:关于 TEB 和 KPCR 的获取,不能直接使用 FS:[0]
,该方法获取的是 SEH 链条。而应该在 3 环使用 FS:[0x18]
,在 0 环使用 FS:[0x1C]
。
关于 ASLR 攻击方式可以参考《0Day安全第二版 第13章》。
3 系统空间地址管理
4 用户空间内存管理
- 内核空间:也就是我们常说的高2G,内核空间分配的地址是通过链表串起来的。遍历链表便可以找到各片地址的属性。之所以使用链表,主要是依据不同进程的高2G地址往往是相同的,因此高2G的地址变化较少,使用链表足矣。
- 工作集:工作集是指一个进程当前正在使用的物理页面的集合。
- 用户空间:进程空间已分配的内存是通过一个平衡二叉树(搜索二叉树)来管理的。
对于进程地址空间,用户程序必须经过保留(reserve
)和提交(commit
)两个阶段才可以使用一段地址范围。
- 保留一段地址范围(
reserve
):将这段地址范围保留起来,但并不真正使用,由于这段地址范围不占用任何物理内存或其他外存空间,所以并不会形成实质的开销(但是占用虚拟内存空间,如果已经使用VirtualAlloc
指定MEM_RESERVE/MEM_COMMIT
某个内存页,当再次MEM_RESERVE/MEM_COMMIT
这个页就会报错ERROR_INVALID_ADDRESS
)。这对于有些需要连续地址空间的程序有意义,它们可用在初始时保留一段大地址范围,以后需要的时候陆续使用。(页面被留后如果不提交是无法进行访问的) - 提交地址范围(
commit
):是指这段地址终究要消耗物理内存,由于 Windows 支持物理内存和页面文件之间的交换,因此可提交的内存数量是:commit = 工作集(已在物理内存中)页+换出物理内存的页
。(只要内存页面一提交就可以被换入换出内存了)。页面必须被提交了才可以使用!!!
在已提交的地址范围中,当页面被访问时,一定要先映射到物理内存页面上(利用延迟计算lazy evaluation)。这些页面要么是一个进程私有的、不共享的,要么被映射到一个内存区的视图上(可以被多个进程共享)。
在 Windows 提供的 API 中,VirtualAlloc
或 VirtualAllocEx
被用来保留或提交地址范围,通过这两个API申请的内存就是私有内存,只有申请内存的进程可使用;之后可用通过 VirtualFree
或 VirtualFreeEx
函数来解除已提交的地址范围,或者完全释放此地址范围。解除提交是指回到保留状态。因此,对于进程地址空间中的任何一个页面的地址范围,它一定处于三种状态之一:空闲的,保留的,已提交的。
当我们调用VirtualAlloc时如果传入的参数flAllocationType的值为reserved保留,这时windows操作系统也会构造并增加一个VAD结点来描述这块内存块。但是因为只是预留了一块虚拟地址,操作系统并未为其映射实际的物理内存,所以此时此VAD结点是没有实际意义的。但是这样做的好处是通过较小的开销(VAD结点构造)来预留一块虚拟地址待需要使用再分配PTE页表等结构为其映射实际的物理内存。(线程栈就是这样做的,当线程栈首先预留一块较大的虚拟内存,随着线程栈的增长不断提交新的虚拟内存块,映射到实际的物理内存中)。《VAD(Virtual Address Descriptor)虚拟地址描述符》
1 | PVOID __stdcall VirtualAllocEx( |
4.0 内存申请函数
4.1 VAD树介绍
内存管理器使用一个按需换页(demand-paging)的算法来计算何时将页面加载到内存中,它要等到有一个线程引用一个地址并且招致一个页面错误时,才从磁盘中获取该页面的数据。如同写时复制一样,按需换页也是一种延迟计算(lazy evaluation )– 即等待到真正需要的时候才执行一项任务。
VirtualAlloc
或 VirtualAllocEx
来提交内存时,该内存的PTE并不会被立即分出来,内存管理器会等待创建页表的时机,一直等到有一个线程引发了一个页面错误时,再为该页面创建一个页表。这种方法可以大大地提高性能。
这些暂时还不存在的页表将要占用的虚拟地址空间会被记到进程的页面文件配额和系统提交用量上面(已提交但是未使用的内存)。这样就确保了:一旦它们被实际地创建出来时,这些空间是可用的。采用了延迟计算的算法以后,即使分配大块内存也是一个快速的操作。当一个线程申请内存时,内存管理器必须用一段地址范围作为响应,以供线程使用。为了做到这一点,内存管理器维护了另一组数据结构,以跟踪和记录在进程的地址空间中哪些虚拟地址已经被保留了,哪些虚拟地址尚未被保留。这些数据结构称为虚拟地址描述符 (VAD,virtual address deseriptor)。VAD 是从非换页池中分配出来的。
进程地址空间管理就是地址空间中 保留区域的管理。 ,使用虚拟地址描述符(VAD,Virtual Address Descriptor)来管理。VAD 对象描述了一段连续的地址范围。在整个地址空间中,保留的或提交的地址范围有可能是不连续的,所以,Windows 使用一棵平衡二叉搜索树(称为 AVL树,比根节点左小右大)来管理 VAD 对象。
EPROCESS
中的成员 VadRoot
指向这颗平衡二叉树的根节点。
进程用户空间内存主要分为两类:
- 用户申请的私有内存(Private,使用
VirtualAlloc
等函数)。 - 磁盘文件映射的内存(Mapping,包括用户数据、用户DLL、系统共享DLL等)。
VAD树的每个节点,能够反映节点表示范围的虚拟地址空间中中内存是私有的,还是映射的(如果是映射的还会将映射文件名称这些信息进行反馈)。
- VAD树中的节点描述的内存为保留的或已提交的,是进程用户空间已分配的内存。
- 进程用户空间未分配的内存,使用VAD位图(
_MI_SYSTEM_INFORMATION._MI_SECTION_STATE.ImageBitMapNative
)来进行检索,位图置 1 的 bit 表示对应的内存已经分配,为 0 的表示空闲位(在 ASLR 章节提到过,本章后面还会再分析)。
Windows 10 21H2 x86:
1 | //0x500 bytes (sizeof) |
虽然VadRoot
被定义为RTL_AVL_TREE
结构,以及每个字节点被定义为RTL_BALANCED_NODE
结构。但是当使用 AVL 树来管理虚拟地址空间中的 VAD 时,树中节点的真正类型是 _MMVAD
(该结构详细可看《用户范围内存管理》):
1 | //0x4c bytes (sizeof) |
可以看到 MMVAD
是对 RTL_BALANCED_NODE
结构的扩展,增加了很多成员。
4.2 _RTL_AVL_TREE
先查看VadRoot
对应的结构_RTL_AVL_TREE
。
1 | kd> dt _RTL_AVL_TREE |
注意:
VadRoot
并不是二叉树的根节点,而是指向根节点。- 真正的根节点是
Root
,该节点的类型为_RTL_BALANCED_NODE
,系统将使用该类型来遍历整棵二叉树,用于遍历二叉树。 - 在AVL树中,每个节点都是
_MMVAD
结构(包括根节点),并使用该类型来详细描述每个节点对应内存块的属性信息。
EPROCESS.VadRoot
从 XP 开始被重复设计了三次,如下图(《The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory》):
- XP:
VadRoot
直接指向MMVAD_SHORT, MMVAD, MMVAD_LONG
节点结构。 - Vista~Windows 8:
VadRoot
指向_MM_AVL_Table
结构,该结构中的_MMADDRESS_NODE
成员为二叉树的根节点。 - 8.1~Windows 11:
VadRoot
指向_RTL_AVL_TREE
结构,该结构中的_RTL_BALANCED_NODE
成员为二叉树的根节点。
_RTL_AVL_TABLE
也可以遍历整棵 VAD 树,该结构从 Vista~Windows 11 基本没变(参考VERGILIUS项目)。
可以使用_RTL_BALANCED_NODE
遍历整棵树。
1 | //0xc bytes (sizeof) |
4.3 _MMVAD
我们可以将每一个 VAD 树节点转换为_MMVAD_SHORT/_MMVAD/_MMVAD_LONG
结构,内核根据相应的的 API 和传递的参数来准确决定要创建哪个结构。但是我们在查看 VAD 节点时可以根据Vad Tags
准确的来判断_RTL_BALANCED_NODE
是被转换为哪一个结构。
很多书上没有讲明白这几个结构的关系,我参考Michael Hale Ligh的书和VERGILIUS项目来说明他们的关系:
可以看到从 Windows 8开始取消了_MMVAD_LONG
结构、_MMADDREDD_NODE
结构,并且在_MMVAD
中新增了_SUBSECTION
结构的成员来描述映射文件(操作系统使用它来跟踪映射到该区域的文件或 DLL 的信息)。最后我们还可以看到在 Windows 10 中可以直接将_RTL_BALANCED_NODE
结构直接转换为_MMVAD
使用。
1 | kd> dt _MMVAD |
_MMVAD
结构分为两个部分来看:
- 第一部分为
_MMVAD_SHORT
成员:私有内存(Private)仅使用该部分内容,不使用后面的其他成员。但是映射内存(Mapping)使用全部成员。相应 VAD 树结点仅关注_MMVAD_SHORT
类型成员。 - 第二部分:除第一个成员后面的内容,是操作系统使用它来跟踪映射到该区域的文件或 DLL 的信息的。
内存取证:所以当你正在搜索注入的代码时,可以忽略_MMVAD
中除_MMVAD_SHORT
之外的成员(注入到进程中的shellcode不需要存在于磁盘上,因此他不需要进行文件映射)。
我们有 2 中方法来判断每个 VAD 节点描述范围的内存是 Private
还是 Mapped
:
- 使用 Vad Tags
- x86:在每个
_MMVAD
前面的0x4
字节,有一个标签可以用来指示该节点描述内存是私有还是映射的。 - x64:在每个
_MMVAD
前面的0xC
字节。(下一节讲解)
- x86:在每个
- 使用
_MMVAD._MMVAD_SHORT._MMVAD_FLAGS.PrivateMemory
成员来判断——1:私有内存,0:映射内存。
4.3.1 Vad Tags
Vad Tags 是 Volatility 内存取证工具为了方便攻击者/取证人员使用而定义的结构,并不是 Windows 定义的内容。实际上每一个 _MMVAD
节点前都有一个 _POOL_HEADER
结构,这里说的 Vad Tags 指的就是 _POOL_HEADER.PoolTag
成员。Windows 会给内存池分配相应的标签,用标签来表明这块内存中数据的类型。(当我们在内存取证时,如果是ShellCode仅需关注Tags为VadS的VAD节点)
Vad Tags 是一个枚举值:
但是需要注意,在 x86 和 x64 平台上 _POOL_HEADER
定义有所区别:
1 | //Windows Vista 32Bit~Windows 10 32Bit |
所以:
- x86:将
_MMVAD - 0x4
即为PoolTag
成员。 - x64:将
_MMVAD - 0xC
即为PoolTag
成员。
下面展示Windows 10 21H2 x86 一个进程的VAD(VadS表示Private,Vad表示Mapped):
1 | kd> db 0xa25162b8-4 |
扩展知识٩(。•́‿•̀。)۶━✿✿✿✿✿✿:
在《The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory》5.2 标签扫描章节提到_POOL_HEADER.PoolTag
成员是使用ExAllocatePoolWithTag
(Windows 10使用ExAllocatePool3
)等函数申请内存时指定的参数Tag
。
1 | DECLSPEC_RESTRICT PVOID ExAllocatePool3( |
tag
:用于分配内存的池标签。将池标签指定为1到4个非零字符,放在单引号(例如,‘Tag1’
)中。字符串通常按相反的顺序指定(例如,‘1gaT’
)。标签中的每个ASCII字符必须是0x20(空格)到0x7E(波浪号)范围内的值。每个分配代码路径都应使用唯一的池标签来帮助调试器和验证器识别代码路径。
工具 Volatility 已经提取了如下第二列,Windows默认使用的 Tag(可能Windows以后会更改这些字符):
这部分内容可以参看:
- The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory
- Memory Forensics : Tracking Process Injection
- Windows Process Internals: A few Concepts to know before jumping on Memory Forensics [Part 4] — VADs
- 内存取证原理学习及Volatility - 篇二
4.3.2 FLAGSx
_MMVAD
主要由_MMVAD_SHORT
和其后用于描述映射内存的其他成员组成。在MMVAD
大结构下有许多个xxxFlags
,他们都是用来描述该节点表示范围内存的属性,这些 flags 位于u/u1/u2/u3...
,都是一些联合体。Windbg 的符号将他们解析为<anonymous-tag>
,他们是及其重要的。
1 | //0x4C bytes (sizeof) |
4.4 _MMVAD_SHORT
1 | //0x28 bytes (sizeof) |
- StartingVpn:当前节点对应的内存的线性地址起始位置(以页为单位,乘以0x1000)。
- EndingVpn:当前节点对应的内存的线性地址结束位置(以页为单位,乘以0x1000+0xFFF)
- ReferenceCount:当前
MMAVD
节点被引用的次数。 - VadFlags:用来描述当前节点范围内存页的属性,非常重要。
- VadFlags1:用来描述当前节点范围内存提交情况,非常重要。
Virtual page numbers, VPNs(VPNs are page numbers, not address):32 bit Address Space = 20 bit Virtual Page Number (VPN) + 12 bit OFFSET。
4.5 _MMVAD_FLAGS
VadFlags
为 _MMVAD_SHORT
成员,结构为_MMVAD_FLAGS
。
Windows 10 21H2 x86 的_MMVAD_FLAGS
结构如下:
1 | //0x4 bytes (sizeof) |
NoChange:关于锁页技术。是否允许应用层修改对应内存页的保护属性(置1的话,应用层调用VirtualProtect无法改变内存页的保护属性)。
VadType:如果是映射可执行文件则值为2。
Protection:此字段指示应允许对内存区域进行哪种类型的访问(下面详细探讨)。
PageSize:目前还没发现该位实际意义,但基本都是0。可能类似于之前的
LargePage
大页标志?PrivateMemory:对应内存区域是私有的(1:Private)还是映射的(0:Mapped)。
4.5.1 Protection
Protection
字段为 VadFlags
从下标 7 开始的 3 Bit,指示允许对内存区域进行哪种类型的访问,对应于使用VirtualAllocEx
的flProtect
参数。
但是注意,flProtect
参数的十六进制值并不等于Protection
字段的值,只是有对应关系,看下图。
不能使用 PAGE_EXECUTE
来存储映射的文件,其中 PAGE_EXECUTE _WRITECOPY
写拷贝仅对映射文件(通常是 DLL)有效。PAGE_READWRITE
的内存区域很可能就是注入代码存放的区域。
请注意:Protection
域仅仅为第一次申请内存保留或提交时指定的属性,如果后续 VirtualProtectEx
修改了内存页的属性,该字段的值不会变。这是因为:Protection
域是用来标志 VAD 节点表示范围所有页面的,所以不要惊讶恶意代码所在的内存属性为不可访问或者只读。所以这个字段其实并不更靠谱。
因此我们在注入 ShellCode 前,申请内存使用 PAGE_NOACCESS
,然后使用PAGE_EXECUTE_READWRITE
去提交部分使用的内存,所以这个字段并不靠谱。要想查看某个内存真正的读写属性需要查看PDE、PTE,或者使用 VirtualQueryEx
函数(该函数并不是查询 Protection
的值)。
扩展知识:
我们可以看到,Protection
字段有 8 个取值,如 MM_EXECUTE_READWRITE
为 6
。这个数字是一个索引值,每次查找使用该索引到 n t!MmProtectToValue
表中查询。查看了 XP 的源码,意思大概是使用 MmProtectToValue
去 MmProtectToPteMask
中查询。
这一转换过程可参看:《R3环申请内存时页面保护与_MMVAD_FLAGS.Protection位的对应关系》。
1 | // Map a page protection from the Pte.Protect field into a protection mask. |
4.5.2 PrivateMemory
PrivateMemory
字段为 VadFlags
从下标 20 开始的 1 Bit。
- PrivateMemory = 1:私有内存。
- PrivateMemory = 1:非私有内存(映射文件、写时拷贝的DLL、命名的共享内存等)。
私有内存:在已提交的页面中,不能和其他进程共享、不能被其他进程继承的内存页。(进程的堆、栈、使用 VirtualAlloc/VirtualAllocEx
申请的内存)
可与其他进程共享的内存:映射文件、写时拷贝的DLL、命名的共享内存。
由于可以使用 VirtualAllocEx
可以在其他进程申请内存,所以 ShellCode 是放在 PrivateMemory
中的(在内存取证时提供一个思路)。
4.6 _MMVAD_FLAGS1
VadFlags1
为 _MMVAD_SHORT
成员,结构为_MMVAD_FLAGS1
,主要描述内存提交情况。
1 | kd> dt _MMVAD_FLAGS1 |
- CommitCharge:当前 VAD 节点表示内存范围中,已提交的页数量。
- MemCommit:第一次申请内存时是否已指定
MEM_COMMIT
。
我们关心此字段的原因是,从以往经历看,当恶意软件注入代码时他会将目标进程中申请的内存预先提交所有页面——它不会先仅保留它们,然后返回并稍后提交它们(尽管它很可能)。因此,您可以使用这些附加特征来帮助识别注入的内存区域。
4.7 _SUBSECTION
_MMVAD.Subsection
:操作系统使用它来跟踪映射到该区域的文件或 DLL 的信息。该成员的结构为 _SUBSECTION
。它类似于结构 _SECTION
后面章节会分析该结构。
1 | kd> dt _SUBSECTION |
该结构中,第一个成员 ControlArea
颇为重要,它的结构为 _CONTROL_AREA
。对于映射到该 VAD 节点范围内存的映射文件,我们都可以通过该结构来进行查看相关信息。
4.8 _CONTROL_AREA
该结构的信息如下:
1 | kd> dt _CONTROL_AREA |
这里我们重点关注成员 FilePointer
,它是一个 _EX_FAST_REF
快速引用指针类型。当我们在映射内存区域中查看映射文件详细信息时就要使用该成员来定位到映射文件 _FILE_OBJECT
。
对于 _EX_FAST_REF
结构,系统中有很多地方使用到,下面就来单独研究一下。
参考:
4.9 _EX_FAST_REF
_EX_FAST_REF
结构很重要很关键,在如下的文章从 EPROCESS
偷取 Token 时经常会使用到,以及在 VAD 节点中获取映射文件的 _FILE_OBJECT
等都使用到。
在 x86 和 x64 中是不一样的:
1 | // Windows XP~Windows 10 21H2 x86 |
- RefCnt:Reference Count 缩写,是快速引用这个对象时使用的。
EX_FAST_REF
指针是围绕这样一个事实构建的,即从池分配的数据结构总是在64位系统的16字节边界上对齐,在32位系统上总是在8字节边界上对齐。由于这种对齐,指针中的几个备用位可用于参考计数目的。此机制用于内核的内部使用,不会暴露给驱动程序开发人员。
最多可以引用的数量(EX_FAST_REF.RefCnt)在x64上为15个,在x86上为7个。
RefCnt
成员的使用方法是从大到小,EX_FAST_REF
指针使用ObInitializeFastReference()
进行初始化,该函数将EX_FAST_REF.Value
设置为指向池分配的结构,并将EX_FAST_REF.RefCnt
初始化为引用的最大数量。使用多处理器安全无锁操作InterlockedCompareExchangePointer()
快速获取和删除引用。- 当在
EX_FAST_REF
指针上进行快速引用时,RefCnt
会减少。当没有更多可用的快速引用(EX_FAST_REF.RefCnt == 1
)时,快速引用函数ObFastReferenceObject()
采取缓慢的路径,即ObReferenceObject()
并将引用计数补充到最大值。- 同样,当删除
EX_FAST_REF
指针上的快速引用时,RefCnt
会增加,当它达到最大值时,缓慢的取消引用路径,即采用OffDereferenceObject()
。- 系统中
EX_FAST_REF
指针的例子有OBJECT_HEADER.SecurityDescriptor
、SHARED_CACHE_MAP.FileObjectFastRef
、EPROCESS.Token
和EPROCESS.PrefetchTrace
。
如果我们要获得 _FILE_OBJECT 结构的地址,与上掩码:
- x86:
_FILE_OBJECT FileObj = (_FILE_OBJECT)(_EX_FAST_REF.Object & 0xFFFFFFF8)
。 - x64:
_FILE_OBJECT FileObj = (_FILE_OBJECT)(_EX_FAST_REF.Object & 0xFFFFFFFF'FFFFFFF0)
。
使用结构EPROCESS.Token
获取进程 Token 的文章:
- Exploit Development: Panic! At The Kernel Token Stealing Payloacs Revisited on Windows 10 x64 and Bypassing SMEP
- Token Abuse for Privilege Escalation in Kernel
- Better know a data source: Access tokens (and why they’re hard to get)
- Stealing Tokens In Kernel Mode With A Malicious Driver
- Windows Kernel Shellcode : TokenStealer
- Windows Kernel Exploitation Notes(一)——HEVD Stack Overflow
_EX_FAST_REF:
http://www.m5home.com/bbs/thread-9466-1-1.html
https://codemachine.com/articles/exfastref_pointers.html
https://shasaurabh.blogspot.com/2017/08/memory-forensics-tracking-process.html (我们必须用0xfffffff8掩码/0xffffffff`fffffff0)
详细可看《用户范围内存管理》
这部分的分析难度较大,建议先看豆哥逆向1909的内存学技术打豆豆。
4.10 VAD 位图
每个进程为用户模式地址分配情况维护一个结构,叫做用户地址空间位图,存在于 _MI_SYSTEM_INFORMATION._MI_SECTION_STATE.ImageBitMapNative
,是一个_RTL_BITMAP
结构。VAD 分配情况也会在这个位图中进行反映,有关用户地址空间分配情况的部分,称为 VAD 位图(VAD 位图只是 UMAB 的一部分)。
在本章一开始就提到过:
- VAD树中的节点描述的内存为保留的或已提交的,是进程用户空间已分配的内存。
- 进程用户空间未分配的内存,使用VAD位图(
_MI_SYSTEM_INFORMATION._MI_SECTION_STATE.ImageBitMapNative
)来进行检索,位图置 1 的 bit 表示对应的内存已经分配,为 0 的表示空闲位(在 ASLR 章节提到过,本章后面还会再分析)。
VAD 位图使用 _RTL_BITMAP
结构,该结构类似于上一节的 _EX_FAST_REF
在很多地方被使用,定义如下:
1 | // Windows XP~Windows 10 21H2 x86 |
- Buffer:指向位图数组的起始地址,该位图是一个
ULONG
类型的大数组,数组每个元素占4字节(每个元素32位),每一位表示这个64KB是否已经分配。 - SizeOfBitMap:表示位图中位的总数。
VAD 位图页面,位于当前进程的超空间区域,确切的位置由宏 VAD_BITMAP_SPACE
指定。
如上位图中是我自己根据_RTL_BITMAP
结构、论文《基于内存池标记快速扫描技术的Windows 内核驱动对象扫描-西北工业大学学报》及Kernel Mode Basics: An Introduction to Bitmaps画的,上图很形象展示了“图”的概念。
- 置 1 的位,标识内存已提交或保留,有对应的VAD。
- 置 0 的位,表示空闲。
虽然位图中每一个位表示64KB,但是 VirtualAllocEx
等函数的分配粒度就是 4KB(由堆管理器管理),这将会在下一篇文章堆管理部分讲解。
涉及 VAD 位图的两数主要有 4 个:MmCreateProcessAddressSpace、MilnsertVadCharges、 MiRetumPageTablePageCommitment 和 MiFindEmptyAddressRange。
MiFindEmptyAddressRange 函数本质上是扫描位图的过程。如果要找的地址范围正好是 64 KB 的倍数,则首先尝试在进程的 VAD 位图中找到连续的位。若不成功,则退回到 VAD 树,利用 MiFindEmptyAddressRangelnTree 函数在 VAD 树中查找相邻两个 VAD 之间有足
够的地址间隔。《Windows内核原理与实现p240》。