balrog~ ELF file analysis tool to be used after firmware extraction |
| git clone https://github.com/djnnvx/balrog.git |
| log | files | refs | archive | README |
load.rs (5157B)
1 use crate::arch::Arch; 2 use anyhow::{bail, Result}; 3 use goblin::elf::Elf; 4 use std::collections::{HashMap, HashSet}; 5 use std::path::{Path, PathBuf}; 6 7 const SHN_UNDEF: usize = 0; 8 const MAX_CSTR_LEN: usize = 512; 9 10 fn is_text_byte(byte: u8) -> bool { 11 matches!(byte, 0x09..=0x0d | 0x20..=0x7e) 12 } 13 14 pub struct Seg { 15 pub vaddr: u64, 16 pub off: u64, 17 pub filesz: u64, 18 pub exec: bool, 19 } 20 21 pub struct Binary { 22 pub path: PathBuf, 23 pub data: Vec<u8>, 24 pub arch: Arch, 25 pub little: bool, 26 pub entry: u64, 27 pub segs: Vec<Seg>, 28 pub got2sym: HashMap<u64, String>, 29 pub undef_stub: Vec<(String, u64)>, 30 pub imports: HashSet<String>, 31 pub exports: Vec<(String, u64)>, 32 pub locals: Vec<(String, u64)>, 33 pub gp: Option<u64>, 34 } 35 36 fn machine(elf: &Elf) -> Result<Arch> { 37 Ok(match elf.header.e_machine { 38 goblin::elf::header::EM_ARM => Arch::Arm, 39 goblin::elf::header::EM_AARCH64 => Arch::Aarch64, 40 goblin::elf::header::EM_MIPS => Arch::Mips, 41 goblin::elf::header::EM_386 => Arch::X86, 42 goblin::elf::header::EM_X86_64 => Arch::X86_64, 43 m => bail!("unsupported machine {m}"), 44 }) 45 } 46 47 pub(crate) fn sym_name(tab: &goblin::strtab::Strtab, idx: usize) -> Option<String> { 48 tab.get_at(idx) 49 .filter(|n| !n.is_empty()) 50 .map(str::to_string) 51 } 52 53 fn segments(elf: &Elf) -> Vec<Seg> { 54 elf.program_headers 55 .iter() 56 .filter(|p| p.p_type == goblin::elf::program_header::PT_LOAD) 57 .map(|p| Seg { 58 vaddr: p.p_vaddr, 59 off: p.p_offset, 60 filesz: p.p_filesz, 61 exec: p.is_executable(), 62 }) 63 .collect() 64 } 65 66 fn plt_imports(elf: &Elf) -> HashMap<u64, String> { 67 let mut got2sym = HashMap::new(); 68 for reloc in elf.pltrelocs.iter() { 69 let Some(sym) = elf.dynsyms.get(reloc.r_sym) else { 70 continue; 71 }; 72 if let Some(name) = sym_name(&elf.dynstrtab, sym.st_name) { 73 got2sym.insert(reloc.r_offset, name); 74 } 75 } 76 got2sym 77 } 78 79 #[derive(Default)] 80 struct DynSyms { 81 imports: HashSet<String>, 82 undef_stub: Vec<(String, u64)>, 83 exports: Vec<(String, u64)>, 84 } 85 86 fn dyn_symbols(elf: &Elf) -> DynSyms { 87 let mut syms = DynSyms::default(); 88 for sym in elf.dynsyms.iter() { 89 let Some(name) = sym_name(&elf.dynstrtab, sym.st_name) else { 90 continue; 91 }; 92 if sym.st_shndx == SHN_UNDEF { 93 if sym.st_value != 0 { 94 syms.undef_stub.push((name.clone(), sym.st_value)); 95 } 96 syms.imports.insert(name); 97 } else if sym.st_value != 0 && sym.is_function() { 98 syms.exports.push((name, sym.st_value)); 99 } 100 } 101 syms 102 } 103 104 fn local_funcs(elf: &Elf) -> Vec<(String, u64)> { 105 elf.syms 106 .iter() 107 .filter(|sym| sym.is_function() && sym.st_value != 0) 108 .filter_map(|sym| sym_name(&elf.strtab, sym.st_name).map(|name| (name, sym.st_value))) 109 .collect() 110 } 111 112 pub(crate) fn dyn_val(elf: &Elf, tag: u64) -> Option<u64> { 113 let dynamic = elf.dynamic.as_ref()?; 114 let entry = dynamic.dyns.iter().find(|entry| entry.d_tag == tag)?; 115 Some(entry.d_val) 116 } 117 118 impl Binary { 119 pub fn load(path: &Path) -> Result<Binary> { 120 let data = std::fs::read(path)?; 121 let elf = Elf::parse(&data)?; 122 let arch = machine(&elf)?; 123 let little = elf.little_endian; 124 let entry = elf.header.e_entry; 125 let segs = segments(&elf); 126 let mut got2sym = plt_imports(&elf); 127 let syms = dyn_symbols(&elf); 128 let gp = match arch { 129 Arch::Mips => crate::arch::mips::resolve_got(&elf, &mut got2sym), 130 _ => None, 131 }; 132 let locals = local_funcs(&elf); 133 drop(elf); 134 135 Ok(Binary { 136 path: path.to_path_buf(), 137 data, 138 arch, 139 little, 140 entry, 141 segs, 142 got2sym, 143 undef_stub: syms.undef_stub, 144 imports: syms.imports, 145 exports: syms.exports, 146 locals, 147 gp, 148 }) 149 } 150 151 pub fn read_vaddr(&self, va: u64, len: usize) -> Option<&[u8]> { 152 let seg = self 153 .segs 154 .iter() 155 .find(|seg| (seg.vaddr..seg.vaddr + seg.filesz).contains(&va))?; 156 let start = (seg.off + (va - seg.vaddr)) as usize; 157 if start >= self.data.len() { 158 return None; 159 } 160 let seg_end = (seg.off + seg.filesz) as usize; 161 let end = (start + len).min(seg_end).min(self.data.len()); 162 Some(&self.data[start..end]) 163 } 164 165 pub fn cstr_at(&self, va: u64) -> Option<String> { 166 let window = self.read_vaddr(va, MAX_CSTR_LEN)?; 167 let nul = window.iter().position(|&byte| byte == 0)?; 168 let text = &window[..nul]; 169 if text.is_empty() || !text.iter().all(|&byte| is_text_byte(byte)) { 170 return None; 171 } 172 Some(String::from_utf8_lossy(text).into_owned()) 173 } 174 175 pub fn name(&self) -> String { 176 self.path 177 .file_name() 178 .map(|s| s.to_string_lossy().into_owned()) 179 .unwrap_or_default() 180 } 181 }