Skip to content
Lesson 14.4Versioned Evidence, State, and Integrity

Capture Provenance and Validation

A plausible translation can still describe the wrong process, build, or moment; provenance and independent checks give offline bytes meaning.

Lesson 14.4
Read or listenPrint chapter or book
About 34 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 14.4Key ideas, reference and questions

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
Download cheatsheet (.md)

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:

no
yes
NonCanonical, NotPresent,
or OutOfRange
physical address
no
yes
no
yes
capture file
SHA-256 matches the
provenance digest?
stop: the file changed
walk the page tables
with the recorded CR3
stop: no translation
a second translator
gives the same address?
stop: translation disagreement;
check implementation and capture layout
decode the object layout
every invariant holds?
reject: wrong object,
wrong build, or stale pointer
accept a PlayerSnapshot

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.

Three independent checks catch different mistakes:

  1. the capture translator’s physical result;
  2. a debugger or hypervisor translation for the same snapshot;
  3. 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:

+0x00vptr
+0x08health
+0x0Cx
+0x10y
+0x14z
+0x18begin
+0x20end
+0x28capacity
in module .rdata
0 to max
finite floats, no NaN or infinity
item list, begin ≤ end ≤ capacity
A 48-byte PlayerLayout: 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.

Four checks on one decoded playerEach candidate is 48 bytes decoded with the same PlayerLayout. A health of 0 to 100 and a vptr inside the game’s .rdata are expected.

Freed memory, filled with zeros

Decoded with PlayerLayout

Field (offset)
0x0vptr (+0x00)
0health (+0x08)
0, 0, 0x, y, z (+0x0C)
0x0 ≤ 0x0 ≤ 0x0begin, end, capacity (+0x18)

Checks on different bytes

Each check
fails: vptr in .rdata
passes: 0 to 100
passes: finite floats
passes: begin ≤ end ≤ capacity
Verdict
reject: 1 of 4 checks fail

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 validated

Result: 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
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    build = expected; identity = expected.

  2. Step 2, line 1. This model always reaches mapped physical address 0x7020.

    build = expected; identity = expected; translated = true; physical = 0x7020.

  3. Step 3, line 2. Compare provenance with the layout profile.

    build = expected; identity = expected; translated = true; physical = 0x7020; build_matches = true.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

bytes, or a MemoryError
PlayerSnapshot, or a PlayerReadError
capture layer
physical ranges, page tables
layout layer
fields and invariants
presentation layer
prints the snapshot

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.

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:

no
yes
no
yes
no
yes
no
yes
write request
write policy
allows writes?
Denial::ReadOnly
build_id matches
the verified build?
Denial::WrongBuild
range inside a
writable region?
Denial::OutsideAllowedRegion
current bytes equal
the expected bytes?
Denial::StateChanged
Ok, the write may proceed

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.

Advanced

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:

  1. Choose an open-source game or toy program you can rebuild with symbols.
  2. Reconstruct one class or component from the stripped build.
  3. Locate its constructors, destructors, vtable, containers, and ownership patterns.
  4. Create an offline snapshot or synthetic fixture.
  5. Translate one virtual address with the capture reader.
  6. Decode only bounded fields into a typed snapshot.
  7. Validate the snapshot with several invariants.
  8. Check the reconstruction against the debug symbols or source.
  9. Turn every discovered assumption into a unit test.
  10. 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?

Knowledge check

Check more than one field

A capture reader decodes a player object and its health field reads 100. What makes the decoded object trustworthy?

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.

Chapter 14: 10 lessons

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.