|
| 1 | +# ntfs-forensic |
| 2 | + |
| 3 | +**A from-scratch NTFS reader and a graded anomaly auditor — reconstruct full file paths from the `$UsnJrnl:$J` change journal (even for deleted, MFT-reused files), and surface the timestomping, alternate data streams, deleted records, and MFT slack that a "clean" filesystem driver is built to hide.** |
| 4 | + |
| 5 | +Two crates, one workspace: |
| 6 | + |
| 7 | +- **[`ntfs-core`](https://crates.io/crates/ntfs-core)** — the reader: `$MFT`, attributes, indexes, data runs, LZNT1, `$UsnJrnl:$J` change-journal record decode, and `NtfsFs` path navigation over any `Read + Seek` source. No `unsafe`, no C bindings. |
| 8 | +- **[`ntfs-forensic`](https://crates.io/crates/ntfs-forensic)** — the auditor: turns parsed MFT records into severity-graded [`forensicnomicon::report::Finding`](https://crates.io/crates/forensicnomicon)s, so an NTFS volume's anomalies aggregate uniformly with the partition and container layers. |
| 9 | + |
| 10 | +## Audit a raw MFT record in 30 seconds |
| 11 | + |
| 12 | +```toml |
| 13 | +[dependencies] |
| 14 | +ntfs-forensic = "0.5" # pulls in ntfs-core |
| 15 | +``` |
| 16 | + |
| 17 | +```rust |
| 18 | +use ntfs_forensic::audit_record; |
| 19 | +use forensicnomicon::report::Source; |
| 20 | + |
| 21 | +let src = Source { analyzer: "ntfs-forensic".into(), scope: "NTFS".into(), version: None }; |
| 22 | + |
| 23 | +// Feed it a single raw 1024-byte MFT record; get back graded anomalies. |
| 24 | +for anomaly in audit_record(&mft_record_bytes) { |
| 25 | + let finding = anomaly.to_finding(src.clone()); |
| 26 | + println!("[{:?}] {} — {}", finding.severity, finding.code, finding.note); |
| 27 | + // e.g. [Some(High)] NTFS-TIMESTOMP — $SI created before $FN … |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +`audit_record` parses the header and attributes, extracts `$STANDARD_INFORMATION`/`$FILE_NAME`, and grades what it finds. A record whose header does not parse yields no anomalies (structural corruption is surfaced by the reader/carver, never a panic). |
| 32 | + |
| 33 | +## The anomaly codes |
| 34 | + |
| 35 | +Each anomaly is an **observation** ("consistent with …"); the examiner draws the conclusions. Codes are a stable, published contract. |
| 36 | + |
| 37 | +| Code | Severity | What it observes | |
| 38 | +|---|---|---| |
| 39 | +| `NTFS-TIMESTOMP` | High | `$STANDARD_INFORMATION` times show forgery tells vs. the harder-to-forge `$FILE_NAME` times (`$SI` predates `$FN`, or lands on a whole second) | |
| 40 | +| `NTFS-ADS` | Low | A named `$DATA` attribute — an alternate data stream (also used benignly, e.g. `Zone.Identifier`) | |
| 41 | +| `NTFS-SLACK-RESIDUE` | Low | Non-zero residue in an MFT record's slack, past its used size | |
| 42 | +| `NTFS-DELETED-RECORD` | Info | An MFT record not in use — a recoverable deleted file | |
| 43 | +| `NTFS-MFTMIRR-MISMATCH` | High | A system record in `$MFT` differs from its `$MFTMirr` copy | |
| 44 | +| `NTFS-LOGFILE-CLEARED` | Medium | `$LogFile` shows restart-area gaps consistent with the journal having been cleared | |
| 45 | + |
| 46 | +## The reader: navigate a volume |
| 47 | + |
| 48 | +`NtfsFs` (in `ntfs-core`, imported as `ntfs_core`) reads files and directories from any `Read + Seek` source: |
| 49 | + |
| 50 | +```rust |
| 51 | +use ntfs_core::NtfsFs; |
| 52 | +use std::fs::File; |
| 53 | + |
| 54 | +let mut fs = NtfsFs::open(File::open("ntfs.img")?)?; |
| 55 | + |
| 56 | +// Read a file by path… |
| 57 | +let hosts = fs.read_file(r"\Windows\System32\drivers\etc\hosts")?; |
| 58 | + |
| 59 | +// …or list the root directory (MFT record 5). |
| 60 | +let root = fs.read_record(5)?; |
| 61 | +for entry in fs.directory_entries(&root)? { |
| 62 | + if let Some(name) = entry.file_name { |
| 63 | + println!("{}", name.name); |
| 64 | + } |
| 65 | +} |
| 66 | +# Ok::<(), ntfs_core::NtfsError>(()) |
| 67 | +``` |
| 68 | + |
| 69 | +The bare crate name `ntfs` on crates.io is Colin Finck's general-purpose reader, so this crate publishes as `ntfs-core` and imports as `ntfs_core`. |
| 70 | + |
| 71 | +## `$UsnJrnl:$J`: reconstruct full paths — even for deleted files |
| 72 | + |
| 73 | +The USN change journal records *what* changed and *which* MFT entry — but only the file's **own name**, never its path. `ntfs-core` reconstructs the **full path** of every journal event, including files that were deleted and whose `$MFT` record was later reused, by walking the journal with the *Rewind* algorithm — two passes (reverse, then forward) so a rename or MFT-entry reuse resolves to the correct path at each point in time. |
| 74 | + |
| 75 | +> **Credit:** the journal-`$J` path-reconstruction technique was pioneered by [**CyberCX**](https://cybercx.com/) — see their writeup [*NTFS Usnjrnl Rewind*](https://cybercx.com/blog/ntfs-usnjrnl-rewind/) (April 2024) and the reference tool [`CyberCX-DFIR/usnjrnl_rewind`](https://github.com/CyberCX-DFIR/usnjrnl_rewind). This is an independent, clean-room Rust implementation built on `ntfs-core`'s own parsers; its SQLite export is column-compatible with `usnjrnl_rewind`. |
| 76 | +
|
| 77 | +## Trust, but verify |
| 78 | + |
| 79 | +`ntfs-forensic` is built for untrusted disk images from potentially compromised systems: |
| 80 | + |
| 81 | +- **`#![forbid(unsafe_code)]`** across both crates — no C bindings, no FFI. |
| 82 | +- **Panic-free on malicious input** — every length and offset is validated against both the structure's declared size and the actual buffer; the workspace denies `clippy::unwrap_used` and `clippy::expect_used` in production code. |
| 83 | +- **Fuzzed** — seven `cargo-fuzz` targets (`boot`, `record`, `attributes`, `attribute_list`, `runlist`, `index_buffer`, `compress`); a `fuzz.yml` CI workflow builds and smoke-runs each. |
| 84 | +- **Validated on real artifacts** — the boot parser is cross-validated against The Sleuth Kit on a real disk image, and MFT parsing is cross-checked against the `mft` crate as an independent oracle. |
| 85 | +- **100% line coverage** enforced in CI (`cargo llvm-cov --lib`, failing on any zero-hit line). |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +[Privacy Policy](privacy.md) · [Terms of Service](terms.md) · © 2026 Security Ronin Ltd |
0 commit comments