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 segmentHeap). // 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 }