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::diag;
18use crate::partition::PartitionEntry;
19use crate::Error;
20
21// EBR sector layout (identical to the MBR partition-table region).
22/// Size of one EBR/MBR sector, in bytes.
23const SECTOR_LEN: usize = 512;
24/// Offset of the logical-partition entry (entry 0).
25const LOGICAL_ENTRY: usize = 446;
26/// Offset of the next-EBR pointer entry (entry 1).
27const NEXT_ENTRY: usize = 462;
28/// Offset of the reserved slack region (entries 2–3).
29const SLACK: usize = 478;
30/// Offset of the boot signature (`0x55 0xAA`).
31const BOOT_SIG: usize = 510;
32
33/// A single link in the EBR chain.
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36pub struct EbrEntry {
37 /// Absolute byte offset of this EBR sector in the disk image.
38 pub ebr_offset: u64,
39 /// Absolute LBA of this EBR sector.
40 pub ebr_lba: u64,
41 /// The logical partition described by this EBR.
42 pub logical: PartitionEntry,
43 /// Absolute LBA start of the logical partition.
44 pub logical_lba_start: u64,
45 /// Raw bytes of EBR entries 2 and 3 (bytes 478–509). Non-zero = slack.
46 pub slack: [u8; 32],
47 /// `true` when `slack` contains at least one non-zero byte.
48 pub has_slack: bool,
49}
50
51/// Result of walking the full EBR chain.
52#[derive(Debug, Clone, Default)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize))]
54pub struct EbrChain {
55 pub entries: Vec<EbrEntry>,
56 /// `true` if the chain was terminated by a cycle rather than a zero next pointer.
57 pub had_cycle: bool,
58 /// `true` if traversal was capped by the depth limit.
59 pub depth_exceeded: bool,
60}
61
62impl EbrChain {
63 /// An empty chain — no extended partition, or the walk could not start.
64 #[must_use]
65 pub fn empty() -> Self {
66 Self::default()
67 }
68}
69
70/// Maximum EBR chain depth before we stop and flag `depth_exceeded`.
71const MAX_DEPTH: usize = 64;
72
73/// Walk the EBR chain starting at the extended partition.
74///
75/// `ext_start_lba` is the LBA of the extended partition container entry.
76/// `sector_size` is the logical sector size (typically 512).
77///
78/// Returns `Ok(EbrChain { entries: vec![], .. })` if the entry type is not
79/// an extended partition.
80pub fn walk_ebr_chain<R: Read + Seek>(
81 reader: &mut R,
82 ext_start_lba: u64,
83 sector_size: u64,
84) -> Result<EbrChain, Error> {
85 let mut entries = Vec::new();
86 let mut had_cycle = false;
87 let mut depth_exceeded = false;
88
89 // Track visited EBR LBAs to detect cycles.
90 let mut visited = std::collections::HashSet::new();
91
92 let mut next_ebr_lba = ext_start_lba;
93
94 loop {
95 if entries.len() >= MAX_DEPTH {
96 depth_exceeded = true;
97 break;
98 }
99 if !visited.insert(next_ebr_lba) {
100 had_cycle = true;
101 break;
102 }
103
104 // Guard against byte-offset overflow for adversarial sector sizes
105 // (callers may pass any `sector_size`; LBAs are bounded but the product
106 // is not).
107 let Some(ebr_byte_offset) = next_ebr_lba.checked_mul(sector_size) else {
108 break; // byte offset overflow — corrupt image
109 };
110 reader.seek(SeekFrom::Start(ebr_byte_offset))?;
111 let mut sector = [0u8; SECTOR_LEN];
112 if reader.read_exact(&mut sector).is_err() {
113 diag::ebr_truncated(next_ebr_lba);
114 break; // truncated disk image — terminate gracefully
115 }
116
117 // Validate boot signature.
118 if sector[BOOT_SIG] != 0x55 || sector[BOOT_SIG + 1] != 0xAA {
119 diag::ebr_no_signature(next_ebr_lba);
120 break;
121 }
122
123 let logical_raw: &[u8; 16] = sector[LOGICAL_ENTRY..NEXT_ENTRY].try_into().unwrap();
124 let next_raw: &[u8; 16] = sector[NEXT_ENTRY..SLACK].try_into().unwrap();
125 let slack_bytes: [u8; 32] = sector[SLACK..BOOT_SIG].try_into().unwrap();
126
127 let logical = PartitionEntry::from_bytes(logical_raw);
128 let next_entry = PartitionEntry::from_bytes(next_raw);
129
130 // Logical partition LBA is relative to this EBR sector.
131 // Use saturating_add: malicious logical.lba_start cannot cause overflow panic.
132 let logical_lba_start = next_ebr_lba.saturating_add(logical.lba_start as u64);
133
134 let has_slack = slack_bytes.iter().any(|&b| b != 0);
135
136 entries.push(EbrEntry {
137 ebr_offset: ebr_byte_offset,
138 ebr_lba: next_ebr_lba,
139 logical,
140 logical_lba_start,
141 slack: slack_bytes,
142 has_slack,
143 });
144
145 // Next EBR LBA is relative to the extended partition start.
146 // checked_add: overflow → corrupt/adversarial chain, terminate safely.
147 if next_entry.lba_start == 0 {
148 break;
149 }
150 let Some(next_lba) = ext_start_lba.checked_add(next_entry.lba_start as u64) else {
151 break; // arithmetic overflow in EBR chain — corrupt or adversarial
152 };
153 next_ebr_lba = next_lba;
154 }
155
156 Ok(EbrChain {
157 entries,
158 had_cycle,
159 depth_exceeded,
160 })
161}