Joe1sn's Cabinet

【Win Pwn】Windows内核池笔记

文章来自blackhat 2021的文章《Windows-Heap-Backed-Pool-The-Good-The-Bad-And-The-Encoded》

原文链接:https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Windows-Heap-Backed-Pool-The-Good-The-Bad-And-The-Encoded.pdf

Youtube:https://www.youtube.com/watch?v=VvxNc8GTFfk

粗略的分配

内核动态内存:和R3中的对差不多

类型为可分页和不可分页,程序按照4KB分页,之前写过一点相关的:Joe1sn’s Cabinet | windows内核驱动 2-页表探索

旧API初始化中不会将内存置零,导致信息泄露

一些API

image-20240317095459904

在RS5(Redstone 5)版本前(及Windows 1809,在2018年10月前的版本)

image-20240317095835831

没有任何校验、加密等等,但是之后就变得复杂得多了(严重怀疑微软借鉴了glibc的内存管理)

image-20240317100321871

  • 新版本的池设计管理和R3下是同一个库
  • 每一个独立的池由SEGMENT_HEAP结构体管理,后文简写为SEG_HEAP
  • 池的申请根据其大小进行不同的处理
    • 不同大小申请机制不同
    • 大内存池仍然由VA管理

SEGMENT_HEAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
kd> dt nt!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : Uint4B
+0x014 GlobalFlags : Uint4B
+0x018 Interceptor : Uint4B
+0x01c ProcessHeapListIndex : Uint2B
+0x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : Uint8B
+0x028 UserContext : Ptr64 Void
+0x030 ReservedMustBeZero2 : Uint8B
+0x038 Spare : Ptr64 Void
+0x040 LargeMetadataLock : Uint8B
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : Uint8B
+0x060 LargeCommittedPages : Uint8B
+0x068 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : Uint2B
+0x0dc GlobalLockOwner : Uint4B
+0x0e0 ContextExtendLock : Uint8B
+0x0e8 AllocatedBase : Ptr64 UChar
+0x0f0 UncommittedBase : Ptr64 UChar
+0x0f8 ReservedLimit : Ptr64 UChar
+0x100 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x280 VsContext : _HEAP_VS_CONTEXT
+0x340 LfhContext : _HEAP_LFH_CONTEXT

文章选了一些重点来

image-20240317101151659

根据上图缩略结构体

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt nt!_SEGMENT_HEAP
....
+0x048 LargeAllocMetadata : _RTL_RB_TREE
....
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS //大内存页状态
....
+0x0e8 AllocatedBase : Ptr64 UChar //已分配内存
+0x0f0 UncommittedBase : Ptr64 UChar //未提交内存
+0x0f8 ReservedLimit : Ptr64 UChar
+0x100 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x280 VsContext : _HEAP_VS_CONTEXT //VS类型堆管理链表
+0x340 LfhContext : _HEAP_LFH_CONTEXT //KLFH类型堆管理链表

根据文章,SEG_HEAP通过两个SegContexts来处理前两种大小的内存(小堆和中堆)

  • 0~508KB(0-0x7F000)
  • 508KB~8128KB(0x7F000-0x7F0000)
  • 大于8128KB(大堆处理)

内核通过位图(bitmap,做过算法的应该知道吧)保存每次申请的大小的类型

1
nt!ExPoolState‐>HeapManager‐>AllocTracker‐>AllocTrackerBitma

关于位图

image-20240317102907777

  • 每两字节(2Byte)表示在内核内存中的地址
  • 位图存在三层

_HEAP_SEG_CONTEXT结构体:

SEG_HEAP

  • _HEAP_SEG_CONTEXT[0]:以 1 页的基本单位处理 1MB 段
  • _HEAP_SEG_CONTEXT[1]:以 16 页的基本单位处理 16MB 段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1: kd> dt nt!_HEAP_SEG_CONTEXT
+0x000 SegmentMask : Uint8B
+0x008 UnitShift : UChar
+0x009 PagesPerUnitShift : UChar
+0x00a FirstDescriptorIndex : UChar
+0x00b CachedCommitSoftShift : UChar
+0x00c CachedCommitHighShift : UChar
+0x00d Flags : <anonymous-tag>
+0x010 MaxAllocationSize : Uint4B
+0x014 OlpStatsOffset : Int2B
+0x016 MemStatsOffset : Int2B
+0x018 LfhContext : Ptr64 Void
+0x020 VsContext : Ptr64 Void
+0x028 EnvHandle : RTL_HP_ENV_HANDLE
+0x038 Heap : Ptr64 Void
+0x040 SegmentLock : Uint8B
+0x048 SegmentListHead : _LIST_ENTRY
+0x058 SegmentCount : Uint8B
+0x060 FreePageRanges : _RTL_RB_TREE
+0x070 FreeSegmentListLock : Uint8B
+0x078 FreeSegmentList : [2] _SINGLE_LIST_ENTRY

简化一下重要信息

1
2
3
4
5
6
1: kd> dt nt!_HEAP_SEG_CONTEXT
+0x000 SegmentMask : Uint8B //如何从池内存到当前段
.....
+0x048 SegmentListHead : _LIST_ENTRY //连接所有已分配的堆
.....
+0x078 FreeSegmentList : [2] _SINGLE_LIST_ENTRY //链接所有已释放的堆

段(segment)链接的结构体为`HEAP_PAGE_SEGMENT

_HEAP_PAGE_SEGMENT结构体

1
2
3
4
5
6
0: kd> dt nt!_HEAP_PAGE_SEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Signature : Uint8B
+0x018 SegmentCommitState : Ptr64 _HEAP_SEGMENT_MGR_COMMIT_STATE
+0x020 UnusedWatermark : UChar
+0x000 DescArray : [256] _HEAP_PAGE_RANGE_DESCRIPTOR

这里抹除类型,给点注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0: kd> dt nt!_HEAP_PAGE_SEGMENT
+0x000 ListEntry : //结构体`SEG_CONTECT`指向,可参考下面的图
+0x010 Signature : //可以找到`SEG_CONTEXT`,但是被异或加密了
//解密方法 `段地址 ^ 堆密钥(heap key) ^ 魔数`
........
+0x000 DescArray : [256] //每个保存一个单元,还有子段(subseg)类型和偏移
// +0x000 TreeNode : _RTL_BALANCED_NODE
// +0x000 TreeSignature : Uint4B
// +0x004 UnusedBytes : Uint4B
// +0x008 ExtraPresent : Pos 0, 1 Bit
// +0x008 Spare0 : Pos 1, 15 Bits
// +0x018 RangeFlags : UChar
// +0x019 CommittedPageCount : UChar
// +0x01a Spare : Uint2B
// +0x01c Key : _HEAP_DESCRIPTOR_KEY
// +0x01c Align : [3] UChar
// +0x01f UnitOffset : UChar
// +0x01f UnitSize : UChar

总结下当前的内核相关信息,从SEG_HEAP开始

image-20240317103818286

关于两种大小的内存管理

两种子段 VS和LFH

1. LFH - Low Fragmentation Heap

_HEAP_LFH_SUBSEGMENT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: kd> dt nt!_HEAP_LFH_SUBSEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Owner : Ptr64 _HEAP_LFH_SUBSEGMENT_OWNER
+0x010 DelayFree : _HEAP_LFH_SUBSEGMENT_DELAY_FREE
+0x018 CommitLock : Uint8B
+0x020 FreeCount : Uint2B
+0x022 BlockCount : Uint2B
+0x020 InterlockedShort : Int2B
+0x020 InterlockedLong : Int4B
+0x024 FreeHint : Uint2B
+0x026 Location : UChar
+0x027 WitheldBlockCount : UChar
+0x028 BlockOffsets : _HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS
+0x02c CommitUnitShift : UChar
+0x02d CommitUnitCount : UChar
+0x02e CommitStateOffset : Uint2B
+0x030 BlockBitmap : [1] Uint8B
  • 用于 129 种常见大小的分配

    image-20240317112023052

  • 所有分配的子段有一样的大小

  • 最大大小是0x4000

  • 为了节省空间,没有独特的head

  • 堆块的状态由head中的位图决定

2. VS - Variable Size

_HEAP_VS_SUBSEGMENT

1
2
3
4
5
6
7
0: kd> dt nt!_HEAP_VS_SUBSEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 CommitBitmap : Uint8B
+0x018 CommitLock : Uint8B
+0x020 Size : Uint2B
+0x022 Signature : Pos 0, 15 Bits
+0x022 FullCommit : Pos 15, 1 Bit
  • 处理所有LFH不能处理的bucket
  • 没有个块(block)都有一个自己的头描述

两者的对比

image-20240317110710922

  • VS头中的大小使用heap key加密(其实是编码,这里为了语义通顺),LFH使用自己的LFH Key 加密。(这显然让堆利用变得困难,你猜为啥我要反过来读这篇文章而不是直接做HEVD)
  • 子段大小不固定
  • 子段包含多个页,必须从头开始计算偏移去寻找(单独寻找某个页已经几乎不可能)
  • 已分配的块仍然会存在POOL_HEAD,但是释放掉的不会

相关的“加密"

  • HEAP_LFH_SUBSEGMENT
    1. 块大小和偏移量位于编码的 BlockOffsets 字段中
    2. 数据(Data) = EncodedData ^ LfhKey ^ ((ULONG)(Subsegment) >> 12)
    3. 子段列表没被加密
  • HEAP_VS_SUBSEGMENT
    1. 用于链接子段的列表被当前子段地址加密了
  • HEAP_VS_CHUNK_HEADER
    1. 每一个VS子段中头存在
    2. 块大小和分配状态在Size头中被加密
    3. 数据(Data)= Sizes.HeaderBits ^ HeapKey ^ ChunkHeader

两种上下文

最开始的SEG_HEAP中有

1
2
+0x280 VsContext        : _HEAP_VS_CONTEXT
+0x340 LfhContext : _HEAP_LFH_CONTEXT

_HEAP_LFH_CONTEXT

1
2
3
4
5
6
7
8
9
10
11
0: kd> dt nt!_HEAP_LFH_CONTEXT
+0x000 BackendCtx : Ptr64 Void
+0x008 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0x030 AffinityModArray : Ptr64 UChar
+0x038 MaxAffinity : UChar
+0x039 LockType : UChar
+0x03a MemStatsOffset : Int2B
+0x03c Config : _RTL_HP_LFH_CONFIG
+0x040 BucketStats : _HEAP_LFH_SUBSEGMENT_STATS
+0x048 SubsegmentCreationLock : Uint8B
+0x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET

注释关键信息

1
2
3
4
5
6
7
0: kd> dt nt!_HEAP_LFH_CONTEXT
...
+0x008 Callbacks : //子堆分配回调函数
......
+0x080 Buckets : [129] //前面提到过LFH处理常见129种堆
//这里的bucket就存放每个堆的列表
//保存有关块大小、子段计数、块计数的数据

_HEAP_VS_CONTEXT

1
2
3
4
5
6
7
8
9
10
11
12
0: kd> dt nt!_HEAP_VS_CONTEXT
+0x000 Lock : Uint8B
+0x008 LockType : _RTLP_HP_LOCK_TYPE
+0x010 FreeChunkTree : _RTL_RB_TREE
+0x020 SubsegmentList : _LIST_ENTRY
+0x030 TotalCommittedUnits : Uint8B
+0x038 FreeCommittedUnits : Uint8B
+0x040 DelayFreeContext : _HEAP_VS_DELAY_FREE_CONTEXT
+0x080 BackendCtx : Ptr64 Void
+0x088 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0x0b0 Config : _RTL_HP_VS_CONFIG
+0x0b4 Flags : Uint4B

注释关键信息

1
2
3
4
5
6
7
8
0: kd> dt nt!_HEAP_VS_CONTEXT
......
+0x020 SubsegmentList : //所有VS类型的子段
+0x030 TotalCommittedUnits : //计数
+0x038 FreeCommittedUnits : //计数
......
+0x088 Callbacks : //allocate, free, commit等子段回调函数
......

总结下两种上下文

image-20240317112721755