Capture Provenance and Validation
A plausible translation can still describe the wrong process, build, or moment; provenance and independent checks give offline bytes meaning.
Cheatsheet for Lesson 14.4
A one-page summary of “Capture Provenance and Validation” you can keep beside your notes. Plain Markdown.
Lesson 14.4 — Capture Provenance and Validation: cheatsheet
- Remember: Successful translation proves that the saved tables lead somewhere; identity and semantic checks determine whether it is the intended object.
- Also useful: Use three separate desks: capture parsing finds bytes, layout decoding names their fields, and presentation explains the checked values.
Reading map
- A clean translation can still be the wrong one
- Preserve capture provenance
- Cross-check a translation three ways
- Separate capture parsing from game meaning
- Account for IOMMU and platform protections
- A bypass reveals a failed assumption
- What this chapter intentionally does not provide
- A final advanced workflow
Review
- Q: A capture reader decodes a player object and its health field reads 100. What makes the decoded object trustworthy? A: Provenance matches, the identity path is confirmed, and independent invariants hold
- Q: Why does a wrong CR3 value usually not make the page-table walk fail? A: It is still a valid page-table root for some other address space
A clean translation can still be the wrong one
Section titled “A clean translation can still be the wrong one”Page-table translation can produce an address and still answer the wrong question. You may have the wrong CR3, a stale object pointer, a different build, or a capture taken between two related updates.
None of those mistakes makes the walk fail. A wrong CR3 is still a valid page-table root for some other address space, a stale pointer can still land on mapped memory, and another build’s object still has bytes where the old layout expects fields. A successful walk means only that the page tables in the file lead somewhere; each stage below checks, in a different way, that the somewhere is the object you meant:
Preserve capture provenance
Section titled “Preserve capture provenance”A capture file is only bytes; nothing inside it says where it came from. The facts needed to interpret it travel beside it, in a provenance record that covers:
- who created it and how it was captured;
- source machine or virtual machine;
- operating-system and game build;
- CPU architecture and paging mode;
- capture tool and settings;
- UTC capture time;
- physical address ranges included or omitted;
- CR3 source and the process it belongs to;
- SHA-256 digest of the unchanged file.
Keep the original read-only and run experiments on a working copy, so a mistake can never alter the only copy. If the original’s digest stops matching the record, stop: every translation made from the file assumed the recorded bytes, so none of them can be trusted until the change is explained.
Cross-check a translation three ways
Section titled “Cross-check a translation three ways”Three independent checks catch different mistakes:
- the capture translator’s physical result;
- a debugger or hypervisor translation for the same snapshot;
- semantic structure checks on the resulting bytes.
The second translator catches a bug in the first. The structure checks catch the case both translators share: a correct translation of the wrong thing.
For an object model, semantic checks might include:
- vptr points into the expected module’s read-only section;
- several vtable slots point into executable sections;
- health falls within the game’s valid range;
- position components are finite floating-point values;
- name length and termination are bounded;
- container begin/end/capacity relationships make sense;
- handles resolve with matching generations.
Here is how several of those checks land on one small object, a toy 64-bit player layout:
.rdataPlayerLayout: an 8-byte vtable pointer, a 4-byte health value, three 4-byte floats, and three 8-byte container pointers, so 8 + 4 + 3 × 4 + 3 × 8 = 48 = 3 × 16 = 0x30 bytes, and PlayerLayout::SIZE is 48. Each brace is a check on different bytes.A single field can look right by accident: plenty of unrelated memory holds a small integer that could pass for health. Four checks on different bytes, each failing in its own way, rarely all pass at a wrong address. Try the four candidates below; only the intended player passes every check.
Freed memory, filled with zeros
Decoded with PlayerLayout
Checks on different bytes
3 of 4 checks pass by accident. One failing check is enough to reject the bytes.
A successful walk still needs the intended object. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
walk the saved page tables
check the recorded capture build
check the selected object identity
check field relationships
accept only when all evidence agrees
report translated versus validatedResult: all selected fixture checks agree; return a labelled snapshot.
Final values: build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true; identity_matches = true; health = 80; maximum = 100; fields_plausible = true; accepted = true.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
build = expected; identity = expected.
Step 2, line 1. This model always reaches mapped physical address 0x7020.
build = expected; identity = expected; translated = true; physical = 0x7020.
Step 3, line 2. Compare provenance with the layout profile.
build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true.
Step 4, line 3. A plausible health value does not identify the entity.
build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true; identity_matches = true.
Step 5, line 4. The toy health 80 and maximum 100 satisfy their relation in both objects.
build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true; identity_matches = true; health = 80; maximum = 100; fields_plausible = true.
Step 6, line 5. Translation is only one stage of validation.
build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true; identity_matches = true; health = 80; maximum = 100; fields_plausible = true; accepted = true.
Step 7, line 6. Result: all selected fixture checks agree; return a labelled snapshot.
build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true; identity_matches = true; health = 80; maximum = 100; fields_plausible = true; accepted = true.
Separate capture parsing from game meaning
Section titled “Separate capture parsing from game meaning”Use layers:
fn read_player( capture: &Capture, cr3: PhysicalAddress, address: VirtualAddress,) -> Result<PlayerSnapshot, PlayerReadError> { let bytes = capture.read_virtual(cr3, address, PlayerLayout::SIZE)?; PlayerLayout::decode(&bytes)}The capture layer understands physical ranges and page tables. The layout layer understands fields and invariants. The presentation layer prints a PlayerSnapshot.
This separation keeps an object-layout mistake from corrupting translation code and makes each layer independently testable.
Account for IOMMU and platform protections
Section titled “Account for IOMMU and platform protections”Modern systems use an IOMMU and operating-system policy to restrict DMA-capable devices. On supported Windows systems, Kernel DMA Protection can isolate peripherals and block unsafe access, especially around externally accessible buses.
For defensive validation:
- check Windows Security or System Information for the reported protection state;
- keep firmware, Windows, and device drivers updated;
- use trusted devices and cables;
- lock or shut down a machine before leaving it unattended;
- follow organizational policy for external PCIe/Thunderbolt devices;
- investigate unexpected DMA-capable hardware.
Do not disable Secure Boot, virtualization-based protections, an IOMMU, or Kernel DMA Protection to make an experiment easier. A blocked path is a successful defense, not a translation failure.
A bypass reveals a failed assumption
Section titled “A bypass reveals a failed assumption”Our capture workflow makes several promises: the hash names the bytes being read, CR3 names the intended address space, bounds protect every physical read, and the decoded object passes its game-specific invariants. A successful page-table walk alone guarantees none of the other promises. A bypass in the wider sense is the same mismatch: a protection checked one signal while the unwanted effect arrived by another path. The table transfers the capture lesson to the controls studied in Chapter 13:
| Defensive gap | What the control wrongly assumed | Safer lab test |
|---|---|---|
| Coverage gap | Every execution, input, and memory path passes the point it observes | Send labeled events through toy paths and compare logs |
| Identity gap | The name, signer, PID, or handle it trusted cannot change | Restart/rebuild the toy target and verify identity is re-evaluated |
| Time gap | Trusted state stays the same between the check and the use | Pause between validation and use, mutate a fixture, and expect rejection |
| Parser gap | Producer and consumer agree about lengths and encodings | Feed a corpus of bounded malformed fixtures into both parsers |
| Privilege gap | Requests reaching a higher-privilege component are already safe to accept | Exercise a fake broker with least-privilege test tokens |
| Telemetry gap | A blocked action needs no event of its own | Assert that every allow, deny, timeout, and parse failure has an event |
| Recovery gap | A crash cannot leave hooks, input, bytes, or handles in a bad state | Inject worker errors and verify reverse-order cleanup |
An assumption becomes testable once it is stated as an invariant, a condition
that must hold every time. For example, an offline tool may promise: “a local
fixture write is accepted only when the build matches, the range is expected,
the old bytes match, and observation-only mode is off.” The sketch below is a
policy check over a toy LabState and WriteRequest, not a function that
writes to a process. It returns one named reason for each refusal:
fn validate_write_request(request: &WriteRequest, state: &LabState) -> Result<(), Denial> { // ✅ Each independent assumption gets a named, testable rejection. state.write_policy.allows_writes().then_some(()).ok_or(Denial::ReadOnly)?; (request.build_id == state.verified_build).then_some(()).ok_or(Denial::WrongBuild)?; state.memory_map.contains_writable(request.range()).then_some(()) .ok_or(Denial::OutsideAllowedRegion)?; (request.expected_bytes == state.current_bytes(request.range())?) .then_some(()) .ok_or(Denial::StateChanged)?; Ok(())}Each ? is a separate exit, so each broken assumption produces its own named denial, and a test can target exactly one of them:
The byte comparison runs last, immediately before the toy tool would act. It shortens a time gap; it does not make a remote read and write atomic. A tool that needs that guarantee must arrange exclusive ownership or have the owner of the state validate and apply the request as one operation.
The defensive lesson is to make assumptions explicit, require several independent signals, keep permissions narrow, and fail closed with useful telemetry. Chapter 13 applies the same checklist to game state, integrity checks, hooks, and decision traces.
What this chapter intentionally does not provide
Section titled “What this chapter intentionally does not provide”The chapter does not include:
- live DMA device setup;
- custom device firmware;
- kernel-driver installation for memory access;
- anti-cheat bypass or stealth;
- memory writes to a running game;
- credential or key extraction;
- targeting a third-party machine.
Those actions are unnecessary for learning page tables, memory models, object patterns, obfuscation, encryption, or forensic validation. The offline lab teaches the computer science directly and remains reproducible for every reader.
A final advanced workflow
Section titled “A final advanced workflow”Here is the capture chapter’s sequence in one place. Each step either records the identity needed by the next one or checks a claim made by an earlier step:
- Choose an open-source game or toy program you can rebuild with symbols.
- Reconstruct one class or component from the stripped build.
- Locate its constructors, destructors, vtable, containers, and ownership patterns.
- Create an offline snapshot or synthetic fixture.
- Translate one virtual address with the capture reader.
- Decode only bounded fields into a typed snapshot.
- Validate the snapshot with several invariants.
- Check the reconstruction against the debug symbols or source.
- Turn every discovered assumption into a unit test.
- Store or delete the capture according to its data-handling rules.
The capture path ends with one validated snapshot and a record of its limits. Chapter 13 turns that evidence into a question about valid game state: whichever path observed or changed the value, which state must remain valid and which boundary actually enforces it?
Check more than one field
A capture reader decodes a player object and its health field reads 100. What makes the decoded object trustworthy?
A wrong CR3 or stale pointer can still lead to a valid-looking object. Confirm the capture and process identity, the expected path to this entity, and its generation as well as independent field relationships. Two agreeing translators establish the translation, not the intended object's identity.
Finished Lesson 14.4?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.