Validation Report¶
Record-level comparison of usnjrnl-forensic against MFTECmd (Eric Zimmerman), usnjrnl_rewind (Yogesh Khatri / CyberCX), usn.py (PoorBillionaire), dfir_ntfs (Maxim Suhanov), usnrs (Airbus CERT), and Velociraptor (Rapid7) using three publicly available forensic disk images.
Every USN record is compared by Update Sequence Number — not sampled.
Test Environment¶
| Component | Version | Source |
|---|---|---|
| usnjrnl-forensic | 0.4.0 | crates.io |
| MFTECmd | 1.3.0.0 (.NET 9) | GitHub Releases |
| usn.py (usnparser) | 4.1.5 | pip3 install usnparser |
| dfir_ntfs (ntfs_parser) | 1.1.20 | pip3 install git+https://github.com/msuhanov/dfir_ntfs.git |
| usnrs (usnrs-cli) | 0.5.0 | cargo install --git https://github.com/airbus-cert/usnrs --features usnrs-cli |
| usnjrnl_rewind | 0.6 (cec4dca) |
git clone https://github.com/CyberCX-DFIR/usnjrnl_rewind.git |
| Velociraptor | 0.74.5 | GitHub Releases |
| The Sleuth Kit (icat, fls, mmls) | 4.12.1 | brew install sleuthkit |
| Rust (rustc) | 1.88.0 (6b00bc388) | rustup.rs |
| .NET Runtime | 10.0.103 | dotnet.microsoft.com |
| Python | 3.11.14 | brew install python@3.11 |
| Platform | macOS Darwin 24.6.0, arm64 (Apple Silicon) | — |
Test Images¶
1. Szechuan Sauce (DESKTOP-SDN1RPT)¶
| Property | Value |
|---|---|
| Challenge | The Stolen Szechuan Sauce (James Smith) |
| Catalog | CFREDS — HackTheBox / SzechuanSauce |
| Download | The Evidence Locker (Kevin Pagano) |
| Filename | 20200918_0417_DESKTOP-SDN1RPT.E01 through .E04 |
| Format | EWF v1, multi-segment (E01-E04) |
| Media size | 16,106,127,360 bytes (15.0 GiB) |
$MFT |
107,479,040 bytes (102.5 MB) |
$UsnJrnl:$J |
27,209,456 bytes (25.9 MB) |
2. MaxPowers C Drive¶
| Property | Value |
|---|---|
| Challenge | MUS CTF 2018 (David Cowen & Matt Seyer) |
| Catalog | CFREDS — AcademicChallenges / MaxPowers |
| Download | The Evidence Locker (Kevin Pagano) |
| Filename | MaxPowersCDrive.E01 |
| Format | linen 5, single segment |
| Media size | 53,687,091,200 bytes (50.0 GiB) |
$MFT |
302,252,032 bytes (288.2 MB) |
$UsnJrnl:$J |
1,465,037,896 bytes (1.4 GB) |
3. PC-MUS-001¶
| Property | Value |
|---|---|
| Challenge | MUS CTF 2019 (David Cowen) |
| Catalog | CFREDS |
| Download | The Evidence Locker (Kevin Pagano) |
| Filename | PC-MUS-001.E01 |
| Format | EWF v1, single segment |
| Media size | 256,060,514,304 bytes (238.5 GiB) |
$MFT |
531,890,176 bytes (507.2 MB) |
$UsnJrnl:$J |
846,561,440 bytes (807.3 MB) |
Artifacts extracted using the ewf crate (v0.1.1) with NTFS partition detection and MFT-based file extraction.
Reference Tools¶
MFTECmd (Eric Zimmerman)¶
Industry-standard .NET tool for parsing NTFS artifacts. Parses $UsnJrnl:$J directly and outputs CSV with full record details including USN offset, timestamp, reason flags, file attributes, and MFT references. Supports an optional -m flag to provide the $MFT for parent path resolution via point-in-time MFT lookup.
usn.py (PoorBillionaire)¶
Python-based USN journal parser. Outputs CSV with timestamp, filename, file attributes, and reason flags.
dfir_ntfs (Maxim Suhanov)¶
Python library and CLI (ntfs_parser) for parsing NTFS artifacts. Its --usn mode combines the $MFT and $UsnJrnl:$J to produce a CSV with USN values, reason flags, timestamps, filenames, and file paths resolved via point-in-time MFT lookup.
usnrs (Airbus CERT)¶
Rust-based USN journal parser from Airbus CERT. The CLI (usnrs-cli) supports an optional --mft flag for path resolution via MFT lookup.
usnjrnl_rewind (CyberCX)¶
Python post-processing script that implements the Rewind algorithm (Yogesh Khatri, CyberCX). It takes MFTECmd CSV output as input and reconstructs full paths by walking the journal in reverse chronological order. This is a proof-of-concept — not a standalone parser. It requires MFTECmd to be run first, producing two CSV files (one for $UsnJrnl:$J, one for $MFT). Output is CSV or SQLite.
usnjrnl-forensic implements the same Rewind algorithm natively in Rust with no external tool dependency. Key differences:
| usnjrnl-forensic | usnjrnl_rewind | |
|---|---|---|
| Standalone parser | Yes — parses raw $UsnJrnl:$J and $MFT directly |
No — requires MFTECmd CSV as input |
| External dependencies | None | MFTECmd (.NET) |
| USN versions | V2, V3, V4 | Via MFTECmd (V2, V3) |
| Output formats | CSV, JSONL, SQLite, body, TLN, XML | CSV, SQLite |
| Additional analysis | Timestomping, $LogFile correlation, $MFTMirr integrity, rule engine, USN carving | Path resolution only |
| Automated tests | 433 unit tests | None |
| Language | Rust (compiled) | Python |
# Extract artifacts with The Sleuth Kit
mmls image.E01 # find NTFS partition offset
icat -o <offset> image.E01 0 > '$MFT'
icat -o <offset> image.E01 <usnjrnl_inode>-128-3 > '$J'
# Produce MFTECmd CSVs (required input for usnjrnl_rewind)
dotnet MFTECmd.dll -f '$MFT' --csv . --csvf mft.csv
dotnet MFTECmd.dll -f '$J' --csv . --csvf usnjrnl.csv
# Run usnjrnl_rewind
python3 usnjrnl_rewind.py -u usnjrnl.csv -m mft.csv output_dir/
Velociraptor¶
Velociraptor (v0.74.5, macOS arm64) supports parse_usn() with usn_filename and mft_filename parameters for offline analysis. However, its parser cannot handle extracted sparse $J files — it reads the first data cluster, parses a few valid records, then stops at the first large zero-filled gap. Against the Szechuan Sauce $UsnJrnl:$J (43,463 records), Velociraptor returned only 2 valid records.
velociraptor query 'SELECT * FROM parse_usn(
usn_filename="/path/to/$UsnJrnl_$J",
mft_filename="/path/to/$MFT",
accessor="file")'
Results¶
Record Counts¶
| Image | usnjrnl-forensic | MFTECmd | usn.py | dfir_ntfs | usnrs |
|---|---|---|---|---|---|
| Szechuan Sauce | 43,463 | 43,463 | 43,463 | 43,463 | 43,463 |
| MaxPowers | 333,135 | 333,135 | 333,135 | 333,135 | 333,135 |
| PC-MUS-001 | 380,893 | 380,893 | 380,893 | 380,893 | 380,893 |
| Total | 757,491 | 757,491 | 757,491 | 757,491 | 757,491 |
All five tools produce identical record counts across all three images.
Velociraptor was excluded from this table. Its offline parse_usn() cannot handle extracted sparse $J files — it reads the first data cluster, parses a few valid records, then stops at the first large zero-filled gap. Against the Szechuan Sauce image (43,463 records), Velociraptor returned only 2 valid records. The remaining images were not tested as the parser fundamentally cannot process sparse journal files offline.
USN Offset Comparison¶
For each image, all Update Sequence Numbers were extracted from both usnjrnl-forensic and MFTECmd outputs, sorted numerically, and compared with diff. Zero differences across all three images — every USN offset in our output exists in MFTECmd's output, and vice versa.
Path Resolution: Rewind Algorithm vs Point-in-Time MFT Lookup¶
Three of the tested tools — MFTECmd, dfir_ntfs, and usnrs — support parent path resolution by looking up the parent MFT entry number in the current $MFT. This is a point-in-time lookup: it resolves correctly only if the parent directory's MFT entry still refers to the original directory. When an MFT entry has been reallocated (a deleted directory's entry reused for a different file), the lookup fails.
The Rewind algorithm (Yogesh Khatri, CyberCX) takes a different approach: it walks the USN journal chronologically, tracking directory creates, renames, and deletes to maintain a historical parent-child map. This resolves paths even when parent directories were later deleted and their MFT entries recycled. The original CyberCX proof-of-concept is a Python script that post-processes MFTECmd CSV output; usnjrnl-forensic implements the algorithm natively in Rust with no external tool dependency.
| Image | Records | usnjrnl-forensic | usnjrnl_rewind | MFTECmd | dfir_ntfs | usnrs |
|---|---|---|---|---|---|---|
| Szechuan Sauce | 43,463 | 43,463 (100%) | 43,452 (99.97%) | 33,002 (75.9%) | 27,671 (63.7%) | 26,011 (59.8%) |
| MaxPowers | 333,135 | 333,135 (100%) | 300,115 (90.1%) | 264,415 (79.4%) | 200,038 (60.0%) | 186,186 (55.9%) |
| PC-MUS-001 | 380,893 | 380,893 (100%) | 368,173 (96.7%) | 336,575 (88.4%) | 243,824 (64.0%) | 201,343 (52.9%) |
| Total | 757,491 | 757,491 (100%) | 711,740 (94.0%) | 633,992 (83.7%) | 471,533 (62.3%) | 413,540 (54.6%) |
Across 757,491 records from three real forensic images:
- usnjrnl-forensic resolved 757,491 of 757,491 records to correct full paths (100%)
- usnjrnl_rewind resolved 711,740 of 757,491 records to correct full paths (94.0%) — the remaining 45,751 paths are incorrect due to retroactive rename application and ADS name inclusion (see details below)
- MFTECmd resolved 633,992 of 757,491 records (83.7%) — returned
PathUnknownfor 123,499 - dfir_ntfs resolved 471,533 of 757,491 records (62.3%) — returned empty paths for 285,958
- usnrs resolved 413,540 of 757,491 records (54.6%) — returned filename-only for 343,951
usn.py does not support path resolution at all — its CSV output contains only timestamp, filename, fileattr, and reason with no parent path column. Velociraptor supports path resolution via mft_filename, but its offline parser only returned 2 records and cannot be meaningfully compared.
usnjrnl-forensic is the only tool tested that provides correct full path resolution for every record.
Path Resolution Comparison¶
xychart-beta
title "Correct Full Path Resolution Rate (%) — 757,491 records across 3 images"
x-axis ["usnjrnl-forensic", "usnjrnl_rewind", "MFTECmd", "dfir_ntfs", "usnrs"]
y-axis "Resolution Rate (%)" 0 --> 100
bar [100, 94.0, 83.7, 62.3, 54.6]
Examples¶
Below are representative records where point-in-time MFT lookup fails because the parent directory's MFT entry was reallocated after the original directory was deleted. Tools handle this failure differently:
- MFTECmd checks the MFT entry sequence number against the USN record's parent reference. When they don't match, it returns
PathUnknown— an honest admission that the path cannot be resolved. - dfir_ntfs and usnrs do not check sequence numbers. They resolve to whatever currently occupies the MFT entry, producing a silently incorrect path — a worse failure mode than
PathUnknown, because the analyst has no indication the path is wrong. - usnjrnl_rewind uses the Rewind algorithm but applies renames retroactively (see root cause below), producing incorrect paths when the parent directory was renamed between the event and the end of the journal.
- usnjrnl-forensic uses the Rewind algorithm with correct chronological scoping, reconstructing the path as it existed at the time of each event.
Szechuan Sauce — File: content.phf (USN 22120200, FILE_CREATE, parent MFT entry 86780 seq 9)
| Tool | Parent Path | Correct Full Path |
|---|---|---|
| usnjrnl-forensic | .\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\Cache\e4622eecf4cef8d28ec1969654794692c968a961 |
Yes |
| usnjrnl_rewind | .\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\Cache\e4622eecf4cef8d28ec1969654794692c968a961 |
Yes |
| MFTECmd | .\PathUnknown\Directory with ID 0x000152FC-00000009 |
No |
| dfir_ntfs | (empty) | No |
| usnrs | content.phf (filename only) |
No |
Szechuan Sauce — File: AgentPlaceholder.png (USN 22257480, SECURITY_CHANGE, parent MFT entry 250 seq 1)
| Tool | Parent Path | Correct Full Path |
|---|---|---|
| usnjrnl-forensic | .\Program Files\WindowsApps\Microsoft.GetHelp_10.1706.13331.0_x64__8wekyb3d8bbwe\Assets |
Yes |
| usnjrnl_rewind | .\Program Files\WindowsApps\Microsoft.GetHelp_10.1706.13331.0_x64__8wekyb3d8bbwe6eea7c0a-406b-4ef5-8479-7482f9705336\Assets |
Wrong |
| MFTECmd | .\PathUnknown\Directory with ID 0x000000FA-00000001 |
No |
| dfir_ntfs | .\Program Files\WindowsApps\Microsoft.GetHelp_10.2004.31291.0_x64__8wekyb3d8bbwe\Assets |
Wrong |
| usnrs | Program Files\WindowsApps\Microsoft.GetHelp_10.2004.31291.0_x64__8wekyb3d8bbwe\Assets\AgentPlaceholder.png |
Wrong |
Note the version number difference: the event occurred when Microsoft.GetHelp was at version 10.1706.13331.0. By the time the MFT was captured, MFT entry 250 had been reallocated to the newer 10.2004.31291.0 version. MFTECmd correctly reports PathUnknown. dfir_ntfs and usnrs silently return the wrong version. usnjrnl_rewind returns the correct version but with a staging GUID (6eea7c0a-406b-...) that was not appended until USN 23,489,328 — over 1.2M bytes after this event. An analyst using usnjrnl_rewind would look for a directory name that never existed at the time of this event.
MaxPowers — File: CL_Utility.ps1 (USN 1428280528, FILE_CREATE, parent MFT entry 2058 seq 17)
| Tool | Parent Path | Correct Full Path |
|---|---|---|
| usnjrnl-forensic | .\Windows\Temp\SDIAG_8e0546f4-e6d9-467c-8529-c7cb7bd3c437 |
Yes |
| usnjrnl_rewind | .\Windows\Temp\SDIAG_8e0546f4-e6d9-467c-8529-c7cb7bd3c437 |
Yes |
| MFTECmd | .\PathUnknown\Directory with ID 0x0000080A-00000011 |
No |
| dfir_ntfs | (empty) | No |
| usnrs | CL_Utility.ps1 (filename only) |
No |
PC-MUS-001 — File: BIT2860.tmp (USN 805870216, FILE_CREATE, parent MFT entry 30642 seq 2)
| Tool | Parent Path | Correct Full Path |
|---|---|---|
| usnjrnl-forensic | .\Users\borch\AppData\Local\Temp\chrome_BITS_7300_492499470 |
Yes |
| usnjrnl_rewind | .\Users\borch\AppData\Local\Temp\chrome_BITS_7300_492499470 |
Yes |
| MFTECmd | .\PathUnknown\Directory with ID 0x000077B2-00000002 |
No |
| dfir_ntfs | (empty) | No |
| usnrs | BIT2860.tmp (filename only) |
No |
PC-MUS-001 — File: th[1].jpg (USN 805900592, FILE_CREATE, parent MFT entry 1621 seq 5)
| Tool | Parent Path | Correct Full Path |
|---|---|---|
| usnjrnl-forensic | .\Users\borch\AppData\Local\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\AC\INetCache\6UNECTZR |
Yes |
| usnjrnl_rewind | .\Users\borch\AppData\Local\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\AC\INetCache\6UNECTZR |
Yes |
| MFTECmd | .\PathUnknown\Directory with ID 0x00000655-00000005 |
No |
| dfir_ntfs | (empty) | No |
| usnrs | th[1].jpg (filename only) |
No |
In each case, the parent directory's MFT entry was reallocated to a different file after the original directory was deleted. Point-in-time MFT lookup either fails honestly (MFTECmd's PathUnknown), fails silently with the wrong path (dfir_ntfs and usnrs), or returns no path at all. usnjrnl_rewind resolves most paths correctly but applies renames retroactively — producing incorrect paths when the parent directory was renamed between the event and the end of the journal (see AgentPlaceholder.png above).
usnjrnl_rewind Path Resolution Bugs¶
Record-by-record comparison against usnjrnl-forensic across all three images, verified against the journal's own rename events:
| Image | Records | Correct Paths (usnjrnl_rewind) | Incorrect Paths |
|---|---|---|---|
| Szechuan Sauce | 43,463 | 43,452 (99.97%) | 11 |
| MaxPowers | 333,135 | 300,115 (90.09%) | 33,020 |
| PC-MUS-001 | 380,893 | 368,173 (96.66%) | 12,720 |
| Total | 757,491 | 711,740 (94.0%) | 45,751 |
Two bugs account for all 45,751 incorrect paths:
- ADS names in directory paths — usnjrnl_rewind includes NTFS alternate data stream identifiers as path components (e.g.,
Microsoft Office:Win32App_1instead ofMicrosoft Office,OneDrive:${GUID}.SyncRootIdentityinstead ofOneDrive). ADS names are metadata about the directory entry, not filesystem path components. - Retroactive rename application — usnjrnl_rewind applies directory renames to records that occurred before the rename. For example, the directory
_8wekyb3d8bbwewas renamed to_8wekyb3d8bbwe6eea7c0a-406b-...at USN 23,489,328, but usnjrnl_rewind uses the post-rename name for events at USN 22,257,480. Similarly,WSAAwas recycled to$R6CHL9Iat USN 820,975,808, but usnjrnl_rewind uses the Recycle Bin name for events at USN 818,716,760.
Root cause: usnjrnl_rewind processes the journal in reverse order (newest to oldest) and accumulates directory renames without scoping them chronologically. When it encounters a rename (e.g., WSAA → $R6CHL9I), it applies the new name to all events referencing that MFT entry — including earlier events where the directory still had its original name. usnjrnl-forensic walks the journal forward, applying each rename only to events after the rename, which is the correct interpretation of the Rewind algorithm.
Unallocated Space Carving¶
The preceding sections validate usnjrnl-forensic's core parser and path resolution against other tools using allocated journal records. But the allocated $UsnJrnl:$J represents only a window into the volume's history — as new records are written, old ones are overwritten. usnjrnl-forensic extends this window by carving deleted USN records and MFT entries from unallocated disk space (--carve-unallocated).
No other USN journal tool provides this capability (NTFS Log Tracker carves from $LogFile but not from unallocated space), so cross-tool comparison is not possible. This section documents the methodology, results, and validation approach.
Methodology¶
The scanner reads the entire NTFS partition in overlapping 4 MB chunks (64 KB overlap to handle USN records spanning chunk boundaries). Each chunk is processed by two carvers:
- USN carver: scans on 8-byte aligned boundaries for USN_RECORD_V2/V3 signatures. Validates record length, version field, filename offset/length, UTF-16 alignment, and timestamp range (2000–2030).
- MFT carver: scans on 1024-byte aligned boundaries for "FILE" signatures. Validates sequence number > 0, first attribute offset, and presence of a FILE_NAME attribute with extractable filename and parent reference.
Carved records are deduplicated against allocated artifacts:
- USN records: by the usn field (journal byte offset — unique per record). If a carved record's USN offset matches one from the allocated $J, it is discarded.
- MFT entries: by (entry_number, sequence_number). If a carved entry matches an allocated MFT entry, it is discarded. Entries with the same entry number but a different sequence number are kept — these represent historical versions from before the MFT entry was reused, which are valuable for Rewind path resolution.
Because the scanner reads the entire partition (not just unallocated clusters), it also finds records in slack space and other residual areas. The deduplication step ensures only genuinely new records are reported.
Results¶
| Szechuan Sauce | MaxPowers C Drive | PC-MUS-001 | |
|---|---|---|---|
| Partition size | 14,735 MB | 50,231 MB | 243,318 MB |
| Allocated USN records | 43,463 | 333,135 | 380,893 |
| Allocated MFT entries | 104,383 | 293,303 | 515,876 |
| Carved USN records | 12,191 | 5,012,740 | 3,943 |
| Carved MFT entries | 5 | 636 | 23,131 |
| USN duplicates removed | 51,343 | 645,690 | 423,250 |
| MFT duplicates removed | 105,920 | 313,932 | 670,149 |
| Scan time (release build) | 32 s | 112 s | 309 s |
Analysis¶
MaxPowers C Drive produced the highest USN carving yield: 5,012,740 carved records versus 333,135 allocated — a 15× increase in timeline coverage. This indicates significant journal wrapping: the $UsnJrnl:$J file recycled its space many times, but the underlying disk clusters were not overwritten. These carved records extend the investigable timeline far beyond what the active journal contains.
PC-MUS-001 produced a large number of carved MFT entries (23,131) relative to USN records (3,943). This pattern is consistent with substantial MFT entry reuse — the NTFS volume has reallocated many MFT entry numbers to new files over time. These historical MFT entries enable the Rewind engine to resolve paths for older USN records that reference now-reused entry numbers.
Szechuan Sauce produced moderate yields (12,191 USN, 5 MFT), consistent with a smaller partition with less history.
Validation Approach¶
Since no other tool carves USN records from unallocated space in disk images, cross-tool comparison is not possible. Instead, carved records are validated through structural and semantic checks:
-
Structural validation — Each candidate must pass binary format checks: correct version field at the expected offset, record length within valid bounds, filename offset matching the version-specific constant (0x3C for V2, 0x4C for V3), even filename byte length (UTF-16), and filename fitting within the declared record length. For MFT entries: "FILE" signature, non-zero sequence number, valid first-attribute offset, and a parseable FILE_NAME attribute.
-
Timestamp validation — USN record timestamps must fall within 2000–2030 (Windows FILETIME range). This eliminates the vast majority of false positives from random byte sequences that happen to match version fields.
-
Deduplication consistency — The deduplication counts confirm the carvers are finding real records. For example, MaxPowers found 645,690 USN records that matched allocated
$Jentries by USN offset, plus 5,012,740 that did not. The high match rate for allocated records demonstrates the carver's accuracy — it correctly identifies the same records the standard parser finds, then surfaces additional ones. -
Field sanity — All carved records have non-empty filenames, valid reason flags (for USN), and non-zero sequence numbers (for MFT). The E2E tests assert these properties programmatically across all three test images.
How to Reproduce¶
- Download E01 images from The Evidence Locker
- Extract NTFS artifacts with The Sleuth Kit:
- Run usnjrnl-forensic (parsing + path resolution): Or with carving directly from the image:
- Run MFTECmd (with
-mfor path resolution comparison): - Run usnjrnl_rewind (requires MFTECmd CSVs from step 4):
- Run usn.py:
- Run dfir_ntfs:
- Run usnrs:
- Compare record counts, USN offsets, and resolved paths across all outputs.