[翻譯]Windows 內核池原理 (win7 x86)

0x00:前言

查了一下好像沒看到啥關於內核池原理的翻譯,國外的一個論文有詳細說Win7池溢出的原理。內容比較長,今天來翻譯閱讀一下Win 7內核的池部分的原理。

0X01:正文

在本節中,我們將詳細介紹內核池管理結構和涉及池內存分配和釋放的算法。理解內核池行爲對於正確評估其安全性和健壯性至關重要。爲了簡單起見,我們假設是x86架構(32位)。但是,大多數結構都適用於AMD64/x64(64位)。第2.9節討論了x86和x64架構之間內核池的顯著差異。

1. 非一致內存體系結構(NUMA)

對於每個新版本的Windows,內存管理器都得到了增強,以更好地支持非統一內存體系結構(NUMA),這是一種在現代多處理器系統中使用的內存設計體系結構。NUMA將不同的內存庫分配給不同的處理器,允許更快地訪問本地內存,而更慢地訪問遠程內存。處理器和內存被分組在稱爲節點的更小的單元中,由執行內核中的KNODE結構定義。

typedef struct _KNODE
{
/*0x000*/ union _SLIST_HEADER PagedPoolSListHead;
/*0x008*/ union _SLIST_HEADER NonPagedPoolSListHead[3];
/*0x020*/ struct _GROUP_AFFINITY Affinity;
/*0x02C*/ ULONG32 ProximityId;
/*0x030*/ UINT16 NodeNumber;
/*0x032*/ UINT16 PrimaryNodeNumber;
/*0x034*/ UINT8 MaximumProcessors;
/*0x035*/ UINT8 Color;
/*0x036*/ struct _flags Flags;
/*0x037*/ UINT8 NodePad0;
/*0x038*/ ULONG32 Seed;
/*0x03C*/ ULONG32 MmShiftedColor;
/*0x040*/ ULONG32 FreeCount[2];
/*0x048*/ struct _CACHED_KSTACK_LIST CachedKernelStacks;
/*0x060*/ LONG32 ParkLock;
/*0x064*/ ULONG32 NodePad1;
/*0x068*/ UINT8 _PADDING0_[0x18];
} KNODE, *PKNODE;

在多節點系統(nt!KeNumberNodes > 1),內存管理器總是試圖從理想的節點分配。因此,KNODE提供了關於在color字段中何處找到本地內存的信息。此值是一個數組索引,分配和自由算法使用該索引將節點與其首選池關聯起來。此外,KNODE爲空閒池頁面定義了四個單鏈接的每個節點後備列表(在第6節中討論)。

2. 系統內存池

在系統初始化時,內存管理器根據系統節點的數量創建動態大小的內存池。每個池都由一個池描述符定義,這是一個管理結構,用於跟蹤池的使用並定義池屬性(如內存類型)。有兩種不同類型的池內存:分頁內存和非分頁內存。

分頁池內存可以從任何進程上下文中分配和訪問,但只能在中斷級別小於DPC/dispatch級別使用。正在使用的分頁池的數量由nt!ExpNumberOfPagedPools提供。在單處理器系統上,定義了四(4)個分頁池描述符,用nt中的索引1到4表示!ExpPagedPoolDescriptor數組。在多處理器系統上,每個節點定義一個分頁池描述符。在這兩種情況下,爲原型池/全頁分配定義了一個額外的分頁池描述符,在nt!ExpPagedPoolDescriptor中以索引0表示。因此,在大多數桌面系統中定義了五(5)個分頁池描述符。

此外,會話池內存(由win32k使用)用於會話空間分配,並且對於每個用戶會話都是惟一的。當非分頁會話內存使用全局非分頁池描述符時,分頁會話池內存在nt!MM_SESSION_SPACE中定義了自己的池描述符。爲了獲得會話池描述符,Windows 7解析相關的nt!EPROCESS結構去獲取繪畫空間結構,然後找到嵌入的分頁池描述符。

3. 內核池的描述符

與用戶模式堆非常相似,每個內核池都需要一個管理結構。池描述符負責跟蹤正在運行的分配的數量、正在使用的頁面和關於池使用的其他信息。它還幫助系統跟蹤可重用的池塊。池描述符由以下結構定義(nt!_POOL_DESCRIPTOR)。

typedef struct _POOL_DESCRIPTOR
{
/*0x000*/ enum _POOL_TYPE PoolType;
union {
/*0x004*/ struct _KGUARDED_MUTEX PagedLock;
/*0x004*/ ULONG32 NonPagedLock;
};
/*0x040*/ LONG32 RunningAllocs;
/*0x044*/ LONG32 RunningDeAllocs;
/*0x048*/ LONG32 TotalBigPages;
/*0x04C*/ LONG32 ThreadsProcessingDeferrals;
/*0x050*/ ULONG32 TotalBytes;
/*0x054*/ UINT8 _PADDING0_[0x2C];
/*0x080*/ ULONG32 PoolIndex;
/*0x084*/ UINT8 _PADDING1_[0x3C];
/*0x0C0*/ LONG32 TotalPages;
/*0x0C4*/ UINT8 _PADDING2_[0x3C];
/*0x100*/ VOID** PendingFrees;
/*0x104*/ LONG32 PendingFreeDepth;
/*0x108*/ UINT8 _PADDING3_[0x38];
/*0x140*/ struct _LIST_ENTRY ListHeads[512];
} POOL_DESCRIPTOR, *PPOOL_DESCRIPTOR;

池描述符包含內存管理器使用的幾個重要列表。pendingfrees指向的延遲釋放列表是等待釋放的池塊的單鏈接列表。第8節對此作了詳細說明。ListHeads是一個由相同大小的空閒池塊的雙鏈表組成的數組。與延遲的空閒列表不同,ListHeads列表中的塊已經被釋放,可以由內存管理器在任何時候分配。在下一節中,我們將討論ListHeads。

4. ListHeads Lists (Free Lists)

ListHeads列表(或空閒列表)按8字節粒度大小排序,用於分配高達4080字節的數據。空閒塊按塊大小索引到ListHeads數組中,計算爲請求的字節數四入到8的倍數併除以8,或BlockSize = (NumberOfBytes+0xF) >> 3。執行舍入是爲了爲池標頭保留空間,池標頭是位於所有池塊之前的結構。在x86 Windows上,池標頭的定義如下

typedef struct _POOL_HEADER
{
union {
struct {
/*0x000*/ UINT16 PreviousSize : 9;
/*0x000*/ UINT16 PoolIndex : 7;
/*0x002*/ UINT16 BlockSize : 9;
/*0x002*/ UINT16 PoolType : 7;
};
/*0x000*/ ULONG32 Ulong1;
};
union {
/*0x004*/ ULONG32 PoolTag;
struct {
/*0x004*/ UINT16 AllocatorBackTraceIndex;
/*0x006*/ UINT16 PoolTagHash;
};
};
} POOL_HEADER, *PPOOL_HEADER

池頭包含分配和自由算法正常運行所需的信息。PreviousSize指示前一個池塊的塊大小。由於內存管理器總是試圖通過合併邊界空閒塊來減少碎片,所以它通常用於定位前一個塊的池頭。PreviousSize的大小也可以是零,在這種情況下,池塊位於池頁面的開頭。

PoolIndex將索引提供到關聯的池描述符數組中,例如nt!ExpPagedPoolDescriptor。free算法使用它來確保池塊被釋放到正確的池描述符ListHeads。在第喫溢出攻擊的第四節中,我們將展示攻擊者如何破壞這個值,以便將池標頭損壞(例如池溢出)擴展爲任意內存損壞。

顧名思義,PoolType定義塊的池類型。但是,它還指示某個塊是繁忙的還是空閒的。如果塊是空閒的,則PoolType設置爲零。另一方面,如果一個塊很忙,PoolType被設置爲它的描述符的池類型(池類型enum中的值,如下所示),或者使用池中使用的位掩碼’ed。這個位掩碼在Vista和更高版本中被設置爲2,而在XP/2003中被設置爲4。例如,對於Vista和Windows 7中繁忙的分頁池塊,PoolType = PagedPool|2 = 3

typedef enum _POOL_TYPE
{
NonPagedPool = 0 /*0x0*/,
PagedPool = 1 /*0x1*/,
NonPagedPoolMustSucceed = 2 /*0x2*/,
DontUseThisType = 3 /*0x3*/,
NonPagedPoolCacheAligned = 4 /*0x4*/,
PagedPoolCacheAligned = 5 /*0x5*/,
NonPagedPoolCacheAlignedMustS = 6 /*0x6*/,
MaxPoolType = 7 /*0x7*/,
NonPagedPoolSession = 32 /*0x20*/,
PagedPoolSession = 33 /*0x21*/,
NonPagedPoolMustSucceedSession = 34 /*0x22*/,
DontUseThisTypeSession = 35 /*0x23*/,
NonPagedPoolCacheAlignedSession = 36 /*0x24*/,
PagedPoolCacheAlignedSession = 37 /*0x25*/,
NonPagedPoolCacheAlignedMustSSession = 38 /*0x26*/
} POOL_TYPE, *PPOOL_TYPE;

如果一個池塊是空閒的,並且在一個ListHeads列表中,它的池頭立即後面跟着一個LIST_ENTRY結構。由於這個原因,單塊大小(8字節)的塊不會由ListHeads維護,因爲它們不夠大,容納不了這個結構。

typedef struct _LIST_ENTRY
{
/*0x000*/ struct _LIST_ENTRY* Flink;
/*0x004*/ struct _LIST_ENTRY* Blink;
} LIST_ENTRY, *PLIST_ENTRY;

列表條目結構用於在雙鏈表上聯接池塊。歷史上,它一直是利用用戶模式堆[5]和內核池中的內存損壞漏洞的目標,這主要是由於衆所周知的“write-4”利用技術。隨着Windows XP SP2的發佈,微軟解決了用戶模式堆中的列表條目攻擊問題,同樣,在Windows 7的內核池中也解決了這個問題。

5. Lookaside Lists

內核使用單鏈接的lookaside (LIFO)列表來更快地分配和釋放小池塊。它們被設計用於操作高度一致的刪除條目。爲了更好地利用CPU緩存,在處理器控制塊(KPRCB)中每個處理器都定義了lookaside 列表。KPRCB結構保存分頁分配(Paged Lookaside List)和非分頁分配(PPNaged Lookaside List)的lookaside列表,以及用於頻繁請求的固定大小分配的專用後備列表(PPLookasideList)(例如I/O請求包和內存描述符列表,當前代碼並在添加和時使用原子比較和交換的指令)

typedef struct _KPRCB
{
...
/*0x5A0*/ struct _PP_LOOKASIDE_LIST PPLookasideList[16];
/*0x620*/ struct _GENERAL_LOOKASIDE_POOL PPNPagedLookasideList[32];
/*0xF20*/ struct _GENERAL_LOOKASIDE_POOL PPPagedLookasideList[32];
...
} KPRCB, *PKPRCB;

對於分頁和非分頁的lookaside列表,最大塊大小爲0x20。因此,每個類型有32個惟一的lookaside列表。每個lookaside列表由常規lookaside池結構定義,如下所示。

typedef struct _GENERAL_LOOKASIDE_POOL
{
union
{
/*0x000*/ union _SLIST_HEADER ListHead;
/*0x000*/ struct _SINGLE_LIST_ENTRY SingleListHead;
};
/*0x008*/ UINT16 Depth;
/*0x00A*/ UINT16 MaximumDepth;
/*0x00C*/ ULONG32 TotalAllocates;
union
{
/*0x010*/ ULONG32 AllocateMisses;
/*0x010*/ ULONG32 AllocateHits;
};
/*0x014*/ ULONG32 TotalFrees;
union
{
/*0x018*/ ULONG32 FreeMisses;
/*0x018*/ ULONG32 FreeHits;
};
/*0x01C*/ enum _POOL_TYPE Type;
/*0x020*/ ULONG32 Tag;
/*0x024*/ ULONG32 Size;
union
{
/*0x028*/ PVOID AllocateEx;
/*0x028*/ PVOID Allocate;
};
union
{
/*0x02C*/ PVOID FreeEx;
/*0x02C*/ PVOID Free;
};
/*0x030*/ struct _LIST_ENTRY ListEntry;
/*0x038*/ ULONG32 LastTotalAllocates;
union
{
/*0x03C*/ ULONG32 LastAllocateMisses;
/*0x03C*/ ULONG32 LastAllocateHits;
};
/*0x040*/ ULONG32 Future[2];
} GENERAL_LOOKASIDE_POOL, *PGENERAL_LOOKASIDE_POOL;

對於結構,SingleListHead。指向單鏈接lookaside列表上的第一個空閒池塊。後備列表(lookaside list)的大小受深度值的限制,由平衡集管理器根據後備列表上的命中次數和未命中次數定期調整。因此,經常使用的後備列表比不經常使用的列表具有更大的深度值。初始深度爲(nt!ExMinimumLookasideDepth),最大深度 爲MaximumDepth(256)。如果後備列表已滿,則池塊將被釋放到適當的ListHeads列表。

同時也爲會話(Session pool)池定義了後備列表。分頁會話池分配使用在會話空間中定義的獨立後備列表(nt!ExpSessionPoolLookaside)。每個會話後備列表的最大塊大小是0x19,由nt!ExpSessionPoolSmallLists設置。會話池後備列表使用常規後備池結構,與常規後備池相同,但有額外的填充。對於非分頁的會話池分配,使用前面討論的非分頁的每個處理器後備列表。

如果設置了熱/冷頁分離池標誌,則禁用池塊的後備列表(nt!ExpPoolFlags & 0 x100)。該標誌是在系統啓動期間設置的,以提高速度和減少內存佔用。計時器(在nt!ExpBootFinishedTimer中設置)在啓動後2分鐘關閉熱/冷頁面分離。

6. Large Pool Allocations

池描述符ListHeads維護的塊小於一個頁面。大於4080字節(需要一個頁面或更多)的池分配由nt!ExpAllocateBigPool處理。反過來,這個函數調用nt!MiAllocatePoolPages,池頁分配器,它將請求的大小舍入到最接近的頁大小。塊大小爲1頁並且Previous size 大小爲0的碎片塊被立即放置在大池分配之後,以便池分配器可以使用剩餘的頁片段。然後將多餘的字節放回適當的池描述符ListHeads列表的尾部。

回顧第1節,每個節點(由KNODE定義)都有4個與之關聯的單鏈接lookaside列表。池頁分配器在快速處理小頁計數請求時使用這些列表。對於分頁內存,KNODE爲單頁分配定義了一個lookaside列表(PagedPoolSListHead)。對於非分頁分配,定義了分頁計數1、2和3的lookaside列表(非pagedpoolslisthead[3])。池頁面lookaside列表的大小由系統中存在的物理頁面的數量決定。

如果不能使用後備列表,則使用分配位圖來獲取請求的池頁面。位圖(在RTL位圖中定義)是一個位數組,它指示正在使用哪些內存頁,併爲每個主要池類型創建。搜索第一個索引,該索引保存請求的未使用頁面的數量。對於分頁池,位圖在

MM_PAGED_POOL_INFO息結構中定義,由nt!MmPagedPoolInfo指向。對於非分頁池,位圖由nt!MiNonPagedPoolBitMap指向。對於會話池,位圖在MM_SESSION_SPACE結構中定義

對於大多數大池分配,nt!ExAllocatePoolWithTag將請求額外的4個字節(x64上爲8)來存儲池體末端的分配大小。隨後,當釋放分配(在ExFreePoolWithTag中)以捕獲可能的池溢出時,將檢查此值。

7. 分配算法

爲了分配池內存,內核模塊和第三方驅動程序調用了執行內核導出的ExAllocatepoolwithtag(或它的任何包裝器函數)。這個函數首先嚐試使用lookaside列表,然後是ListHeads列表,如果不能返回池塊,則從池頁分配器請求一個頁面。下面的僞代碼大致描述了它的實現。

PVOID
ExAllocatePoolWithTag( POOL_TYPE PoolType,
SIZE_T NumberOfBytes,
ULONG Tag)
// call pool page allocator if size is above 4080 bytes
if (NumberOfBytes > 0xff0) {
// call nt!ExpAllocateBigPool
}
// attempt to use lookaside lists
if (PoolType & PagedPool) {
if (PoolType & SessionPool && BlockSize <= 0x19) {
// try the session paged lookaside list
// return on success
}
else if (BlockSize <= 0x20) {
// try the per-processor paged lookaside list
// return on success
}
// lock paged pool descriptor (round robin or local node)
}
else { // NonPagedPool
if (BlockSize <= 0x20) {
// try the per-processor non-paged lookaside list
// return on success
}
// lock non-paged pool descriptor (local node)
}
// attempt to use listheads lists
for (n = BlockSize-1; n < 512; n++) {
if (ListHeads[n].Flink == &ListHeads[n]) { // empty
continue; // try next block size
}
// safe unlink ListHeads[n].Flink
// split if larger than needed
// return chunk
}
// no chunk found, call nt!MiAllocatePoolPages
// split page and return chunk

如果從ListHeads[n]列表返回一個大於所請求大小的塊,則對該塊進行分割。爲了減少碎片,分配器返回的過大塊的部分取決於它的相對頁面位置。如果塊是頁面對齊的,則從塊的前面部分切割分配請求的大小。如果塊沒有頁面對齊,則從塊的後面分配請求的大小。無論如何,分割塊的剩餘(未使用的)片段被放在適當的ListHeads列表的尾部。

8. 釋放算法

free算法由ExFreePoolWithTag實現,它檢查要釋放的塊的池頭並將其釋放到適當的列表中。爲了減少碎片,它還嘗試合併相鄰的空閒塊。下面的僞代碼展示了算法是如何工作的。

VOID
ExFreePoolWithTag( PVOID Entry,
ULONG Tag)
if (PAGE_ALIGNED(Entry)) {
// call nt!MiFreePoolPages
// return on success
}
if (Entry->BlockSize != NextEntry->PreviousSize)
BugCheckEx(BAD_POOL_HEADER);
if (Entry->PoolType & SessionPagedPool && Entry->BlockSize <= 0x19) {
// put in session pool lookaside list
// return on success
}
else if (Entry->BlockSize <= 0x20) {
if (Entry->PoolType & PagedPool) {
// put in per-processor paged lookaside list
// return on success
}
else { // NonPagedPool
// put in per-processor non-paged lookaside list
// return on success
} }
if (ExpPoolFlags & DELAY_FREE) { // 0x200
if (PendingFreeDepth >= 0x20) {
// call nt!ExDeferredFreePool
}
// add Entry to PendingFrees list
}
else {
if (IS_FREE(NextEntry) && !PAGE_ALIGNED(NextEntry)) {
// safe unlink next entry
// merge next with current chunk
}
if (IS_FREE(PreviousEntry)) {
// safe unlink previous entry
// merge previous with current chunk
}
if (IS_FULL_PAGE(Entry))
// call nt!MiFreePoolPages
else {
// insert Entry to ListHeads[BlockSize - 1]
} }

延遲釋放池標誌(nt!ExpPoolFlags & 0x200)支持一次釋放多個池分配的性能優化,以攤銷池獲取和釋放。僅當釋放的塊的標誌MmNumberOfPhysicalPages >= 0x1fc00時開啓延時釋放。如果列表包含32個或更多的塊(由PendingFreeDepth決定),它將在對ExDeferredFreePool的調用中被處理。這個函數遍歷每個條目並將其釋放到適當的ListHeads列表中,如下面的僞代碼所示。

VOID
ExDeferredFreePool( PPOOL_DESCRIPTOR PoolDesc,
BOOLEAN bMultipleThreads)
for each (Entry in PendingFrees) {
if (IS_FREE(NextEntry) && !PAGE_ALIGNED(NextEntry)) {
// safe unlink next entry
// merge next with current chunk
}
if (IS_FREE(PreviousEntry)) {
// safe unlink previous entry
// merge previous with current chunk
}
if (IS_FULL_PAGE(Entry))
// add to full page list
else {
// insert Entry to ListHeads[BlockSize - 1]
} }
for each (page in full page list) {
// call nt!MiFreePoolPages
}

釋放到lookaside和listhead總是放在適當列表的前面。(這個規則的例外是放在列表末尾的分割塊的剩餘片段。當內存管理器返回大於請求大小的塊(如2.7節所解釋的)時,塊就會被分割,例如在ExpBigPoolAllocation中被分割的完整頁面和在locatepoolwithtag中被分割的ListHeads條目。)爲了儘可能頻繁地使用CPU緩存,分配總是從最近使用的塊開始,從適當列表的前面開始。

9. AMD64/x64 Kernel Pool Changes

儘管支持更大的物理地址空間,但x64 Windows並沒有對內核池引入任何重大更改。但是,爲了適應指針寬度的變化,塊大小粒度增加到16字節,計算方法是BlockSize = (NumberOfBytes+0x1F) >> 4。爲了反映這一變化,將相應地更新池標頭。

typedef struct _POOL_HEADER
{
union
{
struct
{
/*0x000*/ ULONG32 PreviousSize : 8;
/*0x000*/ ULONG32 PoolIndex : 8;
/*0x000*/ ULONG32 BlockSize : 8;
/*0x000*/ ULONG32 PoolType : 8;
};
/*0x000*/ ULONG32 Ulong1;
};
/*0x004*/ ULONG32 PoolTag;
union
{
/*0x008*/ struct _EPROCESS* ProcessBilled;
struct
{
/*0x008*/ UINT16 AllocatorBackTraceIndex;
/*0x00A*/ UINT16 PoolTagHash;
/*0x00C*/ UINT8 _PADDING0_[0x4];
};
};
} POOL_HEADER, *PPOOL_HEADER;

由於塊大小粒度的變化,以前的塊大小和塊大小都減少到8位。因此,池描述符ListHeads包含256個雙鏈表,而不是x86上的512個。這還允許爲PoolIndex分配額外的位,因此在x64上可能支持256個節點(池描述符),而在x86上支持128個節點。此外,池標頭被擴展到16字節,幷包括配額管理中用於標識爲分配收費的進程的processbilling指針。在x86上,這個指針存儲在池體的最後四個字節中。我們將在第3.5節中討論利用配額進程指針的攻擊。

0x02:明日計劃

繼續翻譯並閱讀該論文的內核攻擊部分

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章