Skip to main content
ClaudeWave
Skill391 repo starsupdated yesterday

windiff-version-diff-analysis

This Claude Code skill analyzes security-relevant differences between two Windows operating system builds by comparing binary symbol, type, and syscall data. It uses the WinDiff repository's command-line tools to generate per-binary JSON databases, then interprets the deltas to identify new attack surface, exploitation vectors, and defensive implications. Use this when conducting Windows security research that requires understanding what changed between specific OS versions and why those changes matter for system hardening or threat modeling.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/ergrelet/windiff /tmp/windiff-version-diff-analysis && cp -r /tmp/windiff-version-diff-analysis/skills/windiff-version-diff-analysis ~/.claude/skills/windiff-version-diff-analysis
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# WinDiff Version Diff Analysis

Compare two Windows builds and turn the raw symbol/type/syscall delta into a
security-research report: what was added, what it probably *does*, and why it
matters for attack surface, exploitation, or defense.

Run this skill from a **WinDiff** repository checkout. It uses `windiff_cli` to
generate the per-binary JSON databases, then diffs and interprets them. The
interpretation is the point: explain intent from Windows internals conventions
instead of merely listing symbols.

## Locate bundled resources

Resolve all `scripts/` and `references/` paths relative to this `SKILL.md`, not
relative to the current working directory and not through a harness-specific
directory such as `.claude/` or `.agents/`. Before running a bundled script, set
`SKILL_DIR` to the absolute directory containing this file. The examples below
assume that has been done:

```bash
SKILL_DIR="<absolute directory containing this SKILL.md>"
```

If separate shell-tool calls do not share environment, substitute that absolute
path for `$SKILL_DIR` in each command instead of relying on prior shell state.

Also identify the repository root (the directory containing `windiff_cli/`,
`windiff_frontend/`, and `ci/`) and run repository commands from there. Keep
generated configs, databases, and analysis artifacts under its git-ignored
`local/` directory.

## Workflow

### 1. Pin down scope

Establish, asking the user only if genuinely ambiguous:

- **Two OS versions** as WinDiff triples `version / update / architecture`
  (e.g. `21H2 / BASE / amd64` and `11-24H2 / KB5074105 / amd64`). `update` is
  `BASE` for an RTM image or a `KB...` number for a patch. The path suffix used
  in filenames is `version_update_architecture`, e.g. `11-24H2_KB5074105_amd64`.
- **Binaries** to compare. Default to the security-relevant core when the user is
  vague: `ntoskrnl.exe`, `ntdll.dll`, `win32k.sys`, `win32kbase.sys`,
  `win32kfull.sys`, `ci.dll`, `cng.sys`. Read
  `$SKILL_DIR/references/windows-components.md` for what each one governs.
- **Focus**: syscalls, mitigation flags, new attack surface, a specific
  component/feature, etc. This steers interpretation, not data generation.

`ci/db_configuration.json` is the canonical list of tracked versions and binaries
— consult it for valid `version`/`update` spellings.

### 2. Generate the databases with windiff_cli

Write a **minimal** config containing only the two OS versions and the chosen
binaries, then run the CLI into a scratch output dir (keep it under the repo's
git-ignored `local/`). Use `$SKILL_DIR/scripts/make_config.py` to build the
config:

```bash
python3 "$SKILL_DIR/scripts/make_config.py" \
  --os "21H2:BASE:amd64" --os "11-24H2:KB5074105:amd64" \
  --binary ntoskrnl.exe --binary ntdll.dll --binary win32k.sys --binary ci.dll \
  > local/windiff_diff_config.json

cd windiff_cli
cargo run --release -- --low-storage-mode \
  ../local/windiff_diff_config.json ../local/windiff_diff_out/
```

This downloads PEs from Winbindex and PDBs from MSDL, so it **needs network
access** and takes minutes per binary. Follow the active harness's normal
permission or approval flow for networked commands. `--low-storage-mode` keeps
memory bounded. If the CLI fails for one OS (a build may be missing from
Winbindex), report which version/update is unavailable and suggest the nearest
tracked one from `ci/db_configuration.json`.

If the user says the databases already exist (e.g. in `windiff_frontend/public/`),
skip generation and point the diff script at that directory instead.

### 3. Diff each binary

`$SKILL_DIR/scripts/windiff_diff.py` does the deterministic set/text diff so you
never hand-compute it. Run it per binary; it prints a summary to stderr and
structured JSON to stdout.

```bash
python3 "$SKILL_DIR/scripts/windiff_diff.py" \
  local/windiff_diff_out ntoskrnl.exe 21H2_BASE_amd64 11-24H2_KB5074105_amd64 \
  > local/diff_ntoskrnl.json
```

Use `--list` to see available suffixes, `--kinds` to restrict (e.g.
`--kinds syscalls types`). Anonymous `_unnamed_0xNNNN` types are hidden from the
top-level added/removed/modified lists by default (their synthetic ids churn
between builds — noise); pass `--include-anon` only if you specifically need them.

**`resolved_member_changes` — where new mitigation flags actually show up.**
Bitfields like `_EPROCESS::MitigationFlagsValues`, `MitigationFlags2Values`, or
`_KPROCESS` flag words are typed as *anonymous* `_unnamed_0xNNNN` structs, and the
individual bits (e.g. `RedirectionTrustPolicyEnabled : 1`) live inside them. When
Microsoft adds a mitigation, a new bit appears in that anonymous struct — and its
synthetic id churns, so a naive diff would either hide it or show it as noise. The
script resolves this for you: the `types.resolved_member_changes` array follows
each anonymous member back to its named parent (across the id change) and reports
the real per-member delta as `<parent>::<member>` with the added/removed
declarations. **This is the first place to look for new mitigation bits and other
new bitfield flags** — e.g. a new bit under `_EPROCESS::MitigationFlags2Values`, or
a new `_KALPC_MESSAGE::u1::s1` flag. Resolution recurses through nested anonymous
structs/unions, so the `path` may be several `::` levels deep.

**Noise to discount when reading the output:**
- The script already strips `modified` lines that differ only by an anonymous type
  id, and folds genuine anonymous-struct changes into `resolved_member_changes`.
  What remains in `modified` is real: renamed/added named fields, size changes, new
  enum values. Still sanity-check against `resolved_member_changes` for the bits.
- Exports differing only by ordinal/decoration are usually not meaningful.
- Syscall renumbering with no name change is a rebuild artifact (see
  `$SKILL_DIR/references/windows-internals.md` §3).

### 4. Interpret with Windows internals knowledge — the core of the analysis

For every meaningful addition, infer **what it is and why it matte