ntfs-forensic 0.3.0

Forensic-grade, from-scratch NTFS filesystem reader — MFT, attributes, indexes, data runs, and deleted/slack/anti-forensic recovery over any Read + Seek source
Documentation
ntfs-forensic-0.3.0 has been yanked.

ntfs-forensic

Crates.io docs.rs License: MIT CI Sponsor

Forensic-grade NTFS reader for Rust. A clean, from-scratch implementation that reads files and directories from any Read + Seek source — and goes beyond a normal filesystem driver to surface the artifacts an examiner needs: timestomping indicators, alternate data streams, deleted MFT records, and record slack that a "clean" reader is designed to hide.

It is the NTFS FS-layer foundation for the SecurityRonin forensic family: usnjrnl-forensic and issen consume it as their single, auditable NTFS engine.

Rust library

[dependencies]
ntfs-forensic = "0.1"

Quick start

use ntfs_forensic::NtfsFs;
use std::fs::File;

// Open an NTFS volume (a raw partition image, or any Read + Seek source).
let mut fs = NtfsFs::open(File::open("ntfs.img")?)?;

// Read a file by path…
let hosts = fs.read_file(r"\Windows\System32\drivers\etc\hosts")?;

// …or list the root directory (MFT record 5).
let root = fs.read_record(5)?;
for entry in fs.directory_entries(&root)? {
    if let Some(name) = entry.file_name {
        println!("{}", name.name);
    }
}
# Ok::<(), ntfs_forensic::NtfsError>(())

What makes this different from a general-purpose NTFS crate

Most NTFS crates answer one question: "what files are on this volume?" ntfs-forensic answers the questions a digital forensics examiner actually needs:

Capability General-purpose NTFS crate ntfs-forensic
MFT record + attribute parsing
Directory index traversal ($INDEX_ROOT / INDX)
Data runs, sparse files, LZNT1 decompression
$ATTRIBUTE_LIST (heavily fragmented files) partial
$SI-vs-$FN timestomping detection
Alternate data stream enumeration
Deleted-record carving (unallocated FILE/BAAD)
MFT record slack extraction
Update-sequence (fixup) torn-write / tamper detection
Partition-window isolation (cannot read past the volume)
Adversarial-input hardening + fuzz testing
#![forbid(unsafe_code)]

Forensic capabilities

Every analysis is a pure function over already-parsed structures — exact and side-effect free.

Timestomping detection

NTFS keeps two timestamp sets: $STANDARD_INFORMATION (updatable via the Win32 API — what timestomp tools target) and $FILE_NAME (set by the kernel on create/rename). Divergence between them, or $SI times landing on a whole second, is a tampering tell.

use ntfs_forensic::detect_timestomp;

let flags = detect_timestomp(&std_info, &file_name);
if flags.is_suspicious() {
    // si_created_before_fn, created_mismatch, si_whole_second
    eprintln!("timestomp indicators: {flags:?}");
}

Alternate data streams

use ntfs_forensic::alternate_data_streams;

for ads in alternate_data_streams(&attributes) {
    println!("ADS: {}", ads.name.as_deref().unwrap_or(""));  // e.g. "Zone.Identifier"
}

Deleted records and record slack

use ntfs_forensic::{carve_file_records, is_deleted, record_slack, MftRecordHeader};

// Scan a raw $MFT for FILE/BAAD records at record-size boundaries.
for offset in carve_file_records(&mft_bytes, 1024) {
    let header = MftRecordHeader::parse(&mft_bytes[offset..])?;
    if is_deleted(&header) {
        // residue past the record's used size may hold a previous resident attribute
        let slack = record_slack(&mft_bytes[offset..offset + 1024], &header);
        println!("deleted record {} ({} slack bytes)", header.record_number, slack.len());
    }
}
# Ok::<(), ntfs_forensic::NtfsError>(())

Opening a partition inside a whole disk

OffsetReader re-bases a partition to offset 0 and structurally cannot read past the partition boundary — feed it the offset and length from mbr-forensic / gpt-forensic:

use ntfs_forensic::{NtfsFs, OffsetReader};
use std::fs::File;

let part = OffsetReader::new(File::open("disk.img")?, 1_048_576, 500_000_000)?;
let mut fs = NtfsFs::open(part)?;
# Ok::<(), ntfs_forensic::NtfsError>(())

API

Item Purpose
NtfsFs::open / read_file / read_record / directory_entries / resolve_path Navigate a volume by path or MFT record number
BootSector::parse Volume boot record (BPB / extended BPB)
MftRecordHeader::parse / apply_fixup FILE records and update-sequence-array fixup
parse_attributes / Attribute Resident and non-resident attribute walking
StandardInformation / FileName The two timestamp sets
decode_runlist / read_attribute_value Data runs (VCN→LCN), sparse + non-resident reads
IndexRoot::parse / parse_index_buffer Directory B-tree ($INDEX_ROOT / INDX)
parse_attribute_list Extension records for fragmented files
decompress LZNT1 decompression
detect_timestomp / alternate_data_streams / record_slack / is_deleted / carve_file_records Forensic Tier-2
OffsetReader Bounded partition window

All parsers accept &[u8] or a Read + Seek source and return a typed Result<_, NtfsError>.

Security

ntfs-forensic is designed for use on untrusted disk images from potentially compromised systems:

  • No panics on malicious input — every length and offset is validated against both the structure's declared size and the actual buffer; arithmetic is checked or saturating
  • #![forbid(unsafe_code)] across the whole crate
  • Bounded allocationstry_reserve_exact and explicit ceilings refuse allocation bombs (e.g. a crafted runlist or LZNT1 stream)
  • Loop caps — attribute chains, runlists, and index entries are bounded against non-terminating walks
  • Fixup verification — torn writes and USA tampering surface as FixupMismatch rather than silently-wrong output
  • Partition isolationOffsetReader makes reading past the volume boundary structurally impossible
  • Fuzz-tested — seven cargo-fuzz targets, tens of millions of executions; the one panic they found (an LZNT1 chunk-size overflow) is fixed and pinned as a regression test

Running the fuzz targets

# Requires nightly Rust and cargo-fuzz
rustup install nightly
cargo install cargo-fuzz

cargo +nightly fuzz run compress     # LZNT1 — loops + back-references
cargo +nightly fuzz run record       # MFT record + fixup
cargo +nightly fuzz run attributes   # attribute chain walking

Testing

140 unit tests plus a real-image cross-validation test, covering every public API, every error path, and adversarial inputs (truncated records, crafted runlists, torn fixups, out-of-bounds indexes). The boot parser is cross-validated against The Sleuth Kit's fsstat on a real disk image. No source line is left uncovered — enforced in CI.

cargo test
cargo install cargo-llvm-cov
cargo llvm-cov --lib --show-missing-lines

Aggregate line coverage can read slightly under 100% because the generic, reader-agnostic functions (NtfsFs<R>, OffsetReader<R>) are monomorphized once per reader type in the tests; the CI gate confirms no source line is left uncovered (zero zero-hit lines in lcov).

Related

ntfs-forensic reads an NTFS volume. To get a Read + Seek over a disk image, and to locate the NTFS partition within it, these crates compose upstream:

Crate Role
disk-forensic Orchestrator — auto-detects MBR / GPT / APM and yields each partition's offset / length
mbr-forensic MBR partition table → NTFS partition offset / length
gpt-forensic GPT partition table → NTFS partition offset / length
apm-forensic Apple Partition Map (classic Mac / hybrid media — rarely hosts NTFS)
ewf-forensic E01 / Expert Witness Format container
vhdx-forensic VHDX container

Sibling crates

One forensic reader per filesystem — each a Read + Seek library that composes with the container and partition crates above:

Crate Filesystem
ext4fs-forensic ext2 / ext3 / ext4
ntfs-forensic NTFS

Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd