面向Windows 10系统段堆的内存取证研究
2021-11-18翟继强陈攀徐晓杨海陆
翟继强, 陈攀, 徐晓, 杨海陆
(哈尔滨理工大学 计算机科学与技术学院, 黑龙江 哈尔滨 150080)
近年来,黑客经常通过网络传播恶意程序,当计算机染上恶意程序时,计算机内存会留下恶意活动痕迹[1-2],这时通过内存取证技术就可以捕获到这些痕迹并落实网络犯罪的数字证据。段堆中含有进程运行时生成的重要信息,提取出段堆中的信息可以了解进程的运行情况,因此对段堆的内存取证研究在信息安全的防护领域意义重大。
目前,对堆的内存取证研究根据操作系统的不同可分为基于Linux系统的堆取证研究、基于安卓环境的堆取证研究、基于Windows 7系统的堆取证研究。在Linux系统中, Block研究了glibc库创建的堆结构,使用了堆结构定位及关键字段偏移法复现堆内部信息,解决了标签搜索不准确问题[3]。在安卓系统中,张俊芙研究出了3种提取堆信息的方法,分别为:把目标值做为搜索对象进行搜索;使用相关对象定位引用对象和同类对象;使用目标数据猜测法搜索目标数据[4]。在Windows 7系统中, Cohen研究了NT堆中低碎片化堆的创建并且使用硬件PTE解析算法,复现NT堆中无效页面信息[5]。
然而,上述的堆取证研究并没有针对Windows 10系统中的段堆,通过现有的内存取证技术不能重现段堆信息,进而不能获取针对段堆的堆溢出攻击的取证信息。同时段堆尚未在MSDN文档上公开而且目前对段堆结构的研究还并不充分,因此需要进一步研究段堆结构。为了弥补这些缺陷,本文研究了多个版本的Windows 10段堆并且提出一种利用池扫描技术识别内核对象,再结合字段信息偏移定位的方法,提取段堆内部信息。经过测试,该方法能成功复现段堆内部信息,这些信息能帮助调查人员获取堆溢出攻击的数字证据。本文研究的主要内容如下:
1) 研究了多个Windows 10版本的段堆及其组件结构中字段的作用;
2) 根据段堆特征值定位段堆的位置,并提取出段堆内部信息;
3) 提取出大块分配组件分配堆块的元数据信息;
4) 定位可变大小分配组件结构和低碎片化堆分配组件结构的位置并且提取出它们分配的内存信息;
5) 使用本文研发出的插件检测堆溢出攻击。
1 Windows 10段堆
1.1 进程环境块
当创建新的进程时,Windows内核会在内核内存的非分页池中创建-EPROCESS结构,通过该结构的内部信息可以定位用户模式下进程环境块的位置。进程环境块存放的是进程信息,其中就包括进程堆信息、当前的工作目录、环境变量、命令行参数等[6]。
1.2 段堆创建
Windows程序管理器创建进程时,内核会对进程分配4GB的虚拟内存,并创建和初始化堆管理器,不同的堆管理器对应着不同的特征值。Windows 10内核分配堆块时,根据堆的特征值进行相应堆块的分配。Windows 10内核有较好的安全机制,分配堆块的过程中使用临界区和原子操作函数实现线程同步,确保堆块分配成功。堆块的分配依赖于内部的4个组件,分配的过程中根据堆块大小选择合适的组件进行分配。
1.3 段堆介绍
段堆是特殊的内存管理器,它只存在于Windows 10系统中。段堆及其组件每次在分配内存之前会预先申请一块内存区域,然后再从内存区域中分配进程请求的内存。在可变大小分配组件分配的内存中已申请的内存大部分已被分配,只留有少数空闲内存未被使用,有些段堆中可能不含有该组件分配的子段。低碎片化堆组件分配的内存相比其他组件分配的内存要小很多,而且基本上不会产生空闲块。大块分配组件中未使用的内存相比其他组件要大很多,因此为了减少内存浪费,大块分配组件在段堆中很少使用。
段堆的结构和Windows系统中其他堆的结构类似,都是由字段偏移量、字段、字段数据类型组成。Windows 10有许多的版本,不同版本的Windows 10系统含有不同的段堆结构。随着段堆结构的更新,段堆的安全机制在不断完善并且内存分配效率也在不断提高。图1显示了段堆的结构信息(17134版本),这些信息记录了组件的偏移量、不同内存状态的页面数量等。段堆同内核对象一样,在内存中存在独属的结构体,结构体中含有段堆的信息。
1.4 段堆和NT堆的比较
由于进程地址空间的对齐粒度为64 kB,因此内存管理器分配的最小内存为64 kB,当要分配小于64 kB的内存时,会产生较大的内存碎片,而堆管理器分配的内存可以小于64 kB,因此可以减少内存浪费,提高内存利用率。进程开始运行时,会创建一个默认堆,使用HeapCreate函数可以创建额外的私有堆,在Windows 10系统中,私有堆包括段堆和NT堆。经分析发现,它们之间存在着较大的差异,段堆和NT堆之间存在的差异如下所示:
1) 在内存分配频繁的情况下,NT堆分配内存的速度要比段堆快,因为段堆在分配内存时需要经过更多的操作步骤;
2) 段堆具有更好的安全机制,在段堆中,对元数据的访问是互斥进行的,在同一时刻,只允许一个线程对其进行操作,而实际数据不是,因此段堆中的元数据独立于实际数据。然而在NT堆中,元数据不是互斥访问,于是元数据和实际数据混在一起;
3) 段堆使用4个组件对堆块进行分配,而NT堆只依赖于2个组件对堆块进行分配。段堆细化了堆块的分配范围,因此具有更高的内存利用率;
4) NT堆具有更完善的内存管理机制,它能够更全面地定位跟踪NT堆中内存释放与分配情况,因此NT堆结构中含有更全面的信息;
5) 段堆是新出现的堆管理器,内部的管理机制需要不断完善,段堆的结构会随着Windows 10系统的更新而升级。由于NT堆的内存管理机制已趋于成熟,那么在后期,NT堆将不再更新;
6) 段堆是Windows 10出现后引进的,因此段堆只出现在Windows 10系统中,而NT堆存在于所有的Windows系统中。
2 段堆组件
段堆对内存的管理依赖于内部的4个组件,组件主要负责对内存的释放与分配[7]。不同的组件分配不同的内存大小:低碎片化堆分配组件分配不大于16 kB的内存区域;可变大小分配组件从段堆中请求分配的内存大小不大于128 kB;后端分配组件分配内存块的大小介于128 kB到508 kB之间;大块分配组件分配内存块的大小大于508 kB[7]。经分析发现,随着Windows 10系统的更新升级,段堆的组件结构也相应地发生了变化,这些变化让系统更准确地检测内存分布情况,从而更合理地分配堆内存,提高内存利用率。Mark研究了14295版本的段堆[7],但他分析的结构字段并不全面,本文补充分析了14295版本的段堆。在深入研究15063版本、16299版本和17134版本的段堆之后发现这3个版本相比于14295版本具有更好的安全性并且字段的位置及数量也发生了改变。
2.1 低碎片化堆分配组件
在段堆分配堆块时,优先给低碎片堆组件分配,若能进行分配,则遍历Buckets数组找到大小合适并处于激活状态的Bucket,未能找到则分配新的Bucket。该组件分配的堆块具有最高的内存利用率,几乎不产生内存碎片。堆块是从子段中进行分配,低碎片化堆中的子段以链表的形式串连在一起。在-HEAP-LFH-CONTEXT结构中,BucketStats字段记录了低碎片化堆子段在子段链表中的位置及每个子段中拥有处于激活状态的Bucket数量,系统通过该字段定位子段的位置,判断子段内存中空闲内存与已分配内存的情况,据此对子段中的内存进行释放与分配。MemStats字段记录了进程运行时,低碎片化堆中处于不同状态的内存大小,其中包括已申请内存大小、已分配内存大小、空闲内存大小,进程运行时,该字段可以让系统了解低碎片化堆内部的内存状态。图2显示了低碎片化堆分配组件的结构信息。
2.2 可变大小分配组件
当低碎片化堆无法分配堆块时,段堆管理器会再次判断堆块的大小,如果在可变大小分配组件分配的内存范围时,则在可变大小分配组件分配的子段中分配堆块,若堆块分配的大小超过了子段的空闲内存范围,则会新建子段进行堆块分配。在-HEAP-VS-CON TEXT结构中,FreeCommittedUnits字段记录了已分配内存中已释放的内存大小。TotalCommittedUnits字段记录了进程运行时,可变大小分配组件分配的内存中处于已分配状态的内存大小。Lock字段是一个内存结构,标记了已分配内存的访问请求状态,其中包括锁住状态、等待状态、唤醒状态、共享状态等,该字段能够让系统知道内存状态,从而限制线程对已分配内存的操作。LockType字段只有3种取值:当值为0时,表示系统以页为单位锁住内存;当值为1时,表示系统不是按页为单位锁住内存;当值为2时,表示系统锁住整个可变大小分配的内存。图3显示了可变大小分配组件的结构信息。
2.3 后端分配组件
在14295版本的段堆中,SegmentCount、SegmentListHead、FreePageRanges字段记录了后端分配组件分配的内存信息,而在15063版本、16299版本和17134版本的段堆中,只有SegContexts字段记录了后端分配组件分配的内存信息。在-HEAP-SEG-CONTEXT结构中,FreePageRanges字段是一个红黑树结构,空闲内存之间通过指针相互连接形成红黑树结构。SegmentLock字段标记了已分配子段的访问请求状态,其中包括锁住状态、等待状态、唤醒状态、共享状态等。LfhContext字段和VsContext字段为结构体指针,分别指向低碎片化堆组件和可变大小分配组件结构。MaxAllocationSize字段的值是一个子段分配的最大内存大小,如果分配的内存超过这个值,就会再分配一个子段。图4显示了后端分配组件的结构信息。
图4 后端分配组件结构信息
2.4 大块分配组件
大块分配组件分配的堆块会产生较大的内存碎片,在段堆中,为了提高内存利用率,该组件很少使用。在图1中,段堆结构中有4个字段记录的是大块分配组件相关的信息。LargeReservedPages字段记录的是大块分配组件中申请的内存大小,LargeCommittedPages字段记录的是大块分配组件分配内存的大小,大块分配组件分配堆块的单元数据相互连接在一起形成红黑树结构,LargeAllocMetadata记录的是单元数据红黑树的根地址[7]。
3 插件的实现及试验
3.1 池扫描技术
系统内存池分布了很多内核对象,其中就包括进程对象,池扫描技术可以定位内核对象[8]。每个内核对象头部结构都是-POOL-HEADER结构,该结构中含有四字节标签,对该标签扫描可以定位需要分析的内核对象[9-10]。池扫描技术是研发本文5个插件的前提技术,当定位进程对象时,就可以定位进程环境块结构中的ProcessHeaps字段[6]。
3.2 内存取证框架
本文研发的功能插件,都是基于内存取证框架实现的。内存取证框架是内存取证工具,也是取证技术的载体,它可以提取进程中的信息[11]。当网络犯罪发生时,它能获取电脑、手机等设备的数字证据[12]。内存取证框架可以从转储文件和硬件磁盘镜像中解析休眠文件与页面文件信息,通过这2个文件信息的对比能获取隐藏进程的证据[13]而且还可以使用池扫描技术定位pico进程并解析pico进程内部信息[14],使用可执行页面检测算法遍历内存页并恢复可执行页面,帮助调查人员识别代码注入[15]等。
Volatility框架中含有各个Windows 10系统版本的配置文件,配置文件里面组合了许多vtype 描述信息,用来生成与单个统一编译单元一致的信息。在对内存对象进行分析的时候,这些信息可以让Volatility框架对转储文件中的数据进行解析[16]。在15063版本、16299版本、17134版本(操作系统内部版本)的配置文件中,没有段堆及其组件的vtype描述信息,本文提取出段堆及其组件结构信息后导入到配置文件中。
3.3 heapscan插件
heapscan插件是基于池扫描技术实现的,当识别出取证文件为Windows 10系统的转储文件时,使用池扫描技术扫描内核空间并定位需要分析的进程,接着扫描进程堆空间,使用段堆的特征值定位段堆的位置。heapscan插件可以重现进程运行时,段堆的内部信息。该插件可以输出进程中所有段堆的子段数量、不同状态内存的大小和不同类型堆的数量。heapscan插件解析段堆时,执行的步骤如下:
步骤1读取配置文件信息和pid信息,确定转储文件的结构定义和解析语言,加载地址空间;
步骤2使用池扫描技术扫描转储文件的物理地址空间,识别地址空间硬编码,找到含有"proc"标记的位置,根据字段信息确定进程pid对应的内核对象;
步骤3根据进程内部信息,定位PEB结构位置;
现代木结构建筑设计应遵循模数协调原则,建立标准化结构体系,优化建筑空间尺寸[13]。项目建筑设计未严格遵循选材的模数要求,在项目围护体系制作过程中,材料出现多次裁剪,造成了一定的浪费。通过项目实践深切体会到,模数化是建筑工业化的基础,实现预制构件和内装部品的标准化、系列化和通用化[9]13,有利于组织生产、提高效率、降低成本。
步骤4根据PEB对应的vtype描述信息,定位到进程堆空间;
步骤5扫描进程堆,根据特征值区分NT堆和段堆,进而定位段堆的位置;
步骤6根据段堆的vtype描述信息,提取出段堆信息并显示。
根据以上步骤,整理出如下heapscan插件实现的流程图:
图5 heapscan插件实现的流程图
heapscan插件实现的伪代码如下:
if profile is Windows10:
LoadAddressSpace()
if PoolScan(Address) is vaild:
Peb<-getPeb(proc)
AllHeap<-getHeaps()
forheapin AllHeap:
ifheapis SegmentHeap:
yield (0,[Address(heap),
str(Signature),
int(getTotalCommittedPages()),
int(getTotalReservedPages()),
str("Segmentheap"),
int(getSegmentCount())])
3.4 showvscontext插件
使用可变大小分配组件分配堆块时,-HEAP-VS-CONTEXT结构会时时跟踪可变大小组件对堆块的分配与释放情况,那么showvscontext插件可以对内存中-HEAP-VS-CONTEXT结构进行定位并对内部信息进行解析,该插件输出的信息有子段的数量、空闲块数量等。showvscontext插件解析可变大小分配组件时,执行的步骤如下:
步骤1基于段堆结构对应的vtype描述信息,定位-HEAP-VS-CONTEXT结构位置;
步骤2提取空闲块根结点地址,扫描可变大小分配内存中的空闲块,统计空闲块数量;
步骤3定位子段的位置,扫描所有的子段并定位子段的头部结构,解析子段头部信息,统计子段大小和子段数量,输出解析后的信息。
实现showvscontext插件的伪代码如下:
SegmentHeap<-getSegmentHeap()
VSContext<-getVSContext()
SubSeglist <-getSubsegmentList()
FreeChunkTreeRoot<-getFreeChunkTreeRoot()
FreeChunkNum<-getTotalFreeChunkNum()
forsegin SubSeglist:
Add(SubNum, getSubnum())
Add(Subsize,getSize())
yield (0,[Address(BackendCtx), int(getTotalCommittedUnits()),
int(getFreeCommittedUnits()), int(getSubsize()),
int(getSubnum()),int(FreeChunkNum)])
3.5 showlfhcontext插件
在段堆中,-HEAP-LFH-CONTEXT结构含有低碎片堆内部信息,该插件可以复现低碎片堆分配内存的情况。showlfhcontext插件解析低碎片堆内部信息时,执行的步骤如下:
步骤1解析段堆结构,定位低碎片堆位置;
步骤2定位并扫描Buckets数组,判断Bucket状态,提取出处于激活状态的Bucket;
步骤3定位每个处于激活状态的-HEAP-LFH-BUCKET结构,统计堆块的数量、子段数量、处于激活状态的Bucket数量;
步骤4通过-HEAP-LFH-AFFINITY-SLOT结构定位到-HEAP-LFH-SUBSEGMENT-OWNER结构,统计低碎片化堆子段的数量,显示提取后的信息。
实现showlfhcontext插件的伪代码如下:
SegmentHeap<-getSegmentHeap()
fortaskin SegmentHeap:
buckets<-getBuckets()
forbin buckets:
ifb.Invalid exists:
Add(ActiviatedBucketsNum,getActiviatedBucketsNum)
Add(totalblock,getTotalBlockCount())
Add(totalsubseg,getTotalSubsegmentCount())
affslot<-getAffinitySlots()
foraffsin affslot:
AddSubsegmentCount()
3.6 showlargeblockinfo插件
大块分配组件分配的内存块中存在着大块单元数据,大块单元数据在内存中呈现红黑树结构。该插件以遍历红黑树的方式定位大块单元数据结构中TreeNode字段,进而扫描所有的大块分配组件分配的单元数据结构。showlargeblockinfo插件解析大块分配组件时,执行的步骤如下:
步骤1获取heapscan插件传送过来的段堆对象,根据段堆结构在内存中的信息分布规律,定位大块分配组件分配堆块的根结点位置;
步骤2使用遍历红黑树的方法,遍历所有大块的单元数据,统计未被使用的内存大小和已分配的内存大小;
步骤3提取并输出大块单元数据信息。
showlargeblockinfo插件实现的伪代码如下:
SegmentHeap<-getSegmentHeap()
fortaskin SegmentHeap:
for LargeAllocMeta in TraverseMetadata():
yield(0,[Address(getVirtualAddress()),
Address(getTreeNode().Left),
Address(getTreeNode().Right),
str(getUnusedBytes()),
str(getAllocatedPages())])
3.7 showsegcontext插件
创建段堆后,在段堆中通过2个-HEAP-SEG-C ONTEXT结构记录后端分配组件分配内存的情况,通过-SEGMENT-HEAP结构中的SegContexts字段可以提取后端分配组件的内部信息。showsegcontext插件解析后端分配组件时,执行的步骤如下:
步骤1使用vtype描述信息解析段堆内存对象,定位_HEAP_SEG_CONTEXT结构位置;
步骤2使用_HEAP_SEG_CONTEXT结构的vtype描述信息解析后端分配组件内存对象;
步骤3定位子段位置,扫描各个子段,统计子段数量和子段中已分配内存页的大小;
步骤4定位页范围描述结构起始位置,使用扫描红黑树的方法扫描-HEAP-PAGE-SEGMENT结构,统计空闲页面数量,显示解析结果。
showsegcontext插件实现的伪代码如下:
SegmentHeap<-getSegmentHeap()
VSContext<-getSegContext()
SegList <-getSegmentList()
FreePageRanges<-getFreePageRangesTreeRoot()
forsegin SegList:
Add(SubNum, getSubNum())
Add(CommPageCount, getCommPageCount())
for FreeTree in Traverse(FreePageRanges):
Add(PageCount,getPageCount())
Add(UnusedBytes,getUnusedBytes())
yield (0,[Address(getHeap()), int(SubNum()),
int(CommittedPageCount()), int(PageCount),
int(UnusedBytes)])
4 测试与分析
测试分为信息提取测试和堆溢出检测测试两部分,信息提取测试是为了验证插件能否提取出转储文件中段堆的信息,堆溢出测试是为了验证插件是否能检测出堆溢出攻击。实验环境如下:
主机操作系统为Windows 10 version 1903 64位,CPU为2.20 GHz,内存大小8 G,硬盘容量2 T。
4.1 信息提取测试
选取calculator进程和svchost进程作为实验对象,calculator进程为系统自带程序且属于用户进程,svchost进程是服务主程序,该程序在系统运行中起到非常重要的作用。
在15063版本、16299版本、17134版本的Windows 10系统中运行calculator程序,随后分别对系统内存进行转储生成转储文件,使用本文研发好的5个插件分别提取calculator进程和svchost进程中段堆的信息。
4.1.1 heapscan插件测试
本文研发的5个插件中,heapscan插件最为关键,它可以定位段堆的位置并为其他插件传递段堆内存对象。heapscan插件根据段堆结构的vtype描述信息解析段堆内部数据。在程序中,使用HeapAlloc、HeapCreate等函数可以向内存中申请连续的内存区域,当对这块内存区域进行初始化并使用时,这块内存就处于已分配状态。表1记录了heapscan插件提取的数据。
表1 heapscan插件提取的数据
4.1.2 showvscontext插件测试
该插件根据段堆结构信息中的VsContext字段定位可变大小组件结构。当可变大小分配组件释放堆块时,释放的堆块会被视为结点插入到由空闲块组成的红黑树中,遍历空闲块红黑树可以定位每个空闲块,进而判断空闲堆块有没有发生堆溢出。当定位到可变大小分配组件子段的位置时,加上偏移就可以定位处于分配状态的堆块,再把堆块的大小作为偏移就可以定位每个堆块,进而判断已分配堆块有没有发生堆溢出。表2记录了showvscontext插件提取的信息。
表2 showvscontext插件提取的数据
4.1.3 showlfhcontext插件测试
低碎片化堆分配组件是把分配的内存放到Buckets数组中,当Bucket处于激活状态时,表明该Bucket可以分配内存。遍历子段时,通过堆块的偏移就能定位到已分配堆块的位置,根据填充数据有没有被覆盖可以判断堆块有没有发生堆溢出。表3记录了showlfhcontext插件提取的数据。
表3 showlfhcontext插件提取的数据
4.1.4 showlargeblockinfo插件测试
大块分配组件不同于其他组件,根据它的结构定位不到堆块的位置,只能定位堆块的单元数据结构,通过该结构,可以统计大块组件申请的内存中处于分配状态的内存页数量和未被进程使用的内存页数量。表4记录了showlargeblockinfo插件提取的数据。
表4 showlargeblockinfo插件提取的数据
4.1.5 showsegcontext插件测试
由于可变大小分配组件和低碎片化堆分配组件都依赖于后端分配组件实现内存分配,因此后端分配组件子段数量为段堆所有的子段数量。遍历空闲页描述符可以知道后端分配组件申请分配的内存中未被进程使用的和已被进程释放的内存页数量。表5记录了showsegcontext插件提取的数据。
表5 showsegcontext插件提取的数据
实验结果表明本文研发的插件能成功地提取出不同Windows 10版本的段堆信息。从表中的信息可以看出,使用段堆及其组件结构的vtype描述信息可以成功解析段堆内部数据。随着Windows 10系统的更新,段堆及其组件结构中字段位置发生了变化,但这并不影响对段堆信息的提取,因为当使用vtype描述信息解析段堆时,根据信息名称就能进行解析,因此本文研发的插件具有较强的兼容性。
4.2 堆溢出攻击检测测试
在段堆内部存在较完善的安全机制,我们通过实验发现,在段堆中不可能通过覆盖堆块头中的前后指针实现DWORD SHOOT攻击。我们分析发现通过覆盖虚表指针或者通过修改堆块内部数据分配大小的方式,泄漏虚表指针可以产生堆溢出攻击。
当用插件提取段堆信息时,插件会检测段堆中是否出现堆溢出攻击。堆块头部结构含有堆块信息,通过核对头部信息的方法就可以检测出异常堆块。当定位到异常堆块时,使用识别地址的正则表达式检测堆块中有无虚表地址,若有则说明发生了虚表地址覆盖攻击,若没有,则通过识别堆块中的填充数据或堆块块头的方式定位到下个堆块,检测下个堆块中有无虚表地址,有的话,则说明发生了虚表地址泄漏攻击。
4.2.1 虚表地址泄漏攻击检测测试
堆块分配后,可以在堆块中分配标识内存大小的数据类型,通过堆溢出,修改该数据类型大小,就可以泄漏堆块信息。如果在堆块头被覆盖堆块的相邻堆块中存放了C++对象,那么通过虚表指针泄漏的方式可以调用恶意虚函数。以CVE-2020-0787漏洞为例进行测试,该漏洞为任意文件移动漏洞。在所有的Windows 10系统中,利用exploit程序泄漏虚表地址,可以导致恶意虚函数通过符号链接重定向文件移动函数,把恶意目录中的提权dll加载进System32文件夹中,当加载提权dll时,就能获得系统管理员权限。测试使用的关键exploit程序如下所示:
DoVftableFunc()
{
CreateFileAndWriteFile (SourceFilePath, fileContent......);
CreateGroupAndJob(group, job ,……);
InitFileInfo();
AddFiles(1, &fileInfoArray);
hRes = FindFirstFile(SearchPath, &FindData);
StringCchCat(BitsFileName,x, FindData.cFileN);
oplock =CreateLock(BitsTempFilePath, ......);
CreateSymlink(nullptr, LinkName, LinkTarget);
CompleteJob();
}
void TriggerHeapOverFlow
{
while(i a[i]= HeapAllocAndInit(Hheap,0,size); HeapFree(Hheap,x,a[k]); While(i bStrings[i] = SysAllocString(LongStr); HeapFree(Hheap,x,a[k+1]); While(i vector memcpy(a[k-1],ShellCode,sizeof(ShellCode)); DoVftableFunc= ReadVftable (); DoVftableFunc(); } 运行exploit程序后,对系统内存进行转储并用本文研发的插件进行信息提取。图6显示了段堆中的恶意痕迹信息,从中可以看出可变大小分配组件分配堆块的头部和填充数据都被溢出数据覆盖,增大了BSTR型变量的数据长度,导致了虚表地址泄漏。 图6 虚表地址泄漏攻击信息 4.2.2 虚表地址覆盖攻击检测测试 通过堆溢出可以覆盖C++对象的虚表地址,当调用虚函数时,会查找伪造的虚表并调用其中的恶意虚函数。以CVE-2020-0796漏洞为例进行测试,该漏洞是SMB远程代码执行漏洞。在1903版本和1909版本的Windows 10系统中,利用exploit程序覆盖虚表地址并通过恶意虚函数让SMB以不合理长度解压数据包,可以导致权限提升,进而攻击SMB服务器执行恶意代码。测试使用的关键exploit程序如下所示: Class Object{ virtual void MaliciousCode() { const uint8-t buf[ ] = { ........ 0xFF, 0xFF, 0xFF, 0xFF, //异常原始未压缩数据长度 0x02, 0x00, //压缩算法 ........}; send(sock, packet, len, 0)); hProc =getProcessHandleByName(ProcessName); lpMem = VirtualAllocEx(hProc,….); WriteProcessMemory(hProc, lpMem, shellcode,…); CreateRemoteThread(hProc,….); }} void TriggerHeapOverFlow { While(i a[i]=HeapAllocAndInit(Hheap,0,size); HeapFree(Hheap,x,a[k]); While(i vector memcpy(a[k-1],ShellCode,sizeof(ShellCode)); v[0].at(0)->MaliciousCode(); } 同样使用本文研发的插件对转储文件进行信息提取,根据系统的内部版本,我们使用18362版本的Windows 10配置文件解析转储文件。图7显示了段堆中的恶意信息,从中可以看出低碎片堆组件分配堆块的填充数据和堆块中的虚表地址被溢出数据覆盖。 图7 虚表地址覆盖攻击信息 在上述的测试中,我们使用了相似性匹配的快速检测方法检验虚函数在内存中的shellcode,都发现了恶意shellcode,说明了泄漏的虚表和覆盖后伪造的虚表中都有恶意虚函数,验证了本插件能检测出针对段堆的堆溢出攻击。 为了提取出段堆中的信息,本文分析了段堆结构中含有的字段并结合池扫描技术和字段在结构信息中的偏移量,设计出获取段堆及其组件内部信息的算法并在内存取证框架中研发出功能插件,这些插件可以解析Windows 10系统中段堆内部含有的信息。实验结果表明本文提出的方法可以重现进程运行时段堆及其内部组件在内存中的分配情况,进而反映进程中段堆内存信息,这些信息可以为系统遭到网络攻击或者网络犯罪提供取证依据。5 结 论