翟继强, 孙宏泰, 赵洛平, 杨海陆
(哈尔滨理工大学 计算机科学与技术学院, 黑龙江 哈尔滨 150080)
计算机取证是网络安全应急响应的基本步骤,主要包括磁盘取证[1]、实时分析[2]和内存取证[3]。恶意软件在计算机系统中执行的异常或未授权操作,可通过分析系统的磁盘驱动器或内存进行检测。近年来,一些恶意软件不会将关键的数据写入磁盘,这使得磁盘取证无法取得有效的数字证据,而内存取证技术针对易失性存储器进行分析,弥补了磁盘取证的不足,逐渐在数字取证领域中发挥出不可替代的作用[4]。
以往的内存取证研究专注于内存内核地址空间的结构和内容,对用户地址空间的结构和内容研究较少[5-6]。内存注入攻击是一种针对进程用户地址空间注入恶意代码的攻击技术,如Shellcode注入[7]、DLL注入[8]、Atom Bombing注入和Hollowing注入等,通常对目标进程用户地址空间的堆栈缓冲区、动态链接库、共享内存和其他内存区域注入并执行恶意代码。Windows 10 64位系统作为目前主流的操作系统,是网络攻击者的主要目标。对于64位操作系统,在高达128 TB的地址空间中检测恶意代码加重了取证分析人员的工作量,增加了安全事件响应所需的时间。在保证不流失有效数字证据的前提下筛选出可访问的内存区域,并明确其详细信息,能够减少取证分析人员检测、定位和提取恶意代码的工作量,减少安全事件响应所需的时间。
Gavitt[9]最早从内存取证的角度分析内存VAD树,并明确了用户地址空间中映射文件和共享内存的遍历方法,但是只适用于旧版本的Windows XP 32位系统,Windows 10 64位系统的VAD树结构与旧版本的不同,这使得映射文件、共享内存的遍历方法也不同。White等[10]提出了一种用户地址空间遍历方法,并通过Volatility插件实现了该方法,但是只适用于Windows XP和Windows 7的32位操作系统。与Windows 10之前的版本相比,用于描述用户地址空间的元数据变动较大,这使得针对旧版本系统的用户地址空间遍历方法不再适用于Windows 10系统。 翟继强等[11]详细地阐述了Windows 10系统中段堆的结构和工作原理,但是涉及的数据结构只适用于低版本的Windows 10系统,并且方法尚未应用到内存取证领域。Otsuki等[12]提出了一种Windows 64位系统的栈追踪方法,方法中明确64位系统运行的WOW64(Windows on Windows 64)进程分为WOW64层和32位应用层,每个线程维护2个栈,但没有明确WOW64栈的遍历方法。
针对以上用户地址空间遍历方法中完整度低和不兼容Windows 10 64位系统的问题,提出了一种基于VAD树遍历Windows 10 64位系统用户地址空间的方法。经测试,本文方法能够完整地展示Windows 10 64位系统进程的用户地址空间布局,相比于传统的遍历方法,提高了遍历的完整度,并适用于目前所有版本的Windows 10 64位系统。
Windows系统每个进程都拥有自己的虚拟地址空间,根据地址高地,可将虚拟地址空间划分为内核和用户地址空间两部分。对于32位进程,用户地址空间为低于0x80000000地址部分,64位进程用户地址空间为低于0x800000000000地址部分。Windows系统以连续的虚拟地址范围管理用户地址空间,这些连续的地址范围称为用户分配。在每个进程的内核空间中维护着1个自平衡二叉树,称之为VAD树,树中每一个节点存储1个用户分配的基本信息(如起始地址、结束地址、分配保护和内存类型等),但是VAD节点并不存储详细的描述信息。通过解析相关元数据能够补充用户分配的描述信息,弥补VAD树中描述信息不足的缺陷。
本文中所有涉及的相关元数据是基于WinDbg[13]调试Windows 10 64位各个版本的操作系统所确定的。表1中列出了现存用户分配研究与本文的不同侧重点。
表1 现存研究与本文研究的侧重点
VAD树的每个节点由_MMVAD结构维护,在Windows 10 64位系统下使用WinDbg调试其结构如图1所示。结构中Core成员是-MMVAD-SHORT结构,它的StartingVpn成员和EndingVpn成员记录了每个用户分配的起止虚拟地址;u成员记录了内存分配的权限。
lkd>dt-MMVAD
nt!-MMVAD
+0x000 Core :-MMVAD-SHORT
+0x000 NextVad :Ptr64-MMVAD-SHORT
+0x008 ExtraCreateInfo :Ptr64 Void
+0x000 VadNode :-RTL-BALANCED-NODE
+0x018 StartingVpn :Uint4B
+0x01c EndingVpn :Uint4B
⋮
+0x030u:
⋮
+0x040 u2 :
+0x048 Subsection :Ptr64-SUBSECTION
⋮
+0x080 FileObject : Ptr64-FILE-OBJECT
图1-MMVAD结构
定义用户分配算法的伪代码如算法1所示,由于VAD树是自平衡二叉树,通过递归所有左子树和右子树的方法即可遍历全部节点。分配的起始地址和终止地址来源于VAD节点的StartingVpn和EndingVpn成员。结构体的u成员以索引值的形式标记每个用户分配的保护,每个索引值对应不同的分配保护,这些在WinNT.h中定义。当VAD树节点的ControlArea字段为有效指针时,对应分配的内存类型为共享类型,其余为私有类型。
算法1定义用户分配算法
输入:EPROCESS进程对象E;
输出:初始化的用户分配集合U.
1:V← TraverseVadTree(E.VadRoot);/*从VAD树根节点遍历全部节点*/
2:U←Ø;
3: forv∈Vdo
4:u.vad←v;/*用户分配对应的VAD节点*/
5:u.start←v.StartingVpn;/*定义起始地址*/
6:u.end←v.EndingVpn;/*定义终止地址*/
7:u.permission← Transform(v.u);/*根据索引值确定保护*/
8:u.type← ‘private’;/*确定内存类型*/
9: ifv.Subsection.ControlArea≠None then
10:u.type← ‘shared’;
11: end if
12: end for
传统的遍历映射文件方法采用解析进程句柄表的方式获取内核文件对象,但是这种方法很容易受到Rootkit隐遁攻击的影响[5],使得方法无法获取被隐藏的文件对象。本文通过解析VAD树发现,VAD树节点的FileObject成员同样指向了文件对象结构体,通过遍历全部VAD节点能够获取进程维护的全部文件对象。此外,进程VAD树结构在系统中很不稳定,如果恶意软件企图通过非法方式访问或修改VAD树中的数据,很容易造成系统崩溃,因此VAD树中的数据具有可靠性。
每个进程对象结构都包含1个指向其进程句柄表的指针,句柄表包含进程正在使用的句柄。通过解析句柄表可获取维护共享内存的-SECTION结构。通过WinDbg调试Windows 10 64位系统内存数据结构发现,当-SECTION结构的ControlArea成员与部分VAD节点中的ControlArea成员指向相同的地址,说明VAD树与-SECTION结构描述的是同一用户分配,结构之间具体关系示意图如图2所示。
图2 共享内存结构示意图
遍历映射文件和共享内存的算法伪代码如算法2所示。首先从-MMVAD结构中获取文件对象结构,将FlieName成员作为映射文件的描述信息。通过解析进程句柄表获取-SECTION对象,当VAD节点的ControlArea成员与-SECTION对象的ControlArea成员指向相同的地址时,以-SECTION对象中存储的信息描述用户分配,当-SECTION对象中的信息为空字符串或不可打印的字符串时,则以共享内存描述该区域。
算法2遍历映射文件和共享内存算法
输入:用户分配集合U;
输出:包含映射文件与共享内存信息的用户分配集合U.
1: forv∈Vdo
2: ifv.FileObject≠None then /*判断该VAD节点描述的区域是否包含映射文件*/
3:u.description←v.FileObject.FileName;
4: end if
5:S← Handle(); /*通过进程句柄表获取section对象集合*/
6: fors∈Sdo
7: ifv.Subsection.ControlArea=
s.ControlArea then
8:u.description←s.Name or ‘Shared’;/*描述共享内存*/
9: end if
10: end for
11: addutoU;
12: end for
13: returnU
从Windows 10系统开始,在原有NT堆的基础上新增了段堆机制, White等[10]详细地阐述了NT堆的遍历方法,本文重点研究段堆的遍历方法。
本文通过调试Windows 10 64位系统进程堆相关的元数据时发现,段堆堆块的起始地址与部分VAD树节点中记录的起始地址相同,这说明段堆堆块也分布在用户地址空间中。遍历段堆算法伪代码如算法3所示。首先通过进程环境块(process environment block,PEB)的ProcessHeaps成员获取全部指向进程堆结构的指针。解析进程堆首先要根据堆签名将这些进程堆分为NT堆和段堆,堆签名为0xffeeffee表示NT堆,堆签名为0xddeeddee表示段堆。在遍历段堆的后端分配前需要对Windows 10系统版本进行判断,当系统内部版本号低于或等于15063时,通过SegmentListHead成员遍历链表,能够获取所有后端分配的基址;当内部版本号高于15063时,将SegContext成员解析为2个指向-HEAP-SEG-CONTEXT结构的指针,通过这个结构的SegmentListHead成员遍历链表,获取所有后端分配的基址。对于大块分配,则从根节点遍历所有-HEAP-LARGE-ALLOC-DATA结构,当用户分配的起始地址与段堆分配的起始地址相匹配时将段堆分配信息写入用户分配的描述中。
算法3遍历段堆算法
输入:用户分配集合U;
输出:包含段堆信息的用户分配集合U.
1:P=E.Peb
2:S←Ø; /*初始化段堆集合*/
3:B←Ø; /*初始化后端分配集合*/
4:L←Ø; /*初始化大块分配集合*/
5:H←P.ProcessHeaps;/*遍历进程全部NT堆和段堆*/
6: forh∈Hdo
7: ifh.Signature=0xddeeddee then /*根据签名判断堆类型*/
8: addhtoS;
9: end if
10: end for
11: fors∈Sdo
12: foru∈Udo
13: ifu.start=s.addr then
14:u.description← ‘Segment Heap’;/*描述段堆*/
15: if System version < 15063 then/*判断Windows 10系统版本*/
16:B← Followlist(s.SegmentListHead);/*遍历后端分配*/
17: else /*若系统版本高于15063则采用另一种方法遍历后端分配*/
18:B← Followlist(s.SegContext[0].SegmentListHead)+Followlist(s.SegContext[1].SegmentListHead);
19: end if
20:L← TraverseAVLTree(s.LargeAllocMetadata);/*遍历大块分配*/
21: end if
22: forb∈Bdo
23: ifu.start←b.addr then
24:u.description← ‘Backend Alloc’;/*描述后端分配*/
25: end if
26: end for
27: forl∈Ldo
28: ifu.start←l.VirtualAddress then
29:u.description← ‘Large block Alloc’;/*描述大块分配*/
30: end if
31: end for
32: end for
33: end for
34: returnU
在64位系统中既运行64位进程,也运行32位进程,后者这种情况称为WOW64进程。 WOW64进程的每个线程拥有2个执行上下文,分别由-TEB和-TEB32线程环境块结构维护。-TEB结构中的DeallocationStack成员指向了内存中线程栈的起始地址。通过WinDbg调试Windows 10 64位系统发现,-TEB32结构位于-TEB结构的+0x2000偏移处,其DeallocationStack成员指向用户地址空间的WOW64栈。
遍历线程栈算法伪代码如算法4所示。首先遍历线程链表获取进程所有的线程对象,通过Teb成员获取-TEB结构,如果结构的起始地址在某个用户分配的地址范围内,那么将-TEB添加到相应用户分配的描述信息中。从-TEB结构+0x2000的偏移量处获取-TEB32结构,这个结构也包含Dellocation-Stack成员,并且与-TEB结构中同名成员指向不同的地址,当分配地址与用户分配的基地址相匹配时,将WOW64栈分配信息写入用户分配的描述中。
算法4遍历线程栈算法
输入:用户分配集合U;
输出:包含段堆信息的用户分配集合U.
1:T← Followlist(E.Pcb.ThreadListEntry);/*遍历线程链表获取所有线程对象*/
2: foru∈Udo
3: fort∈Tdo
4: teb←t.Teb; /*获取TEB*/
5: ifE.Wow64Process then/*判断是否为WOW64进程*/
6: teb32← teb+0x2000;/*获取-TEB32*/
7: stack32← teb32.DeallocationStack;/*获取WOW64栈*/
8: if stack32.addr=u.Start then
9:u.description← ‘wow64 stack’;/*描述WOW64栈*/
10: end if
11: end if
12: end for
13: end for
14: returnU
本文基于Volatility和Rekall这2款开源内存取证框架提供的基础功能[14-15],实现了2款取证框架下的遍历Windows 10 64位用户地址空间插件Win10userspace。具体执行步骤如下:
步骤1 读取系统版本,确定配置文件;
步骤2 根据配置文件导入内核符号;
步骤3 解析VAD树定义用户分配;
步骤4 解析进程句柄表以遍历映射文件和共享内存,将映射文件所在的磁盘路径和共享内存信息添加到输出结果中;
步骤5 通过PEB结构遍历NT堆和段堆,将NT堆、块分配、虚拟分配、段堆、后端分配和大块分配信息添加到输出结果中;
步骤6 通过PCB遍历线程栈,若此时解析的进程为WOW64进程,还需遍历WOW64栈。
步骤7 输出所有用户分配信息。
插件在Rekall和Volatility下运行后结果如图3~4所示。
图3 Win10userspace-Rekall运行结果
图4 Win10userspace-Volatility运行结果
本文选择使用微软官方提供的调试工具WinDbg作为基准,以WinDbg获取的用户分配数计为总数。如果WinDbg获取的用户分配起始地址与插件获取的起始地址相同,则计为有效遍历数。评价指标为遍历比,其计算方式为有效遍历数占总数的百分比,遍历比越高,说明方法对用户地址空间遍历得越完整。
作为测试进程,选取开启段堆机制的Caculator.exe 64位进程,在Windows 10 64位系统进行测试,系统版本号为1607,所有实验分别进行10次,取平均值作为最终结果。
3.1.1 基于内存转储测试
本文基于Volatility实现的Win10userspace插件与White[10]的userspace插件进行对比,测试实验结果如表2所示。
表2 基于内存转储测试结果对比
结果表明,userspace在Windows 10 64位系统仍能够遍历部分用户分配,说明userspace插件的部分遍历方法能够在Windows XP、Windows 7和Windows 10系统中通用,而Win10userspace的遍历方法来源于对Windows 10 64位系统用户分配相关的元数据进行解析,因此对Windows 10 64位系统更具针对性。映射文件、段堆和线程栈分配项没有完全与总数相同,这是由于表中各个分配项的总数是使用WinDbg实时调试得出的,而Win10userspace中各个分配项计数是分析内存转储得出的,在实时调试过程中内存随着时间动态变化,使得实时调试与分析内存转储的结果存在微小的不同。
3.1.2 基于实时响应测试
Gavitt[9]提出的方法在Rekall中编写了名为Vad的插件,微软官方提供了用户地址空间遍历工具Vmmap[16],本文选取以上2个方法作为对比,测试实验结果如表3所示。
表3 基于实时响应测试结果对比
结果表明,Vad只能够获取内存映射文件,微软官方软件Vmmap能够有效遍历常见分配项,但是对于共享内存、堆块分配、虚拟分配、段堆分配、后端分配和大块分配没有实现有效遍历,Win10userspace能够有效遍历表中所有用户分配项。虽然Vmmap也具有较高的百分比,但是微软官方没有公开Vmmap工具的源码,无法得知工具运行的内部原理,这增加了对其研究和扩展的难度。Win10userspace基于开源的Rekall框架,便于开发者和研究人员对插件分析和优化。
Windows平台大部分恶意软件都是32位程序,原因之一是32位程序具有更好的兼容性,因此,评估插件能否有效遍历WOW64进程的用户地址空间具有重要意义。本文选取常见的32位程序,在Windows 10 64位系统进行测试,系统版本号为1607,所有实验分别进行10次,取平均值作为最终结果。由于插件有效性在3.1节得到验证,下文实验结果不再详细列出各分配项的具体计数。
3.2.1 基于内存转储测试
基于转储测试WOW64进程结果如表4所示。
表4 基于内存转储测试结果对比
实验结果表明,Win10userspace能够有效遍历WOW64进程的用户地址空间,有效遍历的百分比都保持在90%以上。而userspace的运行结果表明,userspace插件的方法已经不适用于Windows 10 64位系统。
3.2.2 基于实时响应测试
表5为基于实时响应测试WOW64进程结果。
表5 基于实时响应测试结果对比
实验结果表明,Win10userspace在实时响应的情况下能够有效遍历常见WOW64进程的用户地址空间,有效遍历百分比高于vad和Vmmap。
Windows 10系统更新频繁,因此版本数量众多,用户地址空间布局随着版本更新发生变化。为了验证插件在不同版本Windows 10 64位操作系统下的兼容性,本文对目前所有版本Windows 1064位系统进行测试,测试进程为Chrome.exe的64位和32位程序,所有实验分别进行10次,取平均值作为最终结果。测试实验结果如表6所示。
表6 不同版本Windows 10 64位系统下插件兼容性测试结果
实验结果表明,Win10userspace能够兼容表中所有版本的Windows 10 64位系统,结果与前文的有效性测试结果相符合。从表中还发现同一版本下WOW64进程的有效遍历百分比略低于64位进程,说明WOW64进程地址空间相对于64位进程更为复杂,存在部分插件无法遍历的分配项或结构。
本文选取7个近3年使用内存注入攻击技术的恶意软件,使用逆向技术分析其内存注入攻击原理,恶意软件的详细信息如表7所示。
表7 恶意软件样本信息
在Windows 10 64位系统下运行这些恶意软件样本,系统版本号为1607。运行后分别基于内存转储和实时响应使用Win10userspace插件分析目标进程的用户地址空间,运行结果如表8所示。
表8 恶意软件样本测试结果
表中可疑用户分配数表示在进程用户地址空间中可能包含恶意代码的用户分配数,可疑用户分配的判定条件需同时满足以下两点:
1)用户分配具有可执行权限。
2)用户分配的描述信息为空或不是映射文件。
Block等[17]研究表明,在绝大部分情况下,良性进程中只有映射文件具有可执行权限。若出现了同时满足以上2点条件的用户分配,那么这个用户分配会被判定为可疑用户分配。
测试结果表明,Win10userspace能够有效遍历目标进程的用户地址空间,遍历百分比能够保持在90%以上。除了Olympic Destroyer样本外,其余的目标进程中都出现了可疑用户分配。通过逆向分析Olympic Destroyer样本,发现该恶意软件首先会在目标进程中以READWRITE权限分配内存,随后使用VirtualProtect()函数将用户分配内的页面修改为EXECUTE-READWRITE。修改权限这一过程不会改变VAD树中记录的权限值,因此Win10userspace没有输出修改后的权限。
本文针对当前内存取证领域的用户地址空间遍历方法无法兼容Windows 10系统,提出了一种基于VAD树的用户地址空间遍历方法,满足了目前对Windows 10 64位系统用户地址空间遍历的取证需求。经过测试与分析,本文方法能够有效获取Windows 10 64位系统中64位进程和WOW64进程用户分配的详细信息,遍历的完整度优于现有方法;此外,本文针对不同版本Windows 10 64位系统测试了插件的兼容性,结果表明插件能够适用于目前所有版本的Windows 10 64位系统。经过应用实例测试证明,插件在实际应用过程中能够重现进程用户地址空间详细布局,辅助取证分析人员快速找出被注入恶意代码的内存区域。
由于Windows系统的不完全开源性,用户地址空间中仍存在部分无法描述的区域,针对此问题,未来的研究可专注于Windows系统内存元数据的调试和解析以提高遍历用户地址空间中可描述分配的百分比。此外,常见的内存注入攻击针对用户进程堆栈缓冲区和映射文件区域注入恶意代码,本文方法能够在保证不流失有效数字证据的前提下有效遍历这些易受到注入的用户分配,因此未来的研究也可基于本文的方法进一步研究如何自动化检测和提取用户地址空间中被注入恶意代码的区域,以提高取证工作的效率。