Aggregator
NCSC advice on the use of equipment from high risk vendors in UK telecoms networks
Charles的一次破解之旅
July 2020 Security Update: CVE-2020-1350 Vulnerability in Windows Domain Name System (DNS) Server
【渗透神器系列】Metasploit
未授权访问漏洞的检测与利用
【工具开源】MysqlSql语句监控工具——MysqlLogMonitor
腾讯御见UEBA
腾讯御见UEBA
技艺丛谈推出有声版
第一次勘误及为啥暂时不建立读者群
漫谈漏洞扫描器的设计与开发
Segment Heap的简单分析和Windbg Extension
Author: k0shl of 360 Vulcan Team
简述微软在Windows 10启用了一种新的堆管理机制Low Fragmentation Heap(LFH),在常规的环三应用进程中,Windows使用Nt Heap,而在特定进程,例如lsass.exe,svchost.exe等系统进程中,Windows采用Segment Heap,关于Nt Heap,可以参考Angel boy在WCTF赛后的分享Windows 10 Nt Heap Exploitation,而Segment Heap可以参考MarkYason在16年Blackhat上的议题Windows 10 Segment Heap Internals。
在Yason的议题中对于Segment Heap的分析已经足够详细,NT Heap和Segment Heap的结构差异较大,我在这篇文章中只对Segment Heap在Windows ntdll中的代码逻辑实现进行简单分析,以及我针对Segment Heap编写的windbg extension简单介绍。
Segment Heap的创建Windows在系统进程中使用Segment Heap,部分应用也使用了Segment heap,比如Edge,如果想调试自己的程序,可以在注册表中添加相应键值开启Segment Heap。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\(executable) FrontEndHeapDebugOptions = (DWORD)0x08通过windbg !heap命令可以看到当前进程的堆布局。
2: kd> !process 1f0 0 Searching for Process with Cid == 1f0 PROCESS ffffcf026f1cc0c0 SessionId: 0 Cid: 01f0 Peb: 1803b03000 ParentCid: 01e8 DirBase: 01850002 ObjectTable: ffffbd0dfbaea080 HandleCount: 574. Image: csrss.exe 2: kd> .process /i /p ffffcf026f1cc0c0 You need to continue execution (press 'g' <enter>) for the context to be switched. When the debugger breaks in again, you will be in the new process context. 2: kd> g 0: kd> .reload /user Loading User Symbols .................... 0: kd> !heap Heap Address NT/Segment Heap 14bff720000 Segment Heap 7df42cce0000 NT Heap关于Segment Heap和Nt Heap通过其头部结构的Signature成员变量区分,Signature保存在Heap Header+0x10位置,当Signature为0xDDEEDDEE时,该堆为Segment Heap,而当Signature为0xFFEEFFEE时,该堆为Nt Heap。
0: kd> dq 14bff720000 l3//Segment Heap 0000014b`ff720000 00000000`01000000 00000000`00000000 0000014b`ff720010 00000000`ddeeddee 0: kd> dq 7df42cce0000 l3//Nt Heap 00007df4`2cce0000 00000000`00000000 01009ba1`00f60fd8 00007df4`2cce0010 00000001`ffeeffee当进程初始化时,进程会调用RtlInitializeHeapManager函数创建堆管理结构,内层函数调用RtlpHpOptIntoSegmentHeap决定是否创建SegmentHeap,在RtlpHpOptIntoSegmentHeap函数中会检查进程明程等内容,当属于指定系统进程或者Package时,会设置对应的Feature,最后创建Segement Heap设置_SEGMENT_HEAP->Signature值为0xDDEEDDEE。
__int64 __fastcall RtlpHpOptIntoSegmentHeap(unsigned __int16 *a1) { v1 = a1; v16 = L"svchost.exe"; //----->指定的系统进程 v2 = 0; v17 = L"runtimebroker.exe";//----->指定的系统进程 v18 = L"csrss.exe";//----->指定的系统进程 v19 = L"smss.exe";//----->指定的系统进程 v20 = L"services.exe";//----->指定的系统进程 v21 = L"lsass.exe";//----->指定的系统进程 ... } //调用路径 LdrpInitializeProcess |__RtlInitializeHeapManager |__RtlpHpOptIntoSegmentHeap //最终在RtlpHpHeapCreate函数中将+0x10 Signature值置为0xDDEEDDEE __int64 __fastcall RtlpHpHeapCreate(unsigned __int32 a1, unsigned __int64 a2, __int64 a3, __m128i *a4) { v9 = (__m128i *)RtlpHpHeapAllocate(v6, v7, (__m128i *)&v36); v9[1].m128i_i32[0] = 0xDDEEDDEE;//mov dword ptr [rax+10h], 0DDEEDDEEh }因此我在编写segment heap的windbg extension时,通过查看的Bucket Block地址找到Segment Heap Header之后通过查看对应Signature是否为0xDDEEDDEE用于确认查找的地址是否是一个有效的Bucket地址。
Segment Heap LFHAllocate接下来对Segment Heap的分配和释放进行简单分析,首先我们需要了解_SEGMENT_HEAP中的一个关键结构_HEAP_LFH_CONTEXT,其成员在偏移0x340位置,在_HEAP_LFH_CONTEXT结构偏移0x80位置存放着一个Bucket Table,其结构关系如下。
0: kd> dt _SEGMENT_HEAP LfhContext ntdll!_SEGMENT_HEAP +0x340 LfhContext : _HEAP_LFH_CONTEXT 0: kd> dt _HEAP_LFH_CONTEXT Buckets ntdll!_HEAP_LFH_CONTEXT +0x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET在BucketTable中存放不同Size的Bucket Manager pointer,其实LFH并非在最开始就处于待分配状态,在堆最开始分配的时候是通过正常的Variable Size分配,关于vs heap的分配可以参考Yason的slide,当进程申请堆时会调用ntdll!RtlAllocateHeap,在分配时会检查Signature是否是SegmentHeap。
__int64 __fastcall RtlAllocateHeap(_SEGMENT_HEAP *a1, unsigned int a2, __int64 a3) { if ( !a1 ) RtlpLogHeapFailure(19i64, 0i64); if ( a1->Signature == 0xDDEEDDEE ) return RtlpHpAllocWithExceptionProtection((__int64)a1, a3, a2); if ( RtlpHpHeapFeatures & 2 ) return RtlpHpTagAllocateHeap((__int64)a1, a3, a2); return RtlpAllocateHeapInternal(a1, a3, a2, 0i64); }若Signature值为0xDDEEDDEE时,会调用RtlpHpAllocWithExceptionProtection创建segment heap block,在最开始的时候,会检查Bucket Table中lfh是否已经激活,也就是第一比特是否为1,当第一比特为1时,当前Bucket处于未激活lfh的情况,会创建vs heap,我们暂不讨论vs heap的申请。
3: kd> dq 116abf90000+340+80//Bucket Table 00000116`abf903c0 00000000`00000001 00000000`00000001 00000116`abf903d0 00000000`026e0001 00000116`abf90900//已经激活LFH索引的指针 00000116`abf903e0 00000000`01ee0001 00000000`030f0001//未激活的索引 00000116`abf903f0 00000000`04100001 00000000`00820001 00000116`abf90400 00000000`01280001 00000000`00e30001 00000116`abf90410 00000000`00210001 00000000`00410001Segment Heap的分配实现在RtlpAllocateHeapInternal函数中,由于代码逻辑较长但并不复杂,我这里只标明与我本文相关的逻辑部分,具体逻辑需要感兴趣的读者自行逆向。
__int64 __fastcall RtlpAllocateHeapInternal(_SEGMENT_HEAP *HeapBase, unsigned __int64 InSize, __int64 a3, __int64 a4) { …… if ( InSize <= (unsigned int)WORD2(HeapBase->LfhContext.Buckets[0x13]) - 0x10 )//--->(0) { if(!(BucketTable[SizeIndex] & 1){//--->(1) RtlpHpLfhSlotAllocate() } else if(Allocate enough blocks){ //--->(2) RtlpHpLfhBucketActivate() } else{ do something//--->(3) } } if ( InSize > 0x20000 ) { RtlpHpLargeAlloc()//--->(4) } else{ RtlpHpVsContextAllocateInternal()//--->(5) } …… }接下来我会就代码中的逻辑进行简要说明。
(0) 分配时首先判断申请堆的大小是否小于等于0x4000-0x10,也就是0x3ff0,若大于0x4000且小于等于0x20000,则直接使用Variable Size Heap Allocate,如果大于0x20000则使用Large Heap Allocate。 (1) 若申请堆大小小于等于0x3ff0,则会在Bucket Table中找到分配大小对应Size的索引,之后判断其是否已经激活LFH(第一比特是否为1),当LFH已经激活时,if语句判断返回TRUE,直接调用RtlpHpLfhSlotAllocate申请Block。 (2) 否则检查当前申请的堆大小的已申请数量是否已经满足激活LFH所需的数量,若满足,则调用RtlpHpLfhBucketActivate函数激活Bucket,此时Bucket Table对应位置会被Bucket Header赋值。 (3) 如果分配数量还不满足则进行一些Flag的赋值后跳出if语句。 (4) 当申请堆大小大于0x20000时,则调用RtlpHpLargeAlloc申请Large Heap。 (5) 当满足(0)条件或者在(3)中没有达到激活LFH条件时,调用RtlpHpVsContextAllocateInternal申请VS Heap,也就是说(5)不一定只满足大于0x4000小于等于0x20000的情况,小于等于0x4000时也有可能会走VS Heap,这取决于已分配Block的数量。这里我们不讨论VS Heap和Large Heap,只讨论LFH Heap的情况。当LFH被激活时,RtlpHpLfhBucketActivate会创建一个Bucket Manager,并且将这个Manager指针放到Bucket Table对应Size Index的位置,我们要研究申请堆的Block的分配需要从这个Bucket Manager入手。
Block的申请在RtlpHpLfhSlotAllocate()函数中,关于这个函数代码逻辑比较复杂,我将从Bucket Manager入手结合关键的代码逻辑和大家分享LFH Block的分配过程。由于调试过程比较复杂,这里我不再贴出调试步骤记录占用篇幅,感兴趣的读者可以在RtlpHpLfhSlotAllocate单步跟踪加以印证。
Bucket Manager是一个名为_HEAP_LFH_BUCKET的结构,其成员变量包含一个重要结构_HEAP_LFH_AFFINITY_SLOT,该结构中包含的重要成员变量结构为_HEAP_LFH_SUBSEGMENT_OWNER,关于结构关系如下(重要结构我用*表示)。
1: kd> dt _HEAP_LFH_BUCKET 116`abf90b00 ntdll!_HEAP_LFH_BUCKET +0x000 State : _HEAP_LFH_SUBSEGMENT_OWNER +0x038 TotalBlockCount : 0x5b7 +0x040 TotalSubsegmentCount : 0x10 +0x048 ReciprocalBlockSize : 0x3333334 +0x04c Shift : 0x20 ' ' +0x04d ContentionCount : 0 '' +0x050 AffinityMappingLock : 0 +0x058 ProcAffinityMapping : 0x00000116`abf90b80 "" * +0x060 AffinitySlots : 0x00000116`abf90b88 -> 0x00000116`abf90bc0 _HEAP_LFH_AFFINITY_SLOT 1: kd> dt _HEAP_LFH_AFFINITY_SLOT 116`abf90bc0 ntdll!_HEAP_LFH_AFFINITY_SLOT * +0x000 State : _HEAP_LFH_SUBSEGMENT_OWNER +0x038 ActiveSubsegment : _HEAP_LFH_FAST_REF 1: kd> dt _HEAP_LFH_SUBSEGMENT_OWNER 116`abf90bc0 ntdll!_HEAP_LFH_SUBSEGMENT_OWNER +0x000 IsBucket : 0y0 +0x000 Spare0 : 0y0000000 (0) * +0x001 BucketIndex : 0x5 '' +0x002 SlotCount : 0 '' +0x002 SlotIndex : 0 '' +0x003 Spare1 : 0 '' * +0x008 AvailableSubsegmentCount : 1 +0x010 Lock : 0 * +0x018 AvailableSubsegmentList : _LIST_ENTRY [ 0x00000116`ac5d4000 - 0x00000116`ac5d4000 ] * +0x028 FullSubsegmentList : _LIST_ENTRY [ 0x00000116`ac0f7000 - 0x00000116`ac5d0000 ]LHF的Bucket是通过双向链表的方法管理,AvailableSubsegmentList是存在Free状态的Block的Bucket链表,FullSubsegmentList是已经满了的Bucket的链表,这两个链表存放的就是各个Bucket的Bucket Header,当LFH分配Block时,会检查Bucket Manager中AvailableSubsegementCount的值,若其值小于等于0,则继续判断AvailableSubsegementList,在AvailableSubsegmentList中没有可用的Bucket header时,其值指向自己。
1: kd> dq 116`abf90bc0//_HEAP_LFH_SUBSEGMENT_OWNER结构 00000116`abf90bc0 00000000`00000500 00000000`00000001//有可用的Bucket 00000116`abf90bd0 00000000`00000000 00000116`ac5d4000//AvailableSubsegmentList 00000116`abf90be0 00000116`ac5d4000 00000116`ac0f7000//FullSubsegmentList 00000116`abf90bf0 00000116`ac5d0000 00000000`00000000 3: kd> dq 116`abf908c0//_HEAP_LFH_SUBSEGMENT_OWNER结构 00000116`abf908c0 00000000`00000c00 00000000`00000000//可用的Count为0 00000116`abf908d0 00000000`00000000 00000116`abf908d8//AvailableSubsegmentList指向本身 00000116`abf908e0 00000116`abf908d8 00000116`abf908e8//FullSubsegmentList指向本身 00000116`abf908f0 00000116`abf908e8 00000000`00000000 v10 = &a3->State.AvailableSubsegmentCount; if ( a3->State.AvailableSubsegmentCount <= 0 )//当Count小于0 { …… v121 = (__int64 **)&a2->State.AvailableSubsegmentList; if ( *v121 == (__int64 *)v121//链表指针指向本身 || ((RtlAcquireSRWLockExclusive(&a2->State.Lock), *v121 == (__int64 *)v121) ? (_RSI = 0i64) : (_RSI = RtlpHpLfhOwnerMoveSubsegment((__int64)a2, *v121, 2)), RtlReleaseSRWLockExclusive(&a2->State.Lock), !_RSI) ) { _RSI = (__int64 *)RtlpHpLfhSubsegmentCreate(a1, a2, a5); if ( !_RSI ) goto LABEL_52; } …… }如果满足上述条件,则当前没有可用的Bucket,LFH调用RtlpHpLfhSubsegmentCreate创建一个新的Bucket,在RtlpHpLfhSubsegmentCreate函数中,我们可以看到实际上在_HEAP_LFH_SUBSEGMENT_OWNER中的BucketIndex成员变量用于在ntdll的一个全局变量RtlpBucketBlockSizes中获取这个Bucket Manager所管理的Bucket中Block的Size,也就是我们申请堆的Size。
v3 = a2->State.BucketIndex; v4 = RtlpHpLfhPerfFlags; v10 = a3; v8 = (unsigned __int16)RtlpBucketBlockSizes[v3]; v33 = (unsigned __int16)RtlpBucketBlockSizes[v3]; 1: kd> dq ntdll!RtlpBucketBlockSizes 00007ffc`5cbe1270 00300020`00100000 00700060`00500040//Block Size 00007ffc`5cbe1280 00b000a0`00900080 00f000e0`00d000c0 00007ffc`5cbe1290 01300120`01100100 01700160`01500140 00007ffc`5cbe12a0 01b001a0`01900180 01f001e0`01d001c0 00007ffc`5cbe12b0 02300220`02100200 02700260`02500240 00007ffc`5cbe12c0 02b002a0`02900280 02f002e0`02d002c0在RtlpHpLfhSubsegmentCreate函数最终会分配出一个Bucket,将Bucket Header赋值给AvailableSubsegementList,同时这个函数中会按照RtlpBucketBlockSizes对应BlockIndex的地址,返回Size,最终切割好Block。
一旦存在可用的Bucket,则来到分配的最后一步,实际上理解分配最后一步非常简单,在Bucket创建时,所有可用的堆已经被切割好,LFH会随机取一块Block,并且将这个Block的地址返回,这个地址就是我们申请堆的地址,这一步全部依靠Bucket Header完成。
在Segment Heap LFH中,堆不再具有头部,取而代之的是通过Bucket Header来管理Bucket中的所有Block。Bucket Header结构体叫做_HEAP_LFH_SUBSEGMENT
1: kd> dt _HEAP_LFH_SUBSEGMENT 116`ac0f7000 FreeCount, BlockCount, BlockBitmap ntdll!_HEAP_LFH_SUBSEGMENT +0x020 FreeCount : 0 +0x022 BlockCount : 0x32 +0x030 BlockBitmap : [1] 0x55555555`55555555 1: kd> dq 116`ac0f7000 00000116`ac0f7000 00000116`ac1f9000 00000116`abf90be8//List_Entry 00000116`ac0f7010 00000116`abf90bc0 00000000`00000000 00000116`ac0f7020 0001002c`00320000 0040010c`60b53c07 00000116`ac0f7030 55555555`55555555 fffffff5`55555555 00000116`ac0f7040 00000000`00000001 00000000`00000000在Bucket Header中,Bitmap中存放的是这个Bucket中所有Block的状态,关于这个状态在Yason的slide中有相关介绍,这里我就不赘述了,值得一提的是,当你申请堆的大小恰好和RtlpBucketBlockSizes中存放的大小相等时,Bitmap的01代表已分配状态,00代表空闲状态,而当你申请的大小与RtlpBucketBlockSizes中存放大小不等时,则Bucket依然会按照RtlpBucketBlockSizes中存放的大小切割,但11代表已分配状态,10代表空闲状态,比方说我申请0xc10大小,但实际Block大小会按照0xC80切割,同时bitmap中高位会置1,这一切都取决于Bucket的索引在RtlpBucketBlockSizes数组中对应位置存放的Size。
分配时,会在bitmap中找到随机一个空闲状态的Block并返回,同时会将bitmap中对应位置置成分配状态(低位置1),并且FreeCount减1,当FreeCount减到0时,证明Bucket全部分配满,LFH会将该Bucket从AvailableSubsegmentList链表中unlink,并插入FullSubsegmentList中。
同理释放时,会将bitmap对应的位置置成空闲状态,FreeCount加1,若当前Bucket在FullSubsegmentList中,则会从该链表unlink,并加入到AvailableSubsegmentList中。
最后,关于创建Bucket的时候到底分配多少Block,这个并不是固定的,而是根据_HEAP_LFH_BUCKET中的TotalSubsegmentCount以及申请堆的大小决定的,其函数实现在RtlpGetSubSegmentBlockCount中。
__int64 __fastcall RtlpGetSubSegmentBlockCount(unsigned int HeapSize, unsigned int TotalSubSegmentCount, char AlwaysZero, int IsFirstBucket) { v5 = AlwaysZero - 1; if ( HeapSize >= 0x100 ) v5 = AlwaysZero; v6 = v5 - 1; if ( !IsFirstBucket )//如果是这个Size的第一个Bucket v6 = v5; if ( TotalSubSegmentCount < 1 << (3 - v6) ) TotalSubSegmentCount = 1 << (3 - v6); if ( TotalSubSegmentCount < 4 ) TotalSubSegmentCount = 4; if ( TotalSubSegmentCount > 0x400 ) TotalSubSegmentCount = 0x400; return TotalSubSegmentCount; }随着该Size分配的堆数量的增加,最终一个Bucket中创建的Blocks也会增加。
在我的Windbg Extension中,由于Bucket Header都是按页对齐,因此通过查询的堆地址直接与0xff..f000做与运算后就可以找到页头部,假设该头部是Bucket Header时,其_HEAP_LFH_SUBSEGMENT的_HEAP_LFH_SUBSEGMENT_OWNER成员变量指向Bucket Manager,之后可以找到整个Segment Heap的头部,通过Signature就可以判断Bucket Header是否是有效的Bucket Header,如果不是,则将当前页头部-0x1000,继续按页查找,因为当前分配的Block可能不止一页。
之后根据Bucket Header的Bucket Index可以在全局变量RtlpBucketBlockSizes数组中找到当前Bucket的Size,通过bitmap可以打印最终的Bucket布局。
1: kd> !heapinfo 116`ac0f7060 Try to find Bucket Manager. Bucket Header: 0x00000116ac0f7000 Bucket Flink: 0x00000116ac1f9000 Bucket Blink: 0x00000116abf90be8 Bucket Manager: 0x00000116abf90bc0 ---------------------Bucket Info--------------------- Free Heap Count: 0 Total Heap Count: 50 Block Size: 0x50 --Index-- | -----Heap Address----- | --Size-- | --State-- 0000 | *0x00000116ac0f7050 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0001 | 0x00000116ac0f70a0 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0002 | 0x00000116ac0f70f0 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0003 | 0x00000116ac0f7140 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0004 | 0x00000116ac0f7190 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0005 | 0x00000116ac0f71e0 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0006 | 0x00000116ac0f7230 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 0007 | 0x00000116ac0f7280 | 0x0050 | Busy --------- | ---------------------- | -------- | --------- 引用MarkYason, "Windows 10 Segment Heap Internals"
My Project: SegmentHeapExt