Verify the image. Trust the evidence.
ewf-forensic is a pure-Rust integrity analyser for EWF images — no libewf, no C toolchain, no build complexity. It supports EWF v1 (E01 multi-segment with sibling auto-discovery), EWF v2 (Ex01/Lx01), SHA-1 and SHA-256 from digest sections, chain-of-custody external hash comparison (MD5, SHA-1, SHA-256), and optional header metadata extraction.
The analyser reports exactly what is structurally wrong across eight layers: signature forgery, broken section chains, cyclic chain attacks, Adler-32 descriptor corruption, volume geometry inconsistencies, table mismatches, out-of-bounds chunk pointers, MD5/SHA-1/SHA-256 hash mismatches, per-chunk checksum errors, and EWF v2 per-section data integrity. 40 distinct anomaly types across four severity levels.
Install
[]
= "0.4"
Optional Serde support for serialising anomalies and progress events to JSON:
= { = "0.4", = ["serde"] }
What It Checks
Layer 1 — File Header
| Anomaly | Severity |
|---|---|
InvalidSignature — EVF magic bytes corrupted or absent |
Critical |
SegmentNumberZero — segment number field is 0 (invalid) |
Error |
Layer 2 — Section Descriptor Integrity
| Anomaly | Severity |
|---|---|
SectionDescriptorCrcMismatch { offset, section_type, computed, stored } — Adler-32 over descriptor bytes [0..72] does not match stored checksum |
Error |
Layer 3 — Section Chain
| Anomaly | Severity |
|---|---|
SectionChainBroken { at_offset, next_offset } — next pointer is zero, past EOF, or points backward (cycle) |
Critical |
SectionGapNonZero { gap_offset, gap_size } — non-zero bytes exist between consecutive sections |
Warning |
SectionGapZero { gap_offset, gap_size } — zero-filled bytes between sections (alignment padding; noted as structural anomaly) |
Info |
Layer 4 — Section Completeness
| Anomaly | Severity |
|---|---|
VolumeSectionMissing — neither volume nor disk section found |
Critical |
SectorsSectionMissing — no sectors section found; sector data absent |
Error |
TableSectionMissing — no table section found; chunk index unusable |
Error |
UnknownSectionType { offset, type_name } — section type string not in the EWF v1 spec |
Warning |
DoneSectionMissing — chain ends without a done section |
Warning |
Layer 5 — Volume Geometry
| Anomaly | Severity |
|---|---|
BytesPerSectorInvalid { bytes_per_sector } — not 512 or 4 096 |
Error |
VolumeBodyCrcMismatch { computed, stored } — Adler-32 over volume section body does not match (EWF v2) |
Error |
ChunkSizeInvalid { sectors_per_chunk, bytes_per_sector } — zero or not a power of two |
Error |
SectorCountMismatch { declared, expected } — sector_count is outside the valid range; last-chunk padding is normal and not flagged |
Error |
MediaTypeUnknown { media_type } — media_type byte is not a recognised EWF v2 value |
Warning |
SetIdentifierMismatch { segment, expected } — set GUID in segment header does not match the first segment (EWF v2 multi-segment) |
Error |
Layer 6 — Table Integrity
| Anomaly | Severity |
|---|---|
TableChunkCountMismatch { in_volume, in_table } — entry count in table header differs from volume |
Error |
TableHeaderAdler32Mismatch { computed, stored } — Adler-32 over table header bytes does not match stored checksum |
Error |
Table2Mismatch { chunk_index, offset_in_table, offset_in_table2 } — table and table2 entries disagree for the same chunk index |
Error |
TableEntryOutOfBounds { chunk_index, entry_offset, file_size } — chunk offset resolves past EOF |
Error |
TableEntryOutsideSectorsRange { chunk_index, entry_offset, sectors_start, sectors_end } — entry resolves inside the file but outside the sectors data body |
Error |
Ewf2ChunkTableChecksumMismatch { segment, computed, stored } — EWF v2 chunk table Adler-32 does not match |
Error |
ChunkChecksumMismatch { chunk_index, computed, stored } — per-chunk Adler-32 (appended after uncompressed chunk data) does not match |
Error |
ChunkDecompressionError { chunk_index } — compressed chunk cannot be decompressed (corrupt DEFLATE stream) |
Error |
UnsupportedCompressionAlgorithm { chunk_index, algorithm } — compression method is not deflate |
Error |
Layer 7 — Hash Verification
| Anomaly | Severity |
|---|---|
HashMismatch { computed, stored } — MD5 of all sector data does not match stored hash |
Error |
HashSectionMissing — no hash section found |
Warning |
DigestSha1Mismatch { computed, stored } — SHA-1 of all sector data does not match digest section |
Error |
DigestSha256Mismatch { computed, stored } — SHA-256 of all sector data does not match digest section |
Error |
BadSectorsPresent { count } — error2 section reports unreadable sectors at acquisition time |
Warning |
Layer 8 — Multi-segment and External Reference
| Anomaly | Severity |
|---|---|
SegmentOutOfOrder { segment_number, expected } — supplied segments are not in sequential order |
Error |
ExternalMd5Mismatch { computed, expected } — computed MD5 does not match externally-supplied chain-of-custody hash |
Critical |
ExternalSha1Mismatch { computed, expected } — computed SHA-1 does not match externally-supplied reference |
Critical |
ExternalSha256Mismatch { computed, expected } — computed SHA-256 does not match externally-supplied reference |
Critical |
EWF v2 — Per-section and Media Integrity
| Anomaly | Severity |
|---|---|
Ewf2SectionDataHashMismatch { offset, section_type_id, computed, stored } — MD5 of section body does not match data_integrity_hash in the descriptor |
Error |
Ewf2EncryptedSection { offset } — encrypted section found; content cannot be verified |
Warning |
Ewf2HashSectionMissing — no hash section (type 0x08 or 0x09) in the final segment |
Warning |
Ewf2MediaInfoMissing — no media_info section found in the image |
Warning |
Ewf2MediaInfoParseFailed — media_info section body is not a valid zlib stream |
Error |
Usage
Analyse an E01 file (recommended path-based API)
use ;
In-memory API (e.g., when you already have the bytes)
use ;
let data = read.unwrap;
let findings = new.analyse;
Multi-segment image — explicit paths
use EwfIntegrityPath;
let findings = from_paths.analyse?;
Verify against a chain-of-custody hash
use EwfIntegrityPath;
let coc_md5: = /* bytes from acquisition report */;
let coc_sha256: = /* bytes from acquisition report */;
// ExternalMd5Mismatch / ExternalSha256Mismatch (Critical) fire if image was altered.
let findings = from_path
.with_expected_md5
.with_expected_sha256
.analyse?;
Compute hashes independently
use EwfIntegrityPath;
// Returns None if the image is not a valid EWF.
if let Some = from_path.compute_hashes?
Single-pass: analyse and compute hashes together
use EwfIntegrityPath;
let = from_path
.analyse_and_compute_hashes?;
// Both results from a single read pass — useful for large images.
Progress callback (long images, pipelines)
use ;
let = from_path
.analyse_with_progress?;
Read acquisition metadata from the header
use ;
let data = read.unwrap;
if let Some = new.header_metadata
Triage by severity
use ;
let findings = from_path.analyse?;
let critical: = findings.iter
.filter
.collect;
if !critical.is_empty
Serde — serialise findings to JSON
= { = "0.4", = ["serde"] }
use EwfIntegrityPath;
let findings = from_path.analyse?;
let json = to_string_pretty.unwrap;
println!;
CLI — ewf-check
ewf-check [OPTIONS] <segment>...
ARGUMENTS
<segment>... One or more segment paths. When a single .E01 is given,
consecutive siblings are discovered automatically.
OPTIONS
--min-severity=<level> Only report anomalies at or above this level.
Levels: info, warning, error, critical [default: info]
--json Emit machine-readable JSON.
--hash-md5=<hex> Compare computed MD5 against this hex string.
--hash-sha1=<hex> Compare computed SHA-1 against this hex string.
--hash-sha256=<hex> Compare computed SHA-256 against this hex string.
--print-hashes Compute and print MD5, SHA-1, and SHA-256.
--progress Show a progress bar on stderr during analysis.
--help / --version
EXIT CODES
0 Clean — no anomalies at or above --min-severity
1 Anomalies found
2 Usage error or I/O failure
Example: verify an 8-segment acquisition against an external hash manifest:
# auto-discovers evidence.E02 … evidence.E08
Design
- File-based API uses memory-mapped I/O —
EwfIntegrityPathmmaps each segment rather than reading it into aVec<u8>. Large images (100 GB+) do not require 100 GB of RAM. - No unsafe code in ewf-forensic — the crate itself contains no
unsafeblocks.memmap2wraps the OS mmap syscall but its unsafety is isolated to that dependency. - No panics on adversarial input — every parser path is bounded; cycle attacks and integer overflows are explicitly handled. Verified by libfuzzer (4.5 M iterations, zero crashes) and proptest (property-based, runs in
cargo test). - Validated against 11 committed real-world fixtures — seven acquisition-tool images (EWF v1 and EWF v2, all confirmed clean by
ewfverify), plus CTF and sleuthkit test-corpus images including structurally invalid zero-byte inputs. See docs/validation.md for image sources and reproduction steps. - MSRV 1.85 — no nightly, no unstable features.
Fuzzing
Both targets run in CI for 30 seconds on every push. To run longer locally, remove -max_total_time.
Anomaly Catalog
docs/anomaly-catalog.md maps every detectable anomaly to its threat scenario — evidence suppression, modification, insertion, redirection, and parser exploitation — and documents known detection limits.
Limitations
MD5 is cryptographically broken
EWF v1 stores an MD5 digest in the hash section. MD5 chosen-prefix collisions are feasible on consumer hardware. A sufficiently resourced adversary can modify sector data, construct a new sectors body with the same MD5, and store the original hash — HashMismatch is not reported. ewf-forensic cannot detect a valid MD5 collision.
Mitigation: Supply --hash-sha256 (or .with_expected_sha256()) with a SHA-256 computed at acquisition time and stored separately from the image.
Sector content not inspected beyond hash verification
The analyser decompresses and hashes every chunk but does not parse filesystem structures within those sectors. It cannot identify which specific LBA ranges were modified, detect filesystem-level tampering (MFT manipulation, journal editing), or report what changed. That requires a full EWF reader such as ewf.
Recomputed Adler-32 does not prove a descriptor is unmodified
If an attacker modifies a section descriptor field and recomputes the Adler-32 over the modified bytes, the descriptor verifies correctly. Only an external hash over the full image taken at acquisition time can detect this. SectionDescriptorCrcMismatch catches the lazy attacker; it does not catch the careful one.
EWF v2 encrypted sections are not verified
When Ewf2EncryptedSection is reported, the section body is skipped. Content inside an encrypted section cannot be integrity-checked. If every data section is encrypted, the analyser provides structural checks only.
Progress callbacks report chunks_total = None for EWF v1
EWF v1 does not declare the total chunk count in a header field — it is discovered by walking the section chain. AnalysisProgress.chunks_total is None during EWF v1 analysis and Some(n) during EWF v2 analysis where the chunk table declares its entry count up front.
FTK Imager and X-Ways real-fixture tests are deferred
tool_fixtures_tests includes tests against real ftk_imager_clean.E01 and xways_clean.E01 fixtures. These are marked #[ignore] because the acquisition tools are Windows-only and not available in CI. The format variations they exercise are covered by always-on synthetic builder tests.
Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd