156 lines
5.6 KiB
Go
156 lines
5.6 KiB
Go
package winpwn
|
|
|
|
import "testing"
|
|
|
|
// These tests pin heap_lfh.go's decoding against field *values* captured
|
|
// from live runs of examples/heap_lfh.exe (Windows build 10.0.26100) rather
|
|
// than raw byte dumps, the same style heap_test.go's
|
|
// TestReadHeapDecodesRealCapturedFields uses -- the offsets themselves were
|
|
// confirmed via `dt ntdll!_LFH_HEAP`/`dt ntdll!_HEAP_SUBSEGMENT` and the
|
|
// values via cdb reads cross-checked against winpwn's own output, not
|
|
// assumed.
|
|
|
|
func TestReadLFHBuckets(t *testing.T) {
|
|
buf := make([]byte, lfhOffBuckets+lfhBucketCount*lfhBucketStride)
|
|
// Bucket 2: 48-byte blocks (3 granularity units), the bucket that
|
|
// actually served examples/heap_lfh's 32-byte Note/buffer allocations
|
|
// once LFH took over that size class -- see FindLFHBucket's doc comment
|
|
// for why 32-byte requests land in the 48-byte bucket, not the 32-byte
|
|
// one.
|
|
off := lfhOffBuckets + 2*lfhBucketStride
|
|
buf[off] = 3
|
|
buf[off+1] = 0
|
|
buf[off+2] = 2 // SizeIndex
|
|
|
|
buckets, err := ReadLFHBuckets(newByteReaderAt(buf), 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if buckets[2].BlockUnits != 3 || buckets[2].BlockSize() != 48 {
|
|
t.Errorf("bucket[2] = %+v, want BlockUnits=3 BlockSize=48", buckets[2])
|
|
}
|
|
if buckets[0].BlockUnits != 0 {
|
|
t.Errorf("bucket[0] should be unused, got %+v", buckets[0])
|
|
}
|
|
}
|
|
|
|
func TestFindLFHBucketAccountsForBlockHeader(t *testing.T) {
|
|
var buckets [lfhBucketCount]HeapBucket
|
|
buckets[1] = HeapBucket{Index: 1, BlockUnits: 2} // 32 bytes total, 16 usable -- too small
|
|
buckets[2] = HeapBucket{Index: 2, BlockUnits: 3} // 48 bytes total, 32 usable -- the real fit
|
|
|
|
got, err := FindLFHBucket(buckets, 32)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.Index != 2 {
|
|
t.Errorf("FindLFHBucket(32) chose bucket %d, want 2 (the naive BlockSize()>=wantSize check would wrongly pick bucket 1)", got.Index)
|
|
}
|
|
}
|
|
|
|
func TestFindLFHBucketNoneActive(t *testing.T) {
|
|
var buckets [lfhBucketCount]HeapBucket
|
|
if _, err := FindLFHBucket(buckets, 32); err == nil {
|
|
t.Fatal("expected an error when no bucket has BlockUnits set")
|
|
}
|
|
}
|
|
|
|
func TestActiveSubsegment(t *testing.T) {
|
|
const lfhHeapAddr = 0x1000
|
|
buf := make([]byte, 0x2000)
|
|
const segInfoAddr = 0x1500
|
|
const subsegAddr = 0x1700
|
|
binaryLEPutUint64(buf[lfhHeapAddr+lfhOffSegmentInfoArrays+2*8:], segInfoAddr)
|
|
binaryLEPutUint64(buf[segInfoAddr+hlsiOffActiveSubsegment:], subsegAddr)
|
|
|
|
got, err := ActiveSubsegment(newByteReaderAt(buf), lfhHeapAddr, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != subsegAddr {
|
|
t.Errorf("ActiveSubsegment = 0x%x, want 0x%x", got, subsegAddr)
|
|
}
|
|
|
|
if _, err := ActiveSubsegment(newByteReaderAt(buf), lfhHeapAddr, 5); err == nil {
|
|
t.Fatal("expected an error for a bucket with no segment info")
|
|
}
|
|
}
|
|
|
|
// TestReadLFHSubsegmentBusyBitmap pins ReadLFHSubsegment against the exact
|
|
// shape captured from a live run: bucket 2 (48-byte blocks), a 19-block
|
|
// subsegment, with one bit clear (an early, pre-LFH-activation allocation
|
|
// that never got a tracked slot -- see heap_lfh.go's package comment) and
|
|
// the rest busy.
|
|
func TestReadLFHSubsegmentBusyBitmap(t *testing.T) {
|
|
const subsegAddr = 0x713480
|
|
const userBlocksAddr = 0x713050
|
|
buf := make([]byte, 0x800000)
|
|
|
|
binaryLEPutUint64(buf[subsegAddr+subsegOffUserBlocks:], userBlocksAddr)
|
|
buf[subsegAddr+subsegOffBlockSize] = 3 // granularity units -> 48 bytes
|
|
buf[subsegAddr+subsegOffBlockCount] = 19
|
|
|
|
const bitmapBufferAddr = 0x713200
|
|
binaryLEPutUint64(buf[userBlocksAddr+userDataOffBitmapSize:], 19)
|
|
binaryLEPutUint64(buf[userBlocksAddr+userDataOffBitmapBuffer:], bitmapBufferAddr)
|
|
// bits: every slot busy except slot 5 (0-indexed) -- matches a real
|
|
// captured run's BusyBitmap word.
|
|
var word uint64 = 0
|
|
for i := 0; i < 19; i++ {
|
|
if i != 5 {
|
|
word |= 1 << uint(i)
|
|
}
|
|
}
|
|
binaryLEPutUint64(buf[bitmapBufferAddr:], word)
|
|
|
|
subseg, err := ReadLFHSubsegment(newByteReaderAt(buf), subsegAddr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if subseg.BlockSize != 48 || subseg.BlockCount != 19 {
|
|
t.Errorf("BlockSize/BlockCount = %d/%d, want 48/19", subseg.BlockSize, subseg.BlockCount)
|
|
}
|
|
if subseg.UserBlocksAddr != userBlocksAddr {
|
|
t.Errorf("UserBlocksAddr = 0x%x, want 0x%x", subseg.UserBlocksAddr, userBlocksAddr)
|
|
}
|
|
for i, busy := range subseg.Busy {
|
|
want := i != 5
|
|
if busy != want {
|
|
t.Errorf("Busy[%d] = %v, want %v", i, busy, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCalibrateLFHFirstBlockOffsetMatchesLiveAddress pins
|
|
// CalibrateLFHFirstBlockOffset/BlockAddress/SlotOf against a real
|
|
// before/after-free pair captured live: freeing the note at 0x7132b0
|
|
// flipped exactly Busy[11] from true to false, and slot 11's address by
|
|
// BlockAddress must equal 0x7132b0 -- not 0x713280 (one block short), which
|
|
// is what the naive "(addr-UserBlocksAddr) % BlockSize" formula gives
|
|
// before the +BlockSize correction (see CalibrateLFHFirstBlockOffset's doc
|
|
// comment for why that's wrong: it lands on _HEAP_USERDATA_HEADER's own
|
|
// reserved region, one block before any real, bit-tracked slot).
|
|
func TestCalibrateLFHFirstBlockOffsetMatchesLiveAddress(t *testing.T) {
|
|
subseg := &LFHSubsegment{
|
|
UserBlocksAddr: 0x713050,
|
|
BlockSize: 48,
|
|
BlockCount: 19,
|
|
}
|
|
const victimAddr = 0x7132b0
|
|
|
|
off := CalibrateLFHFirstBlockOffset(subseg, victimAddr)
|
|
if off != 80 {
|
|
t.Fatalf("CalibrateLFHFirstBlockOffset = %d, want 80", off)
|
|
}
|
|
if got := subseg.BlockAddress(off, 11); got != victimAddr {
|
|
t.Errorf("BlockAddress(80, 11) = 0x%x, want 0x%x", got, victimAddr)
|
|
}
|
|
idx, ok := subseg.SlotOf(off, victimAddr)
|
|
if !ok || idx != 11 {
|
|
t.Errorf("SlotOf = (%d, %v), want (11, true)", idx, ok)
|
|
}
|
|
if _, ok := subseg.SlotOf(off, 0xdeadbeef); ok {
|
|
t.Error("SlotOf should reject an address outside the subsegment")
|
|
}
|
|
}
|