Create your own
Lesson illustration

Documenting Build, Mitigations, Symbols, Inputs, and Crash Signatures

Good debugging notes become increasingly valuable as a lab grows: they prevent accidental comparisons between different binaries, inputs, and mitigation states. In the previous lesson, you reconstructed a crash from the exception context outward—faulting instruction, register dataflow, ABI artifacts, and source correlation. This lesson turns that analysis into a lab record: a compact, evidence-backed specification of one reproducible target and one expected failure.

The aim is not to write a narrative postmortem. It is to create a record that lets you—or a later exploit-development session—answer five questions without guessing:

  1. Exactly which executable build crashed?
  2. Which mitigations were compiled, linked, and active at runtime?
  3. Which symbols and source paths made the analysis trustworthy?
  4. Which input and launch conditions triggered it?
  5. What stable crash signature should a correct reproduction produce?

1. Treat a crash as an experiment with a fixed configuration

A crash is only comparable to another crash if the relevant variables are controlled. For this course, a useful unit of work is:

This is more stringent than recording “NativeLabPlain.exe crashed with my malformed file.” The same filename can refer to a rebuilt executable; the same source can compile differently under changed optimization or linker flags; and the same malformed file can reach different behavior after a parser change.

A lab record should therefore distinguish facts observed from the artifact from settings claimed by the build configuration:

CategoryStrong evidenceWhy it matters
Target identityFile path, SHA-256, PE machine type, file size, build timestampDistinguishes a precise executable from another build with the same name.
Build intentExact compiler and linker command lines, project settings, build configurationExplains what the build was supposed to contain.
Binary propertiesdumpbin / PE inspection output, loaded-image informationChecks what was actually emitted into the PE image.
Runtime policyCaptured process-mitigation output from the launched processSome policies can be imposed or altered outside the image itself.
Symbol provenancePDB path, load status, signature/age where available, source pathPrevents source lines and function names from being treated as trustworthy when they are not.
TriggerInput hash, delivery method, command line or UI sequenceMakes the crash reproducible.
SignatureException, module-relative fault location, instruction, normalized stackAllows equivalent failures to be recognized despite ASLR.

The principle is simple: do not replace evidence with a label. For example, “CFG enabled” is incomplete unless your record says whether that conclusion came from linker settings, PE load-configuration metadata, runtime process policy, or all three.


2. Capture target identity before interpreting the crash

Start each record with a target identity block. A cryptographic hash is the anchor; timestamps and filenames are useful supporting metadata but are not unique identifiers.

For a laboratory target, collect this before opening the dump:

$target = "D:\Lab\NativeLab\bin\NativeLabPlain.exe"

Get-Item $target |
    Select-Object FullName, Length, LastWriteTimeUtc

Get-FileHash $target -Algorithm SHA256

& dumpbin /headers /loadconfig $target

Also preserve the build’s:

  • Git commit or source archive identifier, if your lab uses one;
  • configuration and architecture, such as Release|x64;
  • compiler and linker versions;
  • full compiler/linker command lines, or an archived build log;
  • exact VM snapshot name and OS build;
  • any target-side configuration file that changes parsing or allocation behavior.

A SHA-256 has a specific role: it identifies the bytes you analyzed. It does not prove that the running process actually mapped those same bytes. That is why the dump-side symbol and module checks later in this lesson remain necessary.

For a native x64 target, record the architecture explicitly. An x86-oriented observation—such as SafeSEH—is not meaningful for an x64 image and should be recorded as not applicable, not as “disabled.”


3. Record mitigation state in layers

Mitigations have different origins. The compiler may emit instrumentation; the linker may add PE metadata; Windows may apply process policy at launch; and hardware-dependent facilities may be unavailable or inactive. A useful record preserves those distinctions rather than collapsing them into one yes/no column.

Build reliable and secure C++ programs | Microsoft Learn

Read Microsoft Learn’s mitigation and debug-provenance guidance to anchor the fields you will preserve in the lab record. Focus on the difference between compiler/linker opt-ins and the build artifacts—especially PDBs—needed to investigate a specific build later.

In the section beginning “Mark binaries as compatible with OS runtime security mitigations,” read the mitigation options, noting /GS, /DYNAMICBASE, /HIGHENTROPYVA, /guard:cf, /CETCOMPAT, and /guard:ehcont. Then continue through the data-execution-prevention bullet, ending with the DEP note. In the later section, “Maximize code provenance guarantees and efficiency of security response,” read the provenance discussion; focus on why private matching PDBs and source information must be archived with the build.

For the current course stage, use a mitigation matrix like this:

Mitigation / propertyBuild setting or sourcePE evidenceRuntime evidenceRecord as
Stack cookie/GSSecurity-cookie-related metadata where applicable; compiler/linker logCrash behavior only if observedenabled / disabled / unknown
ASLR/DYNAMICBASEPE DLL characteristicsLoaded module base across launchesenabled / disabled
High-entropy ASLR/HIGHENTROPYVAPE characteristicsx64 observed base variationenabled / disabled / not applicable
DEP compatibility/NXCOMPATPE DLL characteristicsProcess DEP policy if collectedenabled / default x64 behavior / unknown
CFG/guard:cf at compile and linkGuard flags/load-config metadataProcess CFG policyenabled / disabled / unknown
CET compatibility/CETCOMPATImage compatibility metadataUser shadow-stack policy and hardware supportcompatible / active / inactive / unknown
EH continuation protection/guard:ehcontEH continuation metadataRelevant process policy where applicableenabled / disabled / unknown
SafeSEH/SAFESEHx86-only featurenot applicable on x64

Two rules keep this table honest:

  • Do not infer compiler instrumentation solely from a source-project checkbox. Preserve the actual command line or build log and inspect the resulting image.
  • Do not equate an image opt-in with active runtime enforcement. An image can be CET-compatible while a process has no active user-mode shadow-stack policy.

Detailed interpretation of CFG, CET, XFG, EH continuation protection, ACG, and CIG follows in the later mitigation module. Here, your immediate objective is to collect enough evidence that future testing is performed against a known policy state.

To capture process policy for a running laboratory target, start it in a controlled run and save the raw output:

$p = Start-Process `
    -FilePath "D:\Lab\NativeLab\bin\NativeLabPlain.exe" `
    -ArgumentList '"D:\Lab\cases\case-001.bin"' `
    -PassThru

Get-ProcessMitigation -Id $p.Id |
    Out-File -Encoding utf8 ".\case-001-process-mitigations.txt"

If your target is GUI-driven, launch it normally, identify the intended process ID, and run the same command before triggering the crash. Record the command output verbatim as an attachment and summarize only the relevant settings in the main record. Do not silently substitute system defaults for process-specific evidence.


4. Make symbol provenance part of target identity

A matching PDB is not a convenience; it affects the credibility of source lines, local variables, function names, and unwound stack frames. Microsoft public symbols help interpret Windows components, but they do not replace the private PDB for your own target build.

WinDbg showing the command window alongside source, disassembly, memory, registers, and stack panes. A lab record should preserve command-derived evidence—especially module and symbol status—rather than relying on a screenshot alone.

Use a controlled symbol path that combines a cache, Microsoft’s symbol server, and your archived private symbols:

.symfix C:\Symbols
.sympath+ D:\Lab\NativeLab\symbols
.reload /f NativeLabPlain.exe
.sympath
lmv m NativeLabPlain
!lmi NativeLabPlain
.srcpath

Record at least:

  • the active symbol path from .sympath;
  • the source path from .srcpath, if source debugging is used;
  • lmv m NativeLabPlain output, including Symbol Type, Symbol Status, and Symbol File Name;
  • the private PDB’s archival path;
  • PDB signature and age when available from image/module information;
  • whether target source lines are available, unavailable, or suspected mismatched;
  • the symbol-server/cache paths used for Windows modules.

A correct PDB match should resolve your target’s crash location consistently in ln @rip, uf @rip, and kv. A superficially plausible function name is not enough evidence on its own.

Introduction to Windbg Series 1 Part 3 - Introduction To debug Symbols

Watch “Introduction to Windbg Series 1 Part 3 - Introduction To debug Symbols” by TheSourceLens for a compact demonstration of symbol-path setup, reloading, module symbol status, and address-to-symbol lookup.

Watch the WinDbg workflow. Focus on the purpose of the local symbol cache, the .reload step, lmv for loaded-PDB status, and the complementary roles of x and ln. In your record, preserve the observed lmv status rather than merely writing “symbols loaded.”

A useful practical distinction:

  • NativeLabPlain!ReadField+0x34 means a symbol was found.
  • NativeLabPlain+0x1234 means the module is known but no function-level target symbol was resolved.
  • A source line and dv /t locals may require more complete debug information than a simple function name.

For later exploit analysis, the most important failure mode is an accidental target/PDB mismatch. If the PDB is not demonstrably tied to the target binary, write symbol reliability: unverified and avoid treating source-level values as facts.


5. Specify the trigger as carefully as the executable

A crash reproducer needs an identity, a transport, and a procedure.

For a file-processing target, record:

$input = "D:\Lab\cases\case-001.bin"
Get-Item $input | Select-Object FullName, Length, LastWriteTimeUtc
Get-FileHash $input -Algorithm SHA256

Then state:

  • the input filename and SHA-256;
  • input size;
  • generator or mutator script version and arguments, if applicable;
  • delivery surface: command-line file argument, File Open dialog, drag-and-drop, clipboard, plugin, or IPC;
  • exact launch command line;
  • UI actions, in order, if a GUI operation is required;
  • any necessary timing, wait condition, or configuration state;
  • whether a fresh process is required;
  • whether the crash occurs under a debugger, without one, or both.

For example:

Input:
  ID: case-001
  Path: D:\Lab\cases\case-001.bin
  SHA-256: <record actual digest>
  Size: 4,112 bytes
  Delivery: File > Open, then select case-001.bin
  Required setting: “Preview records” enabled
  Target launch: NativeLabPlain.exe
  Process state: clean launch; no previously opened documents

Avoid descriptions such as “open the malformed file.” They do not identify the file or clarify whether a parser path was entered through command-line processing, a dialog callback, a preview handler, or a plugin.


6. Define a stable crash signature

Absolute virtual addresses vary under ASLR, and raw stack addresses vary with thread and process layout. Therefore, a crash signature should use normalized properties.

For an image mapped at runtime base , with exception-time instruction pointer , the image-relative location is:

For the exact same binary, this RVA remains stable across ordinary ASLR relocations. A symbolic function-plus-offset, such as NativeLabPlain!ReadField+0x34, is often more readable, but it depends on trustworthy symbols. Preserve both when possible.

Run this evidence-capture sequence in WinDbg after opening the dump:

!analyze -v
.exr -1
.ecxr
r
ln @rip
u @rip L8
kv
lmv m NativeLabPlain
!lmi NativeLabPlain

Using the Analyze Extension - Windows drivers

Read Microsoft’s user-mode !analyze -v example as a guide to the fields that belong in a concise crash signature. The automated analysis is a starting point: confirm its claims against the exception record, exception context, disassembly, and matching symbols.

In “A User-Mode !analyze -v Example,” begin with the command’s purpose. Then follow the user-mode example from the FAULTING_IP output through BUCKET_ID. Pay particular attention to the displayed exception record, process name, symbolic fault location, and STACK_TEXT; the example explains that the stack and symbol fields are debugger-produced evidence you can repeat with .ecxr ; kb.

A robust expected signature contains the following:

FieldExample formWhy it is stable/useful
Exception0xC0000005Distinguishes access violation from a fail-fast, breakpoint, or other exception.
Access operationread, write, or executeComes from the exception parameters and faulting instruction.
Fault moduleNativeLabPlain.exeIdentifies the responsible image.
Normalized fault locationNativeLabPlain!ReadField+0x34; RVA recorded separatelySurvives ASLR if the binary and symbols match.
Faulting instructionmovzx eax, byte ptr [rax+rdx]Captures the actual machine operation.
Top stack framesnormalized module/function/offset framesDistinguishes parser routes reaching similar faults.
Trigger identitycase-001 plus input hashConnects the crash to exact bytes and delivery method.
Build identitytarget SHA-256Prevents cross-build comparison.

Do not use the following as the sole signature:

  • the absolute RIP value;
  • a Windows Error Reporting bucket ID;
  • a single function name without offset;
  • the first stack frame alone;
  • a debugger screenshot;
  • “crashes reliably.”

Those can be helpful supplementary artifacts, but none uniquely defines a reproducible laboratory failure.

A compact expected-signature entry might look like this:

Expected crash signature:
  Exception: 0xC0000005, invalid read
  Fault module: NativeLabPlain.exe
  Fault location: NativeLabPlain!ReadField+0x34
  Fault RVA: 0x0000000000012A34
  Faulting instruction: movzx eax, byte ptr [rax+rdx]
  Top normalized frames:
    NativeLabPlain!ReadField+0x34
    NativeLabPlain!ParseRecord+0x91
    NativeLabPlain!OpenDocument+0x1C8
  Symbol requirement: private PDB matches target SHA-256
  Acceptance rule: all fields above match; absolute addresses may differ

The expected signature is a prediction made before a later run. The observed signature is the evidence captured from that run. Keeping them separate exposes drift immediately: an input may still crash, but in a different parser function, with a different exception type, or only after a target rebuild.


7. Assemble the record: concise, attached to evidence, reproducible

Use one lab record per target/input/crash case. Markdown is practical because it accommodates prose, command output attachments, and version control.

# Lab case: case-001 — baseline invalid read

## Scope and environment
- Purpose: reproduce an intentional laboratory parser crash.
- VM snapshot: WS2022-ExploitLab-Base-01
- OS: Windows Server 2022, build <record actual build>
- Debugger: WinDbg <record version>
- Run type: clean process launch; user-mode full dump.

## Target build identity
- Executable: D:\Lab\NativeLab\bin\NativeLabPlain.exe
- SHA-256: <actual digest>
- Architecture: x64
- Build configuration: Release|x64
- Source revision: <commit or archive ID>
- Compiler/linker: <actual versions>
- Build-log attachment: build-case-001.txt

## Mitigation evidence
| Property | Build evidence | PE evidence | Runtime evidence | Status |
|---|---|---|---|---|
| /GS | <command/log> | <inspection result> | n/a | <status> |
| ASLR | <command/log> | <inspection result> | <policy/output> | <status> |
| CFG | <command/log> | <load-config result> | <policy/output> | <status> |
| CET | <command/log> | <PE result> | <policy/output> | <status> |
| EHCONT | <command/log> | <PE result> | <policy/output> | <status> |

- Full PE-inspection attachment: target-headers-loadconfig.txt
- Full process-policy attachment: case-001-process-mitigations.txt

## Symbols and source
- Symbol path: <.sympath output>
- Source path: <.srcpath output>
- Target PDB: <path and load status from lmv>
- PDB identity: <GUID/age if available>
- Windows symbols: Microsoft public server via <cache path>
- Symbol reliability: verified / unverified
- Module-information attachment: lmv-target.txt

## Trigger input and procedure
- Input: D:\Lab\cases\case-001.bin
- SHA-256: <actual digest>
- Size: <actual size>
- Delivery path: <CLI / File Open / drag-and-drop / etc.>
- Reproduction steps:
  1. Launch the target from a clean process state.
  2. Deliver the named input through the stated path.
  3. Save a full user-mode dump at the first-chance/second-chance condition defined for this lab.

## Expected crash signature
- Exception:
- Access type:
- Module and normalized fault location:
- Fault RVA:
- Faulting instruction:
- Required top stack frames:
- Acceptance rule:
- Allowed variability: ASLR-dependent absolute addresses only.

## Observed crash evidence
- Dump: <path and SHA-256>
- !analyze -v attachment:
- .exr -1 output:
- .ecxr register output:
- Disassembly around RIP:
- kv stack:
- Register-to-source reconstruction:
- Match to expected signature: match / mismatch / partial

## Notes and unresolved evidence
- Direct observations:
- Hypotheses:
- Known differences from baseline:

The “Notes and unresolved evidence” section matters. It prevents an early source-level interpretation—such as “the index is attacker-controlled”—from being copied forward as a fact before the input-to-register path has been proven.

A high-quality record is short enough to use during a live debugging session, but complete enough that a clean VM run several weeks later can reproduce the same target, input, and expected crash class.


Key takeaways

A lab record is the reproducibility contract for the rest of the course:

  • Identify the target by SHA-256 and architecture, not filename alone.
  • Record mitigation evidence in layers: build intent, PE properties, and runtime policy are related but not interchangeable.
  • Preserve matching private PDB provenance and loaded-symbol status alongside Microsoft public-symbol configuration.
  • Identify inputs by hash and document the exact delivery path and clean-launch conditions.
  • Normalize crash signatures using module-relative locations, symbolic offsets, instruction details, and top stack frames—not absolute ASLR-dependent addresses.
  • Keep expected and observed signatures separate, and distinguish direct evidence from hypotheses.

The next module moves beneath these artifacts into PE loading and process memory: how image sections, directories, relocations, mapped regions, and loaded DLLs explain the addresses that appear in your crash records.

Can't find a good explanation? Sign up and we'll make it for you

Sign up