Commit Graph
100 Commits
Author SHA1 Message Date
NK e35f5050a1 Bump to 1.4.4-beta.1 2026-06-07 19:04:11 +02:00
NK 899da8acf1 fix(chrome): drop heuristic in-process extractors, keep discovery only
User testing of --chrome-process-scan on a VMware Win10 Edge session
exposed the heuristic's structural limits. The password matcher captured
URL-path fragments as usernames (`internal/`, `api/v1/`), CJK noise from
random bytes read as UTF-16, and chrome.dll auth-flow constants
(login.microsoft.com etc) — 71 reported "passwords", 0 real. The cookie
matcher fared similarly: 99k hits whose top hosts looked real
(americanexpress.com, youtube.com, apartments.com) but whose individual
rows were dominated by UUID-prefixed concatenated hostnames, JS-token
names (`typeof`, `viz.mojom.GpuHostMessageHeader`) and minified-JS
values (`Symbol&&Symbol.`, `||void 0===t||t`).

Tighter and tighter filters reduced the noise (TLD allowlist; RFC 6265
token-shape names; URL-fragment + auth-noise + JS-keyword rejection;
mixed-class value enforcement; dedup) but each pass either still leaked
thousands of false positives or rejected real cookies. The structural
problem is that chrome process memory contains too many sequences that
look like `domain\0name\0value` without being one; the heuristic has no
way to tell a CanonicalCookie instance from a string-table entry.

Strip both extractors from process_scan. The flag now walks every
running chromium process through the page-table region enumerator and
emits one info-level log line per process ("PID/image/MiB resident")
plus a discovery total — useful as "a browser was active when the
snapshot was taken; here's how much RAM it had touched" intel, no fake
data. ChromeFindings stays empty for the memory side, so disk-side
results aren't polluted.

The fix going forward is the per-Chrome-version `CookieMonster` locator
signature (chrome.dll destructor pattern → vtable → heap scan → RB-tree
walk). The CanonicalCookie struct layouts and tree walker remain in
`cookie_monster.rs` ready for that work — only the locator pattern is
missing, and adding it is non-noisy (it either finds a real
CookieMonster instance or it doesn't).

Tests: 86/86 pass. VMware Win10 hybrid baseline (83 cookies + 1
password from disk) preserved with --chrome-process-scan toggled either
way. Docs updated to match: README "in-process memory scan" section
becomes "in-process discovery", architecture.md adds the rationale,
examples.md replaces the fake-results sample with a real -v discovery
trace.
2026-06-07 19:04:11 +02:00
NK ae464e92e3 Bump to 1.4.3-beta.1 2026-06-07 17:20:24 +02:00
NK 4443df8e46 docs(chrome): document --chrome-process-scan + acknowledge ChromeKatz
README:
- Reframe the chrome module as four extraction vectors (was three): add
  the in-process scan (--chrome-process-scan) alongside disk-only,
  hybrid and memory-only. Note the heuristic-noise tradeoff and the
  upcoming per-Chrome-version locator that will replace it.
- Mention Edge ≤ 147 plaintext-in-memory disclosure (Rønning, April
  2026) as the concrete motivation for the in-process vector.
- Add Acknowledgements entries: ChromeKatz (Meckazin) for the in-process
  scan inspiration + CanonicalCookie struct layouts we ported,
  xaitax/Chrome-App-Bound-Encryption-Decryption as the v20 format
  reference (we intentionally took a different offline-friendly path),
  DonPAPI and dploot as DPAPI chain references.
- Call out what we deliberately do *not* implement: live COM IElevator
  abuse and debugger-based ABE bypasses (VoidStealer et al), since
  vmkatz is an offline forensic tool.

docs/architecture.md:
- Update the src/chrome/ file tree with process_scan.rs and
  cookie_monster.rs, and the new src/paging/regions.rs.
- Add a new "In-process memory scan (opt-in)" subsection explaining the
  page-walk → ProcessMemory → heuristic pipeline, the role of the TLD
  allowlist in plausible_cookie, and the cookie_monster.rs layouts that
  the future locator signature will unwrap.

docs/examples.md:
- New "In-process scan (--chrome-process-scan, opt-in)" example with
  the per-process scan log lines and the Edge memory-resident vault
  use-case.

src/chrome/mod.rs:
- Refresh the module-level rustdoc with the in-process scan entrypoint
  and the two new submodules (process_scan, cookie_monster).
2026-06-07 17:20:14 +02:00
NK 97fc6996b3 feat(chrome): in-process scanner + --chrome-process-scan opt-in flag
Wire up the chrome memory vector that was scaffolded but never reached
from the CLI runner. New `chrome::process_scan::scan_chromium_processes`
walks every chrome.exe / msedge.exe / brave.exe / vivaldi.exe / opera.exe
in the snapshot, dumps each one's mapped userland through the page-walk
region enumerator + `ProcessMemory`, and runs the existing `heuristic`
password / cookie scanners over the bytes.

CLI: `--chrome-process-scan` (off by default). The disk-side decrypt
remains the high-fidelity path; the in-process scan is heuristic and
noisy, so it's opt-in. Findings are tagged `ChromeSource::Memory { pid,
process }` and merged into the same `ChromeFindings` document the disk
path produces, so renderers don't change.

Heuristic tightening: `plausible_cookie` now requires a common TLD on
the host (small allowlist of ~45 entries) and a token-shape cookie name
(RFC 6265 token chars, must start with a letter or underscore, bounded
length). On a VMware Win10 hybrid the TLD filter alone halved the false-
positive cookie count; remaining noise needs the per-version signature
work to go away properly. Updated two existing tests that used
`target.example` / `t.example` hostnames now that the heuristic gates on
TLD.

End-to-end status:
- Default flag set: VMware Win10 hybrid = 83 cookies + 1 password,
  Proxmox VULN-WIN11 hybrid = 15 cookies + 0 passwords (baselines
  unchanged).
- `--chrome-process-scan` on VMware Win10: +99k heuristic cookies + 71
  heuristic passwords from msedge.exe processes. Useful as a "what's in
  memory right now" signal even though many entries are false positives
  until the signature locator lands.

86/86 lib tests pass.
2026-06-07 16:18:01 +02:00
NK 2550ee6b8c feat(chrome): port CanonicalCookie struct layouts + tree walker from ChromeKatz
Add `chrome::cookie_monster` with the precise in-process struct layouts
the upcoming signature-based locator unwraps:

- `OptimizedString`     — MSVC `std::string` short-string-optimization
                          (23-byte inline buffer + length byte; heap
                          pointer + separate length when len > 22)
- `ProcessBoundString`  — Chrome 130 / Edge 130 cookie value with
                          process-bound CryptProtectMemory encryption
                          (we report `<encrypted in-process>` since we
                          can't reverse it offline)
- `RbNode` / `RbRoot`   — MSVC `std::map` red-black tree nodes for the
                          per-CookieMonster cookie store
- `CookieVariant`       — Chrome 124 / Chrome 130 / Chrome 130 PB /
                          Edge 130 / Edge 130 PB / Legacy with the
                          matching field offsets (partition_key size
                          differs per browser, plus the value field
                          flips between OptimizedString and PB)
- `read_optimized_string`, `read_cookie`, `walk_cookie_tree` — readers

The per-Chrome-major-version locator pattern (ChromeKatz's 152-byte
destructor-bytes signature, patched with chrome.dll base) needs IDA
RE per release and is queued as a follow-up. Until then,
`chrome::process_scan` uses the existing heuristic ASCII / UTF-16
scanners; exposing these structs now means the future signature work
only adds the locator, not the readers.
2026-06-07 16:17:39 +02:00
NK b53bfc9b22 feat(paging): page-walk-based user region enumerator (chrome feature only)
Walk a process's page tables top-down (PML4 → PDPT → PD → PT) and emit
every present 4 KiB page in the canonical low half (PML4 indices 0..256)
as contiguous `MappedRegion`s. 1 GiB huge pages and 2 MiB large pages
contribute single regions; adjacent 4 KiB pages are coalesced.

Used by the upcoming chrome in-process scanner to feed the heuristic
pattern matchers without doing a brute-force 128 TiB virtual-address
scan. Capped at 4 GiB total bytes returned per process to prevent
runaway allocations on corrupt page tables.

Gated by `#[cfg(feature = "chrome")]` so the default build is unchanged.
2026-06-07 16:17:25 +02:00
NK b58bc2fbf1 Bump to 1.4.2-beta.1 2026-06-07 14:49:20 +02:00
NK df7053fcc4 docs(chrome): refresh module + user docs to match shipping state
The earlier doc pass described the chrome module as "partial — disk
masterkey derivation in progress" but every stage now lands end-to-end:

- README: replace the WIP blurb with three concrete decryption paths
  (disk-only, hybrid, memory-only), the `--chrome-password` flag, the
  ABE auto-key extraction note, and validation status across Win10 and
  Win11 22H2/24H2/25H2 and Win Server 2019/2022/2025.
- docs/architecture.md: full file tree of `src/chrome/`, the three-stage
  decrypt chain (DPAPI MK → Chrome v10 → v20 ABE), and the candidate-retry
  rationale (`decrypt_blob` has no HMAC verify, so wrong MKs silently
  produce garbage and we pick by layer-shape validation).
- docs/examples.md: real sample output for disk-only, disk-only with
  `--chrome-password`, hybrid mem+disk, and `--chrome-json`.
- docs/tested-targets.md: chrome rows for the validated VMs (VMware Win10
  hybrid 83 cookies; Proxmox VULN-WIN11 hybrid 15 cookies with IV
  recovery; FUZZ-DBG fleet 52 each; ESXi Acronis archive 6-18 cookies).
- src/chrome/mod.rs: replace the placeholder stub doc with a module-level
  overview of the three entrypoints + every submodule, and drop the dead
  `run_placeholder` fn that the CLI never called.

No behavior change.
2026-06-07 14:49:20 +02:00
NK b119c187c4 refactor(chrome): extract decrypt_layer + run_chrome_hybrid for readability
Two clarity passes, no behavior change:

- src/chrome/abe.rs: split `unwrap_app_bound_with_resolvers`. The old
  version held a 60-line `try_layer` closure that took a `&dyn Fn`
  validator plus two separate `validate_layerN` closures. Now there's
  an `AbeLayer { User, System }` enum that owns `label()` and
  `validates()`, and a free `decrypt_layer` fn that takes the layer
  enum directly. The top-level `unwrap_app_bound_with_resolvers` is
  back to a single short function whose body is "build the candidate
  collector and dispatch the two layers."

- src/main.rs: extract the hybrid disk-open block into `run_chrome_hybrid`.
  The inline form used an immediately-invoked closure for the `?`
  early-return — concise but unusual; a named helper is easier to read
  and lifts the "open disk once" invariant into the function signature.

VMware Win10 (83 cookies) and Proxmox VULN-WIN11 (15 cookies) unchanged.
83/83 lib tests still pass.
2026-06-07 14:49:20 +02:00
NK 127b5bbe91 fix(lsass): unify IV heuristics + reject pointer-table candidates
`find_iv_near_handles` (the .data-section fallback path used when the
key-init pattern doesn't match) ran its own ad-hoc rejection checks and
missed pointer-table-shaped candidates. On Win7-x64 it accepted a 16-byte
slice of two consecutive heap pointers as an "IV" (high bytes 0x07ff…
above canonical-x64 user space, so the existing pointer mask said "not
a pointer"), silently corrupting every cred decrypt.

Route both the recovery path and the data fallback through a single
`looks_like_iv` and add a structured-data check: if bytes 1..8 of the
first half equal bytes 9..16 of the second, the candidate is a pointer
pair, not random IV bytes. Drop the duplicate inline checks now that
`looks_like_iv` is authoritative.
2026-06-07 14:49:20 +02:00
NK 2b506f4a8b refactor(chrome): share one disk handle across the hybrid pipeline
The hybrid flow used to open the disk twice — once via
`build_keyrings_from_disk_with_passwords` (which internally calls
`extract_disk_secrets` → `open_disk`) and again via
`run_disk_with_keyring` (which calls `discover_profiles` → `open_disk`).
Each open creates a fresh File handle, so on filesystems that briefly
serialize sequential opens (or hold a residual exclusive lock after
close), the second open can fail with EBUSY and abort the chrome step
even though the first succeeded.

Open the disk exactly once in main.rs and thread the reader through the
new `run_reader_with_keyring` + `build_keyrings_from_reader` helpers in
the runner. Removes the second `open()` syscall entirely and yields
cleaner ownership: the disk handle lives for the full hybrid call and
is dropped at the closure boundary.
2026-06-07 14:49:20 +02:00
NK 616217cec9 fix(lsass): bound IV recovery search and reject UTF-16 strings
The .data scan introduced for paged-out IV globals had no upper bound on
radius, so on builds where no plausible IV lives in the same .data page
as the BCrypt handles it would walk the entire section and return the
first 16 bytes that passed the loose pointer/zero filters — often a
UTF-16 string fragment from a hostname or path literal.

Cap the search at 0x800 bytes (every observed IV-vs-handle delta has
been <= 0x60), reject candidates that look like UTF-16-LE ASCII (>=6 of
the 8 odd-position bytes are 0x00 with printable evens), and require
>=8 distinct byte values for entropy. When no plausible candidate is
in range, return None — the offset set is then skipped, which is
correct: better to fail-fast than to silently use a junk IV that
corrupts every cred decrypt.
2026-06-07 14:49:20 +02:00
NK d05eb27d44 fix(chrome): try every candidate masterkey per ABE layer
decrypt_blob returns garbage (not an error) on a wrong MK because
AES-CBC succeeds regardless of key correctness — so the previous
.or_else() chain in unwrap_app_bound_with_resolvers silently used
the wrong source when the same GUID lived in both mem-cached and
disk-extracted keyrings with differing bytes.

Collect every available candidate per GUID and validate the output
shape per layer: layer1 result must itself parse as a DPAPI blob,
layer2 result must start with a small header_len u32. First candidate
that produces a valid-shaped output wins.

Defensive even now that the underlying mem/disk mismatch is fixed at
its source in the lsass IV path — keeps the chain robust against
other sources of MK divergence (per-MK delivery skew, partial cache
walks, etc).
2026-06-07 14:49:20 +02:00
NK fceadda075 fix(lsass): recover IV from .data when global is paged out
The IV global lives in lsasrv.dll's .data section. When that page is
paged out, virt-mem reads return 16 zero bytes silently — every 3DES-CBC
cred decrypt then corrupts block 0 (first 8 bytes) while bytes 8+
decrypt correctly because CBC self-syncs on subsequent blocks. The bug
went undetected because key-size validation accepted the offset set
before checking IV plausibility.

After reading the IV global, detect all-zeros and recover via a .data
scan anchored at the h3DesKey global: walk outward in 8-byte steps,
pick the first plausible 16-byte non-zero, non-pointer, high-entropy
candidate. On affected memory dumps the recovered IV byte-matches the
ground-truth derived from disk-extracted masterkeys.
2026-06-07 14:49:20 +02:00
NK 043702cb9f feat(chrome): --chrome-password flag (repeatable) for password candidates
Adds a CLI flag for supplying password candidates that aren't in LSA — common
in pivoting / cracking / vuln-lab scenarios where you know the password but
the target hasn't auto-logon. Candidates are merged into the same list that
holds LSA DefaultPassword + every _SC_* ServicePassword, then tried per MK
file.

Threaded through both code paths:
- run_disk_with_passwords / build_keyrings_from_disk_with_passwords (path API)
- run_reader (reader API used by run_vmfs)

Use:
  vmkatz --chrome --chrome-password user --chrome-password vagrant <disk>

On a real vuln-lab Win11 image with `user` user (NT('user') = 57d583aa...)
this decrypted 15 v20 cookies (bing.com session, etc.) that were locked
because LSA had neither DefaultPassword nor any _SC_* service passwords.
2026-06-07 14:49:20 +02:00
NK ce903e89a1 feat(chrome): wire chrome into run_vmfs path + try every LSA password for user MK decrypt
Two coupled improvements unlock chrome extraction on VMFS-backed flat VMDKs:

1. Refactor chrome::runner so discovery and keyring construction can run on an
   already-open disk reader (the run_vmfs path opens its own Box<dyn DiskImage>
   and can't reopen by path):

   - new pub fn run_reader<R: Read+Seek>(reader, &DiskSecrets) -> DiscoverySummary
   - new internal discover_profiles_in_reader and build_keyrings_with_secrets
   - existing path-based run_disk / build_keyrings_from_disk now delegate to
     the reader variants

   In main.rs::run_vmfs, after SAM/LSA extraction finishes, conditionally call
   chrome::runner::run_reader with the same disk handle and the secrets we
   just extracted. No re-open, no second NTFS partition scan.

2. Try every plaintext password recovered from LSA, not just DefaultPassword.
   _SC_* service-account passwords frequently match the password of the
   account that created the service. On a real ESXi-hosted MoveIT server this
   recovered the moveitsvc account's 3 user DPAPI master keys via the LSA
   _SC_MOVEitCentral secret. Same on a LAB-ACRONIS image: 3 MKs for an
   internal service user via the _SC_MICAdmin LSA secret.
2026-06-07 14:49:20 +02:00
NK 0e52cee906 feat(chrome): auto-extract ABE static keys from elevation_service.exe via PE pattern-scan
Adds src/chrome/abe_keys.rs with a small PE/COFF reader (no new deps) plus a
pattern-scanner that walks `.rdata` looking for the 3-entry Chrome ABE key
table (56-byte structs, version bytes 1/2/3, 32-byte AES key per entry). The
runner now opportunistically scans Program Files\<browser>\Application\<ver>\
elevation_service.exe (Chrome, Brave, Vivaldi, Opera) during the existing NTFS
walk and builds a BrowserKeyMap from whichever browsers are installed. The
hardcoded Chrome 135 keys remain as the fallback when no binary is found or
the scan returns empty.

Plumbs the keymap through abe::unwrap_app_bound_key,
unwrap_app_bound_with_resolvers, decrypt_aes_encrypted_key, and
disk::extract_from_disk / derive_keys. Adds a decrypt_aes_encrypted_key_with_fallback
helper so legacy callers keep working.

Verified on a Windows 10 VMware disk: same 83 cookies decrypted as before,
log line "[chrome] extracted 3 ABE keys from chrome Program Files\Google\
Chrome\Application\135.0.7049.115\elevation_service.exe" confirms the scanner
found the live binary, and a /tmp/elev_chrome135.exe roundtrip test in
tests/chrome_abe_keys.rs asserts the three extracted keys match the
hardcoded fallback byte-for-byte.

Test suite goes from 92 to 99 passing.
2026-06-07 14:49:20 +02:00
NK 747a99ad65 feat(chrome): Chrome v20 ABE — static keys reverse-engineered from elevation_service.exe
Final piece of the v20 chain: Chrome's inner-layer 32-byte v20 key is encrypted
with AES-256-GCM using one of three static keys embedded in elevation_service.exe.
The key is selected by a version byte at the start of the inner cipher field:

  cipher = version(1) || nonce(12) || ct(32) || tag(16)  // 61 bytes
  v20_key = AES-256-GCM-decrypt(STATIC_KEY[version], nonce, ct, tag, aad=empty)

Chrome 135.0.7049.115 stores three keys at .rdata:0x1401EC040 in a 56-byte
struct array { u8 version; u8[15] meta; u8[32] aes_key; u8[8] _; }. The
decryption code in sub_1400369C0 zero-inits a 32-byte buffer and XORs the
matching node's key bytes into it (control-flow obfuscation; the bytes
loaded ARE the key). Found via IDA reverse-engineering.

The previously-shipped CHROME_FLAG1_KEY public value (from public RE writeups)
turned out to be wrong in its second 16 bytes — replaced.

Also commit:
- `tools/extract_file.rs` — helper bin that walks an NTFS partition and
  extracts a file by path (used to grab elevation_service.exe from a
  disk image without mounting).
- Make the NTFS helpers in `sam::` `pub` instead of `pub(crate)` so external
  bins like `extract_file` can use them.

Validated on the VMware Win10 image: 83 v20 cookies decrypted across both
Chrome and Edge (was 35 before), including google.com, github.com,
adobe.com sessions and an intranet myHRBrowser cookie.
2026-06-07 14:49:20 +02:00
NK 6db4c4285c feat(chrome): v20 App-Bound Encryption — Edge cookies decrypt end-to-end
Three coordinated fixes wire the full v20 chain on-disk:

1. Walk `Protect\S-1-5-18\User\` in addition to the parent S-1-5-18 dir.
   Chrome's elevation_service.exe writes the v20 outer-layer DPAPI wrap to
   the user-context SYSTEM masterkey dir, which lives at this subpath and
   was previously skipped.

2. Use the correct DPAPI_SYSTEM half per subpath: machine-context MKs
   (parent S-1-5-18) decrypt with DPAPI_SYSTEM.user_key, while user-context
   MKs (S-1-5-18\User) decrypt with DPAPI_SYSTEM.machine_key. Microsoft's
   naming is the opposite of what you'd expect; verified empirically.

3. Rewrite the ABE inner-layer parse. After the two DPAPI layers, the
   "aes_encrypted_key" is NOT [flag(1)][nonce(12)][ct][tag] as I guessed;
   it's a length-prefixed struct:
     header_len(u32 LE) || flag(u8) || install_path
     cipher_len(u32 LE) || cipher
     PKCS#7 padding to a 16-byte boundary
   For Edge flag=2 the cipher field IS the raw 32-byte v20 key — Edge
   skips the inner GCM layer entirely. Chrome flag=2 wraps it under a
   Chromium-version-specific static key (flag-1 key is the only one we
   ship); plug a known key into decrypt_aes_encrypted_key_with_key when
   available.

Also: ABE resolvers now consult both user and system keyrings for each
layer, since the outer DPAPI layer references an MK that lives in the
SYSTEM keyring on these installs.

Result on a real VMware Windows 10 image: 35 Edge v20 cookies decrypt
including bing.com session, MSN, and an SAP single sign-on token, all
tagged [DiskAbe] confirming the App-Bound chain ran end-to-end.
2026-06-07 14:49:20 +02:00
NK 002d40a7a6 fix(chrome): replace standard PBKDF2 with DPAPI's custom XOR-feedback derive
The masterkey-file decrypt is NOT PBKDF2 (RFC 2898). Microsoft's DPAPI uses
a non-standard variant where each iteration's PRF input is the accumulated
XOR rather than the previous round's PRF output. Standard PBKDF2 from the
`pbkdf2` crate produces a different (wrong) AES key, so AES-CBC decrypt of
the masterkey ciphertext yields garbage — bad enough to slip past the
non-validating slice extraction we had before.

This commit:
- Adds dpapi_derive_key_sha512 / _sha1 — direct implementations of the
  impacket MasterKey.deriveKey algorithm (see comment in the function).
- Replaces the pbkdf2_hmac call sites with the new derives.
- Adds HMAC-SHA512/SHA1 validation of the decrypted masterkey against the
  hmacSalt + hmac block at the cleartext head, so wrong pre-keys are rejected
  with a clean error instead of returning random bytes.
- Extracts the masterkey from cleartext[-64..] (impacket convention; the
  earlier code took cleartext[..64] which was wrong).
- Adds decrypt_local_user_masterkey_pw: Win10+ local user chain takes the
  actual password via SHA1(UTF-16-LE password) — the NTLM-hash-only chain
  only works for domain users.
- Wires LSA DefaultPassword discovery in chrome::runner::build_keyrings_from_disk
  so password-protected local profiles decrypt automatically when the password
  is available from auto-logon.
- Switches the SYSTEM masterkey pre-key from DPAPI_SYSTEM.machine_key to
  user_key — Windows uses the user_key half for the S-1-5-18 context despite
  the confusing naming.
- Generalizes run_disk_with_keyring to take distinct user / system resolver
  types so callers can compose HybridKeyring + ComposedResolver freely.

Validated on a VMware Windows 10 image (LSA DefaultPassword = "blahblah",
local user, NTLM hash bbf7d152...): Edge profile now produces a decrypted
saved password for the airbus.onelogin.com login, sourced entirely from
disk without a memory snapshot. All 92 tests pass.
2026-06-07 14:49:20 +02:00
NK 7fee3790a9 fix(chrome): DPAPI blob AES-CBC uses all-zero IV; add v10 GCM AAD support
Two related corrections to the DPAPI blob decrypt and v10 cookie decrypt:

1. DPAPI BLOB AES-256-CBC uses an all-zero IV, not bytes derived from the
   session key. The session key (HMAC-SHA512 output) is purely the AES key
   material — Microsoft uses zero IV. Previous code took session[32..48] as
   the IV which produced a v10 key that didn't match Chrome's actual key.
   Verified against impacket: same input → same plaintext.

2. v10/v11 GCM decrypt now supports optional AAD via decrypt_v10_aad. Modern
   Chrome cookies may bind ciphertext to the host string. extract_cookies
   tries empty AAD first, then host-as-AAD on failure.

Synthetic roundtrip test updated to use zero IV.
2026-06-07 14:49:20 +02:00
NK d7b4ebec55 fix(chrome): preserve raw bytes for SQLite TEXT columns
SQLite type affinity allows raw binary bytes to land in a TEXT-typed column,
and Chromium does exactly this: cookies tables store v10-prefixed encrypted
ciphertext as TEXT. The previous decoder called String::from_utf8_lossy and
replaced every non-UTF-8 byte with U+FFFD, corrupting the ciphertext beyond
any possibility of decryption. Change Value::Text to carry Vec<u8> and decode
to &str lazily via as_text; add an as_bytes accessor that returns the raw
bytes from either TEXT or BLOB columns.

Validated on a real Chrome Cookies table: cookie values now decrypt cleanly
where they previously failed v10 GCM auth.
2026-06-07 14:49:20 +02:00
NK 868eac45e3 feat(chrome): compose memory + disk DPAPI MKs in --chrome --disk path
When both a memory snapshot and a --disk are supplied, build keyrings from
both sources and compose them: try the LSASS-cached user MK cache first,
fall back to the on-disk NT-hash-derived chain. Pass the disk-extracted
SYSTEM MK keyring as the system resolver for v20 App-Bound Encryption.

Generalize run_disk_with_keyring to accept distinct user/system resolver
types so callers can mix HybridKeyring with ComposedResolver.
2026-06-07 14:49:20 +02:00
NK 0909a720da feat(chrome): on-disk DPAPI MK decryption — user (NT hash) + system (DPAPI_SYSTEM)
Implements the cryptographic chain that decrypts DPAPI masterkey files from disk
without a memory snapshot, enabling Chrome / Edge offline decryption in the
disk-only flow.

- sam::dpapi_masterkey: add decrypt_local_user_masterkey (HMAC-SHA1 pre-key over
  NT hash + UTF-16LE SID), decrypt_system_masterkey (DPAPI_SYSTEM machine_key),
  and decrypt_masterkey_with_prekey core dispatching between AES-256/SHA-512
  (mode 15900) and 3DES/SHA-1 (mode 15300) via PBKDF2 + CBC.
- chrome:🏃 add build_keyrings_from_disk that walks SAM + LSA, then
  enumerates every accessible user and S-1-5-18 Protect directory and decrypts
  each MK file. run_disk now consumes both keyrings via extract_from_disk so
  --chrome --disk yields decrypted v10/v11 (and ABE v20 when MKs available).
- Add pbkdf2 dep (hmac feature only, default-features off) gated behind sam.
- 2 round-trip tests for the user-local and SYSTEM AES paths.
2026-06-07 14:49:20 +02:00
NK f9a32d429e feat(chrome): wire LSASS-cached DPAPI masterkeys into disk extraction
When --chrome is passed with both a memory snapshot and --disk, collect every
cleartext DPAPI master key surfaced by the LSASS extractor (Credential.dpapi)
into a HybridKeyring and run chrome disk extraction against that keyring.
Chrome / Edge profiles whose Local State references one of the LSASS-cached
MK GUIDs now decrypt the per-profile v10 AES-GCM key directly, without
needing to crack the user's NTLM hash.

New runner entrypoints:
- run_disk_with_keyring(disk, user_resolver, system_resolver)
- keyring_from_pairs(pairs) — adapter for any (guid, key_bytes) iterator
- ArtifactsTree — in-memory FileTree backed by already-discovered profile
  artifacts so the orchestrator does not re-walk NTFS

Discovery summary now also surfaces the MK GUID per profile so operators can
tell at a glance whether a given run has the keys it needs. Profiles whose
MK GUID is not in the keyring fall back to discovery-only output.

What still needs work: SYSTEM-DPAPI MK decryption (Chrome v127+ v20 cookies),
and on-disk user MK decryption from NTLM hash (for profiles whose MK is not
cached in LSASS at snapshot time).
2026-06-07 14:49:20 +02:00
NK 38fff74eed fix(chrome): match encrypted_value as TEXT or BLOB
Chromium's schema declares encrypted_value as BLOB but SQLite type affinity
sometimes lands the bytes as TEXT, especially for Edge's Cookies table where
v20-prefixed ciphertext is stored as TEXT. Previous code's find_map(as_blob)
picked the first empty BLOB column and missed the actual encrypted value.

New is_chrome_blob helper picks the first column (BLOB or TEXT) whose bytes
start with a v10/v11/v20 prefix. Validated against a real Edge profile where
all 35 cookies are stored as TEXT-typed v20 blobs.
2026-06-07 14:49:20 +02:00
NK 68fbf42793 fix(chrome): DPAPI HMAC key must be SHA1(masterkey), not raw masterkey
DPAPI's protocol derives the session key as HMAC-SHA512(SHA1(masterkey), salt).
LSASS exposes both the raw 64-byte master key AND its SHA-1 specifically for
this reason. The previous implementation passed the raw 64-byte key as the HMAC
input, which only round-tripped because the synthetic test built the blob with
the same wrong key. Validated against real Edge Local State blobs on a Windows
10 VM with cleartext masterkey sourced from LSASS — derived v10 AES-GCM key
now matches the value Chrome encrypts cookies with.

Test updated to use the correct algorithm so it actually exercises the spec.
2026-06-07 14:49:20 +02:00
NK 6b57235afd docs(chrome): document --chrome flag + chrome feature in user docs 2026-06-07 14:49:20 +02:00
NK a91bfa2398 feat(chrome): CLI wiring — --chrome + --chrome-json, disk profile discovery on run_sam 2026-06-07 14:49:20 +02:00
NK be3bf2baae feat(chrome): stdout (grouped) + JSON output formatters 2026-06-07 14:49:20 +02:00
NK 4c81cefd7d feat(chrome): Firefox NSS scaffold (logins.json parser + key4.db reader; NSS derive TODO) 2026-06-07 14:49:20 +02:00
NK d7a3b93602 feat(chrome): hybrid keyring + composed resolver (mem MK cache + fallback) 2026-06-07 14:49:20 +02:00
NK 603a1ce819 feat(chrome): heuristic cookie scanner + memory orchestrator over ProcessSource 2026-06-07 14:49:20 +02:00
NK 194eb6be21 feat(chrome): heuristic password triple scanner (UTF-16LE URL + creds) 2026-06-07 14:49:20 +02:00
NK a4a770fde4 feat(chrome): signature table schema + wildcard scanner (empty initial set) 2026-06-07 14:49:20 +02:00
NK dab2b6b9e5 feat(chrome): memory module skeleton + process classification (browser/network) 2026-06-07 14:49:20 +02:00
NK cb7bb8f8fd feat(chrome): App-Bound Encryption (v20) two-layer DPAPI + key unwrap 2026-06-07 14:49:20 +02:00
NK 4bd1acb67a feat(chrome): disk extraction orchestrator (Local State -> blob -> SQLite) 2026-06-07 14:49:20 +02:00
NK adf52a200e feat(chrome): profile discovery on NTFS (Chromium family + FileTree trait) 2026-06-07 14:49:20 +02:00
NK 8ca252c5fa feat(chrome): v10/v11 AES-GCM blob decrypt + scheme classifier 2026-06-07 14:49:20 +02:00
NK 9cf92a85f2 feat(chrome): DPAPI blob parse + AES-256/SHA-512 decrypt (roundtrip-tested) 2026-06-07 14:49:20 +02:00
NK 25a542f2f5 feat(chrome): Local State JSON parser (v10/v11 + v20 key extraction) 2026-06-07 14:49:20 +02:00
NK 437be49bf5 feat(chrome): sqlite WAL replay (committed frames overlay base pages) 2026-06-07 14:49:20 +02:00
NK 60af9f6f1e feat(chrome): sqlite B-tree walk (leaf + interior) + overflow chain 2026-06-07 14:49:20 +02:00
NK b5a8eb47cd fix(chrome): bounds-check sqlite record decoder against truncated/negative payloads 2026-06-07 14:49:20 +02:00
NK 8a4d178792 feat(chrome): sqlite varint + record (serial type) decoder 2026-06-07 14:49:20 +02:00
NK 5c95a9c2b8 feat(chrome): sqlite file header + page accessor + mini fixture 2026-06-07 14:49:20 +02:00
NK 9c45399d81 feat(chrome): scaffold module, types, base64 + epoch utils, --chrome flag 2026-06-07 14:49:20 +02:00
NK c3d1e264ce Fix LZNT1 off-by-one and relax Kerberos kvno carve filter
- lznt1_displacement_bits: the tier-bump comparison must be `<=`, not
  `<`. At exact power-of-two positions (16, 32, 64, ...) the old code
  returned one bit too few, silently corrupting decompressed Hyper-V
  VMRS RAM pages when a back-reference landed on one of those offsets.

- carve_kerberos_tickets: kvno cap of 20 was too strict. Microsoft
  recommends periodic krbtgt resets and many enterprises rotate often,
  pushing real kvnos past 20. Raise the ceiling to 1000 while keeping
  kvno != 0 to reject obvious garbage from wrong-offset guesses.
2026-04-18 12:27:11 +02:00
NK d725d1896c Bump to 1.4.1 2026-04-15 18:41:01 +02:00
NK da7ac445eb Support FLAT and VMFS extents in VMDK descriptor parser
Windows Server VMs on VMware/ESXi overwhelmingly use FLAT or VMFS
extents (raw disk data in a -flat.vmdk sibling) rather than SPARSE
extents. Previously the descriptor parser blindly called open_extent
on any RW line, which expected the KDMV sparse magic and failed with
"Invalid magic: 0xD08EC033" (the MBR boot code bytes) when the
underlying file was a flat extent.

- Parse the extent type field (SPARSE / FLAT / VMFS / VMFSRAW /
  VMFSRDM / VMFSRDMP / ZERO) and the optional sector offset
- Add open_flat_extent() for direct raw-sector reads
- Handle flat extents in read_virtual (direct seek+read)
- Iterate flat extents in 1 MiB chunks in scan_all_grains
- Extract quoted filename robustly so names containing NBSP or
  other whitespace survive (VMware keeps the user-provided VM name
  verbatim, often with non-breaking spaces on ESXi)

Verified on ESXi 8 against a Windows Server 2022 VMFS flat VMDK:
SAM hashes, LSA secrets, and DPAPI master keys all extract.
2026-04-15 18:29:28 +02:00
NK a73c19306b Fix 5 bugs found during code review
- rc4(): guard against empty key to prevent division-by-zero panic
- hive: only treat values as inline when high bit is set (not data_size <= 4)
- bootkey: scan at 4-byte alignment instead of 8 (cells are 4-byte aligned)
- vmdk: parse RDONLY and NOACCESS extents, not just RW
- ntfs_reader: seek before each chunk in resilient read to fix cursor desync
2026-04-10 01:27:33 +02:00
NK bc54260e37 Fix Kerberos ticket carving misses on Win10 1607+ and all x86 variants
Two bugs in carve_kerberos_tickets that silently missed most tickets:

1. Scan stride was step_by(8), but several TicketOffsets variants place
   ticket_enc_type at a 4-aligned-but-not-8-aligned offset:
     - Win10 1607+ x64: 0x124 (% 8 = 4)
     - Win10 1607+ x86: 0x9C  (% 8 = 4)
     - Win10 1507 x86:  0x8C  (% 8 = 4)
   With standard 16-byte pool alignment the enc_type field lands at an
   address that's 4 mod 8 — never hit by an 8-byte stride starting from
   a page-aligned chunk. This silently missed every carved ticket on
   modern Windows. The AVL walk path still worked, masking the bug.
   Fixed by scanning step_by(4).

2. Chunks were non-overlapping. When enc_type was found near the start
   of a chunk, the backward fields (flags at enc_type - 0x84 for 1607+)
   fell into the previous chunk, failed the bounds check, and the
   candidate was skipped. Fixed with 0x200 overlap between chunks;
   dedup handles the doubled candidates.

step_by(4) doubles the scan cost but correctness > speed.
Clippy + all 32 tests pass.
2026-04-09 01:17:08 +02:00
NK 5f50e7df6f Merge dev into main for v1.4.0 release 2026-04-09 00:44:34 +02:00
NK 7f7d4758bc Skip prerelease tags in changelog for stable releases 2026-04-09 00:44:27 +02:00
NK 91b6ad5550 Fix macOS x86_64 runner: macos-13 retired, use macos-26-intel 2026-04-09 00:39:06 +02:00
NK 518483930f Add Cross.toml pinning ARM targets to :main edge images
The default cross 0.2.5 images for arm-*/armv7-* have an old glibc
that fails when build scripts (e.g. generic-array) use modern rustc.
Using :main edge images resolves this.

Verified locally: all four ARM targets build clean (armv7 + arm,
gnu + musl). Binaries are ~2.5 MB each.
2026-04-09 00:32:52 +02:00
NK 51c29e1b74 Bump to 1.4.0, add ARM builds (aarch64, armv7, armv5/v6)
Release matrix now covers:
- Linux x86_64 (gnu + musl/ESXi)
- Linux aarch64 (gnu + musl, native ARM runners)
- Linux armv7 hf (gnu + musl, cross-compiled)
- Linux arm hf / armv5/v6 (gnu + musl, cross-compiled)
- macOS x86_64 (macos-13) and aarch64 (macos-latest)
- Windows x86_64 and aarch64 (windows-11-arm)

Uses cross-rs for the armv7/arm cross-compilation targets.
Release job only runs on tag pushes so workflow_dispatch can test
builds without publishing a release.
2026-04-09 00:18:24 +02:00
NK f3b4c2269a Fix Python availability claim: included in ESXi 6.x+, not all versions 2026-03-25 00:54:33 +01:00
NK 3ed50ef292 Move loader to tools/, bundle in ESXi release archive
- vmkatz_loader.py moved to tools/vmkatz_loader.py
- Release workflow includes the loader in the esxi-x86_64 tar.gz
- Updated README and docs/esxi.md paths
2026-03-25 00:52:57 +01:00
NK f8c72d0ef8 Fix vmkatz_loader.py header: document Python 2.7+ compatibility 2026-03-25 00:50:40 +01:00
NK 1410fa7c3f Reorganize README: move detailed docs to docs/
README trimmed from 524 to 175 lines. Moved to docs/:
- docs/esxi.md — ESXi deployment, VIB bypass, VMFS raw access
- docs/examples.md — Example output (LSASS, hashcat, NTDS, pagefile)
- docs/architecture.md — Module layout and how it works
- docs/tested-targets.md — Test matrix and known limitations

README now focuses on: intro, what it extracts, supported inputs,
quick start, output formats, ESXi basics, build features, and links.
2026-03-25 00:47:30 +01:00
NK f60065a93f Document Python loader for ESXi VIB bypass, add VIB field to bug template
- README: new section explaining vmkatz_loader.py usage when
  execInstalledOnly is enabled, with command to check VIB status
- Bug template: add ESXi VIB protection field asking for
  execInstalledOnly value and ESXi version
2026-03-25 00:40:09 +01:00
NK 21c39c4451 Merge branch 'mmap-fallback' into dev 2026-03-25 00:31:10 +01:00
NK c22350df31 Add Python in-memory ELF loader for ESXi VIB bypass
Loads vmkatz into anonymous mmap pages with PROT_EXEC, bypassing
execInstalledOnly on ESXi. VMkernel blocks execve on unsigned binaries
but allows PROT_EXEC on anonymous mappings. Python is VIB-signed.

The loader parses ELF64 headers, maps PT_LOAD segments, applies
R_X86_64_RELATIVE relocations for PIE, builds a proper initial
stack with argc/argv/envp/auxv (AT_PHDR, AT_PHNUM, AT_PAGESZ,
AT_RANDOM for musl TLS init), and jumps to _start via trampoline.

Usage: python3 vmkatz_loader.py /tmp/vmkatz [args...]
2026-03-25 00:27:30 +01:00
NK 4b9a84a56b Add volatility-kerberos acknowledgement for ticket carving inspiration 2026-03-22 00:14:25 +01:00
NK 1652c36982 Add Kerberos ticket carving from LSASS memory
Scans LSASS virtual memory for orphaned _KERB_TICKET_INFO structures
that are no longer in the AVL session tree (freed sessions, logged-out
users with memory still resident). Recovers tickets missed by the
normal AVL tree walk.

Approach: scan for valid etype values at ticket_enc_type offsets, then
validate surrounding structure (kvno, ticket_length, flags, service
name pointer) before calling extract_single_ticket for full parsing.
Tries all known TicketOffsets variants (Win7/8/10/11). Deduplicates
against tickets already found by the structured extraction.

Runs automatically after the normal Kerberos provider extraction.
2026-03-21 23:14:53 +01:00
NK d9b35a1eab Add transparent BitLocker disk decryption using FVEK from memory
When BitLocker FVEK keys are extracted from a VM memory snapshot,
vmkatz can now use them to decrypt the corresponding encrypted disk
for SAM/NTDS extraction in the same run.

New modules:
- sam/aes_xts.rs: AES-XTS-128/256 sector-level decryption
- sam/bitlocker_decrypt.rs: transparent Read+Seek wrapper that
  decrypts sectors on-the-fly, validates FVEK by checking for
  NTFS signature in decrypted sector 0

Flow: snapshot → extract FVEK → open disk → detect BitLocker →
try each FVEK candidate → decrypt partition → extract credentials.
Works in directory mode (auto-discovers snapshot + disk) and with
explicit --disk flag.
2026-03-21 15:58:32 +01:00
NK e6d6ef36df Add BitLocker FVEK extraction from VM memory snapshots
Scans physical memory for Windows pool tags to extract Full Volume
Encryption Keys, enabling offline decryption of BitLocker volumes:
- FVEc pool tag (Win7): AES-128/256-CBC, Elephant Diffuser modes
- Cngb pool tag (Win8+): AES-128/256-XTS modes
- Dual-copy validation for Cngb, entropy checks for both
- Tries both x64 and x86 structure offsets automatically

Output: hex FVEK in text/csv/hashcat formats, dislocker-compatible
.fvek files via --bitlocker-fvek <dir>. Works in both normal and
--carve modes (physical scan, no LSASS context needed).
2026-03-21 13:03:52 +01:00
NK b0e6894509 Include file path in mmap fallback warning message 2026-03-21 12:03:40 +01:00
NK 3e811eaf88 Add pread fallback when mmap fails (e.g. ESXi 6.5 VMkernel)
MappedFile now has a Pread variant that uses read_exact_at() instead
of memory mapping. When mmap returns EINVAL (unsupported by kernel),
vmkatz falls back to file I/O with a clear warning message.

Layers updated to use read_at() for PhysicalMemory::read_phys:
- VMware: pread for runtime reads, header parsed from first 8MB buffer
- Hyper-V: pread for all reads
- QEMU ELF: pread for runtime reads, ELF header parsed from 1MB buffer
- QEMU savevm: requires mmap (stream parsing needs full slice access)

Zero host RAM impact — pread reads directly from disk to caller buffer.
2026-03-21 11:51:26 +01:00
NK 95162782e5 Bump version to 1.3.0 2026-03-21 03:22:51 +01:00
NK 1bd901baf2 Fix potential panics: bounds-check QEMU reads, guard VHDX division by zero
- qemu/savevm.rs: read_be_u32/read_be_u64 now use .get() instead of
  direct indexing — returns 0 instead of panicking on truncated input
- disk/vhdx.rs: guard against chunk_ratio=0 which would cause division
  by zero on crafted VHDX files with oversized block_size
- main.rs: suppress dead_code warning for QemuSavevm variant in
  non-qemu single-feature builds
2026-03-21 03:13:20 +01:00
NK 33a357875f Sort orphan VMDK extents in discovery to pick lowest-numbered first
When no text descriptor exists and only -sNNN extent files are present,
the discovery code now sorts extents before picking the first one.
Previously it used filesystem iteration order which was non-deterministic.
2026-03-21 03:03:35 +01:00
NK 044f3b28da Improve mmap failure error message with file size context 2026-03-21 02:39:33 +01:00
NK e4c943ea4f Remove read_to_end fallback from VMware layer
The Vec<u8> fallback was added for ESXi kernels where mmap failed on
VMFS-5 files. Now that mmap_file() handles block devices and edge cases,
the fallback is unnecessary and dangerous — it would silently try to
allocate hundreds of GB for large VMEM files if mmap ever failed.
2026-03-21 02:35:36 +01:00
NK 99abb25518 Centralize mmap and file_size helpers for block device compatibility
Mmap::map() uses fstat which returns 0 for block devices (LVM volumes).
Added utils::mmap_file() that uses seek to get real size and passes it
explicitly to MmapOptions. Replaced all Mmap::map() calls across:
- qemu/savevm.rs (was already fixed inline, now uses shared helper)
- qemu/layer.rs (ELF core dumps)
- hyperv/layer.rs (.bin/.raw dumps)
- vmware/layer.rs (fallback path also used metadata().len())
- hyperv/vmrs.rs (used metadata().len() for size)
- vbox/layer.rs (used metadata().len() for size)

Also added utils::file_size() for non-mmap size queries.
2026-03-21 02:29:53 +01:00
NK e2936dc9d3 Add VMFS-5 support to raw VMFS parser
The parser previously rejected VMFS-5 (majorVersion < 24). Now handles
both VMFS-5 and VMFS-6 with version-dependent branching for:
- File descriptor size (2048 vs 2*mdAlignment) and layout
- Block pointers (32-bit vs 64-bit)
- Address encoding (FB vs SFB/LFB, different bit layouts)
- Directory format (flat 140-byte entries vs block-based 288-byte)
- Pointer blocks (4KB/1024 ptrs vs 64KB/8192 ptrs)
- Resource metadata (no rfmd signature in VMFS-5)
- SFD bootstrap offset formula
- TBZ bits (VMFS-6 only, not in 32-bit addresses)

Verified on both VMFS-5 and VMFS-6 datastores.
2026-03-21 02:22:53 +01:00
NK ade266a88a Fix QEMU savevm detection and mmap on block devices (LVM volumes)
Block devices (e.g. /dev/pve/vm-*-state-*) were incorrectly routed
to SAM disk extraction instead of QEMU savevm parsing. Two fixes:

- Check QEVM magic on block devices before assuming SAM mode
- Use read_exact instead of mmap for is_qemu_savevm() detection
- Use explicit size via seek(End) for mmap in QemuSavevmLayer::open(),
  since fstat returns 0 for block devices
2026-03-21 01:53:24 +01:00
NK 1a7276b870 Update README: mark VHDX as tested (Win2003R2, Win2012R2) 2026-03-21 00:57:42 +01:00
NK 8bc31a4a36 Update README: document SAM account status and DPAPI dedup 2026-03-21 00:53:34 +01:00
NK 56632a865c Improve DPAPI and SAM display: account status, key dedup, NTFS 8.3 fix
- Show account status in SAM text output: (DISABLED), (NO PASSWORD),
  (BLANK PASSWORD) from per-user ACB flags in registry F value
- Add modification date to DPAPI master key text output from NTFS mtime
- Deduplicate DPAPI keys: show only most recent per user (--all for all)
- Filter NTFS DOS 8.3 short names in list_directory() — fixes S-1-5-~1
  SID leak from short name aliases
- Add cargo test step to CI workflow
2026-03-21 00:52:56 +01:00
NK 131a3693b2 Demote empty LSA secret warning to debug level 2026-03-21 00:10:32 +01:00
NK c336acb133 Fix DPAPI hashcat mode: distinguish local (15300/15900) from domain (15310/15910)
The hashcat mode depends on context (local vs domain user), not just the
crypto version. Domain users use NTLM pre-key derivation (modes 15310/15910)
while local users use SHA1 pre-key (modes 15300/15900). The context is
determined by whether a domain key section exists in the master key file.

Previously all hashes were reported as 15300 or 15900 regardless of context.
2026-03-21 00:06:07 +01:00
NK 52692984e6 Log warnings on I/O errors in file discovery instead of silently skipping
Previously, permission errors, mount failures, or I/O issues during VM
file discovery were silently swallowed with if-let-Ok patterns. On a NAS
with partial mounts or restricted permissions, this caused VMs to be
silently missed with no indication of the problem.

Now all I/O errors in discover.rs produce log::warn! messages:
- read_dir failures on scan directories
- metadata/stat failures on individual files
- file open failures in magic-byte checks (ELF, VMDK descriptor)

Added file_size() helper to centralize metadata reads with warnings.
2026-03-20 23:55:28 +01:00
NK 75f495134f Clean up magic numbers and improve readability
- savevm.rs: extract RAM_FLAG_MASK constant, remove unused _block_gpa_bases param
- lsa.rs: extract DES_KEY_SEGMENT constant, add [MS-LSAD] spec references,
  document key rotation algorithm with example
- cloudap.rs: extract MAX_PRT_SIZE constant, remove extra blank line
2026-03-20 23:31:34 +01:00
NK 58cc80abaf Add changelog generation to release workflow 2026-03-20 22:53:10 +01:00
NK 6f0bd040b3 Bump version to 1.3.0-beta.1 and support pre-release tags in CI
Release workflow now marks tags containing -beta, -alpha, or -rc as
GitHub pre-releases.
2026-03-20 22:41:59 +01:00
NK 7ec43a1f59 Merge branch 'main' into dev
# Conflicts:
#	src/lsass/kerberos.rs
#	src/lsass/msv.rs
2026-03-20 22:38:15 +01:00
NK 9506c50bda Update README for QEMU savevm, embedded .vmsn, BitLocker, and legacy LSA support
- Add QEMU/Proxmox savevm and VMware embedded .vmsn to supported inputs
- Add ESXi 6.7 and Proxmox savevm test results to tested targets table
- Document BitLocker detection and QEMU savevm limitations
- Update target OS range to include Windows Server 2003
- Add Proxmox example to quick start
- Update module architecture and feature descriptions
2026-03-20 22:33:37 +01:00
NK 84ec71d5b9 Fix legacy LSA secret decryption (pre-Vista / Windows 2003)
Two bugs fixed:
1. LSA key derivation: MD5 input was salt+bootkey×1000 instead of
   bootkey+salt×1000 (wrong order per impacket/MS-LSAD spec)
2. Secret decryption: used RC4 instead of DES-ECB with rotating
   7-byte key segments (SystemFunction005 / [MS-LSAD] Section 5.1.2)

Implemented:
- DES-ECB decrypt with rotating key (des_ecb_decrypt_rotating)
- transformKey: 7-byte to 8-byte DES key expansion ([MS-LSAD] 5.1.3)
- LSA_SECRET_XP parsing: Length(4) + Version(4) + Secret(Length)

Tested on Windows Server 2003 R2 SP2 VHDX: DPAPI_SYSTEM keys and all
LSA secrets now decrypt correctly.
2026-03-20 22:27:59 +01:00
NK 313b0df30a Fall back to legacy LSA key when PolEKList is missing despite modern revision
Windows Server 2003 R2 SP2 reports PolRevision 0x10007 (modern) but uses
the legacy PolSecretEncryptionKey scheme. Try PolEKList first; if not found,
fall back to PolSecretEncryptionKey instead of failing.
2026-03-20 21:40:11 +01:00
NK 49a3991faa Detect BitLocker-encrypted partitions and warn the user
Check for the -FVE-FS- OEM ID in partition boot records before
attempting NTFS parsing. When BitLocker is detected, skip the
partition and display a clear error message instead of failing
silently with "No registry hives found".

Applied to SAM extraction, LSA secrets, and NTDS.dit extraction paths.
2026-03-20 21:33:51 +01:00
NK febbb0e4d9 Fix embedded .vmsn memory and hide machine account passwords
VMware layer:
- Fix base_offset not set when .vmsn has region tags but no separate
  .vmem file (embedded memory). Reads were at wrong file offset.
- Tested on ESXi 6.7 with Win Server 2016 embedded .vmsn snapshot.

Display:
- Don't display Kerberos/WDigest passwords for machine accounts (username
  ending with $) — they are binary blobs, not useful plaintext. The
  Kerberos keys (AES256, RC4=NT hash) are the exploitable forms.
- Don't count machine passwords in the summary line.
- Truncate long hex passwords to "(hex, N bytes) first32bytes..." when
  displayed for other non-printable passwords.
2026-03-20 21:27:51 +01:00
NK abb72761ab Add QEMU/KVM/Proxmox savevm state parser and improve System process discovery
New parser (src/qemu/savevm.rs):
- Parse QEVM magic, RAM block list, and page entries (PAGE/ZERO)
- MMIO gap remapping for q35+UEFI (below_4g=0x80000000)
- Skip non-RAM device sections (dirty-bitmap, etc.) via forward scanning
- HashMap-based deduplication for dirty page iterations (last-write-wins)
- Auto-detection via QEVM magic in format dispatcher

System process discovery improvements:
- Validate candidates by translating Flink VA to physical and checking the
  linked EPROCESS has a printable ImageFileName and valid kernel-mode Flink
- Rejects stale EPROCESS remnants with corrupted page tables

Tested on Proxmox VMs: DC25 (Win Server 2025, 4GB) and WKS11 (Win11, 8GB)
2026-03-20 19:28:54 +01:00
NK 552d444cfe Unify Kerberos credential extraction for x64/x86
- Merge 8 duplicated _x86 functions into single arch-aware implementations:
  walk_avl_tree, read_kerb_external_name, extract_kerb_password,
  extract_tickets_from_list, extract_single_ticket, detect_kerb_offsets,
  extract_kerb_keys, extract_kerberos_credentials
- All structure offsets computed from arch.ptr_size() and arch.ustr_size()
- Delete extract_kerberos_credentials_arch wrapper and all _x86 variants
- Net reduction: -580 lines
2026-03-20 19:27:19 +01:00
NK 649976e96a Unify MSV session and credential extraction for x64/x86
- Merge extract_msv_sessions_arch and extract_msv_credentials_arch into the
  main functions, selecting offset variants by arch at runtime
- Make all walkers arch-aware: walk_session_buckets, walk_session_list,
  walk_msv_list, walk_hash_table, find_inline_hash_table,
  find_credentials_ptr_in_entry
- Delete 258 lines of duplicated x86 code (extract_msv_sessions_arch,
  extract_msv_credentials_arch, try_extract_primary_cred_x86)
- x86 now gets scoring, enrichment, multi-pass credential scanning, and
  SHA1 validation — previously x64-only features
- Add variant_order_for_build_arch for x86 build-aware variant ordering
2026-03-20 19:26:55 +01:00
NK b0c8bcae49 Refactor LSASS providers: extract shared helpers and unify x64/x86 code paths
- Add read_ptr_from_buf, walk_list, read_data_section, scan_data_for_list_head
  to types.rs, eliminating ~200 lines of duplicated boilerplate across providers
- Unify kerberos.rs: merge 8 duplicated _x86 functions into arch-aware versions
  (walk_avl_tree, extract_single_ticket, read_kerb_external_name, etc.), -440 lines
- Fix CloudAP: read account name from toName_ptr (+0x58) instead of hashName (+0x68)
  which is a SHA hash, not the readable UPN. Add 3 offset variants (1507-1607,
  1703-1709, 1803+) with auto-detection. Remove useless hashName fallback.
- Fix walk_session_buckets using x64-only read_win_unicode_string and read_virt_u64
  for pointers — now uses arch-aware read_ustring and read_ptr
2026-03-20 19:26:30 +01:00
NK 35fc30e0a3 Add GitHub issue templates for bug reports and feature requests
Bug template requires: vmkatz version, input format, guest OS/arch,
host platform, filesystem, full command output, and file details.
Blank issues disabled to enforce structured reports.
2026-03-18 23:26:06 +01:00