Skip to main content

threatflux_binary_analysis/formats/
elf.rs

1//! ELF format parser
2
3use 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
13/// ELF format parser
14pub 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
27/// Parsed ELF binary
28pub 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        // Convert architecture
41        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        // Detect endianness
66        let endian = match elf.header.endianness()? {
67            goblin::container::Endian::Little => Endianness::Little,
68            goblin::container::Endian::Big => Endianness::Big,
69        };
70
71        // Analyze security features
72        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, // Not available in ELF headers
85            compiler_info: extract_compiler_info(&elf, data, &mut output_budget)?,
86            endian,
87            security_features,
88        };
89
90        // Parse sections
91        let sections = parse_sections(&elf, data, &mut output_budget)?;
92
93        // Parse symbols
94        let symbols = parse_symbols(&elf, &mut output_budget)?;
95
96        // Parse imports and exports
97        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                // Check for debug sections by name first
164                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, // ELF sections are generally readable
192            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        // Extract small section data
210        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        // Skip empty names
242        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            // Demangling is intentionally left to the optional symbol-resolution
282            // pass. Returning a fabricated name here would be a false result.
283            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    // Parse dynamic symbols for imports/exports
304    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            // This is an import
313            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, // Library name would need to be resolved from dynamic entries
317                address: None,
318                ordinal: None,
319            });
320        } else if sym.st_bind() == goblin::elf::sym::STB_GLOBAL {
321            // This is an export
322            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    // Check for NX bit (GNU_STACK segment)
339    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    // Check for PIE (Position Independent Executable)
346    features.pie = elf.header.e_type == goblin::elf::header::ET_DYN
347        && (!elf.is_lib || elf.interpreter.is_some());
348
349    // Check for RELRO
350    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; // PIE enables ASLR
368
369    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    // Look for compiler information in .comment section
386    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            // Parse null-terminated strings from the comment section
399            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    // Also look for Go build info
411    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    // Look for Rust-specific sections
422    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}