mbr_forensic/ebr.rs
1//! Extended Boot Record (EBR) chain traversal and forensic inspection.
2//!
3//! An extended partition (type 0x05 / 0x0F / 0x85) contains an EBR chain.
4//! Each EBR sector is structured identically to an MBR: 512 bytes with a
5//! `0x55AA` boot signature at offset 510. Only the first two partition
6//! entries are used:
7//!
8//! - Entry 0: logical partition LBA, **relative to this EBR sector**.
9//! - Entry 1: next EBR LBA, **relative to the extended partition start**
10//! (`ext_start`). Zero = end of chain.
11//!
12//! Entries 2 and 3 are reserved and should be all zero. Non-zero bytes in
13//! those entries constitute slack data that may conceal forensic artefacts.
14
15use std::io::{Read, Seek, SeekFrom};
16
17use crate::partition::PartitionEntry;
18use crate::Error;
19
20/// A single link in the EBR chain.
21#[derive(Debug, Clone)]
22pub struct EbrEntry {
23 /// Absolute byte offset of this EBR sector in the disk image.
24 pub ebr_offset: u64,
25 /// Absolute LBA of this EBR sector.
26 pub ebr_lba: u64,
27 /// The logical partition described by this EBR.
28 pub logical: PartitionEntry,
29 /// Absolute LBA start of the logical partition.
30 pub logical_lba_start: u64,
31 /// Raw bytes of EBR entries 2 and 3 (bytes 478–509). Non-zero = slack.
32 pub slack: [u8; 32],
33 /// `true` when [`slack`] contains at least one non-zero byte.
34 pub has_slack: bool,
35}
36
37/// Result of walking the full EBR chain.
38#[derive(Debug, Clone)]
39pub struct EbrChain {
40 pub entries: Vec<EbrEntry>,
41 /// `true` if the chain was terminated by a cycle rather than a zero next pointer.
42 pub had_cycle: bool,
43 /// `true` if traversal was capped by the depth limit.
44 pub depth_exceeded: bool,
45}
46
47/// Maximum EBR chain depth before we stop and flag `depth_exceeded`.
48const MAX_DEPTH: usize = 64;
49
50/// Walk the EBR chain starting at the extended partition.
51///
52/// `ext_start_lba` is the LBA of the extended partition container entry.
53/// `sector_size` is the logical sector size (typically 512).
54///
55/// Returns `Ok(EbrChain { entries: vec![], .. })` if the entry type is not
56/// an extended partition.
57pub fn walk_ebr_chain<R: Read + Seek>(
58 reader: &mut R,
59 ext_start_lba: u64,
60 sector_size: u64,
61) -> Result<EbrChain, Error> {
62 let mut entries = Vec::new();
63 let mut had_cycle = false;
64 let mut depth_exceeded = false;
65
66 // Track visited EBR LBAs to detect cycles.
67 let mut visited = std::collections::HashSet::new();
68
69 let mut next_ebr_lba = ext_start_lba;
70
71 loop {
72 if entries.len() >= MAX_DEPTH {
73 depth_exceeded = true;
74 break;
75 }
76 if !visited.insert(next_ebr_lba) {
77 had_cycle = true;
78 break;
79 }
80
81 // EBR must not point before the extended partition start.
82 if next_ebr_lba < ext_start_lba {
83 break;
84 }
85 let Some(ebr_byte_offset) = next_ebr_lba.checked_mul(sector_size) else {
86 break; // byte offset overflow — corrupt image
87 };
88 reader.seek(SeekFrom::Start(ebr_byte_offset))?;
89 let mut sector = [0u8; 512];
90 if reader.read_exact(&mut sector).is_err() {
91 break; // truncated disk image — terminate gracefully
92 }
93
94 // Validate boot signature.
95 if sector[510] != 0x55 || sector[511] != 0xAA {
96 break;
97 }
98
99 let logical_raw: &[u8; 16] = sector[446..462].try_into().unwrap();
100 let next_raw: &[u8; 16] = sector[462..478].try_into().unwrap();
101 let slack_bytes: [u8; 32] = sector[478..510].try_into().unwrap();
102
103 let logical = PartitionEntry::from_bytes(logical_raw);
104 let next_entry = PartitionEntry::from_bytes(next_raw);
105
106 // Logical partition LBA is relative to this EBR sector.
107 // Use saturating_add: malicious logical.lba_start cannot cause overflow panic.
108 let logical_lba_start = next_ebr_lba.saturating_add(logical.lba_start as u64);
109
110 let has_slack = slack_bytes.iter().any(|&b| b != 0);
111
112 entries.push(EbrEntry {
113 ebr_offset: ebr_byte_offset,
114 ebr_lba: next_ebr_lba,
115 logical,
116 logical_lba_start,
117 slack: slack_bytes,
118 has_slack,
119 });
120
121 // Next EBR LBA is relative to the extended partition start.
122 // checked_add: overflow → corrupt/adversarial chain, terminate safely.
123 if next_entry.lba_start == 0 {
124 break;
125 }
126 let Some(next_lba) = ext_start_lba.checked_add(next_entry.lba_start as u64) else {
127 break; // arithmetic overflow in EBR chain — corrupt or adversarial
128 };
129 next_ebr_lba = next_lba;
130 }
131
132 Ok(EbrChain {
133 entries,
134 had_cycle,
135 depth_exceeded,
136 })
137}