Post

About Kernel Segment Heap

About Kernel Segment Heap

Target audience: Windows kernel security researchers, anti-cheat developers, kernel driver developers

Reference environment: Windows 10 19H1 (1903) ~ Windows 11 (x64)

Sources: BlackHat USA 2016/2021, SSTIC 2020, Microsoft MSRC, BlueFrost Security, Angelboy (scwuaptx)


1. Background and Introduction Timeline

1.1 Timeline

1
2
3
4
5
6
7
Windows NT ~ 1809   : Legacy NT Pool Manager (ExAllocatePoolWithTag)
Windows 10 19H1     : Kernel Segment Heap introduced (March 2019, build 1903)
                      └─ User-mode Segment Heap ported to the kernel
Windows 10 2004     : ExAllocatePool2 / ExAllocatePool3 added
                      └─ ExAllocatePoolWithTag officially deprecated
Windows 10 20H2~    : Dynamic KDP (Kernel Data Protection) stabilized
Windows 11          : VBS/HVCI enabled by default; Secure Pool usage expanded

Common misconception: Many sources claim “the Segment Heap was introduced in Windows 10 2004,” but the kernel segment heap was actually introduced in 19H1 (1903). Windows 10 2004 is the release that added the new Pool APIs built on top of the segment heap.

1.2 Motivation

Limitations of the legacy NT Pool Manager:

  • Every chunk had a plaintext inline header in front of it, making metadata corruption trivial.
  • Allocation sizes and positions were predictable, making pool spray attacks straightforward.
  • Allocated memory was not zero-initialized by default, frequently leading to uninitialized memory disclosure vulnerabilities.

2. Legacy NT Pool Structure

2.1 _POOL_HEADER (before 19H1)

1
2
3
4
5
6
7
8
9
10
11
12
┌──────────┬────────────────┬─────────┬───────────────────────────┐
│  Offset  │  Field         │  Size   │  Description              │
├──────────┼────────────────┼─────────┼───────────────────────────┤
│  0x00    │  PoolIndex     │  1 B    │  Pool descriptor index    │
│  0x01    │  PreviousSize  │  1 B    │  Previous chunk size      │
│  0x02    │  PoolType      │  1 B    │  Pool type (Paged, etc.)  │
│  0x03    │  BlockSize     │  1 B    │  Current chunk size (>>4) │
│  0x04    │  PoolTag       │  4 B    │  4-byte ASCII tag         │
│  0x08    │  ProcessBilled │  8 B    │  KPROCESS pointer         │
│          │  (union)       │         │  (valid only with PoolQuota) │
└──────────┴────────────────┴─────────┴───────────────────────────┘
  Total size: 16 bytes (x64)

Memory layout:

1
2
[POOL_HEADER 16B][user data ...][POOL_HEADER 16B][user data ...]
      ↑ plaintext, predictable        ↑ adjacent chunk header → overwritable

2.2 Security Weaknesses of the Legacy Structure

TechniqueDescription
Pool WalkingTraverse adjacent chunks linearly by following BlockSize in the inline header to locate kernel objects
Pool OverflowCorrupt an adjacent chunk’s _POOL_HEADER to gain an arbitrary write primitive on free
PoolIndex OverwriteCraft a malicious PoolIndex to trigger an OOB dereference into the pool descriptor array
ProcessBilled OverwriteWith the PoolQuota flag set, dereference ProcessBilled during the free path → arbitrary address dereference primitive

Windows 8 partial mitigation: ExpPoolQuotaCookie was introduced to XOR-encode the ProcessBilled pointer: ProcessBilled = KPROCESS_PTR ^ ExpPoolQuotaCookie ^ CHUNK_ADDR

However, the fundamental issue of a plaintext _POOL_HEADER remained until 19H1.


3. Segment Heap Architecture Overview

3.1 Core Structure: _SEGMENT_HEAP

Each pool type (NonPagedNx, Paged, PagedSession, etc.) is managed by its own independent _SEGMENT_HEAP instance.

1
2
3
4
5
6
7
8
9
10
11
12
_SEGMENT_HEAP (kernel offsets, based on 20H2)
┌──────────┬──────────────────────────────────────────────────────────┐
│  0x000   │  EnvHandle (10 B)   — heap environment handle            │
│  0x010   │  Signature (4 B)    — always 0xDDEEDDEE                  │
│  0x028   │  UserContext (8 B)                                        │
│  0x048   │  AllocatedBase (8 B) — LFH structure allocation base     │
│  0x058   │  SegContexts[2] (0x180 B) — segment context array        │
│  0x100   │  VsContext (0xC0 B)  — VS allocator context              │
│  0x280   │  LfhContext (0x4C0 B) — LFH allocator context            │
│  higher  │  LargeAllocMetadata  — large allocation metadata         │
│  higher  │  LargeReservedPages / LargeCommittedPages                 │
└──────────┴──────────────────────────────────────────────────────────┘

3.2 _SEGMENT_HEAP Instances Per Pool Type

1
2
3
4
5
6
nt!PoolVector (HEAP_POOL_NODES)
├── NonPagedPool (NP)       → _SEGMENT_HEAP instance #1
├── NonPagedPoolNx (NPNx)   → _SEGMENT_HEAP instance #2   ← primary target
├── PagedPool (PP)          → _SEGMENT_HEAP instance #3
├── PagedPoolSession        → _SEGMENT_HEAP stored in current thread
└── (other special pools)

3.3 Allocation Routing Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ExAllocatePoolWithTag / ExAllocatePool2 / ExAllocatePool3
        │
        ▼
  ExAllocateHeapPool (internal to ntoskrnl)
        │
        ├─ size ≤ 0x200 AND LFH activated  ──▶  kLFH
        │   └─ RtlpHpLfhContextAllocate
        │
        ├─ 0x1e1 ≤ size ≤ 0xfe0            ──▶  VS Allocator
        │   └─ RtlpHpVsContextAllocateInternal
        │
        ├─ page-aligned size (0x20000~0x7f0000) ──▶ Segment Allocator
        │   └─ RtlpHpSegAlloc
        │
        └─ large allocation (> 0x7f0000)   ──▶  Large Allocator
            └─ RtlpHpLargeAlloc

4. The Four Allocation Paths

4.1 kLFH (Low Fragmentation Heap)

PropertyDetails
Size range≤ 0x200 bytes (512 B), when LFH is activated for that size class
Activation conditionAutomatically activated after 18 consecutive allocations of the same size
Key functionRtlpHpLfhContextAllocate
Chunk header_POOL_HEADER (16 B, still present)
Metadata location_HEAP_LFH_SUBSEGMENT structure (isolated, not inline)
Bucket count129 (Buckets[129])

LFH bucket structure:

1
2
3
4
5
6
7
_HEAP_LFH_CONTEXT
└── Buckets[129]
     ├── Bucket #0:  size 1~8 B
     ├── Bucket #1:  size 9~16 B
     ├── ...
     └── Bucket #128: size ~0x1FF0B
         (each bucket has AffinitySlots → _HEAP_LFH_SUBSEGMENT)

kLFH security properties:

  • Block placement within a subsegment is randomized, making adjacent chunk prediction difficult.
  • The next allocation position is managed through _HEAP_LFH_SUBSEGMENT.FreeHint, which is encoded with LfhKey.
  • Even if an adjacent chunk overflows, it is difficult to directly corrupt the management structure (_HEAP_LFH_CONTEXT).

4.2 VS Allocator (Variable Size)

PropertyDetails
Size range(a) ≤ 0x1e0 && LFH inactive; (b) 0x1e1~0xfe0; (c) 0x1001~0xffff && non-page-aligned
Key functionRtlpHpVsContextAllocateInternal
Chunk header_HEAP_VS_CHUNK_HEADER (16 B, HeapKey XOR encoded)
Free chunk managementRed-Black Tree (FreeChunkTree)
Allocation algorithmBest-fit

VS chunk header structure:

1
2
3
4
5
6
7
8
9
10
11
_HEAP_VS_CHUNK_HEADER (allocated state)
┌──────────────────────────────────────────────────────────────┐
│  Sizes (8 B)  — XOR encoded: HeaderBits ^ self_addr ^ HeapKey │
│    ├─ UnsafeSize      : chunk size / 16                       │
│    ├─ UnsafePrevSize  : previous chunk size / 16              │
│    ├─ MemoryCost      : number of pages occupied              │
│    └─ UnusedBytes     : whether unused bytes exist            │
│  EncodedSegmentPageOffset (1 B)                               │
│    — (self_addr ^ self ^ HeapKey) & 0xFF                      │
│    — page distance to the start of the VS subsegment          │
└──────────────────────────────────────────────────────────────┘

Memory layout:

1
2
[_HEAP_VS_CHUNK_HEADER 16B][_POOL_HEADER 16B][user data ...]
       ↑ HeapKey XOR         ↑ PoolTag etc. still present

VS subsegment structure:

1
2
3
4
5
6
_HEAP_VS_SUBSEGMENT
├── ListEntry      — subsegment linked list
├── CommitBitmap   — page commit state bitmap
├── CommitLock     — lock used during commit
├── Size (2 B)     — subsegment size (>> 4)
└── Signature (15 bit) + FullCommit (1 bit) — integrity check

4.3 Segment Allocator (Backend)

PropertyDetails
Size range #10x20000 < size ≤ 0x7f000 (128 KB ~ 508 KB)
Size range #20x7f000 < size ≤ 0x7f0000 (508 KB ~ ~7 GB)
Core structure_HEAP_PAGE_SEGMENT + 256 page descriptors
Segment mask0xFFFFFFFFFFF00000

Unlike the user-mode segment heap which uses a single Segment Allocation Context, the kernel segment heap uses two independent SegContexts depending on the size range.

Page segment signature encoding:

1
2
_HEAP_SEG_CONTEXT signature verification:
(page_segment) ^ (page_segment->Signature) ^ 0xA2E64EADA2E64EAD ^ RtlpHpHeapGlobals.HeapKey

4.4 Large Allocator

PropertyDetails
Size range> 0x7f0000 (typically page-aligned large allocations)
Key functionRtlpHpLargeAlloc
Metadata_SEGMENT_HEAP.LargeAllocMetadata
Tracking structureBigPagePoolTable (PoolTrackTable)

5. Header Structure Changes

5.1 Header Combination Per Allocation Path

1
2
3
4
5
6
7
8
9
┌───────────────┬────────────────────────────────────────────────────────────┐
│  Path         │  Memory layout (chunk start → user data)                   │
├───────────────┼────────────────────────────────────────────────────────────┤
│  kLFH         │  [_POOL_HEADER 16B] [data]                                 │
│  VS           │  [_HEAP_VS_CHUNK_HEADER 16B] [_POOL_HEADER 16B] [data]     │
│  Segment      │  [_HEAP_PAGE_SEGMENT header] ... [page descriptors]        │
│  Large        │  Metadata recorded in BigPagePoolTable; no inline header   │
│  CacheAligned │  [_POOL_HEADER #1] ... [_POOL_HEADER #2 (CacheAligned)] [data] │
└───────────────┴────────────────────────────────────────────────────────────┘

5.2 Residual Role of _POOL_HEADER Under Segment Heap

_POOL_HEADER was not fully removed after the segment heap was introduced. It continues to be used for the following purposes:

FieldStatus under Segment Heap
PoolTagStill recorded (for debugging/tracing)
PoolTypeRecorded, but not used for allocator selection in the free path
BlockSizeUnused in the VS path; still present in kLFH
PreviousSizeUnused, set to 0
PoolIndexUnused, set to 0
ProcessBilledValid only with the PoolQuota flag (encoded with ExpPoolQuotaCookie)

6. Pointer Encoding Mechanisms

6.1 Global Key Structure: _RTLP_HP_HEAP_GLOBALS

1
2
3
4
5
6
// Generated randomly at boot time; global in ntoskrnl
_RTLP_HP_HEAP_GLOBALS  (nt!RtlpHpHeapGlobals)
{
    UINT64 HeapKey;   // Used for VS Allocator and Segment Allocator header encoding
    UINT64 LfhKey;    // Used for LFH callback pointer encoding
}

6.2 Encoding Formulas Per Component

VS chunk header — Sizes field:

1
2
encoded value = vs_chunk_header->Sizes.HeaderBits
              = (real Sizes value) ^ (address of vs_chunk_header) ^ RtlpHpHeapGlobals.HeapKey

VS chunk — EncodedSegmentPageOffset:

1
2
encoded value = vs_chunk_header->EncodedSegmentPageOffset
              = ((real page distance) ^ vs_chunk_header ^ RtlpHpHeapGlobals.HeapKey) & 0xFF

Segment context signature:

1
check value = page_segment ^ page_segment->Signature ^ 0xA2E64EADA2E64EAD ^ HeapKey

LFH callback function pointer:

1
encoded pointer = real function address ^ HeapKey ^ address of LfhContext

ProcessBilled (POOL_HEADER, Windows 8+):

1
encoded value = KPROCESS_PTR ^ ExpPoolQuotaCookie ^ CHUNK_ADDR

6.3 Implications for Attackers

To forge an encoded header with a simple overwrite, an attacker must:

  1. First leak the address and contents of RtlpHpHeapGlobals (i.e., HeapKey and LfhKey).
  2. Know the chunk’s own virtual address (self-referential XOR).
  3. Failing encoding validation immediately triggers BugCheck 0x139 (KERNEL_SECURITY_CHECK_FAILURE) or BugCheck 0x13A (KERNEL_MODE_HEAP_CORRUPTION).

7. Dynamic Lookaside and Delay Free

7.1 Dynamic Lookaside

The kernel segment heap operates a dynamic lookaside list based on _RTL_DYNAMIC_LOOKASIDE / _RTL_LOOKASIDE structures as its primary allocation cache.

1
2
3
4
5
_HEAP_VS_CONTEXT
└── Lookaside buckets (_RTL_DYNAMIC_LOOKASIDE)
     ├── Per-size singly-linked lists
     ├── Depth (2 B)     — current list depth
     └── NextEntry (8 B) — pointer to the next cached chunk

Rebalancing behavior:

  • Every 3 scans by the Balance Set Manager, each lookaside bucket is rebalanced.
  • If new allocation count < 25, Depth decreases by 10.
  • If miss ratio ≥ 0.5%, Depth increases; if < 0.5%, Depth decreases by 1.
  • Depth range: minimum 4 ~ MaximumDepth (determined dynamically).

7.2 Delay Free

1
2
3
4
5
6
7
8
9
10
VS Allocator free path:
        │
        ├─ size < 1 KB AND Config.Flags bit 4 == 1
        │       │
        │       ▼
        │  Stored temporarily in DelayFreeContext list
        │       │
        │       └─ Batch freed (real free) after 32 entries accumulate
        │
        └─ Otherwise: inserted immediately into FreeChunkTree

Security implication: Delay free has the side effect of disrupting the attack timing of immediately reusing a freed chunk right after triggering a UAF (Use-After-Free) vulnerability.


8. New Pool APIs: ExAllocatePool2 / ExAllocatePool3

8.1 API Evolution

1
2
3
4
5
6
7
8
ExAllocatePool                (legacy, no tag)
ExAllocatePoolWithTag         (pre-19H1 standard, deprecated in 2004)
ExAllocatePoolWithTagPriority (priority support)
ExAllocatePoolWithQuotaTag    (quota tracking)
        │
        ▼  Windows 10 2004 and later
ExAllocatePool2               (general case, zero-initialized by default)
ExAllocatePool3               (extended parameters, priority + Secure Pool)

8.2 ExAllocatePool2

1
2
3
4
5
PVOID ExAllocatePool2(
    POOL_FLAGS Flags,       // Uses POOL_FLAGS instead of POOL_TYPE
    SIZE_T     NumberOfBytes,
    ULONG      Tag
);

Key changes:

  • Zero-initialized by default: no need to call RtlZeroMemory separately.
  • Returns NULL on failure by default (POOL_FLAG_RAISE_ON_FAILURE converts to an exception).
  • POOL_FLAG_USE_QUOTA integrates the legacy PoolQuota functionality.
1
2
3
// Migration example
// Before: ExAllocatePoolWithTag(NonPagedPoolNx, size, 'Tvk0') + RtlZeroMemory(...)
// After:  ExAllocatePool2(POOL_FLAG_NON_PAGED_EXECUTE, size, 'Tvk0')

8.3 ExAllocatePool3

1
2
3
4
5
6
7
PVOID ExAllocatePool3(
    POOL_FLAGS              Flags,
    SIZE_T                  NumberOfBytes,
    ULONG                   Tag,
    PCPOOL_EXTENDED_PARAMETER ExtendedParameters,
    ULONG                   Count
);

POOL_EXTENDED_PARAMETER_TYPE values:

ValueDescription
PoolExtendedParameterPrioritySpecify allocation priority (e.g., HighPoolPriority)
PoolExtendedParameterSecurePoolAllocate in KDP Secure Pool (VTL0 write-protected memory)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Priority example
POOL_EXTENDED_PARAMETER params = {0};
params.Type     = PoolExtendedParameterPriority;
params.Priority = HighPoolPriority;
PVOID alloc = ExAllocatePool3(POOL_FLAG_NON_PAGED, size, 'Tag1', &params, 1);

// Secure Pool example (KDP)
POOL_EXTENDED_PARAMS_SECURE_POOL sp = {0};
sp.Cookie           = 0xDEADBEEF;
sp.SecurePoolFlags  = SECURE_POOL_FLAGS_FREEABLE | SECURE_POOL_FLAGS_MODIFIABLE;
sp.SecurePoolHandle = g_SecurePoolHandle;
sp.Buffer           = &initialData;
params.Type             = PoolExtendedParameterSecurePool;
params.SecurePoolParams = &sp;
PVOID secureAlloc = ExAllocatePool3(POOL_FLAG_NON_PAGED, size, 'Sec1', &params, 1);

8.4 Down-level Compatibility Wrapper

For drivers that must support OS versions prior to 2004:

1
2
3
4
5
6
7
8
// At the top of the driver header
#define POOL_ZERO_DOWN_LEVEL_SUPPORT

// In DriverEntry
ExInitializeDriverRuntime(DriversRuntimeInitSupportFlags);

// ExAllocatePool2 can then be used
// (internally falls back to alloc + memset on older OS versions)

9. Kernel Data Protection (KDP) and Secure Pool

9.1 Overview

KDP is a platform security technology that leverages the Segment Heap’s Secure Pool feature, allowing drivers to allocate read-only kernel memory that cannot be modified from VTL0.

9.2 Virtual Address Space Layout

1
2
3
4
5
6
7
8
9
10
11
NT kernel virtual address space (512 GB Secure Pool region)
┌─────────────────────────────────────────────────────────────┐
│         Full kernel VA space (256 PML4 entries)              │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Dedicated 512 GB Secure Pool region (1 PML4 entry)  │   │
│  │  Base address: randomized at boot                    │   │
│  │  Managed by: Secure Kernel (VTL1)                    │   │
│  │  VTL0 writes: blocked via NAR (Node Address Range)   │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

9.3 Secure Pool Initialization Flow

1
2
3
4
5
6
7
8
9
NT Memory Manager boot Phase 1
        │
        ├─ Randomly calculate 512 GB Secure Pool virtual address
        │
        ├─ Issue INITIALIZE_SECURE_POOL Secure Call → Secure Kernel
        │
        └─ Secure Kernel:
              ├─ Create NAR on the 512 GB region (prevent VTL0 writes)
              └─ Initialize NTE (Node Table Entry) for that region

9.4 Anti-Cheat Usage Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. Create Secure Pool context (in DriverEntry)
ExCreatePool(POOL_FLAG_NON_PAGED, tag, &securePoolHandle);

// 2. Allocate detection rule table in Secure Pool (with writable init data)
POOL_EXTENDED_PARAMS_SECURE_POOL sp = {
    .Cookie           = MY_COOKIE,
    .SecurePoolHandle = securePoolHandle,
    .Buffer           = &detectionRuleTable,  // initial data
    .SecurePoolFlags  = SECURE_POOL_FLAGS_FREEABLE
    // MODIFIABLE flag omitted → write-protected after init
};
g_DetectionRules = ExAllocatePool3(POOL_FLAG_NON_PAGED, sizeof(detectionRuleTable),
                                   'DRul', &extParams, 1);

// 3. g_DetectionRules is now immutable — no VTL0 code can modify it (hypervisor-enforced)

10. Security Research Perspective: Evolution of Attack Techniques

10.1 Increased Difficulty of Pool Overflow Attacks

TechniqueNT Pool (pre-19H1)Segment Heap (19H1+)
Adjacent header overwrite✅ Easy❌ Blocked by encoding
Pool Walking✅ Possible❌ Impossible due to metadata isolation
ProcessBilled overwrite⚠️ Requires Win8+ cookie⚠️ Requires cookie + HeapKey
kLFH pool spray⚠️ Predictable⚠️ Possible but requires precise control
VS FreeChunkTree corruptionN/A⚠️ Requires HeapKey bypass
Large chunk BigPool trackingPoC existsPoC exists (PoolTrackTable)

10.2 Modern kLFH Exploit Pattern

Core requirements for exploiting kLFH pool overflows in the segment heap era:

1
2
3
4
5
6
7
8
9
1. Find a target object of the same size as the vulnerable object.
   └─ Must land in the same kLFH bucket — size match is mandatory.

2. The target object must contain exploitable members (pointer, function table).

3. The target object allocation must be triggerable repeatedly from user mode.

4. The vulnerable and target objects must reside in the same pool type.
   └─ NonPagedPoolNx and PagedPool use separate _SEGMENT_HEAP instances.

10.3 Mandatory Recovery After VS Chunk Overflow

Fields that must be restored after manipulating a VS chunk to avoid a BugCheck:

1
2
3
4
5
6
7
8
9
// Ghost chunk recovery after overflow (HEVD-style)
// Restore the encoded fields of _HEAP_VS_CHUNK_HEADER
ghost_chunk->Sizes.HeaderBits =
    (real_sizes_value) ^ (ULONG_PTR)ghost_chunk ^ RtlpHpHeapGlobals_HeapKey;

ghost_chunk->EncodedSegmentPageOffset =
    ((real_page_offset) ^ (ULONG_PTR)ghost_chunk ^ RtlpHpHeapGlobals_HeapKey) & 0xFF;

// Failure to restore → BugCheck 0x13A (KERNEL_MODE_HEAP_CORRUPTION)

10.4 Required Pre-Exploit Leak Values

Values that must be obtained before a modern kernel heap exploit:

SymbolPurposeHow to Obtain
nt!RtlpHpHeapGlobalsHeapKey, LfhKeyBinary pattern parsing of ExFreePoolWithTag
nt!ExpPoolQuotaCookieDecode ProcessBilledPattern parsing of ExAllocatePoolWithQuotaTag
nt!PsInitialSystemProcessEPROCESS chain traversalntoskrnl import analysis
Chunk’s own virtual addressSelf-referential XORRequires an info-leak primitive

11. Anti-Cheat Developer Perspective

11.1 The End of Pool Walking and Modern Alternatives

Legacy method (pre-19H1):

1
2
3
4
5
6
// Follow BlockSize in inline header to traverse linearly → no longer works
PPOOL_HEADER header = (PPOOL_HEADER)startAddr;
while (header->BlockSize != 0) {
    if (header->PoolTag == TARGET_TAG) { /* ... */ }
    header += header->BlockSize;  // ❌ Invalid under the segment heap
}

Modern alternatives:

1
2
3
4
5
6
7
8
9
10
11
12
BigPool detection (Large Alloc path):
    Reference nt!PoolBigPageTable (or nt!PoolTrackTable)
    └─ Traverse BigPagePoolTable entries

Small allocation detection:
    _SEGMENT_HEAP → VsContext → SubsegmentList traversal
    _SEGMENT_HEAP → LfhContext → Buckets[] → AffinitySlots → Subsegments traversal

Searching by PoolTag:
    WinDbg: !poolfind <Tag> / !poolused
    Kernel code: access path is limited without private symbols
               → Indirect tracking via public APIs such as PsGetCurrentProcess

11.2 Driver Development Migration Checklist

1
2
3
4
5
6
7
□ ExAllocatePoolWithTag         → Replace with ExAllocatePool2
□ ExAllocatePool (without tag)  → Remove or replace with ExAllocatePool2
□ ExAllocatePoolWithTagPriority → ExAllocatePool3 + PoolExtendedParameterPriority
□ ExAllocatePoolWithQuotaTag    → ExAllocatePool2 + POOL_FLAG_USE_QUOTA
□ RtlZeroMemory after each alloc → Remove (ExAllocatePool2 zero-initializes automatically)
□ Review POOL_FLAG_RAISE_ON_FAILURE (NULL check vs. exception handling)
□ Critical data needing read-only protection → Consider ExAllocatePool3 + Secure Pool

11.3 BugCheck Code Reference

BugCheck CodeNameSegment Heap Trigger
0x139KERNEL_SECURITY_CHECK_FAILUREVS/LFH header integrity check failure
0x13AKERNEL_MODE_HEAP_CORRUPTIONHeap metadata corruption detected
0xC5DRIVER_CORRUPTED_EXPOOLPool accessed at incorrect IRQL
0x19BAD_POOL_HEADER_POOL_HEADER validation failure (LFH path)

12. WinDbg Analysis Commands

// View kernel segment heap global structure
dt nt!_RTLP_HP_HEAP_GLOBALS nt!RtlpHpHeapGlobals

// Check _SEGMENT_HEAP for NonPagedPoolNx
// (PoolVector has no public symbols; requires ntoskrnl offset analysis)

// Dump pool info at a specific address
!pool <address>

// Search for all allocations with a specific PoolTag
!poolfind <Tag> [pool_type]
// e.g.: !poolfind Proc 3

// Statistics by PoolTag
!poolused [flags]
// e.g.: !poolused 2    (sorted by NonPagedPool usage)

// Parse _SEGMENT_HEAP structures directly
dt nt!_SEGMENT_HEAP <address>
dt nt!_HEAP_VS_CHUNK_HEADER <address>
dt nt!_HEAP_LFH_CONTEXT <address>

// Decode a VS chunk header (HeapKey required)
// HeaderBits_raw = poi(<chunk_addr>)
// real Sizes = HeaderBits_raw ^ <chunk_addr> ^ HeapKey

// Traverse BigPool table
dt nt!_POOL_TRACKER_BIG_PAGES nt!PoolBigPageTable

13. Version History Summary

Windows VersionKey Changes
Windows 7Legacy NT Pool Manager; Lookaside List, ListHeads
Windows 8ExpPoolQuotaCookie introduced (ProcessBilled encoding)
Windows 10 RS5 (1809)Preparation stage for Segment Heap; some internal changes
Windows 10 19H1 (1903)Kernel Segment Heap officially introduced (kLFH, VS, Segment, Large)
Windows 10 2004ExAllocatePool2, ExAllocatePool3 added; ExAllocatePoolWithTag deprecated
Windows 10 20H2Dynamic KDP stabilized; Secure Pool usage expanded
Windows 11VBS/HVCI enabled by default; ExAllocatePoolWithTag deprecation warnings strengthened

14. References

ResourceAuthor / PublisherLink
Windows 10 Segment Heap InternalsMark Vincent Yason (IBM X-Force), BlackHat USA 2016PDF
Windows Heap-Backed Pool: The Good, The Bad, and The EncodedYarden Shafir, BlackHat USA 2021Slides
Scoop the Windows 10 PoolCorentin Bayet, Paul Fariello (SSTIC 2020)PDF
Windows Kernel Heap: Segment Heap in Windows Kernel Part 1Angelboy (scwuaptx)Speaker Deck
Windows Segment Heap: Attacking the VS AllocatorBlueFrost SecurityBlog
Solving Uninitialized Kernel Pool Memory on WindowsMicrosoft MSRCBlog
Introducing Kernel Data ProtectionMicrosoft SecurityBlog
Secure Pool Internals: Dynamic KDP Behind The HoodWindows Internals (Alex Ionescu et al.)Blog
Updating Deprecated ExAllocatePool CallsMicrosoft DocsLearn
Swimming In The Kernel Pool (Part 1 & 2)Connor McGarrBlog
Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion PoCSynacktivGitHub
Exploiting CNG.sys IOCTL Pool OverflowPixiePoint SecurityBlog
This post is licensed under CC BY 4.0 by the author.