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