v0.1 - initial commit
This commit is contained in:
+247
@@ -0,0 +1,247 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user