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