Windows內核漏洞學習-內核池原理.md

0x00:前言

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

0X01:正文

1 內核池的基本屬性

  • 內核池被分爲 Non-Paged Pools, Paged Pools, Session Pools等類型。

每個池都由一個池描述符所定義

  • 該結構爲 POOL_DESCRIPTOR

  • 通過該結構我們可以追蹤有多少池處於分配或釋放狀態,或該池所在的頁面是否被使用。

  • 該結構維護着保存着被釋放的chunk的列表。

分頁和非分頁的池被定義在nt!PoolVector數組中。

  • 該數組的每個索引指向一個描述符的數組

2 內核池的描述符

kd> dt nt!_POOL_DESCRIPTOR
   +0x000 PoolType         : _POOL_TYPE
   +0x004 PagedLock        : _KGUARDED_MUTEX
   +0x004 NonPagedLock     : Uint4B
   +0x040 RunningAllocs    : Int4B
   +0x044 RunningDeAllocs  : Int4B
   +0x048 TotalBigPages    : Int4B
   +0x04c ThreadsProcessingDeferrals : Int4B
   +0x050 TotalBytes       : Uint4B
   +0x080 PoolIndex        : Uint4B
   +0x0c0 TotalPages       : Int4B
   +0x100 PendingFrees     : Ptr32 Ptr32 Void
   +0x104 PendingFreeDepth : Int4B
   +0x140 ListHeads        : [512] _LIST_ENTRY

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

在NUMA系統中,處理器和內存如果被分配在一個小的單位裏,該單位就被稱爲節點。

  • 當本地內存在使用中,訪問節點的速度相比訪問其他內存會更快一些。

每個內核池都儘可能的從最理想的節點裏分配內存給進程。

  • 大部分的桌面系統僅僅只有一個節點。

每個節點被KNODE數據結構定義

  • nt!KeNodeBlock數組中存儲的數據指向所有的KNODE結構
  • 大部分的進程可以被鏈接在同一個節點中。

我們可以在WIinDbg裏打印出NUMA的信息。

kd> !numa
NUMA Summary:
------------
    Number of NUMA nodes : 1
    Number of Processors : 1
    MmAvailablePages     : 0x000262D3
    KeActiveProcessors   :
    *------------------------------- (00000001)

    NODE 0 (FFFFFFFF83F3E300):
	Group            : 1 (Assigned, Committed, Assignment Adjustable)
	ProcessorMask    :  (1)
	ProximityId      : 0
	Capacity         : 1
	Seed             : 0x00000000
	Color            : 0x00000000
	MmShiftedColor   : 0x00000000
	Zeroed Page Count: 0x0000000000000000
	Free Page Count  : 0x0000000000000000

4 NUMA節點數據結構(Win7 RTM x86)

kd> dt nt!_KNODE
   +0x000 PagedPoolSListHead : _SLIST_HEADER
   +0x008 NonPagedPoolSListHead : [3] _SLIST_HEADER
   +0x020 Affinity         : _GROUP_AFFINITY
   +0x02c ProximityId      : Uint4B
   +0x030 NodeNumber       : Uint2B
   +0x032 PrimaryNodeNumber : Uint2B
   +0x034 MaximumProcessors : UChar
   +0x035 Color            : UChar
   +0x036 Flags            : _flags
   +0x037 NodePad0         : UChar
   +0x038 Seed             : Uint4B
   +0x03c MmShiftedColor   : Uint4B
   +0x040 FreeCount        : [2] Uint4B
   +0x048 CachedKernelStacks : _CACHED_KSTACK_LIST
   +0x060 ParkLock         : Int4B
   +0x064 NodePad1         : Uint4B

5 非換頁池(Non-Paged Pool)

不可分頁的系統內存

  • 這些內存被保證始終駐留在物理內存中

非換頁內存池的數量被保存在 nt!ExpNumberOfNonPagedPools

在單核處理器系統上,nt!PoolVector數組的第一個索引指向未分頁的池描述符。

  • kd> dt nt!_POOL_DESCRIPTOR poi(nt!PoolVector)

在覈處理器系統中,每個節點有自己的非換頁內存池描述符。

  • 存儲在 nt!ExpNonPagedPoolDescriptor 數組中

6 可分頁池(Paged Pool)

可分頁池的內存

  • 只能在中斷級別小於 DPC/Dispatch級別的情況下訪問。

可分頁池的數量被定義在 nt!ExpNumberOfPagedPools

在單核處理器系統中,4個可分頁池佔據一個描述符。

  • nt!ExpPagedPoolDescriptor 的1到4索引中。

在多核處理器系統中,每個可分頁池描述符被定義在每個節點中。

另外單獨定義了一個分頁池描述符原型池/全頁分配。

  • 保存在 nt!ExpPagedPoolDescriptor 的 索引0中。

7 池描述符中的Free Lists(x86)

每個池描述符有一個長度爲512的數組結構ListHeads,每個索引都是一個雙鏈表結構,保存着一樣大小的可分配的塊。如圖所示:
在這裏插入圖片描述

  • 每個鏈表結構保存的塊大小以8 Byte 遞增。
  • 用於最多4080字節的分配

可分配的塊被保存在該數組中,數組下標與塊大小的對應關係爲

  • index = BlockSize(NumbBytes + 0xF)>>3 即 index = BlockSize/8

每個池塊頭都存放着一個8Byte大小的池頭。

8 內存池頭(x86)

  • kd> dt nt!_POOL_HEADER
       +0x000 PreviousSize     : Pos 0, 9 Bits
       +0x000 PoolIndex        : Pos 9, 7 Bits
       +0x002 BlockSize        : Pos 0, 9 Bits
       +0x002 PoolType         : Pos 9, 7 Bits
       +0x000 Ulong1           : Uint4B
       +0x004 PoolTag          : Uint4B
       +0x004 AllocatorBackTraceIndex : Uint2B
       +0x006 PoolTagHash      : Uint2B
    
  • PreviousSize: 前一個塊的大小(BlockSize)。

  • PoolIndex:在關聯的池描述符數組中的索引

  • BlockSize:(NumberOfBytes+0xF)>>3

  • PooltType:Free=0,Allocated=(PoolType|2)

  • PoolTag:4個可打印的字節去標識這個內存,一般在編程中都是我們自定義的。

9 內存池頭(x64)

  • kd> dt nt!_POOL_HEADER
    +0x000 PreviousSize		: Pos 0, 8 Bits
    +0x000 PoolIndex		: Pos 8, 8 Bits
    +0x000 BlockSize		: Pos 16, 8 Bits
    +0x000 PoolType			: Pos 24, 8 Bits
    +0x004 PoolTag			: Uint4B
    +0x008 ProcessBilled	: Ptr64 _EPROCESS
    
  • BlockSize:(NumberOfBytes + 0x1F)>>4

    • 256個節點的 ListHeads,每個節點之前的遞增距離爲16byte。
  • ProcessBilled:指向池分配的進程管理對象(在配額管理中使用)

10 (Free)釋放池塊

如果一個池塊被釋放到 ListHead 列表中,它的塊頭都跟在一個LINK_ENTRY結構後。

  • ListHead中的雙鏈表指向。

  • kd> dt nt!_LIST_ENTRY
       +0x000 Flink            : Ptr32 _LIST_ENTRY
       +0x004 Blink            : Ptr32 _LIST_ENTRY
    

在這裏插入圖片描述

11 lookaside 列表

內核使用 lookaside 列表用於快速分配和回收小的chunk。

  • 它是一個後進先出的列表
  • 具有性能優化的作用。比如它回收時沒有任何檢查。

爲可分頁和不可分也的內存分配單獨列出每個處理器的 lookaside 列表

  • 它定義在處理器的控制塊中(KPRCB)
  • 最大的塊大小爲0x20(256 bytes)
  • 塊大小以8 byte大小遞增,因此lookaside 共有32個節點。

每個 lookaside 列表被定義在 GENERAL_LOOKASIDE_POOL 結構中。

  • kd> dt _GENERAL_LOOKASIDE_POOL
    nt!_GENERAL_LOOKASIDE_POOL
       +0x000 ListHead         : _SLIST_HEADER
       +0x000 SingleListHead   : _SINGLE_LIST_ENTRY
       +0x008 Depth            : Uint2B
       +0x00a MaximumDepth     : Uint2B
       +0x00c TotalAllocates   : Uint4B
       +0x010 AllocateMisses   : Uint4B
       +0x010 AllocateHits     : Uint4B
       +0x014 TotalFrees       : Uint4B
       +0x018 FreeMisses       : Uint4B
       +0x018 FreeHits         : Uint4B
       +0x01c Type             : _POOL_TYPE
       +0x020 Tag              : Uint4B
       +0x024 Size             : Uint4B
       +0x028 AllocateEx       : Ptr32     void* 
       +0x028 Allocate         : Ptr32     void* 
       +0x02c FreeEx           : Ptr32     void 
       +0x02c Free             : Ptr32     void 
       +0x030 ListEntry        : _LIST_ENTRY
       +0x038 LastTotalAllocates : Uint4B
       +0x03c LastAllocateMisses : Uint4B
       +0x03c LastAllocateHits : Uint4B
       +0x040 Future           : [2] Uint4B
    
    

每個處理器中該 lookaside 列表結構,可以看出它爲單向鏈表。

在這裏插入圖片描述

在會話(Session)中。
在這裏插入圖片描述

12 專用的 Lookaside 列表

NT內核中經常分配的(固定大小的)緩衝區有專門的 lookaside 列表。

  • 對象信息創建
  • IO請求包
  • 內存描述列表

定義處理器控制塊(KPRCP)

  • 16個 PP_LOOKASIDE_LIST 結構,每個定義一個處理器和系統範圍列表。

13 大一點的池的分配(Large Pool)

分配超過 0xff0(4080 bytes)大小的塊。

通過函數 nt!ExpAllocateBigPool 來創建句柄。

  • 在 內部調用 nt!MiAllocatePoolPages
    • 請求的大小四捨五入到最接近的頁面大小。
  • 多餘的字節被放回適當的池描述符ListHeadslist的末尾

每個節點(如處理器)擁有4個單獨的單鏈表 lookaside 列表來分配大的池塊。

  • 1個用於分配可換頁的頁面。
  • 3個用於不可換頁的內存的的分配。
  • 定義在 KNODE 中。

如果 lookside 列表不可用,(bitmaps)分配位圖用於獲取請求的池頁面。

  • 位(bit)數組指示出了哪個頁面正在使用中。
  • RTL_BITMAP 結構定義

位圖將搜索保存請求的未使用頁面數量的第一個索引

位圖對每個主要的池類型用它自己的指定的內存定義。

  • 比如 nt!MiNonPagedPoolBitMap

位數組存放在一個池內存範圍的開始處。

14 位圖搜索(簡化版)

在這裏插入圖片描述

15 分配算法

內核導出幾個分配函數供內核模塊和驅動程序使用

所有到處的內核池分配例程本質上都是 ExAllocatePoolWithTag 的包裝。

分配算法返回一個free狀態的chunk,搜索順序爲

  • Lookaside list
  • ListHeads list
  • Pool page allocator

Windows 7 展示了一個安全的從free鏈表上unlink一個chunk的做法。

在這裏插入圖片描述

可以看出該做法與Linux上一致,首先檢查後一個chunk的Blink是否指向自己,再檢查前一個chunk的Flink是否指向自己。如果都指向自己 ,則進行安全unlink。將自己的node->Flink->Blink改爲node->Blink,將node->Blink->Flink = node->Flink

16 ExAllocatePoolWithTag

該函數定義爲

PVOID ExAllocatePoolWithTag(POOL_TYPE PoolType,SIZE_T NumberOfBytes,ULONG Tag)

if NumberOfBytes>0xff0

  • 調用 nt!ExpAllocateBigPool

如果請求的內存類型爲可分頁類型

  • if(PoolType & SessionPoolMask) and BlockSize<=0x19
    • 嘗試在 session paged lookaside 列表中尋找
    • 返回成功
  • else if BlockSize <=0x20
    • 嘗試在處理器的可分頁類型的 lookadside 列表中找
  • 嘗試並鎖定分頁池描述符(輪詢)

Else 請求的類型爲不可分頁類型

  • if BlockSize <= 0x20
    • 嘗試在每個處理器的不可分頁類型的 lookaside 列表 中 尋找
    • 返回成功
  • 嘗試並鎖定不可分頁類型池描述符(輪詢)

使用當前鎖定池的 ListHeads。

  • For n in range(BlockSize,512)
    	if ListHeads[n] is empty,try next Blocksize
    	Safe unlink first entry and split if larger han needed
    	Return on success
    if failed,通過添加頁來擴張池
    	call nt!MiAllocatePoolPages
    	分解它
    

17 分解池塊

如果從ListHead[n]中返回了一個比較大的塊,超出了我們的需求,那麼這個塊會被分解。

  • 如果這個塊是對齊的,那麼我們請求的塊大小就會從這個塊的前面開始分解。
  • 如果這個塊不是對齊的,那麼我們請求的塊大小就會從這個塊的尾部分解。

剩餘的片段會被添加到適當的ListHeads[n]尾部。

在這裏插入圖片描述

18 釋放算法

釋放算法通過檢查池頭來將被釋放的chunk分配到適當的列表中去。

  • 通過函數 ExFreePoolWithTag 實現。

如果釋放的塊旁邊有空閒的塊,可能會將這兩個塊合併以減少碎片空間。

  • Windows 7 使用安全的unlinking來完成合並過程。

19 ExFreePoolWithTag

函數定義爲

VOID ExFreePoolWithTag(PVOID Address,ULONG Tag)
if Address(chunk) 爲頁對齊
	Call nt!MiFreePoolPages
if Chunk->BlockSize != NextChunk->PreviousSize
	Bug處理
if(PoolType & PagedPoolSession) and BlockSize <=0x19
	插入到 session pool lookaside list中
els if  BlockSize <=0x20 and pool is local to processor
	if(PooltType & PagedPool)
		插入到 per-processor 可換頁 lookaside 中
	else(NonPagedPool)
		插入到 per-processor 不可換頁 lookaside 中
return success
if DELAY_FREE 池標誌被set
	if 等待釋放的塊數目 >=0x20
		call nt!ExDeferredFreePool
	添加至等待釋放的列表(單鏈表)
else
	if 下一個chunk 是free狀態 並且沒有頁對齊
		安全的 unlink 並且與當前chunk合併
	if 前一個chunk是free狀態
		安全的 unlink 並且與當前chunk合併
	if 該chunk是一個滿頁
		call nt!MiFreePoolPages
	else
		添加到適當的 ListHeads 列表頭中

20 合併塊

在這裏插入圖片描述

21 釋放延遲塊

是一種性能優化的方法,一次性釋放多個塊來攤銷多次釋放的消耗。

僅當 MmNumberOfPhysicalPages >= 0x1fc00時開啓

  • 等效於508MB的RAM

每次調用 ExFreePoolWithTag 時候添加一個池塊到一個單鏈表形式的延遲釋放列表中。

  • 當前延遲塊的數目保存啊在 PendingFreeDepth
  • 如果延遲塊的數目超過32,則由函數 ExDeferredFreePool進行釋放處理

22 ExDeferredFreePool

VOID ExDeferredFreePool(PPOOL_DESCRIPTOR PoolDescriptor, BOOLEAN bMultiThreaded){
    For each entry on pending frees list
    	if next chunk is fee and not page aligned
    		合併
    	if previous chunk if free
    		合併
    	if 處理的塊是一整頁
    		添加到 full page list
    	else
    		添加到適當的 ListHeads 列表頭
    For each page in full page list
    	Call nt!MiFreePoolPages
}

23 釋放池塊的順序

釋放到 lookaside 和 pool descriptor Listheads 的塊總是放在適當的列表前部

  • 但是如果是被切割剩餘的塊通常會放到尾部
  • 如果被分配的塊大於申請的塊的時候,該塊就會被切割。
    • 完整的頁切割,使用函數 ExpBigPoolAllocation
    • ListHeads[n] 裏的塊使用 ExAllocatePooolWithTag進行切割

總是從最近最常使用的塊裏進行分配,總是從列表的頭部開始尋找最合適的塊。

0x02:明日計劃

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

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