threatflux_binary_analysis/formats/
elf.rs1use crate::{
4 BinaryError, BinaryFormatParser, BinaryFormatTrait, Result,
5 types::{
6 Architecture, BinaryFormat as Format, BinaryMetadata, Endianness, Export, Import, Section,
7 SectionPermissions, SectionType, SecurityFeatures, Symbol, SymbolBinding, SymbolType,
8 SymbolVisibility,
9 },
10};
11use goblin::elf::Elf;
12
13pub struct ElfParser;
15
16impl BinaryFormatParser for ElfParser {
17 fn parse(data: &[u8]) -> Result<Box<dyn BinaryFormatTrait>> {
18 let elf = Elf::parse(data)?;
19 Ok(Box::new(ElfBinary::new(elf, data)?))
20 }
21
22 fn can_parse(data: &[u8]) -> bool {
23 data.len() >= 4 && &data[0..4] == b"\x7fELF"
24 }
25}
26
27pub struct ElfBinary {
29 metadata: BinaryMetadata,
30 sections: Vec<Section>,
31 symbols: Vec<Symbol>,
32 imports: Vec<Import>,
33 exports: Vec<Export>,
34}
35
36impl ElfBinary {
37 fn new(elf: Elf<'_>, data: &[u8]) -> Result<Self> {
38 let mut output_budget = super::ParseOutputBudget::default();
39
40 let architecture = match elf.header.e_machine {
42 goblin::elf::header::EM_386 => Architecture::X86,
43 goblin::elf::header::EM_X86_64 => Architecture::X86_64,
44 goblin::elf::header::EM_ARM => Architecture::Arm,
45 goblin::elf::header::EM_AARCH64 => Architecture::Arm64,
46 goblin::elf::header::EM_MIPS | goblin::elf::header::EM_MIPS_RS3_LE => {
47 if elf.is_64 {
48 Architecture::Mips64
49 } else {
50 Architecture::Mips
51 }
52 }
53 goblin::elf::header::EM_PPC => Architecture::PowerPC,
54 goblin::elf::header::EM_PPC64 => Architecture::PowerPC64,
55 goblin::elf::header::EM_RISCV => {
56 if elf.is_64 {
57 Architecture::RiscV64
58 } else {
59 Architecture::RiscV
60 }
61 }
62 _ => Architecture::Unknown,
63 };
64
65 let endian = match elf.header.endianness()? {
67 goblin::container::Endian::Little => Endianness::Little,
68 goblin::container::Endian::Big => Endianness::Big,
69 };
70
71 let security_features = analyze_security_features(&elf);
73
74 let metadata = BinaryMetadata {
75 size: data.len(),
76 format: Format::Elf,
77 architecture,
78 entry_point: if elf.entry != 0 {
79 Some(elf.entry)
80 } else {
81 None
82 },
83 base_address: preferred_base_address(&elf),
84 timestamp: None, compiler_info: extract_compiler_info(&elf, data, &mut output_budget)?,
86 endian,
87 security_features,
88 };
89
90 let sections = parse_sections(&elf, data, &mut output_budget)?;
92
93 let symbols = parse_symbols(&elf, &mut output_budget)?;
95
96 let (imports, exports) = parse_imports_exports(&elf, &mut output_budget)?;
98
99 Ok(Self {
100 metadata,
101 sections,
102 symbols,
103 imports,
104 exports,
105 })
106 }
107}
108
109impl BinaryFormatTrait for ElfBinary {
110 fn format_type(&self) -> Format {
111 Format::Elf
112 }
113
114 fn architecture(&self) -> Architecture {
115 self.metadata.architecture
116 }
117
118 fn entry_point(&self) -> Option<u64> {
119 self.metadata.entry_point
120 }
121
122 fn sections(&self) -> &[Section] {
123 &self.sections
124 }
125
126 fn symbols(&self) -> &[Symbol] {
127 &self.symbols
128 }
129
130 fn imports(&self) -> &[Import] {
131 &self.imports
132 }
133
134 fn exports(&self) -> &[Export] {
135 &self.exports
136 }
137
138 fn metadata(&self) -> &BinaryMetadata {
139 &self.metadata
140 }
141}
142
143fn parse_sections(
144 elf: &Elf,
145 data: &[u8],
146 output_budget: &mut super::ParseOutputBudget,
147) -> Result<Vec<Section>> {
148 let mut sections = Vec::new();
149
150 for (i, section_header) in elf.section_headers.iter().enumerate() {
151 output_budget.reserve_record(&mut sections, "ELF sections")?;
152 let fallback_name;
153 let parsed_name = if let Some(name) = elf.shdr_strtab.get_at(section_header.sh_name) {
154 name
155 } else {
156 fallback_name = format!(".section_{i}");
157 &fallback_name
158 };
159 let name = output_budget.copy_name(parsed_name, "ELF section name")?;
160
161 let section_type = match section_header.sh_type {
162 goblin::elf::section_header::SHT_PROGBITS => {
163 if name.starts_with(".debug_") || name.starts_with(".zdebug_") {
165 SectionType::Debug
166 } else if section_header.sh_flags
167 & (goblin::elf::section_header::SHF_EXECINSTR as u64)
168 != 0
169 {
170 SectionType::Code
171 } else if section_header.sh_flags & (goblin::elf::section_header::SHF_WRITE as u64)
172 != 0
173 {
174 SectionType::Data
175 } else {
176 SectionType::ReadOnlyData
177 }
178 }
179 goblin::elf::section_header::SHT_NOBITS => SectionType::Bss,
180 goblin::elf::section_header::SHT_SYMTAB => SectionType::Symbol,
181 goblin::elf::section_header::SHT_STRTAB => SectionType::String,
182 goblin::elf::section_header::SHT_RELA | goblin::elf::section_header::SHT_REL => {
183 SectionType::Relocation
184 }
185 goblin::elf::section_header::SHT_DYNAMIC => SectionType::Dynamic,
186 goblin::elf::section_header::SHT_NOTE => SectionType::Note,
187 _ => SectionType::Other(format!("SHT_{}", section_header.sh_type)),
188 };
189
190 let permissions = SectionPermissions {
191 read: true, write: section_header.sh_flags & (goblin::elf::section_header::SHF_WRITE as u64) != 0,
193 execute: section_header.sh_flags & (goblin::elf::section_header::SHF_EXECINSTR as u64)
194 != 0,
195 };
196
197 let has_file_data = section_header.sh_type != goblin::elf::section_header::SHT_NOBITS
198 && section_header.sh_size != 0;
199 if has_file_data
200 && super::checked_file_range(data, section_header.sh_offset, section_header.sh_size)
201 .is_none()
202 {
203 return Err(crate::BinaryError::invalid_data(format!(
204 "ELF section '{name}' range {}..+{} exceeds the file",
205 section_header.sh_offset, section_header.sh_size
206 )));
207 }
208
209 let section_data = if has_file_data {
211 super::inline_section_data(data, section_header.sh_offset, section_header.sh_size)
212 } else {
213 None
214 };
215
216 sections.push(Section {
217 name,
218 address: section_header.sh_addr,
219 size: section_header.sh_size,
220 offset: section_header.sh_offset,
221 file_size: if has_file_data {
222 section_header.sh_size
223 } else {
224 0
225 },
226 permissions,
227 section_type,
228 data: section_data,
229 });
230 }
231
232 Ok(sections)
233}
234
235fn parse_symbols(elf: &Elf, output_budget: &mut super::ParseOutputBudget) -> Result<Vec<Symbol>> {
236 let mut symbols = Vec::new();
237
238 for sym in &elf.syms {
239 let parsed_name = elf.strtab.get_at(sym.st_name).unwrap_or("unknown");
240
241 if parsed_name.is_empty() {
243 continue;
244 }
245 output_budget.reserve_record(&mut symbols, "ELF symbols")?;
246 let name = output_budget.copy_name(parsed_name, "ELF symbol name")?;
247
248 let symbol_type = match sym.st_type() {
249 goblin::elf::sym::STT_FUNC => SymbolType::Function,
250 goblin::elf::sym::STT_OBJECT => SymbolType::Object,
251 goblin::elf::sym::STT_SECTION => SymbolType::Section,
252 goblin::elf::sym::STT_FILE => SymbolType::File,
253 goblin::elf::sym::STT_COMMON => SymbolType::Common,
254 goblin::elf::sym::STT_TLS => SymbolType::Thread,
255 _ => SymbolType::Other(format!("STT_{}", sym.st_type())),
256 };
257
258 let binding = match sym.st_bind() {
259 goblin::elf::sym::STB_LOCAL => SymbolBinding::Local,
260 goblin::elf::sym::STB_GLOBAL => SymbolBinding::Global,
261 goblin::elf::sym::STB_WEAK => SymbolBinding::Weak,
262 _ => SymbolBinding::Other(format!("STB_{}", sym.st_bind())),
263 };
264
265 let visibility = match sym.st_visibility() {
266 goblin::elf::sym::STV_DEFAULT => SymbolVisibility::Default,
267 goblin::elf::sym::STV_INTERNAL => SymbolVisibility::Internal,
268 goblin::elf::sym::STV_HIDDEN => SymbolVisibility::Hidden,
269 goblin::elf::sym::STV_PROTECTED => SymbolVisibility::Protected,
270 _ => SymbolVisibility::Default,
271 };
272
273 let section_index = if sym.st_shndx == (goblin::elf::section_header::SHN_UNDEF as usize) {
274 None
275 } else {
276 Some(sym.st_shndx)
277 };
278
279 symbols.push(Symbol {
280 name,
281 demangled_name: None,
284 address: sym.st_value,
285 size: sym.st_size,
286 symbol_type,
287 binding,
288 visibility,
289 section_index,
290 });
291 }
292
293 Ok(symbols)
294}
295
296fn parse_imports_exports(
297 elf: &Elf,
298 output_budget: &mut super::ParseOutputBudget,
299) -> crate::types::ImportExportResult {
300 let mut imports = Vec::new();
301 let mut exports = Vec::new();
302
303 for sym in &elf.dynsyms {
305 let parsed_name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("unknown");
306
307 if parsed_name.is_empty() {
308 continue;
309 }
310
311 if sym.st_shndx == (goblin::elf::section_header::SHN_UNDEF as usize) {
312 output_budget.reserve_record(&mut imports, "ELF imports")?;
314 imports.push(Import {
315 name: output_budget.copy_name(parsed_name, "ELF import name")?,
316 library: None, address: None,
318 ordinal: None,
319 });
320 } else if sym.st_bind() == goblin::elf::sym::STB_GLOBAL {
321 output_budget.reserve_record(&mut exports, "ELF exports")?;
323 exports.push(Export {
324 name: output_budget.copy_name(parsed_name, "ELF export name")?,
325 address: sym.st_value,
326 ordinal: None,
327 forwarded_name: None,
328 });
329 }
330 }
331
332 Ok((imports, exports))
333}
334
335fn analyze_security_features(elf: &Elf) -> SecurityFeatures {
336 let mut features = SecurityFeatures::default();
337
338 for phdr in &elf.program_headers {
340 if phdr.p_type == goblin::elf::program_header::PT_GNU_STACK {
341 features.nx_bit = (phdr.p_flags & goblin::elf::program_header::PF_X) == 0;
342 }
343 }
344
345 features.pie = elf.header.e_type == goblin::elf::header::ET_DYN
347 && (!elf.is_lib || elf.interpreter.is_some());
348
349 for phdr in &elf.program_headers {
351 if phdr.p_type == goblin::elf::program_header::PT_GNU_RELRO {
352 features.relro = true;
353 }
354 }
355
356 features.stack_canary = elf.dynsyms.iter().any(|symbol| {
357 elf.dynstrtab
358 .get_at(symbol.st_name)
359 .is_some_and(|name| name == "__stack_chk_fail" || name == "__stack_chk_guard")
360 });
361 features.fortify = elf.dynsyms.iter().any(|symbol| {
362 elf.dynstrtab
363 .get_at(symbol.st_name)
364 .is_some_and(|name| name.ends_with("_chk"))
365 });
366
367 features.aslr = features.pie; features
370}
371
372fn preferred_base_address(elf: &Elf) -> Option<u64> {
373 elf.program_headers
374 .iter()
375 .filter(|header| header.p_type == goblin::elf::program_header::PT_LOAD)
376 .map(|header| header.p_vaddr)
377 .min()
378}
379
380fn extract_compiler_info(
381 elf: &Elf,
382 data: &[u8],
383 output_budget: &mut super::ParseOutputBudget,
384) -> Result<Option<String>> {
385 for section in &elf.section_headers {
387 if let Some(name) = elf.shdr_strtab.get_at(section.sh_name)
388 && name == ".comment"
389 && let Some(section_data) = section_range(data, section.sh_offset, section.sh_size)
390 {
391 if section_data.len() > super::MAX_NAME_BYTES {
392 return Err(BinaryError::invalid_data(format!(
393 "ELF compiler comment is {} bytes; limit is {}",
394 section_data.len(),
395 super::MAX_NAME_BYTES
396 )));
397 }
398 let comment_str = String::from_utf8_lossy(section_data);
400 let comment = comment_str.trim_end_matches('\0').trim();
401
402 if !comment.is_empty() {
403 return output_budget
404 .copy_name(comment, "ELF compiler comment")
405 .map(Some);
406 }
407 }
408 }
409
410 for section in &elf.section_headers {
412 if let Some(name) = elf.shdr_strtab.get_at(section.sh_name)
413 && (name == ".go.buildinfo" || name.contains("go."))
414 {
415 return output_budget
416 .copy_name("Go compiler", "ELF compiler label")
417 .map(Some);
418 }
419 }
420
421 for section in &elf.section_headers {
423 if let Some(name) = elf.shdr_strtab.get_at(section.sh_name)
424 && (name.starts_with(".rustc") || name.contains("rust"))
425 {
426 return output_budget
427 .copy_name("Rust compiler", "ELF compiler label")
428 .map(Some);
429 }
430 }
431
432 Ok(None)
433}
434
435fn section_range(data: &[u8], offset: u64, size: u64) -> Option<&[u8]> {
436 super::checked_file_range(data, offset, size).map(|range| &data[range])
437}