Skip to content

Commit 1d22474

Browse files
committed
updated heap storage to reflect actual bytes
1 parent 2124778 commit 1d22474

3 files changed

Lines changed: 52 additions & 25 deletions

File tree

src/exercises/unit11-heap-internals/hint-56.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ const hint56: Exercise = {
7575
log: ['action', 'malloc(24) -- eighth chunk. This one will be the first to overflow past the tcache limit.'],
7676
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('P7', 'data'); },
7777
},
78+
{
79+
action: 'malloc', size: 24, name: 'P8', srcLine: 7,
80+
log: ['action', 'malloc(24) -- ninth chunk. Both P7 and P8 will go to fastbin when freed, since the tcache will be full.'],
81+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('P8', 'data'); },
82+
},
7883
{
7984
action: 'free', name: 'P0', srcLine: 11,
8085
log: ['action', 'free(P0) -- tcache bin for size 32: count = 1/7. The chunk\'s data area stores an fd pointer to NULL (end of list). Tcache entries form a singly-linked LIFO list.'],
@@ -115,6 +120,11 @@ const hint56: Exercise = {
115120
log: ['warn', 'free(P7) -- tcache bin is full (7/7)! This chunk falls through to the fast bin instead. The fast bin has different security checks (and fewer in older glibc). Many exploits deliberately fill the tcache to force chunks into the more exploitable fast bin path.'],
116121
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('P7', 'freed'); heap.annotateField?.('P7', 'fd', 'FASTBIN (overflow)'); },
117122
},
123+
{
124+
action: 'free', name: 'P8', srcLine: 14,
125+
log: ['warn', 'free(P8) -- also goes to fast bin (tcache still full). Fastbin[32]: P8 \u2192 P7 \u2192 NULL. Both P7 and P8 bypassed tcache and went straight to the classic fast bin path.'],
126+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('P8', 'freed'); heap.annotateField?.('P8', 'fd', 'FASTBIN #2'); },
127+
},
118128
{
119129
action: 'done',
120130
log: ['success', 'Tcache deep dive complete. Key facts: (1) 64 bins, one per size class, max 7 entries each. (2) LIFO singly-linked via fd pointer in the data area. (3) Minimal security checks in early glibc versions (no double-free detection until 2.29). (4) Filling the tcache forces chunks to the classic bin paths, which is a common exploit setup step.'],

src/exercises/unit12-win-heap-internals/whint-60.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,41 +28,55 @@ const exercise: Exercise = {
2828
},
2929
mode: 'step',
3030
vizMode: 'heap',
31-
heapSize: 256,
31+
heapSize: 1024,
3232
steps: [
3333
{
3434
action: 'init',
35-
log: ['info', 'The LFH is the front-end allocator for the NT Heap. It groups allocation sizes into 128 buckets (size classes). Bucket selection: sizes 1-256 use 8-byte granularity (bucket = ceil(size/8)), sizes 257-16384 use wider granularity. The LFH is NOT active by default -- it activates per-bucket.'],
35+
log: ['info', 'The LFH is the front-end allocator for the NT Heap. It groups allocation sizes into 128 buckets (size classes). Bucket selection: sizes 1-256 use 8-byte granularity (bucket = ceil(size/8)), sizes 257-16384 use wider granularity. The LFH is NOT active by default -- it activates per-bucket after enough allocations of the same size.'],
3636
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.clear?.(); },
3737
},
3838
{
39-
action: 'malloc', size: 32, name: 'A', srcLine: 8,
40-
log: ['action', 'HeapAlloc(h, 0, 32) -- first allocation of size 32. The back-end handles this (LFH not yet active). Internally, _HEAP.FrontEndHeapUsageData[] counts how many times each bucket is hit. This counter increments with each HeapAlloc call for this size class.'],
41-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A', 'data'); heap.annotateField?.('A', 'size', 'back-end alloc #1'); },
39+
action: 'malloc', size: 32, name: 'A01', srcLine: 8,
40+
log: ['action', 'HeapAlloc(h, 0, 32) -- alloc #1. The back-end handles this (LFH not yet active). Internally, _HEAP.FrontEndHeapUsageData[] counts how many times each bucket is hit. This counter increments with each HeapAlloc call for this size class.'],
41+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A01', 'data'); heap.annotateField?.('A01', 'size', 'back-end #1'); },
4242
},
4343
{
44-
action: 'init',
45-
log: ['info', 'After 17 consecutive allocations of size class 32, the counter crosses the activation threshold. The heap manager calls RtlpActivateLowFragmentationHeap() for bucket 4 (32/8). This creates an _LFH_HEAP structure and links it to this bucket.'],
46-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.annotateField?.('A', 'size', 'LFH activating (17+)'); },
44+
action: 'malloc', size: 32, name: 'A02', srcLine: 8,
45+
log: ['action', 'HeapAlloc(h, 0, 32) -- alloc #2. Still served by the back-end. Each allocation for bucket 4 (size 32) increments the usage counter.'],
46+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A02', 'data'); heap.annotateField?.('A02', 'size', 'back-end #2'); },
47+
},
48+
{
49+
action: 'malloc', size: 32, name: 'A03', srcLine: 8,
50+
log: ['action', 'HeapAlloc(h, 0, 32) -- alloc #3. The counter is still below 17. All these are back-end allocations from the FreeLists or segment commit.'],
51+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A03', 'data'); },
52+
},
53+
{
54+
action: 'init', srcLine: 8,
55+
log: ['action', 'HeapAlloc × 13 more -- allocs #4 through #16 all served by the back-end. The usage counter climbs: 4... 8... 12... 16. Almost at the activation threshold.'],
56+
vizAction: (_sim: any, heap: any) => { if (!heap) return; for (let i = 0; i < 13; i++) heap.malloc(32); },
57+
},
58+
{
59+
action: 'malloc', size: 32, name: 'A17', srcLine: 8,
60+
log: ['warn', 'HeapAlloc(h, 0, 32) -- alloc #17! The usage counter hits the activation threshold. The heap manager calls RtlpActivateLowFragmentationHeap() for bucket 4 (32/8). This creates an _LFH_HEAP structure and links it to this bucket. LFH is now ACTIVE for size 32.'],
61+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A17', 'data'); heap.annotateField?.('A17', 'size', 'LFH ACTIVATES'); },
4762
},
4863
{
49-
action: 'malloc', size: 32, name: 'B', srcLine: 8,
50-
log: ['action', 'Allocation #18 (post-activation) -- now served by LFH. The LFH allocates a UserBlocks region: a contiguous memory block subdivided into fixed-size slots. Each slot is exactly (header + 32) bytes. The LFH picks a slot using a randomized bitmap scan, not FIFO.'],
51-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('B', 'data'); heap.annotateField?.('B', 'size', 'LFH slot (randomized)'); },
64+
action: 'malloc', size: 32, name: 'A18', srcLine: 8,
65+
log: ['action', 'HeapAlloc(h, 0, 32) -- alloc #18. Now served by LFH! The LFH allocates a UserBlocks region: a contiguous memory block subdivided into fixed-size slots. Each slot is exactly (header + 32) bytes. The LFH picks a slot using a randomized bitmap scan, not FIFO.'],
66+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A18', 'data'); heap.annotateField?.('A18', 'size', 'LFH slot (randomized)'); },
5267
},
5368
{
5469
action: 'init',
5570
log: ['info', 'UserBlocks structure: _HEAP_USERDATA_HEADER at the start, followed by N fixed-size subsegment entries. A bitmap tracks which slots are busy/free. The LFH randomizes the starting position in the bitmap scan, so consecutive HeapAlloc calls do NOT return sequential addresses.'],
56-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('B', 'header'); },
71+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A18', 'header'); },
5772
},
5873
{
5974
action: 'init',
6075
log: ['warn', 'LFH randomization was added in Windows 8 to make heap spraying harder. Before Win8, LFH returned slots in a deterministic pattern. The randomization means an attacker spraying 1000 objects cannot predict which slot index will be adjacent to a target -- but statistical attacks still work with enough spray.'],
61-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('A', 'data'); heap.highlightChunk?.('B', 'data'); },
6276
},
6377
{
6478
action: 'done',
65-
log: ['success', 'LFH summary: activates per-bucket after 17+ allocs of the same size. Allocates from a UserBlocks region of fixed-size slots. Uses bitmap-based tracking with randomized slot selection (Win8+). Reduces fragmentation and raises the bar for heap manipulation attacks.'],
79+
log: ['success', 'LFH activation demonstrated with all 18 allocations. Key facts: (1) LFH activates per-bucket after 17+ allocs of the same size. (2) Allocates from a UserBlocks region of fixed-size slots. (3) Uses bitmap-based tracking with randomized slot selection (Win8+). (4) Reduces fragmentation and raises the bar for heap manipulation attacks.'],
6680
},
6781
],
6882
check() { return false; },

src/exercises/unit12-win-heap-internals/whint-61.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ const exercise: Exercise = {
1414
{ text: '', cls: '' },
1515
{ text: 'int main() {', cls: '' },
1616
{ text: ' HANDLE h = GetProcessHeap();', cls: '' },
17-
{ text: ' void *s = HeapAlloc(h, 0, 128);', cls: 'highlight' },
18-
{ text: ' void *m = HeapAlloc(h, 0, 4096);', cls: 'highlight' },
19-
{ text: ' void *l = HeapAlloc(h, 0, 512000);', cls: 'highlight' },
17+
{ text: ' // LFH tier: small allocs (<512 bytes)', cls: 'cmt' },
18+
{ text: ' void *s = HeapAlloc(h, 0, 32);', cls: 'highlight' },
19+
{ text: ' // VS tier: medium allocs (512B-128KB)', cls: 'cmt' },
20+
{ text: ' void *m = HeapAlloc(h, 0, 512);', cls: 'highlight' },
21+
{ text: ' // Large alloc tier: huge allocs (>128KB)', cls: 'cmt' },
22+
{ text: ' void *l = HeapAlloc(h, 0, 1024);', cls: 'highlight' },
2023
{ text: ' HeapFree(h, 0, s);', cls: '' },
2124
{ text: ' HeapFree(h, 0, m);', cls: '' },
2225
{ text: ' HeapFree(h, 0, l);', cls: '' },
@@ -26,27 +29,27 @@ const exercise: Exercise = {
2629
},
2730
mode: 'step',
2831
vizMode: 'heap',
29-
heapSize: 256,
32+
heapSize: 2048,
3033
steps: [
3134
{
3235
action: 'init',
3336
log: ['info', 'The Segment Heap (_SEGMENT_HEAP) is a complete redesign of the Windows heap introduced in Windows 10. It replaces _HEAP for modern apps. The core idea: route allocations to different backends depending on size, with stronger metadata validation and guard pages.'],
3437
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.clear?.(); },
3538
},
3639
{
37-
action: 'malloc', size: 128, name: 'S', srcLine: 7,
38-
log: ['action', 'HeapAlloc(h, 0, 128) -- small allocation, handled by the LFH component. Segment Heap\'s LFH works similarly to NT Heap\'s LFH: fixed-size slots in UserBlocks, bitmap tracking, randomized selection. Sizes up to ~512 bytes go through this path once LFH is active for the bucket.'],
40+
action: 'malloc', size: 32, name: 'S', srcLine: 8,
41+
log: ['action', 'HeapAlloc(h, 0, 32) -- small allocation, handled by the LFH component. Segment Heap\'s LFH works similarly to NT Heap\'s LFH: fixed-size slots in UserBlocks, bitmap tracking, randomized selection. Sizes up to ~512 bytes go through this path once LFH is active for the bucket.'],
3942
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('S', 'data'); heap.annotateField?.('S', 'size', 'LFH tier (<512B)'); },
4043
},
4144
{
42-
action: 'malloc', size: 4096, name: 'M', srcLine: 8,
43-
log: ['action', 'HeapAlloc(h, 0, 4096) -- medium allocation, routed to the Variable Size (VS) backend. VS allocations use _HEAP_VS_SUBSEGMENT structures within larger committed pages. Each VS block has its own encoded header. The VS backend handles sizes roughly 512 bytes to 128 KB.'],
45+
action: 'malloc', size: 512, name: 'M', srcLine: 10,
46+
log: ['action', 'HeapAlloc(h, 0, 512) -- medium allocation, routed to the Variable Size (VS) backend. VS allocations use _HEAP_VS_SUBSEGMENT structures within larger committed pages. Each VS block has its own encoded header. The VS backend handles sizes roughly 512 bytes to 128 KB.'],
4447
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('M', 'data'); heap.annotateField?.('M', 'size', 'VS tier (512B-128KB)'); },
4548
},
4649
{
47-
action: 'malloc', size: 512000, name: 'L', srcLine: 9,
48-
log: ['action', 'HeapAlloc(h, 0, 512000) -- large allocation (> 128 KB). Routed directly to VirtualAlloc with dedicated pages. A _HEAP_LARGE_ALLOC_DATA record tracks the allocation. These are the easiest to find in memory -- each gets its own page-aligned region.'],
49-
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('L', 'data'); heap.annotateField?.('L', 'size', 'VirtualAlloc (>128KB)'); },
50+
action: 'malloc', size: 1024, name: 'L', srcLine: 12,
51+
log: ['action', 'HeapAlloc(h, 0, 1024) -- large allocation. In a real system, allocations over 128 KB are routed directly to VirtualAlloc with dedicated pages. A _HEAP_LARGE_ALLOC_DATA record tracks the allocation. These are the easiest to find in memory -- each gets its own page-aligned region.'],
52+
vizAction: (_sim: any, heap: any) => { if (!heap) return; heap.highlightChunk?.('L', 'data'); heap.annotateField?.('L', 'size', 'Large (>128KB: VirtualAlloc)'); },
5053
},
5154
{
5255
action: 'init',

0 commit comments

Comments
 (0)