forensicnomicon 0.5.8

The ForensicNomicon — comprehensive DFIR artifact catalog: UserAssist, Shimcache, Amcache, Prefetch, $MFT, ShellBags, EVTX, NTDS.dit, SAM, SRUM, LNK, Jump Lists + KAPE/Velociraptor/Sigma/MITRE. Zero deps.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! NTFS on-disk structure knowledge: signatures, attribute type codes,
//! well-known MFT record numbers, record-header field offsets, flags, and
//! `$FILE_NAME` namespaces.
//!
//! Single source of truth for forensic tools that parse NTFS (e.g. the
//! `ntfs-forensic` crate). This module holds **constants and layout facts
//! only** — no parsing, no I/O.
//!
//! Sources:
//! - Microsoft, "[MS-FSCC]: File System Control Codes" and the NTFS on-disk
//!   format documentation.
//! - Brian Carrier, *File System Forensic Analysis* (Addison-Wesley, 2005),
//!   chapters 11–13 (NTFS).
//! - Joachim Metz, "libfsntfs — NTFS format specification":
//!   <https://github.com/libyal/libfsntfs/blob/main/documentation/New%20Technologies%20File%20System%20%28NTFS%29.asciidoc>
//! - The NTFS Documentation project (Russon & Fledel):
//!   <https://flatcap.github.io/linux-ntfs/ntfs/>

// ── Signatures ────────────────────────────────────────────────────────────────

/// Magic at the start of an in-use MFT file-record segment.
pub const SIGNATURE_FILE: [u8; 4] = *b"FILE";
/// Magic written by `chkdsk` over a record it found corrupt.
pub const SIGNATURE_BAAD: [u8; 4] = *b"BAAD";
/// Magic at the start of an index-allocation buffer (`$INDEX_ALLOCATION`).
pub const SIGNATURE_INDX: [u8; 4] = *b"INDX";
/// OEM identifier at offset 3 of the NTFS boot sector.
pub const OEM_ID: [u8; 8] = *b"NTFS    ";

// ── Attribute type codes ────────────────────────────────────────────────────────

/// NTFS attribute type identifiers (the `type` field of an attribute header).
pub mod attr_types {
    pub const STANDARD_INFORMATION: u32 = 0x10;
    pub const ATTRIBUTE_LIST: u32 = 0x20;
    pub const FILE_NAME: u32 = 0x30;
    pub const OBJECT_ID: u32 = 0x40;
    pub const SECURITY_DESCRIPTOR: u32 = 0x50;
    pub const VOLUME_NAME: u32 = 0x60;
    pub const VOLUME_INFORMATION: u32 = 0x70;
    pub const DATA: u32 = 0x80;
    pub const INDEX_ROOT: u32 = 0x90;
    pub const INDEX_ALLOCATION: u32 = 0xA0;
    pub const BITMAP: u32 = 0xB0;
    pub const REPARSE_POINT: u32 = 0xC0;
    pub const EA_INFORMATION: u32 = 0xD0;
    pub const EA: u32 = 0xE0;
    pub const PROPERTY_SET: u32 = 0xF0;
    pub const LOGGED_UTILITY_STREAM: u32 = 0x100;
    /// End-of-attributes marker.
    pub const END: u32 = 0xFFFF_FFFF;
}

/// Attribute type code → canonical `$NAME`, in ascending code order.
pub const ATTRIBUTE_TYPES: &[(u32, &str)] = &[
    (attr_types::STANDARD_INFORMATION, "$STANDARD_INFORMATION"),
    (attr_types::ATTRIBUTE_LIST, "$ATTRIBUTE_LIST"),
    (attr_types::FILE_NAME, "$FILE_NAME"),
    (attr_types::OBJECT_ID, "$OBJECT_ID"),
    (attr_types::SECURITY_DESCRIPTOR, "$SECURITY_DESCRIPTOR"),
    (attr_types::VOLUME_NAME, "$VOLUME_NAME"),
    (attr_types::VOLUME_INFORMATION, "$VOLUME_INFORMATION"),
    (attr_types::DATA, "$DATA"),
    (attr_types::INDEX_ROOT, "$INDEX_ROOT"),
    (attr_types::INDEX_ALLOCATION, "$INDEX_ALLOCATION"),
    (attr_types::BITMAP, "$BITMAP"),
    (attr_types::REPARSE_POINT, "$REPARSE_POINT"),
    (attr_types::EA_INFORMATION, "$EA_INFORMATION"),
    (attr_types::EA, "$EA"),
    (attr_types::PROPERTY_SET, "$PROPERTY_SET"),
    (attr_types::LOGGED_UTILITY_STREAM, "$LOGGED_UTILITY_STREAM"),
];

/// Look up an attribute type code's canonical name. Returns `None` for unknown
/// codes (including the `END` marker).
#[must_use]
pub fn attribute_type_name(ty: u32) -> Option<&'static str> {
    ATTRIBUTE_TYPES
        .iter()
        .find(|(code, _)| *code == ty)
        .map(|(_, name)| *name)
}

// ── Attribute header field offsets ────────────────────────────────────────────

/// Byte offsets of fields within an attribute header (common header followed by
/// a resident or non-resident body, both beginning at `0x10`).
pub mod attr_offsets {
    // Common header.
    pub const TYPE: usize = 0x00;
    pub const LENGTH: usize = 0x04;
    pub const NON_RESIDENT: usize = 0x08;
    pub const NAME_LENGTH: usize = 0x09;
    pub const NAME_OFFSET: usize = 0x0A;
    pub const FLAGS: usize = 0x0C;
    pub const ATTRIBUTE_ID: usize = 0x0E;
    // Resident body.
    pub const RES_CONTENT_LENGTH: usize = 0x10;
    pub const RES_CONTENT_OFFSET: usize = 0x14;
    // Non-resident body.
    pub const NR_START_VCN: usize = 0x10;
    pub const NR_LAST_VCN: usize = 0x18;
    pub const NR_RUNS_OFFSET: usize = 0x20;
    pub const NR_COMPRESSION_UNIT: usize = 0x22;
    pub const NR_ALLOCATED_SIZE: usize = 0x28;
    pub const NR_REAL_SIZE: usize = 0x30;
    pub const NR_INITIALIZED_SIZE: usize = 0x38;
}

/// Attribute header flags (`flags` field at offset `0x0C`).
pub mod attr_flags {
    pub const COMPRESSED: u16 = 0x0001;
    pub const ENCRYPTED: u16 = 0x4000;
    pub const SPARSE: u16 = 0x8000;
}

// ── File attribute flags ──────────────────────────────────────────────────────

/// Windows `FILE_ATTRIBUTE_*` flags, as stored in `$STANDARD_INFORMATION` and
/// `$FILE_NAME`. Values per [MS-FSCC] §2.6.
pub mod file_attributes {
    pub const READONLY: u32 = 0x0001;
    pub const HIDDEN: u32 = 0x0002;
    pub const SYSTEM: u32 = 0x0004;
    pub const ARCHIVE: u32 = 0x0020;
    pub const TEMPORARY: u32 = 0x0100;
    pub const SPARSE_FILE: u32 = 0x0200;
    pub const REPARSE_POINT: u32 = 0x0400;
    pub const COMPRESSED: u32 = 0x0800;
    pub const ENCRYPTED: u32 = 0x4000;
}

// ── Well-known MFT record numbers ─────────────────────────────────────────────

/// Fixed MFT record numbers for NTFS metadata files (records 0–11).
pub mod mft_records {
    pub const MFT: u64 = 0;
    pub const MFTMIRR: u64 = 1;
    pub const LOGFILE: u64 = 2;
    pub const VOLUME: u64 = 3;
    pub const ATTRDEF: u64 = 4;
    /// The root directory (`.`).
    pub const ROOT: u64 = 5;
    pub const BITMAP: u64 = 6;
    pub const BOOT: u64 = 7;
    pub const BADCLUS: u64 = 8;
    pub const SECURE: u64 = 9;
    pub const UPCASE: u64 = 10;
    pub const EXTEND: u64 = 11;
}

/// Record number → metadata-file name, in ascending order.
pub const MFT_RECORD_NAMES: &[(u64, &str)] = &[
    (mft_records::MFT, "$MFT"),
    (mft_records::MFTMIRR, "$MFTMirr"),
    (mft_records::LOGFILE, "$LogFile"),
    (mft_records::VOLUME, "$Volume"),
    (mft_records::ATTRDEF, "$AttrDef"),
    (mft_records::ROOT, ". (root directory)"),
    (mft_records::BITMAP, "$Bitmap"),
    (mft_records::BOOT, "$Boot"),
    (mft_records::BADCLUS, "$BadClus"),
    (mft_records::SECURE, "$Secure"),
    (mft_records::UPCASE, "$UpCase"),
    (mft_records::EXTEND, "$Extend"),
];

/// Look up a well-known MFT record number's name. Returns `None` for ordinary
/// (non-reserved) records.
#[must_use]
pub fn mft_record_name(n: u64) -> Option<&'static str> {
    MFT_RECORD_NAMES
        .iter()
        .find(|(num, _)| *num == n)
        .map(|(_, name)| *name)
}

// ── MFT record header field offsets ──────────────────────────────────────────

/// Byte offsets of fields within an MFT file-record-segment header.
pub mod mft_offsets {
    pub const SIGNATURE: usize = 0x00;
    pub const USA_OFFSET: usize = 0x04;
    pub const USA_COUNT: usize = 0x06;
    pub const LSN: usize = 0x08;
    pub const SEQUENCE_NUMBER: usize = 0x10;
    pub const HARD_LINK_COUNT: usize = 0x12;
    pub const FIRST_ATTRIBUTE: usize = 0x14;
    pub const FLAGS: usize = 0x16;
    pub const USED_SIZE: usize = 0x18;
    pub const ALLOCATED_SIZE: usize = 0x1C;
    pub const BASE_RECORD: usize = 0x20;
    pub const NEXT_ATTR_ID: usize = 0x28;
    /// MFT record number (Windows XP and later).
    pub const RECORD_NUMBER: usize = 0x2C;
}

/// MFT file-record-segment header flags (`flags` field at offset 0x16).
pub mod mft_flags {
    pub const IN_USE: u16 = 0x0001;
    pub const DIRECTORY: u16 = 0x0002;
    pub const EXTENSION: u16 = 0x0004;
    pub const VIEW_INDEX: u16 = 0x0008;
}

// ── $FILE_NAME namespaces ─────────────────────────────────────────────────────

/// `$FILE_NAME` namespace codes (the `namespace` byte of the attribute).
pub mod filename_namespace {
    pub const POSIX: u8 = 0;
    pub const WIN32: u8 = 1;
    pub const DOS: u8 = 2;
    pub const WIN32_AND_DOS: u8 = 3;

    /// Human-readable namespace name, or `None` for an unknown code.
    #[must_use]
    pub fn name(ns: u8) -> Option<&'static str> {
        match ns {
            POSIX => Some("POSIX"),
            WIN32 => Some("Win32"),
            DOS => Some("DOS"),
            WIN32_AND_DOS => Some("Win32+DOS"),
            _ => None,
        }
    }
}

// ── Boot sector (BPB / extended BPB) field offsets ────────────────────────────

/// Byte offsets of fields within the NTFS boot sector.
pub mod boot_offsets {
    pub const OEM_ID: usize = 0x03;
    pub const BYTES_PER_SECTOR: usize = 0x0B;
    pub const SECTORS_PER_CLUSTER: usize = 0x0D;
    pub const TOTAL_SECTORS: usize = 0x28;
    pub const MFT_LCN: usize = 0x30;
    pub const MFTMIRR_LCN: usize = 0x38;
    pub const CLUSTERS_PER_RECORD: usize = 0x40;
    pub const CLUSTERS_PER_INDEX: usize = 0x44;
    pub const VOLUME_SERIAL: usize = 0x48;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn record_signatures_are_correct() {
        assert_eq!(&SIGNATURE_FILE, b"FILE");
        assert_eq!(&SIGNATURE_BAAD, b"BAAD");
        assert_eq!(&OEM_ID, b"NTFS    ");
    }

    #[test]
    fn attribute_type_codes_resolve() {
        assert_eq!(
            attribute_type_name(attr_types::STANDARD_INFORMATION),
            Some("$STANDARD_INFORMATION")
        );
        assert_eq!(
            attribute_type_name(attr_types::FILE_NAME),
            Some("$FILE_NAME")
        );
        assert_eq!(attribute_type_name(attr_types::DATA), Some("$DATA"));
        assert_eq!(
            attribute_type_name(attr_types::INDEX_ROOT),
            Some("$INDEX_ROOT")
        );
        assert_eq!(
            attribute_type_name(attr_types::LOGGED_UTILITY_STREAM),
            Some("$LOGGED_UTILITY_STREAM")
        );
        assert_eq!(attribute_type_name(0x1234), None);
    }

    #[test]
    fn attribute_type_codes_are_unique() {
        let codes: Vec<u32> = ATTRIBUTE_TYPES.iter().map(|(c, _)| *c).collect();
        let mut sorted = codes.clone();
        sorted.sort_unstable();
        sorted.dedup();
        assert_eq!(sorted.len(), codes.len(), "duplicate attribute type code");
    }

    #[test]
    fn well_known_record_numbers_resolve() {
        assert_eq!(mft_records::MFT, 0);
        assert_eq!(mft_records::ROOT, 5);
        assert_eq!(mft_records::EXTEND, 11);
        assert_eq!(mft_record_name(mft_records::MFT), Some("$MFT"));
        assert_eq!(
            mft_record_name(mft_records::ROOT),
            Some(". (root directory)")
        );
        assert_eq!(mft_record_name(mft_records::LOGFILE), Some("$LogFile"));
        assert_eq!(mft_record_name(99), None);
    }

    #[test]
    fn record_header_offsets_are_in_layout_order() {
        use mft_offsets as o;
        assert_eq!(o::SIGNATURE, 0x00);
        assert_eq!(o::USA_OFFSET, 0x04);
        assert_eq!(o::USA_COUNT, 0x06);
        assert_eq!(o::FLAGS, 0x16);
        assert_eq!(o::FIRST_ATTRIBUTE, 0x14);
        assert_eq!(o::BASE_RECORD, 0x20);
        const _: () = assert!(o::SIGNATURE < o::USA_OFFSET && o::USA_OFFSET < o::USA_COUNT);
        const _: () = assert!(o::FIRST_ATTRIBUTE < o::FLAGS && o::FLAGS < o::BASE_RECORD);
    }

    #[test]
    fn record_flags_are_distinct_single_bits() {
        let bits = [
            mft_flags::IN_USE,
            mft_flags::DIRECTORY,
            mft_flags::EXTENSION,
            mft_flags::VIEW_INDEX,
        ];
        for b in bits {
            assert_eq!(b.count_ones(), 1, "flag must be a single bit: {b:#06x}");
        }
        // No two flags share a bit.
        let or: u16 = bits.iter().fold(0, |a, b| a | b);
        assert_eq!(or.count_ones() as usize, bits.len());
    }

    #[test]
    fn filename_namespaces_resolve() {
        assert_eq!(filename_namespace::POSIX, 0);
        assert_eq!(filename_namespace::WIN32, 1);
        assert_eq!(filename_namespace::DOS, 2);
        assert_eq!(filename_namespace::WIN32_AND_DOS, 3);
        assert_eq!(
            filename_namespace::name(filename_namespace::DOS),
            Some("DOS")
        );
        assert_eq!(filename_namespace::name(9), None);
    }

    #[test]
    fn indx_signature_is_correct() {
        assert_eq!(&SIGNATURE_INDX, b"INDX");
    }

    #[test]
    fn file_attribute_flags_are_distinct_single_bits() {
        use file_attributes as fa;
        let bits = [
            fa::READONLY,
            fa::HIDDEN,
            fa::SYSTEM,
            fa::ARCHIVE,
            fa::TEMPORARY,
            fa::SPARSE_FILE,
            fa::REPARSE_POINT,
            fa::COMPRESSED,
            fa::ENCRYPTED,
        ];
        for b in bits {
            assert_eq!(b.count_ones(), 1, "flag must be a single bit: {b:#06x}");
        }
        let or: u32 = bits.iter().fold(0, |a, b| a | b);
        assert_eq!(or.count_ones() as usize, bits.len(), "flags overlap");
    }

    #[test]
    fn attribute_offsets_in_layout_order() {
        use attr_offsets as o;
        assert_eq!(o::TYPE, 0x00);
        assert_eq!(o::LENGTH, 0x04);
        assert_eq!(o::NON_RESIDENT, 0x08);
        assert_eq!(o::FLAGS, 0x0C);
        assert_eq!(o::ATTRIBUTE_ID, 0x0E);
        // Resident vs non-resident bodies both begin at 0x10.
        assert_eq!(o::RES_CONTENT_LENGTH, 0x10);
        assert_eq!(o::NR_START_VCN, 0x10);
        assert_eq!(o::NR_REAL_SIZE, 0x30);
        const _: () = assert!(o::TYPE < o::LENGTH && o::LENGTH < o::NON_RESIDENT);
        const _: () = assert!(o::NR_RUNS_OFFSET < o::NR_ALLOCATED_SIZE);
    }

    #[test]
    fn attribute_flags_are_single_bits() {
        for f in [
            attr_flags::COMPRESSED,
            attr_flags::ENCRYPTED,
            attr_flags::SPARSE,
        ] {
            assert_eq!(f.count_ones(), 1, "flag must be a single bit: {f:#06x}");
        }
    }

    #[test]
    fn boot_offsets_match_bpb_layout() {
        use boot_offsets as b;
        assert_eq!(b::OEM_ID, 0x03);
        assert_eq!(b::BYTES_PER_SECTOR, 0x0B);
        assert_eq!(b::SECTORS_PER_CLUSTER, 0x0D);
        assert_eq!(b::MFT_LCN, 0x30);
        assert_eq!(b::MFTMIRR_LCN, 0x38);
        assert_eq!(b::CLUSTERS_PER_RECORD, 0x40);
        assert_eq!(b::VOLUME_SERIAL, 0x48);
    }
}