threatflux_binary_analysis/formats/
mod.rs1use crate::{BinaryError, BinaryFormat as Format, BinaryFormatParser, Result};
4#[cfg(any(feature = "elf", feature = "macho", feature = "pe"))]
5use std::ops::Range;
6
7#[cfg(any(feature = "elf", feature = "macho", feature = "pe"))]
8const INLINE_SECTION_DATA_LIMIT: u64 = 1024;
9
10pub const MAX_PARSED_RECORDS: usize = 100_000;
14
15pub const MAX_OWNED_NAME_BYTES: usize = 32 * 1024 * 1024;
17
18pub const MAX_NAME_BYTES: usize = 4 * 1024;
20
21const JAVA_CLASS_MAGIC: &[u8; 4] = b"\xca\xfe\xba\xbe";
22const JAVA_CLASS_HEADER_SIZE: usize = 10;
23const MIN_JAVA_CLASS_MAJOR_VERSION: u16 = 45;
24const FAT_HEADER_SIZE: usize = 8;
25const FAT_ARCH_32_SIZE: usize = 20;
26const FAT_ARCH_64_SIZE: usize = 32;
27const MAX_FAT_ARCHITECTURES: usize = 32;
28
29fn is_java_class_header(data: &[u8]) -> bool {
30 data.len() >= JAVA_CLASS_HEADER_SIZE
31 && data.starts_with(JAVA_CLASS_MAGIC)
32 && u16::from_be_bytes([data[6], data[7]]) >= MIN_JAVA_CLASS_MAJOR_VERSION
33 && u16::from_be_bytes([data[8], data[9]]) != 0
34}
35
36fn is_bounded_universal_macho(data: &[u8]) -> bool {
37 let Some(header) = data.get(..FAT_HEADER_SIZE) else {
38 return false;
39 };
40 let magic = [header[0], header[1], header[2], header[3]];
41 let count = [header[4], header[5], header[6], header[7]];
42 let (architecture_count, record_size) = match magic {
43 [0xca, 0xfe, 0xba, 0xbe] => (u32::from_be_bytes(count), FAT_ARCH_32_SIZE),
44 [0xbe, 0xba, 0xfe, 0xca] => (u32::from_le_bytes(count), FAT_ARCH_32_SIZE),
45 [0xca, 0xfe, 0xba, 0xbf] => (u32::from_be_bytes(count), FAT_ARCH_64_SIZE),
46 [0xbf, 0xba, 0xfe, 0xca] => (u32::from_le_bytes(count), FAT_ARCH_64_SIZE),
47 _ => return false,
48 };
49 let Ok(architecture_count) = usize::try_from(architecture_count) else {
50 return false;
51 };
52 if !(1..=MAX_FAT_ARCHITECTURES).contains(&architecture_count) {
53 return false;
54 }
55 record_size
56 .checked_mul(architecture_count)
57 .and_then(|size| FAT_HEADER_SIZE.checked_add(size))
58 .is_some_and(|end| end <= data.len())
59}
60
61#[cfg(any(
63 feature = "elf",
64 feature = "java",
65 feature = "macho",
66 feature = "pe",
67 feature = "wasm"
68))]
69#[derive(Default)]
70pub(super) struct ParseOutputBudget {
71 records: usize,
72 name_bytes: usize,
73}
74
75#[cfg(any(
76 feature = "elf",
77 feature = "java",
78 feature = "macho",
79 feature = "pe",
80 feature = "wasm"
81))]
82impl ParseOutputBudget {
83 pub(super) fn claim_records(&mut self, additional: usize, context: &str) -> Result<()> {
84 let next = self.records.checked_add(additional).ok_or_else(|| {
85 BinaryError::invalid_data("Parsed structural record count overflows usize")
86 })?;
87 if next > MAX_PARSED_RECORDS {
88 return Err(BinaryError::invalid_data(format!(
89 "{context} exceeds the parser record limit of {MAX_PARSED_RECORDS}"
90 )));
91 }
92 self.records = next;
93 Ok(())
94 }
95
96 pub(super) fn reserve_record<T>(&mut self, output: &mut Vec<T>, context: &str) -> Result<()> {
97 self.claim_records(1, context)?;
98 output.try_reserve(1).map_err(|error| {
99 BinaryError::invalid_data(format!("Unable to reserve {context} output: {error}"))
100 })?;
101 Ok(())
102 }
103
104 pub(super) fn copy_name(&mut self, value: &str, context: &str) -> Result<String> {
105 self.copy_name_parts(&[value], context)
106 }
107
108 pub(super) fn copy_name_parts(&mut self, parts: &[&str], context: &str) -> Result<String> {
109 let length = parts.iter().try_fold(0_usize, |total, part| {
110 total.checked_add(part.len()).ok_or_else(|| {
111 BinaryError::invalid_data(format!("{context} length overflows usize"))
112 })
113 })?;
114 if length > MAX_NAME_BYTES {
115 return Err(BinaryError::invalid_data(format!(
116 "{context} is {length} bytes; per-name limit is {MAX_NAME_BYTES}"
117 )));
118 }
119
120 let next = self.name_bytes.checked_add(length).ok_or_else(|| {
121 BinaryError::invalid_data("Owned parser-name byte count overflows usize")
122 })?;
123 if next > MAX_OWNED_NAME_BYTES {
124 return Err(BinaryError::invalid_data(format!(
125 "Parsed names exceed the aggregate limit of {MAX_OWNED_NAME_BYTES} bytes"
126 )));
127 }
128
129 let mut owned = String::new();
130 owned.try_reserve_exact(length).map_err(|error| {
131 BinaryError::invalid_data(format!("Unable to reserve {context}: {error}"))
132 })?;
133 for part in parts {
134 owned.push_str(part);
135 }
136 self.name_bytes = next;
137 Ok(owned)
138 }
139}
140
141#[cfg(any(feature = "elf", feature = "macho", feature = "pe"))]
147pub(super) fn inline_section_data(data: &[u8], offset: u64, size: u64) -> Option<Vec<u8>> {
148 if size > INLINE_SECTION_DATA_LIMIT {
149 return None;
150 }
151
152 checked_file_range(data, offset, size).map(|range| data[range].to_vec())
153}
154
155#[cfg(any(feature = "elf", feature = "macho", feature = "pe"))]
156pub(super) fn checked_file_range(data: &[u8], offset: u64, size: u64) -> Option<Range<usize>> {
157 let start = usize::try_from(offset).ok()?;
158 let len = usize::try_from(size).ok()?;
159 let end = start.checked_add(len)?;
160 (end <= data.len()).then_some(start..end)
161}
162
163#[cfg(feature = "elf")]
164pub mod elf;
165#[cfg(feature = "java")]
166pub mod java;
167#[cfg(feature = "macho")]
168pub mod macho;
169#[cfg(feature = "pe")]
170pub mod pe;
171#[cfg(feature = "wasm")]
172pub mod wasm;
173
174pub mod raw;
175
176pub fn detect_format(data: &[u8]) -> Result<Format> {
178 if data.is_empty() {
179 return Err(BinaryError::invalid_data("Empty data"));
180 }
181
182 #[cfg(feature = "elf")]
184 if data.len() >= 4 && &data[0..4] == b"\x7fELF" {
185 return Ok(Format::Elf);
186 }
187
188 #[cfg(feature = "pe")]
191 if pe::PeParser::can_parse(data) {
192 return Ok(Format::Pe);
193 }
194
195 if is_bounded_universal_macho(data) {
198 return Ok(Format::MachO);
199 }
200
201 #[cfg(feature = "macho")]
204 if macho::MachOParser::can_parse(data) {
205 return Ok(Format::MachO);
206 }
207
208 if is_java_class_header(data) {
211 return Ok(Format::Java);
212 }
213
214 #[cfg(feature = "java")]
218 if java::JavaParser::can_parse(data) {
219 return Ok(Format::Java);
220 }
221
222 if data.len() >= 4 && &data[0..4] == b"\x00asm" {
224 return Ok(Format::Wasm);
225 }
226
227 Ok(Format::Raw)
229}
230
231pub fn parse_binary(data: &[u8], format: Format) -> crate::types::ParseResult {
233 match format {
234 #[cfg(feature = "elf")]
235 Format::Elf => elf::ElfParser::parse(data),
236 #[cfg(not(feature = "elf"))]
237 Format::Elf => Err(BinaryError::unsupported_format("ELF".to_string())),
238
239 #[cfg(feature = "pe")]
240 Format::Pe => pe::PeParser::parse(data),
241 #[cfg(not(feature = "pe"))]
242 Format::Pe => Err(BinaryError::unsupported_format("PE".to_string())),
243
244 #[cfg(feature = "macho")]
245 Format::MachO => macho::MachOParser::parse(data),
246 #[cfg(not(feature = "macho"))]
247 Format::MachO => Err(BinaryError::unsupported_format("MachO".to_string())),
248
249 #[cfg(feature = "java")]
250 Format::Java => java::JavaParser::parse(data),
251 #[cfg(not(feature = "java"))]
252 Format::Java => Err(BinaryError::unsupported_format("Java".to_string())),
253 #[cfg(feature = "wasm")]
254 Format::Wasm => wasm::WasmParser::parse(data),
255 #[cfg(not(feature = "wasm"))]
256 Format::Wasm => Err(BinaryError::unsupported_format("Wasm".to_string())),
257 Format::Raw => raw::RawParser::parse(data),
258 Format::Unknown => Err(BinaryError::unsupported_format("Unknown".to_string())),
259 }
260}
261
262#[cfg(test)]
263mod detection_tests {
264 use super::{Format, detect_format};
265
266 #[test]
267 fn java_class_header_is_detected_without_relying_on_the_parser() {
268 let class_header = b"\xca\xfe\xba\xbe\0\0\0\x34\0\x01";
269 assert_eq!(detect_format(class_header).unwrap(), Format::Java);
270 }
271
272 #[test]
273 fn bounded_fat_macho_wins_over_java_magic() {
274 let mut universal = vec![0_u8; 8 + 20];
275 universal[..4].copy_from_slice(b"\xca\xfe\xba\xbe");
276 universal[4..8].copy_from_slice(&1_u32.to_be_bytes());
277 assert_eq!(detect_format(&universal).unwrap(), Format::MachO);
278 }
279}
280
281#[cfg(all(test, any(feature = "elf", feature = "macho", feature = "pe")))]
282mod tests {
283 use super::{
284 MAX_NAME_BYTES, MAX_OWNED_NAME_BYTES, MAX_PARSED_RECORDS, ParseOutputBudget,
285 checked_file_range, inline_section_data,
286 };
287
288 #[test]
289 fn inline_section_data_rejects_overflow_and_out_of_bounds_ranges() {
290 let data = b"section";
291
292 assert_eq!(inline_section_data(data, 0, 7), Some(data.to_vec()));
293 assert!(inline_section_data(data, u64::MAX, 2).is_none());
294 assert!(inline_section_data(data, 6, u64::MAX).is_none());
295 assert!(inline_section_data(data, 6, 2).is_none());
296 assert!(inline_section_data(data, 0, 1025).is_none());
297 assert_eq!(checked_file_range(data, 2, 3), Some(2..5));
298 assert!(checked_file_range(data, u64::MAX, 2).is_none());
299 }
300
301 #[test]
302 fn parser_output_budget_enforces_record_and_name_limits() {
303 let mut records = Vec::<()>::new();
304 let mut budget = ParseOutputBudget {
305 records: MAX_PARSED_RECORDS - 1,
306 name_bytes: 0,
307 };
308 budget.reserve_record(&mut records, "test records").unwrap();
309 assert!(budget.reserve_record(&mut records, "test records").is_err());
310
311 let mut budget = ParseOutputBudget {
312 records: 0,
313 name_bytes: MAX_OWNED_NAME_BYTES - 2,
314 };
315 assert_eq!(budget.copy_name("ok", "test name").unwrap(), "ok");
316 assert!(budget.copy_name("x", "test name").is_err());
317 assert!(
318 budget
319 .copy_name(&"x".repeat(MAX_NAME_BYTES + 1), "test name")
320 .is_err()
321 );
322 }
323}