248 lines
9.8 KiB
Go
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
|
|
}
|