Files
go_pwner/heap_segment.go
T
2026-07-18 21:37:15 +03:00

248 lines
9.8 KiB
Go

package winpwn
import (
"fmt"
"io"
)
// This file is the Segment Heap layer on top of heap.go's HeapKind
// detection -- the ROADMAP.md Phase 9 step 4 item: structural parsing of
// _SEGMENT_HEAP and its two sub-backends.
//
// Offsets confirmed via `dt ntdll!_SEGMENT_HEAP`, `dt ntdll!_HEAP_VS_CONTEXT`,
// and `dt ntdll!_HEAP_LFH_CONTEXT` against this machine's build
// (10.0.26100) using the same cdb methodology as heap.go and heap_lfh.go.
//
// What IS and IS NOT implemented in this pass:
//
// - SegmentHeap outer struct (Signature, GlobalFlags, per-backend
// summary fields): fully validated against a live heap_segment.exe
// process reading PEB.ProcessHeaps → DetectHeapKind → ReadSegmentHeap.
//
// - VS context subsegment enumeration (SubsegmentList walk) and summary
// stats (TotalCommittedUnits, FreeCommittedUnits): validated.
//
// - Segment Heap LFH context bucket enumeration (which buckets are
// active, TotalBlockCount per bucket): validated.
//
// - Individual VS chunk headers and Segment Heap LFH subsegment
// BlockOffsets: NOT decoded yet. Both are XOR-encoded against a
// per-subsegment/per-page key (confirmed empirically: direct reads of
// _HEAP_VS_CHUNK_HEADER.Sizes and
// _HEAP_LFH_SUBSEGMENT.BlockOffsets.EncodedData produced implausible
// field values -- same class of problem as NT Heap LFH's EncodedOffsets,
// which took its own empirical investigation pass to fix). Decoding them
// requires recovering the per-page segment offset key, which is its own
// future validation pass. The AdjacentAddressPairs / FindAdjacentPair
// helpers in heap.go fill the practical gap for the most common CTF
// need (finding adjacent same-size allocations from a set of leaked
// pointers) without needing chunk-level decode at all.
// Offsets confirmed via `dt ntdll!_SEGMENT_HEAP` on build 10.0.26100:
const (
segHeapOffSignature = 0x010 // Uint4B -- 0xddeeddee
segHeapOffGlobalFlags = 0x014 // Uint4B
segHeapOffVsContext = 0x280 // inline _HEAP_VS_CONTEXT
segHeapOffLfhContext = 0x340 // inline _HEAP_LFH_CONTEXT
)
// VS context sub-offsets (from `dt ntdll!_HEAP_VS_CONTEXT`):
const (
vsCtxOffFreeChunkTree = 0x010 // _RTL_RB_TREE (16 bytes, free chunk rb-tree)
vsCtxOffSubsegmentList = 0x020 // _LIST_ENTRY (head of all VS subsegments)
vsCtxOffTotalCommitted = 0x030 // Uint8B: committed units
vsCtxOffFreeCommitted = 0x038 // Uint8B: free committed units
)
// LFH context sub-offsets (from `dt ntdll!_HEAP_LFH_CONTEXT`):
const (
segLfhCtxOffBuckets = 0x080 // [129]Ptr64 _HEAP_LFH_BUCKET
segLfhBucketCount = 129
segLfhBucketOffTotalBlocks = 0x038 // _HEAP_LFH_BUCKET.TotalBlockCount (Uint8B)
)
// VS subsegment (from `dt ntdll!_HEAP_VS_SUBSEGMENT`):
const (
vsSubsegOffListEntry = 0x000 // _LIST_ENTRY, links into VsContext.SubsegmentList
vsSubsegOffSize = 0x020 // Uint2B: size in page-granularity units
vsSubsegOffSigBits = 0x022 // bitfield: bits 0-14 = signature, bit 15 = FullCommit
)
// SegmentHeap is the decoded outer shell of ntdll's _SEGMENT_HEAP -- the
// handle passed to HeapAlloc/HeapFree when a process opts into Segment Heap
// (most commonly via an embedded manifest <heapType>segmentHeap</heapType>).
// Use ReadSegmentHeap after DetectHeapKind confirms HeapKindSegment.
type SegmentHeap struct {
Addr uint64
Signature uint32 // 0xddeeddee -- distinct from NT Heap's 0xeeffeeff and segment-signature 0xffeeffee
GlobalFlags uint32
VS SegmentVSContext // variable-size backend summary
LFH SegmentLFHContext // segment-heap-native LFH summary
}
// SegmentVSContext summarises the VS (variable-size) backend inside a
// Segment Heap -- where allocations outside the LFH's fixed-size buckets
// land. CommittedUnits and FreeUnits are in internal granularity units
// (not bytes); SubsegmentCount is the length of the subsegment list.
type SegmentVSContext struct {
Addr uint64 // address of _HEAP_VS_CONTEXT inside the _SEGMENT_HEAP
CommittedUnits uint64
FreeUnits uint64
SubsegmentCount int
Subsegments []uint64 // address of each _HEAP_VS_SUBSEGMENT
}
// SegmentLFHContext summarises the Segment Heap's native LFH backend --
// a completely different structure from NT Heap's _LFH_HEAP, with its own
// bucket scheme. Each active bucket entry (Ptr64 != 0 and != a scheduling
// stub) is reported with its TotalBlockCount.
type SegmentLFHContext struct {
Addr uint64 // address of _HEAP_LFH_CONTEXT inside the _SEGMENT_HEAP
ActiveBuckets []SegmentLFHBucket
}
// SegmentLFHBucket is one active bucket entry in _HEAP_LFH_CONTEXT.Buckets.
// Index is the zero-based slot in the 129-entry array; TotalBlockCount is
// the cumulative allocation count across all subsegments ever created for
// this size class.
type SegmentLFHBucket struct {
Index int
Ptr uint64
TotalBlockCount uint64
}
// ReadSegmentHeap decodes addr's _SEGMENT_HEAP. Returns an error if addr's
// signature isn't the Segment Heap one (use DetectHeapKind first, or
// ReadHeap for NT Heap handles).
func ReadSegmentHeap(r io.ReaderAt, addr uint64) (*SegmentHeap, error) {
kind, err := DetectHeapKind(r, addr)
if err != nil {
return nil, err
}
if kind != HeapKindSegment {
return nil, fmt.Errorf("0x%x is a %s, not a Segment Heap", addr, kind)
}
h := &SegmentHeap{Addr: addr}
if h.Signature, err = readUint32AtValue(r, int64(addr)+segHeapOffSignature); err != nil {
return nil, fmt.Errorf("Signature: %w", err)
}
if h.GlobalFlags, err = readUint32AtValue(r, int64(addr)+segHeapOffGlobalFlags); err != nil {
return nil, fmt.Errorf("GlobalFlags: %w", err)
}
vsCtxAddr := addr + segHeapOffVsContext
h.VS.Addr = vsCtxAddr
if h.VS.CommittedUnits, err = readUint64At(r, int64(vsCtxAddr)+vsCtxOffTotalCommitted); err != nil {
return nil, fmt.Errorf("VS.TotalCommittedUnits: %w", err)
}
if h.VS.FreeUnits, err = readUint64At(r, int64(vsCtxAddr)+vsCtxOffFreeCommitted); err != nil {
return nil, fmt.Errorf("VS.FreeCommittedUnits: %w", err)
}
if h.VS.Subsegments, err = walkVSSubsegmentList(r, vsCtxAddr); err != nil {
return nil, fmt.Errorf("VS subsegment list: %w", err)
}
h.VS.SubsegmentCount = len(h.VS.Subsegments)
lfhCtxAddr := addr + segHeapOffLfhContext
h.LFH.Addr = lfhCtxAddr
if h.LFH.ActiveBuckets, err = readSegmentLFHBuckets(r, lfhCtxAddr); err != nil {
return nil, fmt.Errorf("LFH buckets: %w", err)
}
return h, nil
}
// walkVSSubsegmentList enumerates the _HEAP_VS_SUBSEGMENT addresses by
// following the SubsegmentList LIST_ENTRY chain in the VS context.
func walkVSSubsegmentList(r io.ReaderAt, vsCtxAddr uint64) ([]uint64, error) {
headAddr := vsCtxAddr + vsCtxOffSubsegmentList
flink, err := readUint64At(r, int64(headAddr))
if err != nil {
return nil, fmt.Errorf("reading SubsegmentList head: %w", err)
}
var subsegments []uint64
cur := flink
for cur != headAddr && cur != 0 {
subsegments = append(subsegments, cur) // ListEntry is at offset 0, so cur == subsegment addr
if len(subsegments) > 4096 {
return subsegments, fmt.Errorf("VS SubsegmentList exceeded 4096 entries (corrupted?)")
}
next, err := readUint64At(r, int64(cur)) // Flink is LIST_ENTRY's first field
if err != nil || next == cur {
break
}
cur = next
}
return subsegments, nil
}
// readSegmentLFHBuckets scans the 129-entry _HEAP_LFH_CONTEXT.Buckets array
// and returns every active entry (non-null pointer that isn't a scheduling
// stub, identified by low bit clear in the pointer value).
func readSegmentLFHBuckets(r io.ReaderAt, lfhCtxAddr uint64) ([]SegmentLFHBucket, error) {
var buckets []SegmentLFHBucket
bucketsBase := int64(lfhCtxAddr) + segLfhCtxOffBuckets
for i := 0; i < segLfhBucketCount; i++ {
ptr, err := readUint64At(r, bucketsBase+int64(i)*8)
if err != nil {
return buckets, fmt.Errorf("reading bucket[%d]: %w", i, err)
}
// Low bit set means this entry is a scheduler stub, not a real bucket pointer
if ptr == 0 || ptr&1 != 0 {
continue
}
total, err := readUint64At(r, int64(ptr)+segLfhBucketOffTotalBlocks)
if err != nil {
continue
}
if total == 0 {
continue
}
buckets = append(buckets, SegmentLFHBucket{Index: i, Ptr: ptr, TotalBlockCount: total})
}
return buckets, nil
}
// AdjacentAddressPairs finds all pairs in addrs where the difference is
// exactly step bytes -- the structural-equivalent finder for "which two
// same-size allocations landed adjacent" that examples/heap_segment's
// spray loop discovers by trial and error. On Segment Heap, same-size
// allocations in the same subsegment page are packed step bytes apart
// (step == sizeof(Allocation), before any chunk-header overhead, which
// the Segment Heap's LFH accounts for separately from user data unlike
// NT Heap's HeapEntrySize scheme). Returns all (lo, hi) pairs in
// address order with hi == lo+step.
//
// CAUTION: step is the ALLOCATION GRANULARITY visible at the HeapAlloc
// caller level (e.g. sizeof(Profile)=32 in heap_segment.c), not
// sizeof(struct) + sizeof(chunk_header) -- Segment Heap's metadata
// isolation places chunk headers on a separate metadata page, so the
// gap between two adjacent user payloads really is sizeof(Allocation).
// Verify empirically for your specific build if this doesn't match.
func AdjacentAddressPairs(addrs []uint64, step uint64) [][2]uint64 {
set := make(map[uint64]struct{}, len(addrs))
for _, a := range addrs {
set[a] = struct{}{}
}
var pairs [][2]uint64
for _, a := range addrs {
if _, ok := set[a+step]; ok {
pairs = append(pairs, [2]uint64{a, a + step})
}
}
return pairs
}
// FindAdjacentPair returns the first pair where hi == lo+step, or
// (0, 0, false) if none exists. Convenience wrapper over AdjacentAddressPairs
// for the common "give me any adjacent pair" case.
func FindAdjacentPair(addrs []uint64, step uint64) (lo, hi uint64, found bool) {
pairs := AdjacentAddressPairs(addrs, step)
if len(pairs) == 0 {
return 0, 0, false
}
return pairs[0][0], pairs[0][1], true
}