Lab 04 — C Memory Corruption: Break, Diagnose, Fix, Defend
Language: C17 | Tools: clang/gcc, AddressSanitizer, UBSan, Make | Difficulty: 4/5
Pairs with the Phase 01 WARMUP Chapters 2–4 and 12 (the C abstract machine, the memory-defect families, integer arithmetic, crash triage).
Why This Lab Exists (Purpose & Goal)
The world runs on enormous C and C++ codebases, and you will spend a career auditing, fixing, and defending them. Reading about a buffer overflow is not the same as making one happen, watching a sanitizer catch it, finding the real root cause, and fixing the bug class rather than the one input. The goal of this lab is to take you through that full loop — break, diagnose, fix, defend — on a safe, owned, non-networked toy so the experience is visceral and the reflexes become permanent.
The Concept, In the Weeds
The vulnerable parser copies an attacker-controlled length into a fixed 32-byte stack buffer. The security invariant the fixed version must enforce:
declared_length <= destination_capacity
AND declared_length == remaining_input (exact consumption)
AND the parser never reads the length field before the fixed header exists
Several deep ideas converge here:
- Why a stack overflow is dangerous. A write past a local array can overwrite adjacent locals, saved registers, and the saved return address — historically the path to hijacking control flow. The stack canary exists precisely because the return address lives there.
- Undefined behavior is not "it crashes." In C, an out-of-bounds write is undefined behavior; the optimizer may assume it never happens and delete the very checks you thought protected you. "It ran fine on my machine" proves nothing — which is why you need a sanitizer to make the corruption observable.
- Fix the class, not the input. A naive fix that special-cases the crashing sample leaves every variant exploitable. The real fix restores the violated invariant (capacity and exact-consumption checks, validated before the copy).
Why This Matters for Protecting the Company
Memory-corruption bugs are still, decades later, among the highest-severity vulnerabilities — they yield remote code execution, the most valuable outcome for an attacker. When you can take a crashing input, reproduce it under ASan, reason from the faulting use back to the root-cause bug, and ship a fix with a regression test, you can close the most dangerous bug class in any native codebase the company ships or depends on. Just as importantly, you learn to harden the build (canary, FORTIFY, PIE, RELRO, W^X) and to run risky parsers under least privilege — defense in depth, so that the next unknown bug is contained.
How This Is Used on the Job
This is the daily loop of a product-security engineer or vulnerability researcher: triage a sanitizer/fuzzer crash, find root cause, patch, regress, and harden. The pattern here is the same one you'll scale up in Phase 08 against real open-source parsers.
Files
vulnerable.c— missing bounds and exact-consumption checks.fixed.c— checked header, capacity, length, exact consumption, explicit error codes.test_fixed.c— valid, truncated, oversized, trailing-data, and boundary tests.Makefile— hardened reference build plus an opt-in sanitizer demonstration.
Attack, Fix, Defend (Build Order)
- Read
parse_frame_vulnerableand predict the overwrite before running anything. - Run the ASan demonstration and preserve the sanitizer trace (it names the exact faulting access, the buffer, and the sizes).
- Implement the fix that restores the invariant — not one that merely rejects the supplied sample.
- Add tests for 31, 32, and 33 byte payloads (the boundary is where bugs hide).
- Compile a release build with stack protector, FORTIFY, PIE, RELRO where supported, and W^X.
- Run the parser behind the Phase 06 sandbox with no network and bounded input.
Run
make test
make demonstrate-vulnerability # owned local process; ASan should terminate it
Validation — What You Should Be Able to Do Now
- Read an ASan report and walk crash → reachability → controllability → boundary → impact without overclaiming exploitability.
- Distinguish the use site (where it crashed) from the bug (the missing check) and fix the latter.
- Explain why undefined behavior makes "it didn't crash" weak evidence, and why sanitizers are how you turn silent corruption into a loud, precise failure.
- Build a hardened release and explain what each mitigation changes about exploitability.
The Broader Perspective
This lab is where "security is an adversarial property" stops being a slogan. You wrote code that is correct for every reasonable input and catastrophic for one crafted input — and you learned to think from the attacker's side ("what input maximizes damage?") and then prove that input is rejected. That adversarial mindset, plus the fix-the-class-not-the-symptom discipline and the run-it-sandboxed instinct, is the foundation everything else builds on. The same root-cause class you fixed here is the one behind Heartbleed — you practiced on a safe toy what a researcher does on a real TLS stack.
Interview Angle
- "What makes a use-after-free / overflow exploitable, and how do you fix it?" — Reason through the chain (reachable? controllable value/size? boundary crossed?), fix the violated invariant (lifetime/ bounds) not the crashing input, add a regression test from the minimized case, and check for variants.
- "Why isn't 'it doesn't crash' proof of safety?" — UB can corrupt silently and be steered by an attacker; only instrumentation (ASan/UBSan) makes it observable.
References
- Phase 01 WARMUP Chapters 2–4, 12; SEI CERT C Coding Standard.
- AddressSanitizer / UBSan documentation; "Smashing the Stack for Fun and Profit" (historical).
- "The Heartbleed Bug" (CVE-2014-0160) — the same bounds-check root-cause class.