1use forensicnomicon::ntfs::{
18 attr_flags as flag, attr_offsets as o, attr_types, attribute_type_name,
19};
20
21use crate::error::{NtfsError, Result};
22
23const HEADER_MIN: usize = 0x10;
25const RESIDENT_MIN: usize = 0x18;
27const NONRESIDENT_MIN: usize = 0x40;
29const MAX_ATTRIBUTES: usize = 4096;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Attribute {
35 pub type_code: u32,
37 pub length: u32,
39 pub non_resident: bool,
41 pub name: Option<String>,
43 pub flags: u16,
45 pub attribute_id: u16,
47 pub offset: usize,
49 pub body: AttributeBody,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum AttributeBody {
56 Resident {
58 content_offset: u16,
59 content_length: u32,
60 },
61 NonResident {
63 start_vcn: u64,
64 last_vcn: u64,
65 runs_offset: u16,
66 compression_unit: u16,
67 allocated_size: u64,
68 real_size: u64,
69 initialized_size: u64,
70 },
71}
72
73impl Attribute {
74 #[must_use]
76 pub fn is_compressed(&self) -> bool {
77 self.flags & flag::COMPRESSED != 0
78 }
79
80 #[must_use]
82 pub fn is_encrypted(&self) -> bool {
83 self.flags & flag::ENCRYPTED != 0
84 }
85
86 #[must_use]
88 pub fn is_sparse(&self) -> bool {
89 self.flags & flag::SPARSE != 0
90 }
91
92 #[must_use]
94 pub fn type_name(&self) -> Option<&'static str> {
95 attribute_type_name(self.type_code)
96 }
97
98 #[must_use]
101 pub fn resident_content<'a>(&self, record: &'a [u8]) -> Option<&'a [u8]> {
102 if let AttributeBody::Resident {
103 content_offset,
104 content_length,
105 } = self.body
106 {
107 let start = self.offset.checked_add(content_offset as usize)?;
108 let end = start.checked_add(content_length as usize)?;
109 record.get(start..end)
110 } else {
111 None
112 }
113 }
114}
115
116pub fn parse_attributes(record: &[u8], first_attr_offset: usize) -> Result<Vec<Attribute>> {
125 let mut attrs = Vec::new();
126 let mut pos = first_attr_offset;
127
128 let bad = |offset: usize, detail: &'static str| NtfsError::BadAttribute { offset, detail };
129
130 for _ in 0..MAX_ATTRIBUTES {
131 let Some(type_bytes) = record.get(pos + o::TYPE..pos + o::TYPE + 4) else {
133 break;
134 };
135 let type_code = u32::from_le_bytes(type_bytes.try_into().unwrap());
136 if type_code == attr_types::END {
137 break;
138 }
139
140 if pos + HEADER_MIN > record.len() {
141 return Err(bad(pos, "header runs past record"));
142 }
143
144 let length = u32::from_le_bytes(
145 record[pos + o::LENGTH..pos + o::LENGTH + 4]
146 .try_into()
147 .unwrap(),
148 );
149 if (length as usize) < HEADER_MIN {
150 return Err(bad(pos, "length below header minimum"));
151 }
152 let end = pos
153 .checked_add(length as usize)
154 .ok_or_else(|| bad(pos, "length overflow"))?;
155 if end > record.len() {
156 return Err(bad(pos, "attribute extends past record"));
157 }
158
159 let non_resident = record[pos + o::NON_RESIDENT] != 0;
160 let name_length = record[pos + o::NAME_LENGTH] as usize;
161 let name_offset = u16::from_le_bytes(
162 record[pos + o::NAME_OFFSET..pos + o::NAME_OFFSET + 2]
163 .try_into()
164 .unwrap(),
165 ) as usize;
166 let flags = u16::from_le_bytes(
167 record[pos + o::FLAGS..pos + o::FLAGS + 2]
168 .try_into()
169 .unwrap(),
170 );
171 let attribute_id = u16::from_le_bytes(
172 record[pos + o::ATTRIBUTE_ID..pos + o::ATTRIBUTE_ID + 2]
173 .try_into()
174 .unwrap(),
175 );
176
177 let name = if name_length == 0 {
179 None
180 } else {
181 let nbytes = name_length
182 .checked_mul(2)
183 .ok_or_else(|| bad(pos, "name length overflow"))?;
184 let nstart = pos
185 .checked_add(name_offset)
186 .ok_or_else(|| bad(pos, "name offset overflow"))?;
187 let nend = nstart
188 .checked_add(nbytes)
189 .ok_or_else(|| bad(pos, "name overflow"))?;
190 if nend > end || nend > record.len() {
191 return Err(bad(pos, "name out of bounds"));
192 }
193 let units: Vec<u16> = record[nstart..nend]
194 .chunks_exact(2)
195 .map(|c| u16::from_le_bytes([c[0], c[1]]))
196 .collect();
197 Some(
198 char::decode_utf16(units)
199 .map(|r| r.unwrap_or('\u{FFFD}'))
200 .collect(),
201 )
202 };
203
204 let body = if non_resident {
205 if pos + NONRESIDENT_MIN > end {
206 return Err(bad(pos, "non-resident header runs past attribute"));
207 }
208 let u64at = |rel: usize| {
209 u64::from_le_bytes(record[pos + rel..pos + rel + 8].try_into().unwrap())
210 };
211 let u16at = |rel: usize| {
212 u16::from_le_bytes(record[pos + rel..pos + rel + 2].try_into().unwrap())
213 };
214 AttributeBody::NonResident {
215 start_vcn: u64at(o::NR_START_VCN),
216 last_vcn: u64at(o::NR_LAST_VCN),
217 runs_offset: u16at(o::NR_RUNS_OFFSET),
218 compression_unit: u16at(o::NR_COMPRESSION_UNIT),
219 allocated_size: u64at(o::NR_ALLOCATED_SIZE),
220 real_size: u64at(o::NR_REAL_SIZE),
221 initialized_size: u64at(o::NR_INITIALIZED_SIZE),
222 }
223 } else {
224 if pos + RESIDENT_MIN > end {
225 return Err(bad(pos, "resident header runs past attribute"));
226 }
227 let content_length = u32::from_le_bytes(
228 record[pos + o::RES_CONTENT_LENGTH..pos + o::RES_CONTENT_LENGTH + 4]
229 .try_into()
230 .unwrap(),
231 );
232 let content_offset = u16::from_le_bytes(
233 record[pos + o::RES_CONTENT_OFFSET..pos + o::RES_CONTENT_OFFSET + 2]
234 .try_into()
235 .unwrap(),
236 );
237 let cstart = pos
238 .checked_add(content_offset as usize)
239 .ok_or_else(|| bad(pos, "content offset overflow"))?;
240 let cend = cstart
241 .checked_add(content_length as usize)
242 .ok_or_else(|| bad(pos, "content overflow"))?;
243 if cend > end || cend > record.len() {
244 return Err(bad(pos, "resident content out of bounds"));
245 }
246 AttributeBody::Resident {
247 content_offset,
248 content_length,
249 }
250 };
251
252 attrs.push(Attribute {
253 type_code,
254 length,
255 non_resident,
256 name,
257 flags,
258 attribute_id,
259 offset: pos,
260 body,
261 });
262
263 pos = end;
264 }
265
266 Ok(attrs)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn align8(n: usize) -> usize {
274 (n + 7) & !7
275 }
276
277 fn resident(type_code: u32, name: Option<&str>, flags: u16, content: &[u8]) -> Vec<u8> {
279 let name_chars: Vec<u16> = name.map(|n| n.encode_utf16().collect()).unwrap_or_default();
280 let name_offset = RESIDENT_MIN;
281 let content_offset = align8(name_offset + name_chars.len() * 2);
282 let length = align8(content_offset + content.len());
283 let mut a = vec![0u8; length];
284 a[o::TYPE..o::TYPE + 4].copy_from_slice(&type_code.to_le_bytes());
285 a[o::LENGTH..o::LENGTH + 4].copy_from_slice(&(length as u32).to_le_bytes());
286 a[o::NON_RESIDENT] = 0;
287 a[o::NAME_LENGTH] = name_chars.len() as u8;
288 a[o::NAME_OFFSET..o::NAME_OFFSET + 2].copy_from_slice(&(name_offset as u16).to_le_bytes());
289 a[o::FLAGS..o::FLAGS + 2].copy_from_slice(&flags.to_le_bytes());
290 a[o::ATTRIBUTE_ID..o::ATTRIBUTE_ID + 2].copy_from_slice(&1u16.to_le_bytes());
291 a[o::RES_CONTENT_LENGTH..o::RES_CONTENT_LENGTH + 4]
292 .copy_from_slice(&(content.len() as u32).to_le_bytes());
293 a[o::RES_CONTENT_OFFSET..o::RES_CONTENT_OFFSET + 2]
294 .copy_from_slice(&(content_offset as u16).to_le_bytes());
295 for (i, ch) in name_chars.iter().enumerate() {
296 let p = name_offset + i * 2;
297 a[p..p + 2].copy_from_slice(&ch.to_le_bytes());
298 }
299 a[content_offset..content_offset + content.len()].copy_from_slice(content);
300 a
301 }
302
303 #[allow(clippy::too_many_arguments)]
305 fn nonresident(
306 type_code: u32,
307 name: Option<&str>,
308 flags: u16,
309 start_vcn: u64,
310 last_vcn: u64,
311 allocated: u64,
312 real: u64,
313 initialized: u64,
314 runs: &[u8],
315 ) -> Vec<u8> {
316 let name_chars: Vec<u16> = name.map(|n| n.encode_utf16().collect()).unwrap_or_default();
317 let name_offset = NONRESIDENT_MIN;
318 let runs_offset = align8(name_offset + name_chars.len() * 2);
319 let length = align8(runs_offset + runs.len());
320 let mut a = vec![0u8; length];
321 a[o::TYPE..o::TYPE + 4].copy_from_slice(&type_code.to_le_bytes());
322 a[o::LENGTH..o::LENGTH + 4].copy_from_slice(&(length as u32).to_le_bytes());
323 a[o::NON_RESIDENT] = 1;
324 a[o::NAME_LENGTH] = name_chars.len() as u8;
325 a[o::NAME_OFFSET..o::NAME_OFFSET + 2].copy_from_slice(&(name_offset as u16).to_le_bytes());
326 a[o::FLAGS..o::FLAGS + 2].copy_from_slice(&flags.to_le_bytes());
327 a[o::ATTRIBUTE_ID..o::ATTRIBUTE_ID + 2].copy_from_slice(&2u16.to_le_bytes());
328 a[o::NR_START_VCN..o::NR_START_VCN + 8].copy_from_slice(&start_vcn.to_le_bytes());
329 a[o::NR_LAST_VCN..o::NR_LAST_VCN + 8].copy_from_slice(&last_vcn.to_le_bytes());
330 a[o::NR_RUNS_OFFSET..o::NR_RUNS_OFFSET + 2]
331 .copy_from_slice(&(runs_offset as u16).to_le_bytes());
332 a[o::NR_COMPRESSION_UNIT..o::NR_COMPRESSION_UNIT + 2].copy_from_slice(&0u16.to_le_bytes());
333 a[o::NR_ALLOCATED_SIZE..o::NR_ALLOCATED_SIZE + 8].copy_from_slice(&allocated.to_le_bytes());
334 a[o::NR_REAL_SIZE..o::NR_REAL_SIZE + 8].copy_from_slice(&real.to_le_bytes());
335 a[o::NR_INITIALIZED_SIZE..o::NR_INITIALIZED_SIZE + 8]
336 .copy_from_slice(&initialized.to_le_bytes());
337 for (i, ch) in name_chars.iter().enumerate() {
338 let p = name_offset + i * 2;
339 a[p..p + 2].copy_from_slice(&ch.to_le_bytes());
340 }
341 a[runs_offset..runs_offset + runs.len()].copy_from_slice(runs);
342 a
343 }
344
345 fn record_with(first: usize, attrs: &[Vec<u8>]) -> Vec<u8> {
347 let mut rec = vec![0u8; first];
348 for a in attrs {
349 rec.extend_from_slice(a);
350 }
351 rec.extend_from_slice(&attr_types::END.to_le_bytes());
352 rec
353 }
354
355 #[test]
356 fn parses_resident_attribute() {
357 let content = b"\x10\x00\x00\x00hello"; let attr = resident(attr_types::STANDARD_INFORMATION, None, 0, content);
359 let rec = record_with(0x38, &[attr]);
360 let attrs = parse_attributes(&rec, 0x38).expect("walk");
361 assert_eq!(attrs.len(), 1);
362 let a = &attrs[0];
363 assert_eq!(a.type_code, attr_types::STANDARD_INFORMATION);
364 assert!(!a.non_resident);
365 assert_eq!(a.name, None);
366 assert_eq!(a.type_name(), Some("$STANDARD_INFORMATION"));
367 assert_eq!(a.resident_content(&rec), Some(&content[..]));
368 }
369
370 #[test]
371 fn parses_nonresident_attribute() {
372 let runs = [0x21u8, 0x08, 0x00, 0x10, 0x00];
375 let attr = nonresident(
376 attr_types::DATA,
377 Some("ads"),
378 0,
379 0,
380 7,
381 0x8000,
382 0x7A00,
383 0x7A00,
384 &runs,
385 );
386 let rec = record_with(0x38, &[attr]);
387 let attrs = parse_attributes(&rec, 0x38).unwrap();
388 let a = &attrs[0];
389 assert!(a.non_resident);
390 assert_eq!(a.name.as_deref(), Some("ads"));
391 assert_eq!(
392 a.body,
393 AttributeBody::NonResident {
394 start_vcn: 0,
395 last_vcn: 7,
396 runs_offset: 0x48,
397 compression_unit: 0,
398 allocated_size: 0x8000,
399 real_size: 0x7A00,
400 initialized_size: 0x7A00,
401 }
402 );
403 }
404
405 fn header(type_code: u32, length: u32, non_resident: bool) -> Vec<u8> {
408 let mut a = vec![0u8; length.max(HEADER_MIN as u32) as usize];
409 a[o::TYPE..o::TYPE + 4].copy_from_slice(&type_code.to_le_bytes());
410 a[o::LENGTH..o::LENGTH + 4].copy_from_slice(&length.to_le_bytes());
411 a[o::NON_RESIDENT] = u8::from(non_resident);
412 a
413 }
414
415 #[test]
416 fn resident_content_is_none_for_non_resident() {
417 let runs = [0x21u8, 0x08, 0x00, 0x10, 0x00];
418 let attr = nonresident(
419 attr_types::DATA,
420 None,
421 0,
422 0,
423 7,
424 0x8000,
425 0x7A00,
426 0x7A00,
427 &runs,
428 );
429 let rec = record_with(0x38, &[attr]);
430 let attrs = parse_attributes(&rec, 0x38).unwrap();
431 assert_eq!(attrs[0].resident_content(&rec), None);
432 }
433
434 #[test]
435 fn rejects_header_running_past_record() {
436 let rec = attr_types::DATA.to_le_bytes().to_vec();
438 assert!(matches!(
439 parse_attributes(&rec, 0),
440 Err(NtfsError::BadAttribute { detail, .. }) if detail == "header runs past record"
441 ));
442 }
443
444 #[test]
445 fn rejects_nonresident_header_past_attribute() {
446 let attr = header(attr_types::DATA, 0x20, true);
448 let rec = record_with(0, &[attr]);
449 assert!(matches!(
450 parse_attributes(&rec, 0),
451 Err(NtfsError::BadAttribute { detail, .. })
452 if detail == "non-resident header runs past attribute"
453 ));
454 }
455
456 #[test]
457 fn rejects_resident_header_past_attribute() {
458 let attr = header(attr_types::DATA, 0x10, false);
460 let rec = record_with(0, &[attr]);
461 assert!(matches!(
462 parse_attributes(&rec, 0),
463 Err(NtfsError::BadAttribute { detail, .. })
464 if detail == "resident header runs past attribute"
465 ));
466 }
467
468 #[test]
469 fn rejects_resident_content_out_of_bounds() {
470 let mut attr = header(attr_types::DATA, 0x18, false);
472 attr[o::RES_CONTENT_LENGTH..o::RES_CONTENT_LENGTH + 4]
473 .copy_from_slice(&0xFFFFu32.to_le_bytes());
474 attr[o::RES_CONTENT_OFFSET..o::RES_CONTENT_OFFSET + 2]
475 .copy_from_slice(&0x18u16.to_le_bytes());
476 let rec = record_with(0, &[attr]);
477 assert!(matches!(
478 parse_attributes(&rec, 0),
479 Err(NtfsError::BadAttribute { detail, .. })
480 if detail == "resident content out of bounds"
481 ));
482 }
483
484 #[test]
485 fn decodes_named_ads_attribute() {
486 let attr = resident(
487 attr_types::DATA,
488 Some("Zone.Identifier"),
489 0,
490 b"[ZoneTransfer]",
491 );
492 let rec = record_with(0x38, &[attr]);
493 let attrs = parse_attributes(&rec, 0x38).unwrap();
494 assert_eq!(attrs[0].name.as_deref(), Some("Zone.Identifier"));
495 }
496
497 #[test]
498 fn walks_multiple_attributes_until_end() {
499 let si = resident(attr_types::STANDARD_INFORMATION, None, 0, &[0u8; 48]);
500 let fname = resident(attr_types::FILE_NAME, None, 0, &[0u8; 66]);
501 let data = resident(attr_types::DATA, None, 0, b"file contents");
502 let rec = record_with(0x38, &[si, fname, data]);
503 let attrs = parse_attributes(&rec, 0x38).unwrap();
504 assert_eq!(attrs.len(), 3);
505 assert_eq!(attrs[0].type_code, attr_types::STANDARD_INFORMATION);
506 assert_eq!(attrs[1].type_code, attr_types::FILE_NAME);
507 assert_eq!(attrs[2].type_code, attr_types::DATA);
508 }
509
510 #[test]
511 fn detects_compressed_and_sparse_flags() {
512 let attr = nonresident(
513 attr_types::DATA,
514 None,
515 flag::COMPRESSED | flag::SPARSE,
516 0,
517 0,
518 0x1000,
519 0x800,
520 0x800,
521 &[0x00],
522 );
523 let rec = record_with(0x38, &[attr]);
524 let a = &parse_attributes(&rec, 0x38).unwrap()[0];
525 assert!(a.is_compressed());
526 assert!(a.is_sparse());
527 assert!(!a.is_encrypted());
528 }
529
530 #[test]
531 fn end_marker_at_start_yields_no_attributes() {
532 let rec = record_with(0x38, &[]);
533 assert!(parse_attributes(&rec, 0x38).unwrap().is_empty());
534 }
535
536 #[test]
539 fn rejects_zero_length_attribute() {
540 let mut rec = vec![0u8; 0x40];
542 rec[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
543 rec[0x04..0x08].copy_from_slice(&0u32.to_le_bytes()); assert!(matches!(
545 parse_attributes(&rec, 0x00),
546 Err(NtfsError::BadAttribute { .. })
547 ));
548 }
549
550 #[test]
551 fn rejects_length_below_header_min() {
552 let mut rec = vec![0u8; 0x40];
553 rec[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
554 rec[0x04..0x08].copy_from_slice(&8u32.to_le_bytes()); assert!(matches!(
556 parse_attributes(&rec, 0x00),
557 Err(NtfsError::BadAttribute { .. })
558 ));
559 }
560
561 #[test]
562 fn rejects_attribute_past_record_end() {
563 let mut rec = vec![0u8; 0x20];
564 rec[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
565 rec[0x04..0x08].copy_from_slice(&0x1000u32.to_le_bytes()); rec[0x08] = 0;
567 assert!(matches!(
568 parse_attributes(&rec, 0x00),
569 Err(NtfsError::BadAttribute { .. })
570 ));
571 }
572
573 #[test]
574 fn rejects_name_out_of_bounds() {
575 let mut attr = resident(attr_types::DATA, None, 0, b"x");
577 attr[o::NAME_LENGTH] = 200; attr[o::NAME_OFFSET..o::NAME_OFFSET + 2]
579 .copy_from_slice(&(RESIDENT_MIN as u16).to_le_bytes());
580 let rec = record_with(0x00, &[attr]);
581 assert!(matches!(
582 parse_attributes(&rec, 0x00),
583 Err(NtfsError::BadAttribute { .. })
584 ));
585 }
586
587 #[test]
588 fn missing_end_marker_does_not_overrun() {
589 let attr = resident(attr_types::DATA, None, 0, b"data");
592 let mut rec = vec![0u8; 0];
593 rec.extend_from_slice(&attr);
594 let attrs = parse_attributes(&rec, 0).unwrap();
596 assert_eq!(attrs.len(), 1);
597 }
598}