# Investigating a wrong decision with WinDbg

DecisionLab imports a display name from a raw byte file. The application starts
with its simulated request denied. A name is not supposed to change that
decision, but one file makes the vulnerable version print ALLOW without crashing.
The exercise follows that result backwards to the copy that caused it.

This is an intentionally vulnerable native x64 lab. ALLOW is printed text; the
program performs no privileged action and changes no Windows permissions.

## Build and reproduce

Use Windows 11 x64, PowerShell, current WinDbg, and Visual Studio 2022 C++ Build
Tools with the **Desktop development with C++** workload. Extract this source
package to `C:\Lab\DecisionLab`, open PowerShell there, and run:

```powershell
Set-Location C:\Lab\DecisionLab
.\Build-Lab.ps1
.\New-LabInputs.ps1 -OutputDirectory C:\Lab\DecisionLabInputs
.\bin\DecisionLab.exe vulnerable C:\Lab\DecisionLabInputs\normal-name.bin
.\bin\DecisionLab.exe vulnerable C:\Lab\DecisionLabInputs\crafted-name.bin
.\bin\DecisionLab.exe fixed C:\Lab\DecisionLabInputs\crafted-name.bin
```

Expected results: normal input is accepted and denied; crafted input is accepted
and allowed by `vulnerable`; that same crafted file is rejected and denied by
`fixed`.
Successful execution returns zero even when the simulated decision is wrong.

The generator writes exactly three named files, replacing those fixtures when
rerun. It writes raw bytes, without a text encoding marker or newline:

| File | Bytes | Purpose |
| --- | ---: | --- |
| `normal-name.bin` | 5 | ASCII `guest` |
| `crafted-name.bin` | 36 | 32 ASCII A bytes followed by `01 00 00 00` |
| `non-grant-name.bin` | 36 | Same layout, with `02 00 00 00` at the end |

The last control changes `allowed` to two, which still produces DENY: this sample
grants only when the value equals one. It shows that the chosen bytes matter.

If PowerShell blocks a downloaded script, review its contents, then run it in a
child process with a temporary execution-policy setting, for example:

```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Build-Lab.ps1
```

That setting ends with the child process. Use the same form for
`New-LabInputs.ps1` or `Invoke-Lab.ps1` if needed. No saved policy change is
required. Building and ordinary sample runs need no elevation.

The build locates MSVC with `vswhere.exe`. It creates `bin\DecisionLab.exe` and
its matching private `bin\DecisionLab.pdb`, plus build information under
`build`. Keep that binary/PDB pair with any recording. `/Od /Ob0 /Zi` makes the
copy easy to inspect; ASLR, DEP, `/GS` and CFG remain enabled.

For a build outside the source tree, use `Build-Lab.ps1 -OutputDirectory <dir>`;
that directory will contain `bin` and `build`. An optional
`-VisualStudioPath` selects an installed Visual Studio/Build Tools directory.

## What the file can control

The file is raw bytes intended as a name. The crafted file deliberately has
four extra bytes that will map onto `allowed` in this program.
`ReadNameFile` uses `CreateFileW` and `ReadFile` to load the actual file. Empty,
missing and larger-than-36-byte files fail before the copy; a larger file is
not silently truncated. The exercise deliberately bounds input to keep every
write inside the allocated session.

```cpp
struct Session {
    char name[32];
    std::uint32_t allowed;
};
```

Compile-time assertions require `allowed` at offset 32 and `sizeof(Session)`
equal to 36. The program separately allocates and zero-initializes a session.
Its unchecked helper copies the file's byte count into a byte view of that
entire session. In the tested MSVC x64 build, bytes 32 through 35 set
`allowed` while staying inside the 36-byte heap allocation. The file's last
four bytes are chosen precisely for that result. This is an overbroad
whole-object copy, not an out-of-bounds write or a test of buffer-overflow
mitigations.

The checked helper rejects lengths of 32 or more before copying, allowing up to
31 name bytes plus a zero terminator. It uses the same input file and loader.

The debug globals remain valid through `ReportDecision`:

- `gSession`: pointer to the live session.
- `gInput`: pointer to the file buffer used by the copy.
- `gInputLength`: its byte count.
- `gInputPath`: the original Unicode file path.

## Record and investigate

TTD recording needs administrator rights. In elevated WinDbg choose **File →
Start debugging → Launch executable (advanced)**, select the built executable,
and set arguments to:

```text
vulnerable C:\Lab\DecisionLabInputs\crafted-name.bin
```

Select **Record with Time Travel Debugging**, configure the recording folder,
and record from launch. Let the sample finish, open its `.run` file, and let
WinDbg index it. Keep the matching EXE, PDB and source.

In WinDbg's command window, set symbols and stop at the last recorded decision:

```text
.symfix C:\Lab\Symbols
.sympath+ C:\Lab\DecisionLab\bin
.reload /f DecisionLab.exe
!tt 0
bu DecisionLab!ReportDecision
!tt 100
g-
dt DecisionLab!Session poi(DecisionLab!gSession)
k
```

Save the decision field's address and find its earlier writer:

```text
r $t0 = poi(DecisionLab!gSession) + 0x20
bc *
ba w1 @$t0
g-
dd @$t0 L1
k
```

Watch the first byte because this input changes it from zero to one. Other
bytes are still part of the four-byte field. The replay watchpoint can stop
after the store; inspect both sides rather than assuming RIP names the writer:

```text
l-t
t-
dd @$t0 L1
u @rip L1
r rax,rcx
dv /t
dx input[i]
dx destination + i
t
dd @$t0 L1
```

`l-t` selects instruction stepping. At the store, connect its input pointer to
the loaded file buffer:

```text
t-
dx input == DecisionLab!gInput
du poi(DecisionLab!gInputPath)
dq DecisionLab!gInputLength L1
db poi(DecisionLab!gInput) L24
```

WinDbg's default radix is hexadecimal: `L24` displays 36 bytes. The post explains
each command and the evidence connecting input, write and later decision.
A recording proves the executed sequence, not someone's intent.

For an ordinary live control, launch `fixed` with the same file, leave the TTD
recording checkbox unchecked, and use `bu DecisionLab!ReportDecision` then `g`.
The field stays zero. To attach before a copy instead, append `--pause`; the
program loads the file, prints its PID and waits for Enter.

## Optional PageHeap control

GFlags is part of the Windows SDK's **Debugging Tools for Windows** component.
In elevated PowerShell:

```powershell
$gflags = 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags.exe'
& $gflags /p /enable DecisionLab.exe /full
& $gflags /p
```

Settings apply to newly launched processes. In a fresh live WinDbg session,
with recording unchecked and the symbol setup above, run `vulnerable` with the
crafted file, break at `ReportDecision`, and inspect:

```text
dt DecisionLab!Session poi(DecisionLab!gSession)
!ext.heap -p -a poi(DecisionLab!gSession)
```

The member overwrite stays inside the requested 36-byte allocation. A separate
`boundary` mode allocates 32 bytes and writes at offset 32. Run this control
under a live debugger with full PageHeap enabled:

```text
sxe av
g
.exr -1
u @rip L2
r rax,rcx
!ext.heap -p -a poi(DecisionLab!gBoundaryBuffer)
```

Inspect the actual fault and allocation. Modern WinDbg uses the qualified
`!ext.heap`; `!heap -p` can direct you to that replacement. Windows symbols are
needed for the allocation lookup. This control uses a separate allocation and
does not create a session.

After ending the live sessions, disable the persistent setting:

```powershell
& $gflags /p /disable DecisionLab.exe
& $gflags /p
```

## Baseline evidence and package

`Invoke-Lab.ps1 -InputDirectory C:\Lab\DecisionLabInputs` saves actual output,
input hashes and hex bytes to `evidence\baseline.txt`. Its 13 cases cover the
three main runs, the non-grant control, accepted/rejected name lengths, a
shortened crafted file, Unicode paths, and input/argument failures. It excludes
the intentionally faulting PageHeap boundary control.

The ZIP contains five files: this README, `DecisionLab.cpp`,
`Build-Lab.ps1`, `New-LabInputs.ps1` and `Invoke-Lab.ps1`. The accompanying
SHA-256 sidecar verifies the ZIP. It contains no EXE, PDB, recording or dump.
Compiler versions and build paths can change your binary's hash; keep your own
matching artifacts instead of mixing recordings with a later rebuild.

## References

- [Microsoft TTD walkthrough](https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-walkthrough)
- [TTD recording](https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-record)
- [GFlags and PageHeap](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-and-pageheap)
