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的内核攻击部分

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