mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
ELF files with no section headers show their dynamic symbols and functions (#233)
ELF files whose section headers were removed (common in NAS and router firmware) now show their functions, imports and exports. The dynamic symbol table is found through PT_DYNAMIC (DT_SYMTAB/DT_STRTAB, counted from DT_HASH, then DT_GNU_HASH, then an estimate with a warning), shared with the Flutter reader. The prologue scan walks executable segments when there are no sections. Fixes #223
This commit is contained in:
@@ -1076,17 +1076,26 @@ fn prologue_scan(img: &Image, mode: Mode, covered: &BTreeMap<u64, u64>) -> Vec<u
|
||||
_ => return out,
|
||||
};
|
||||
let is_covered = |a: u64| covered.range(..=a).next_back().is_some_and(|(_, &end)| a < end);
|
||||
for s in img.bin.sections.iter().filter(|s| s.alloc && s.exec && s.file_offset.is_some()) {
|
||||
if s.name.starts_with(".plt") || s.name == "__stubs" || s.name == ".init" || s.name == ".fini" {
|
||||
continue;
|
||||
}
|
||||
let Some(mut a) = s.addr.checked_add(step - 1).map(|a| a & !(step - 1)) else { continue };
|
||||
let end = s.addr.saturating_add(s.file_size);
|
||||
// (start, bytes in the file) of each executable range: sections, or the segments when the file has none.
|
||||
let mut ranges: Vec<(u64, u64)> = img
|
||||
.bin
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| s.alloc && s.exec && s.file_offset.is_some())
|
||||
.filter(|s| !(s.name.starts_with(".plt") || s.name == "__stubs" || s.name == ".init" || s.name == ".fini"))
|
||||
.map(|s| (s.addr, s.file_size))
|
||||
.collect();
|
||||
if img.bin.sections.is_empty() {
|
||||
ranges.extend(img.bin.segments.iter().filter(|s| s.exec && s.file_size > 0).map(|s| (s.addr, s.file_size)));
|
||||
}
|
||||
for (start, size) in ranges {
|
||||
let Some(mut a) = start.checked_add(step - 1).map(|a| a & !(step - 1)) else { continue };
|
||||
let end = start.saturating_add(size);
|
||||
while a.checked_add(4).is_some_and(|e| e <= end) && out.len() < 200_000 {
|
||||
if !is_covered(a)
|
||||
&& let Some(b) = img.read(a, 8)
|
||||
{
|
||||
let prev_ok = a == s.addr || img.read(a - 1, 1).is_some_and(|p| matches!(p.first(), Some(0xc3 | 0xcc | 0x90 | 0x00 | 0xc2)));
|
||||
let prev_ok = a == start || img.read(a - 1, 1).is_some_and(|p| matches!(p.first(), Some(0xc3 | 0xcc | 0x90 | 0x00 | 0xc2)));
|
||||
let hit = match mode {
|
||||
Mode::X64 => {
|
||||
prev_ok
|
||||
@@ -1297,6 +1306,32 @@ mod tests {
|
||||
assert_eq!(asm.first().unwrap().text(), "push rbp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_program_with_no_section_headers_is_found_by_its_dynamic_symbols() {
|
||||
for table in [tk::flutter::SymbolTable::DynamicHash, tk::flutter::SymbolTable::DynamicGnuHash] {
|
||||
let names = ["inflate", "deflate", "adler32"];
|
||||
let (img, p) = analyze(tk::sectionless::sectionless_elf(true, false, &names, table));
|
||||
assert!(img.bin.sections.is_empty());
|
||||
for name in names {
|
||||
let f = p.function_by_name(name).unwrap_or_else(|| panic!("{name} not found"));
|
||||
assert_eq!(f.discovered_by, "symbol");
|
||||
}
|
||||
assert_eq!(p.functions.len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_prologue_scan_walks_executable_segments_when_there_are_no_sections() {
|
||||
let (mut elf, l) = tk::elf64(tk::ElfArch::X86_64, false);
|
||||
// Remove the section header table: offset, count and name-table index.
|
||||
elf[40..48].fill(0);
|
||||
elf[60..64].fill(0);
|
||||
let (img, p) = analyze(elf);
|
||||
assert!(img.bin.sections.is_empty());
|
||||
let hidden = p.functions.get(&(l.code_va + tk::x64::HIDDEN)).expect("the scan finds it without sections");
|
||||
assert_eq!(hidden.discovered_by, "prologue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_cap_marks_discovery_incomplete() {
|
||||
let (elf, _) = tk::elf64(tk::ElfArch::X86_64, true);
|
||||
|
||||
@@ -26,22 +26,13 @@ pub const DART_SYMBOLS: [(&str, &str); 4] = [
|
||||
|
||||
const MAX_PHDRS: usize = 4096;
|
||||
const MAX_SYMBOL_SCAN: u64 = 4_000_000;
|
||||
const MAX_DYNAMIC: usize = 65_536;
|
||||
const MAX_NAME: usize = 4096;
|
||||
const MAX_REGION: usize = 64 << 20;
|
||||
|
||||
const PT_LOAD: u32 = 1;
|
||||
const PT_DYNAMIC: u32 = 2;
|
||||
const PT_NOTE: u32 = 4;
|
||||
const SHT_DYNSYM: u32 = 11;
|
||||
const SHT_NOTE: u32 = 7;
|
||||
const DT_NULL: u64 = 0;
|
||||
const DT_HASH: u64 = 4;
|
||||
const DT_STRTAB: u64 = 5;
|
||||
const DT_SYMTAB: u64 = 6;
|
||||
const DT_STRSZ: u64 = 10;
|
||||
const DT_SYMENT: u64 = 11;
|
||||
const DT_GNU_HASH: u64 = 0x6fff_fef5;
|
||||
|
||||
/// One dynamic symbol of interest.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
@@ -267,57 +258,6 @@ impl<'a> Elf<'a> {
|
||||
self.r.cstr(o, max)
|
||||
}
|
||||
|
||||
fn dynamic(&self) -> Vec<(u64, u64)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(d) = self.phdrs.iter().find(|p| p.ty == PT_DYNAMIC) else { return out };
|
||||
let ent = if self.is64 { 16u64 } else { 8 };
|
||||
let n = d.filesz / ent;
|
||||
for i in 0..n.min(MAX_DYNAMIC as u64) {
|
||||
let Some(o) = d.offset.checked_add(i * ent).and_then(to_usize) else { break };
|
||||
let e = if self.is64 {
|
||||
(self.r.u64_le(o), o.checked_add(8).and_then(|x| self.r.u64_le(x)))
|
||||
} else {
|
||||
(self.r.u32_le(o).map(u64::from), o.checked_add(4).and_then(|x| self.r.u32_le(x)).map(u64::from))
|
||||
};
|
||||
let (Some(tag), Some(val)) = e else { break };
|
||||
if tag == DT_NULL {
|
||||
break;
|
||||
}
|
||||
out.push((tag, val));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn gnu_hash_count(&self, off: u64) -> Option<u64> {
|
||||
let r = self.r;
|
||||
let o = to_usize(off)?;
|
||||
let nbuckets = u64::from(r.u32_le(o)?);
|
||||
let symoffset = u64::from(r.u32_le(o.checked_add(4)?)?);
|
||||
let bloom_size = u64::from(r.u32_le(o.checked_add(8)?)?);
|
||||
if nbuckets > (1 << 24) || bloom_size > (1 << 24) {
|
||||
return None;
|
||||
}
|
||||
let word = if self.is64 { 8 } else { 4 };
|
||||
let buckets = off.checked_add(16)?.checked_add(bloom_size.checked_mul(word)?)?;
|
||||
let mut max = 0u64;
|
||||
for i in 0..nbuckets {
|
||||
let b = u64::from(r.u32_le(to_usize(buckets.checked_add(i * 4)?)?)?);
|
||||
max = max.max(b);
|
||||
}
|
||||
if max < symoffset {
|
||||
return Some(symoffset);
|
||||
}
|
||||
let chains = buckets.checked_add(nbuckets * 4)?;
|
||||
let mut idx = max;
|
||||
loop {
|
||||
let v = r.u32_le(to_usize(chains.checked_add(idx.checked_sub(symoffset)?.checked_mul(4)?)?)?)?;
|
||||
idx = idx.checked_add(1)?;
|
||||
if v & 1 == 1 || idx > MAX_SYMBOL_SCAN {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notes(&self, off: u64, size: u64, out: &mut Option<String>) {
|
||||
let mut pos = 0u64;
|
||||
let r = self.r;
|
||||
@@ -384,36 +324,12 @@ pub fn parse_elf(data: &[u8]) -> Result<DartElf, FormatError> {
|
||||
}
|
||||
}
|
||||
if table.is_none() {
|
||||
let dynv = elf.dynamic();
|
||||
let get = |tag: u64| dynv.iter().find(|(t, _)| *t == tag).map(|(_, v)| *v);
|
||||
let (symtab, strtab) = (get(DT_SYMTAB).and_then(|v| elf.va_to_off(v)), get(DT_STRTAB).and_then(|v| elf.va_to_off(v)));
|
||||
if let (Some(symtab), Some(strtab)) = (symtab, strtab) {
|
||||
let syment = get(DT_SYMENT).unwrap_or(elf.sym_size());
|
||||
if syment != elf.sym_size() {
|
||||
warnings.push(format!("unexpected DT_SYMENT {syment}"));
|
||||
} else {
|
||||
let strsz = get(DT_STRSZ).unwrap_or(0);
|
||||
let mut count = None;
|
||||
if let Some(n) = get(DT_HASH).and_then(|v| elf.va_to_off(v)).and_then(to_usize).and_then(|o| r.u32_le(o.checked_add(4)?)) {
|
||||
count = Some(u64::from(n));
|
||||
count_source = "DT_HASH";
|
||||
}
|
||||
if count.is_none()
|
||||
&& let Some(n) = get(DT_GNU_HASH).and_then(|v| elf.va_to_off(v)).and_then(|o| elf.gnu_hash_count(o))
|
||||
{
|
||||
count = Some(n);
|
||||
count_source = "DT_GNU_HASH";
|
||||
}
|
||||
if count.is_none() && strtab > symtab {
|
||||
count = Some((strtab - symtab) / elf.sym_size());
|
||||
count_source = "estimated";
|
||||
warnings.push("no hash table: the symbol count was estimated from the table layout".into());
|
||||
}
|
||||
if let Some(c) = count {
|
||||
table = Some((symtab, c, strtab, strsz));
|
||||
symbol_source = "dynamic-segment";
|
||||
}
|
||||
}
|
||||
let scan = crate::elf::dynamic_table(data);
|
||||
warnings.extend(scan.warnings);
|
||||
if let Some(t) = scan.table {
|
||||
table = Some((t.symtab, t.count, t.strtab, t.strsz));
|
||||
symbol_source = "dynamic-segment";
|
||||
count_source = t.count_source;
|
||||
}
|
||||
}
|
||||
let mut dart_symbols = Vec::new();
|
||||
|
||||
+334
-31
@@ -1,7 +1,7 @@
|
||||
//! ELF details `object` leaves to us: program headers, the dynamic section, mitigations, and
|
||||
//! core-file notes (registers, signal, mapped files) for crash inspection.
|
||||
|
||||
use crate::binary::{Arch, Binary};
|
||||
use crate::binary::{Arch, Binary, Export, Import, MAX_IMPORTS, MAX_SYMBOLS, Symbol, SymbolKind, demangle};
|
||||
use crate::bytes::Reader;
|
||||
use crate::{FormatError, hex_addr};
|
||||
use object::Object;
|
||||
@@ -10,7 +10,7 @@ use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const MAX_PHDRS: usize = 4096;
|
||||
const MAX_DYN: usize = 8192;
|
||||
const MAX_DYN: usize = 65_536;
|
||||
const MAX_NOTES: usize = 100_000;
|
||||
|
||||
/// Endian/class-aware reads over an ELF image.
|
||||
@@ -104,7 +104,8 @@ pub fn program_headers(data: &[u8]) -> Result<Vec<ProgramHeader>, FormatError> {
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..phnum {
|
||||
let Some(o) = i.checked_mul(phentsize).and_then(|x| x.checked_add(phoff)) else { break };
|
||||
// The fields below sit within `phentsize` bytes of `o`, so their offsets can't overflow either.
|
||||
let Some(o) = i.checked_mul(phentsize).and_then(|x| x.checked_add(phoff)).filter(|&o| o.checked_add(phentsize).is_some()) else { break };
|
||||
let ph = if e.is64 {
|
||||
(e.u32(o), e.u32(o + 4), e.u64(o + 8), e.u64(o + 16), e.u64(o + 32), e.u64(o + 40), e.u64(o + 48))
|
||||
} else {
|
||||
@@ -182,6 +183,24 @@ fn va_to_off(phdrs: &[ProgramHeader], va: u64) -> Option<u64> {
|
||||
phdrs.iter().filter(|p| p.p_type == 1).find(|p| va >= p.vaddr && va - p.vaddr < p.file_size).and_then(|p| p.offset.checked_add(va - p.vaddr))
|
||||
}
|
||||
|
||||
/// The (tag, value) pairs of the dynamic section, up to its `DT_NULL`.
|
||||
fn dynamic_entries(e: &E<'_>, phdrs: &[ProgramHeader]) -> Vec<(u64, u64)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(d) = phdrs.iter().find(|p| p.p_type == 2) else { return out };
|
||||
let ent = 2 * e.word_size();
|
||||
let n = usize::try_from(d.file_size).unwrap_or(0) / ent;
|
||||
let start = usize::try_from(d.offset).unwrap_or(usize::MAX);
|
||||
for i in 0..n.min(MAX_DYN) {
|
||||
let Some(o) = i.checked_mul(ent).and_then(|x| start.checked_add(x)) else { break };
|
||||
let (Some(tag), Some(val)) = (e.word(o), o.checked_add(e.word_size()).and_then(|v| e.word(v))) else { break };
|
||||
if tag == 0 {
|
||||
break;
|
||||
}
|
||||
out.push((tag, val));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Add ELF facts to a parsed binary: image base, interpreter, needed libraries, RELRO, NX, PIE.
|
||||
pub fn enrich(data: &[u8], file: &object::File<'_>, b: &mut Binary) {
|
||||
let Ok(e) = E::new(data) else { return };
|
||||
@@ -196,21 +215,7 @@ pub fn enrich(data: &[u8], file: &object::File<'_>, b: &mut Binary) {
|
||||
if let Some(base) = phdrs.iter().filter(|p| p.p_type == 1).map(|p| p.vaddr).min() {
|
||||
b.image_base = base;
|
||||
}
|
||||
// Dynamic section.
|
||||
let mut dyns: Vec<(u64, u64)> = Vec::new();
|
||||
if let Some(d) = phdrs.iter().find(|p| p.p_type == 2) {
|
||||
let ent = 2 * e.word_size();
|
||||
let n = usize::try_from(d.file_size).unwrap_or(0) / ent.max(1);
|
||||
let start = usize::try_from(d.offset).unwrap_or(usize::MAX);
|
||||
for i in 0..n.min(MAX_DYN) {
|
||||
let Some(o) = start.checked_add(i * ent) else { break };
|
||||
let (Some(tag), Some(val)) = (e.word(o), e.word(o + e.word_size())) else { break };
|
||||
if tag == 0 {
|
||||
break;
|
||||
}
|
||||
dyns.push((tag, val));
|
||||
}
|
||||
}
|
||||
let dyns = dynamic_entries(&e, &phdrs);
|
||||
let strtab = dyns.iter().find(|(t, _)| *t == 5).and_then(|(_, v)| va_to_off(&phdrs, *v));
|
||||
let dstr = |v: u64| -> Option<String> {
|
||||
let off = usize::try_from(strtab?.checked_add(v)?).ok()?;
|
||||
@@ -296,6 +301,43 @@ pub fn enrich(data: &[u8], file: &object::File<'_>, b: &mut Binary) {
|
||||
"dt_flags": format!("{flags:#x}"),
|
||||
"dt_flags_1": format!("{flags1:#x}"),
|
||||
});
|
||||
// With no section headers `object` finds no symbols; the dynamic section still points at them.
|
||||
if b.sections.is_empty()
|
||||
&& let Some(ds) = dynamic_symbols(data, b)
|
||||
{
|
||||
add_dynamic_symbols(b, ds);
|
||||
}
|
||||
}
|
||||
|
||||
/// Put symbols read from `PT_DYNAMIC` into the binary's symbols, imports and exports.
|
||||
fn add_dynamic_symbols(b: &mut Binary, ds: DynamicSymbols) {
|
||||
b.warnings.extend(ds.warnings.into_iter().map(|w| format!("dynamic symbols: {w}")));
|
||||
b.symbols_truncated |= ds.truncated;
|
||||
for (s, defined) in ds.exports.iter().map(|s| (s, true)).chain(ds.imports.iter().map(|s| (s, false))) {
|
||||
if b.symbols.len() >= MAX_SYMBOLS {
|
||||
b.symbols_truncated = true;
|
||||
break;
|
||||
}
|
||||
let kind = match s.kind {
|
||||
"function" | "ifunc" => SymbolKind::Function,
|
||||
"object" | "tls" | "common" => SymbolKind::Data,
|
||||
_ => SymbolKind::Other,
|
||||
};
|
||||
b.symbols.push(Symbol {
|
||||
name: s.name.clone(),
|
||||
demangled: demangle(&s.name),
|
||||
addr: s.addr,
|
||||
size: s.size,
|
||||
kind,
|
||||
global: s.binding != "weak",
|
||||
weak: s.binding == "weak",
|
||||
defined,
|
||||
dynamic: true,
|
||||
});
|
||||
}
|
||||
b.exports.extend(ds.exports.iter().take(MAX_IMPORTS).map(|s| Export { name: s.name.clone(), addr: s.addr }));
|
||||
let room = MAX_IMPORTS.saturating_sub(b.imports.len());
|
||||
b.imports.extend(ds.imports.into_iter().take(room).map(|s| Import { name: s.name, library: s.library, ordinal: None, slot: None }));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
@@ -369,8 +411,11 @@ fn version_defs(e: &E<'_>, base: usize, strs: &[u8]) -> BTreeMap<u16, String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Version needs by index: (name, library).
|
||||
type Needs = BTreeMap<u16, (String, String)>;
|
||||
|
||||
/// Version needs: index → (name, library).
|
||||
fn version_needs(e: &E<'_>, base: usize, strs: &[u8]) -> BTreeMap<u16, (String, String)> {
|
||||
fn version_needs(e: &E<'_>, base: usize, strs: &[u8]) -> Needs {
|
||||
let mut out = BTreeMap::new();
|
||||
let mut pos = 0usize;
|
||||
let mut budget = MAX_VERSIONS;
|
||||
@@ -408,30 +453,204 @@ fn version_needs(e: &E<'_>, base: usize, strs: &[u8]) -> BTreeMap<u16, (String,
|
||||
out
|
||||
}
|
||||
|
||||
/// Read `.dynsym` with its version tables. `None` when the file isn't ELF or has no named
|
||||
/// dynamic symbol section (a file with its section headers removed; use [`Binary`]'s lists then).
|
||||
/// Where the dynamic symbol table sits, as `PT_DYNAMIC` describes it (no section headers needed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DynTable {
|
||||
/// File offset of the symbol table.
|
||||
pub symtab: u64,
|
||||
/// How many entries it has.
|
||||
pub count: u64,
|
||||
/// File offset of the string table.
|
||||
pub strtab: u64,
|
||||
/// `DT_STRSZ`; 0 when the file doesn't say.
|
||||
pub strsz: u64,
|
||||
/// How `count` was found: `DT_HASH`, `DT_GNU_HASH` or `estimated`.
|
||||
pub count_source: &'static str,
|
||||
}
|
||||
|
||||
/// The result of [`dynamic_table`].
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DynTableScan {
|
||||
pub table: Option<DynTable>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
const DT_HASH: u64 = 4;
|
||||
const DT_STRTAB: u64 = 5;
|
||||
const DT_SYMTAB: u64 = 6;
|
||||
const DT_STRSZ: u64 = 10;
|
||||
const DT_SYMENT: u64 = 11;
|
||||
const DT_GNU_HASH: u64 = 0x6fff_fef5;
|
||||
const DT_VERSYM: u64 = 0x6fff_fff0;
|
||||
const DT_VERDEF: u64 = 0x6fff_fffc;
|
||||
const DT_VERNEED: u64 = 0x6fff_fffe;
|
||||
/// Most symbols a `DT_GNU_HASH` chain walk counts.
|
||||
const MAX_HASH_SYMS: u64 = 4_000_000;
|
||||
|
||||
/// How many symbols a `DT_GNU_HASH` table at file offset `off` covers: past the last bucket's
|
||||
/// start, to the end of its chain.
|
||||
fn gnu_hash_count(e: &E<'_>, off: u64) -> Option<u64> {
|
||||
let o = usize::try_from(off).ok()?;
|
||||
let nbuckets = u64::from(e.u32(o)?);
|
||||
let symoffset = u64::from(e.u32(o.checked_add(4)?)?);
|
||||
let bloom_size = u64::from(e.u32(o.checked_add(8)?)?);
|
||||
if nbuckets > (1 << 24) || bloom_size > (1 << 24) {
|
||||
return None;
|
||||
}
|
||||
let buckets = off.checked_add(16)?.checked_add(bloom_size.checked_mul(e.word_size() as u64)?)?;
|
||||
let mut max = 0u64;
|
||||
for i in 0..nbuckets {
|
||||
let b = u64::from(e.u32(usize::try_from(buckets.checked_add(i * 4)?).ok()?)?);
|
||||
max = max.max(b);
|
||||
}
|
||||
if max < symoffset {
|
||||
return Some(symoffset);
|
||||
}
|
||||
let chains = buckets.checked_add(nbuckets * 4)?;
|
||||
let mut idx = max;
|
||||
loop {
|
||||
let v = e.u32(usize::try_from(chains.checked_add(idx.checked_sub(symoffset)?.checked_mul(4)?)?).ok()?)?;
|
||||
idx = idx.checked_add(1)?;
|
||||
if v & 1 == 1 || idx > MAX_HASH_SYMS {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the dynamic symbol table through `PT_DYNAMIC`: `DT_SYMTAB` and `DT_STRTAB`, with the
|
||||
/// entry count from `DT_HASH`, else `DT_GNU_HASH`, else estimated from where the string table
|
||||
/// starts (with a warning). For images whose section headers were removed.
|
||||
pub fn dynamic_table(data: &[u8]) -> DynTableScan {
|
||||
let mut scan = DynTableScan::default();
|
||||
let Ok(e) = E::new(data) else { return scan };
|
||||
let Ok(phdrs) = program_headers(data) else { return scan };
|
||||
let dynv = dynamic_entries(&e, &phdrs);
|
||||
let get = |tag: u64| dynv.iter().find(|(t, _)| *t == tag).map(|(_, v)| *v);
|
||||
let (Some(symtab), Some(strtab)) = (get(DT_SYMTAB).and_then(|v| va_to_off(&phdrs, v)), get(DT_STRTAB).and_then(|v| va_to_off(&phdrs, v))) else {
|
||||
return scan;
|
||||
};
|
||||
let sym_size = e.sym_size();
|
||||
let syment = get(DT_SYMENT).unwrap_or(sym_size);
|
||||
if syment != sym_size {
|
||||
scan.warnings.push(format!("unexpected DT_SYMENT {syment}"));
|
||||
return scan;
|
||||
}
|
||||
let mut found: Option<(u64, &'static str)> = None;
|
||||
if let Some(n) = get(DT_HASH).and_then(|v| va_to_off(&phdrs, v)).and_then(|o| usize::try_from(o).ok()).and_then(|o| e.u32(o.checked_add(4)?)) {
|
||||
found = Some((u64::from(n), "DT_HASH"));
|
||||
}
|
||||
if found.is_none()
|
||||
&& let Some(n) = get(DT_GNU_HASH).and_then(|v| va_to_off(&phdrs, v)).and_then(|o| gnu_hash_count(&e, o))
|
||||
{
|
||||
found = Some((n, "DT_GNU_HASH"));
|
||||
}
|
||||
if found.is_none() && strtab > symtab {
|
||||
found = Some(((strtab - symtab) / sym_size, "estimated"));
|
||||
scan.warnings.push("no hash table: the symbol count was estimated from the table layout".into());
|
||||
}
|
||||
if let Some((count, count_source)) = found {
|
||||
scan.table = Some(DynTable { symtab, count, strtab, strsz: get(DT_STRSZ).unwrap_or(0), count_source });
|
||||
}
|
||||
scan
|
||||
}
|
||||
|
||||
/// One raw symbol table entry.
|
||||
struct RawSym {
|
||||
name: u32,
|
||||
info: u8,
|
||||
other: u8,
|
||||
shndx: u16,
|
||||
value: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
fn raw_sym(e: &E<'_>, at: usize) -> Option<RawSym> {
|
||||
let f = |d: usize| at.checked_add(d);
|
||||
Some(if e.is64 {
|
||||
RawSym { name: e.u32(at)?, info: e.r.u8(f(4)?)?, other: e.r.u8(f(5)?)?, shndx: e.u16(f(6)?)?, value: e.u64(f(8)?)?, size: e.u64(f(16)?)? }
|
||||
} else {
|
||||
RawSym {
|
||||
name: e.u32(at)?,
|
||||
info: e.r.u8(f(12)?)?,
|
||||
other: e.r.u8(f(13)?)?,
|
||||
shndx: e.u16(f(14)?)?,
|
||||
value: u64::from(e.u32(f(4)?)?),
|
||||
size: u64::from(e.u32(f(8)?)?),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl E<'_> {
|
||||
/// Bytes in one symbol table entry.
|
||||
fn sym_size(&self) -> u64 {
|
||||
if self.is64 { 24 } else { 16 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `.dynsym` with its version tables; with no such section, the table `PT_DYNAMIC` points
|
||||
/// at, with its version tables. `None` when the file isn't ELF or has neither.
|
||||
pub fn dynamic_symbols(data: &[u8], b: &Binary) -> Option<DynamicSymbols> {
|
||||
let e = E::new(data).ok()?;
|
||||
let syms = section_bytes(data, b, ".dynsym")?;
|
||||
let strs = section_bytes(data, b, ".dynstr")?;
|
||||
let ent = usize::try_from(e.sym_size()).ok()?;
|
||||
let (Some(syms), Some(strs)) = (section_bytes(data, b, ".dynsym"), section_bytes(data, b, ".dynstr")) else {
|
||||
return segment_dynamic_symbols(data, &e);
|
||||
};
|
||||
let off_of = |name: &str| b.sections.iter().find(|s| s.name == name).and_then(|s| s.file_offset).and_then(|o| usize::try_from(o).ok());
|
||||
let defs = off_of(".gnu.version_d").map(|o| version_defs(&e, o, strs)).unwrap_or_default();
|
||||
let needs = off_of(".gnu.version_r").map(|o| version_needs(&e, o, strs)).unwrap_or_default();
|
||||
let versym = off_of(".gnu.version");
|
||||
let syms_off = off_of(".dynsym")?;
|
||||
let ent = if e.is64 { 24usize } else { 16 };
|
||||
let count = syms.len() / ent;
|
||||
let mut out = DynamicSymbols::default();
|
||||
read_dynsyms(&e, (syms_off, syms.len() / ent), strs, versym, &defs, &needs, &mut out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// [`dynamic_symbols`] for a file without section headers.
|
||||
fn segment_dynamic_symbols(data: &[u8], e: &E<'_>) -> Option<DynamicSymbols> {
|
||||
let scan = dynamic_table(data);
|
||||
let t = scan.table?;
|
||||
let phdrs = program_headers(data).ok()?;
|
||||
let dynv = dynamic_entries(e, &phdrs);
|
||||
let off_of = |tag: u64| dynv.iter().find(|(t, _)| *t == tag).and_then(|(_, v)| va_to_off(&phdrs, *v)).and_then(|o| usize::try_from(o).ok());
|
||||
// A string table with no DT_STRSZ runs to the end of the file (each name is capped).
|
||||
let strs = data.get(usize::try_from(t.strtab).ok()?..)?;
|
||||
let strs = if t.strsz == 0 { strs } else { strs.get(..usize::try_from(t.strsz).unwrap_or(usize::MAX).min(strs.len()))? };
|
||||
let defs = off_of(DT_VERDEF).map(|o| version_defs(e, o, strs)).unwrap_or_default();
|
||||
let needs = off_of(DT_VERNEED).map(|o| version_needs(e, o, strs)).unwrap_or_default();
|
||||
let ent = usize::try_from(e.sym_size()).ok()?;
|
||||
let symtab = usize::try_from(t.symtab).ok()?;
|
||||
// The count comes from the file's own tables: only the entries that lie inside the file are read.
|
||||
let room = data.len().saturating_sub(symtab) / ent;
|
||||
let count = usize::try_from(t.count).unwrap_or(usize::MAX);
|
||||
let mut out = DynamicSymbols { warnings: scan.warnings, ..DynamicSymbols::default() };
|
||||
if count > room {
|
||||
out.truncated = true;
|
||||
out.warnings.push(format!("the symbol table ends early: {room} of {count} entries are in the file"));
|
||||
}
|
||||
read_dynsyms(e, (symtab, count.min(room)), strs, off_of(DT_VERSYM), &defs, &needs, &mut out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Read `count` symbols of the table at `syms_off` into `out`, naming each from `strs` and its
|
||||
/// version from the version tables.
|
||||
fn read_dynsyms(
|
||||
e: &E<'_>,
|
||||
(syms_off, count): (usize, usize),
|
||||
strs: &[u8],
|
||||
versym: Option<usize>,
|
||||
defs: &BTreeMap<u16, String>,
|
||||
needs: &Needs,
|
||||
out: &mut DynamicSymbols,
|
||||
) {
|
||||
let ent = e.sym_size() as usize;
|
||||
if count > MAX_DYNSYMS {
|
||||
out.truncated = true;
|
||||
out.warnings.push(format!("only the first {MAX_DYNSYMS} of {count} dynamic symbols were read"));
|
||||
}
|
||||
for i in 0..count.min(MAX_DYNSYMS) {
|
||||
let at = syms_off.checked_add(i.checked_mul(ent)?)?;
|
||||
let (name, info, other, shndx, value, size) = if e.is64 {
|
||||
(e.u32(at)?, e.r.u8(at + 4)?, e.r.u8(at + 5)?, e.u16(at + 6)?, e.u64(at + 8)?, e.u64(at + 16)?)
|
||||
} else {
|
||||
(e.u32(at)?, e.r.u8(at + 12)?, e.r.u8(at + 13)?, e.u16(at + 14)?, u64::from(e.u32(at + 4)?), u64::from(e.u32(at + 8)?))
|
||||
let Some(RawSym { name, info, other, shndx, value, size }) = i.checked_mul(ent).and_then(|d| syms_off.checked_add(d)).and_then(|at| raw_sym(e, at))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let binding = match info >> 4 {
|
||||
1 => "global",
|
||||
@@ -471,7 +690,6 @@ pub fn dynamic_symbols(data: &[u8], b: &Binary) -> Option<DynamicSymbols> {
|
||||
out.imports.push(row);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Name of an ELF dynamic relocation type.
|
||||
@@ -893,3 +1111,88 @@ mod tests {
|
||||
assert!(b.libraries.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sectionless_tests {
|
||||
use super::*;
|
||||
use crate::binary::SymbolKind;
|
||||
use craft_anything_testkit::flutter::SymbolTable;
|
||||
use craft_anything_testkit::sectionless::sectionless_elf;
|
||||
|
||||
const NAMES: [&str; 3] = ["inflate", "deflate", "adler32"];
|
||||
const TABLES: [(SymbolTable, &str); 2] = [(SymbolTable::DynamicHash, "DT_HASH"), (SymbolTable::DynamicGnuHash, "DT_GNU_HASH")];
|
||||
|
||||
#[test]
|
||||
fn symbols_come_from_the_dynamic_segment_in_every_class_and_byte_order() {
|
||||
for (is64, big) in [(true, false), (false, false), (false, true), (true, true)] {
|
||||
for (table, source) in TABLES {
|
||||
let d = sectionless_elf(is64, big, &NAMES, table);
|
||||
let b = crate::parse_binary(&d).unwrap();
|
||||
assert!(b.sections.is_empty(), "{is64} {big}");
|
||||
let scan = dynamic_table(&d);
|
||||
let t = scan.table.unwrap();
|
||||
assert_eq!((t.count, t.count_source), (5, source), "{is64} {big}");
|
||||
assert!(scan.warnings.is_empty());
|
||||
let funcs: Vec<_> = b.symbols.iter().filter(|s| s.defined && s.kind == SymbolKind::Function && s.dynamic).map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(funcs, NAMES, "{is64} {big} {source}");
|
||||
let exports: Vec<_> = b.exports.iter().map(|e| e.name.as_str()).collect();
|
||||
assert_eq!(exports, NAMES);
|
||||
assert!(b.exports.iter().all(|e| e.addr != 0));
|
||||
assert_eq!(b.imports.iter().map(|i| i.name.as_str()).collect::<Vec<_>>(), ["puts"]);
|
||||
let ds = dynamic_symbols(&d, &b).unwrap();
|
||||
assert_eq!((ds.exports.len(), ds.imports.len()), (3, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_section_stripped_flutter_libraries_name_their_symbols() {
|
||||
for (table, source) in TABLES {
|
||||
let d = craft_anything_testkit::flutter::libapp_so(table);
|
||||
let b = crate::parse_binary(&d).unwrap();
|
||||
assert!(b.sections.is_empty());
|
||||
assert_eq!(dynamic_table(&d).table.unwrap().count_source, source);
|
||||
let mut names: Vec<_> = b.symbols.iter().filter(|s| s.dynamic && s.defined).map(|s| s.name.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, ["_kDartIsolateSnapshotData", "_kDartIsolateSnapshotInstructions", "_kDartVmSnapshotData", "_kDartVmSnapshotInstructions"]);
|
||||
assert_eq!(b.exports.len(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_sectionless_file_never_panics() {
|
||||
for (is64, big) in [(true, false), (false, true)] {
|
||||
for (table, _) in TABLES {
|
||||
let d = sectionless_elf(is64, big, &NAMES, table);
|
||||
for sample in craft_anything_testkit::mutations(&d) {
|
||||
let _ = dynamic_table(&sample);
|
||||
if let Ok(b) = crate::parse_binary(&sample) {
|
||||
let _ = dynamic_symbols(&sample, &b);
|
||||
}
|
||||
}
|
||||
for n in (0..d.len()).step_by(7) {
|
||||
let _ = dynamic_table(&d[..n]);
|
||||
let _ = crate::parse_binary(&d[..n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_count_past_the_end_of_the_file_reads_only_what_is_there() {
|
||||
let mut d = sectionless_elf(true, false, &NAMES, SymbolTable::DynamicHash);
|
||||
// Patch the DT_HASH table's nchain to the largest count.
|
||||
let hash = {
|
||||
let phdrs = program_headers(&d).unwrap();
|
||||
let e = E::new(&d).unwrap();
|
||||
let dynv = dynamic_entries(&e, &phdrs);
|
||||
va_to_off(&phdrs, dynv.iter().find(|(tag, _)| *tag == DT_HASH).unwrap().1).unwrap() as usize
|
||||
};
|
||||
d[hash + 4..hash + 8].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
assert_eq!(dynamic_table(&d).table.unwrap().count, u64::from(u32::MAX));
|
||||
let b = crate::parse_binary(&d).unwrap();
|
||||
let ds = dynamic_symbols(&d, &b).unwrap();
|
||||
assert!(ds.truncated && !ds.warnings.is_empty());
|
||||
assert_eq!(ds.exports.len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub mod packed;
|
||||
pub mod pcap;
|
||||
pub mod program_facts;
|
||||
pub mod sbpf;
|
||||
pub mod sectionless;
|
||||
pub mod sqlite;
|
||||
pub mod wasm_plugin;
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Shared objects with no section headers, as NAS and router firmware ships them: one `PT_LOAD`
|
||||
//! (addresses equal file offsets), a `PT_DYNAMIC` with `DT_SYMTAB`, `DT_STRTAB`, `DT_STRSZ`,
|
||||
//! `DT_SYMENT` and one hash table, an imported `puts` and one global function per name given.
|
||||
//! Any class and byte order, so a big-endian MIPS ELF32 is covered too.
|
||||
|
||||
use crate::flutter::SymbolTable;
|
||||
|
||||
/// How many bytes each function body takes (its first byte is an x86 `ret`).
|
||||
pub const BODY_SIZE: u64 = 16;
|
||||
|
||||
struct W {
|
||||
big: bool,
|
||||
is64: bool,
|
||||
}
|
||||
|
||||
impl W {
|
||||
fn put(&self, v: &mut [u8], at: usize, size: usize, x: u64) {
|
||||
let b = if self.big { x.to_be_bytes() } else { x.to_le_bytes() };
|
||||
let b = if self.big { &b[8 - size..] } else { &b[..size] };
|
||||
v[at..at + size].copy_from_slice(b);
|
||||
}
|
||||
fn push(&self, v: &mut Vec<u8>, size: usize, x: u64) {
|
||||
let at = v.len();
|
||||
v.resize(at + size, 0);
|
||||
self.put(v, at, size, x);
|
||||
}
|
||||
fn word(&self) -> usize {
|
||||
if self.is64 { 8 } else { 4 }
|
||||
}
|
||||
}
|
||||
|
||||
fn align(v: &mut Vec<u8>, a: usize) {
|
||||
while !v.len().is_multiple_of(a) {
|
||||
v.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the file. Function `i` is a [`BODY_SIZE`]-byte body, in order, from a 16-byte aligned
|
||||
/// start. `table` picks `DT_HASH` or `DT_GNU_HASH`; there are never section headers.
|
||||
pub fn sectionless_elf(is64: bool, big: bool, functions: &[&str], table: SymbolTable) -> Vec<u8> {
|
||||
let w = W { big, is64 };
|
||||
let sym_size = if is64 { 24 } else { 16 };
|
||||
let mut f = vec![0u8; 0x100];
|
||||
let dynstr_off = f.len();
|
||||
f.push(0);
|
||||
let mut names = Vec::new();
|
||||
for n in std::iter::once("puts").chain(functions.iter().copied()) {
|
||||
names.push(f.len() - dynstr_off);
|
||||
f.extend_from_slice(n.as_bytes());
|
||||
f.push(0);
|
||||
}
|
||||
let dynstr_size = f.len() - dynstr_off;
|
||||
align(&mut f, 16);
|
||||
let mut bodies = Vec::new();
|
||||
for _ in functions {
|
||||
bodies.push(f.len() as u64);
|
||||
f.push(0xc3);
|
||||
f.extend(std::iter::repeat_n(0x90u8, BODY_SIZE as usize - 1));
|
||||
}
|
||||
align(&mut f, 8);
|
||||
let dynsym_off = f.len();
|
||||
let nsyms = functions.len() + 2;
|
||||
// (name, info, shndx, value, size): the null symbol, the import, then the functions.
|
||||
let mut rows: Vec<(u64, u8, u16, u64, u64)> = vec![(0, 0, 0, 0, 0), (names[0] as u64, 0x12, 0, 0, 0)];
|
||||
for (i, body) in bodies.iter().enumerate() {
|
||||
rows.push((names[i + 1] as u64, 0x12, 1, *body, BODY_SIZE));
|
||||
}
|
||||
for (name, info, shndx, value, size) in rows {
|
||||
if is64 {
|
||||
w.push(&mut f, 4, name);
|
||||
f.extend_from_slice(&[info, 0]);
|
||||
w.push(&mut f, 2, u64::from(shndx));
|
||||
w.push(&mut f, 8, value);
|
||||
w.push(&mut f, 8, size);
|
||||
} else {
|
||||
w.push(&mut f, 4, name);
|
||||
w.push(&mut f, 4, value);
|
||||
w.push(&mut f, 4, size);
|
||||
f.extend_from_slice(&[info, 0]);
|
||||
w.push(&mut f, 2, u64::from(shndx));
|
||||
}
|
||||
}
|
||||
// DT_HASH: one bucket, nchain = the symbol count.
|
||||
align(&mut f, 8);
|
||||
let hash_off = f.len();
|
||||
for v in [1u64, nsyms as u64, 0] {
|
||||
w.push(&mut f, 4, v);
|
||||
}
|
||||
f.extend(std::iter::repeat_n(0u8, 4 * nsyms));
|
||||
// DT_GNU_HASH: one bucket starting at the first function (the import sits before it).
|
||||
align(&mut f, 8);
|
||||
let gnu_off = f.len();
|
||||
for v in [1u64, 2, 1, 6] {
|
||||
w.push(&mut f, 4, v);
|
||||
}
|
||||
w.push(&mut f, w.word(), u64::MAX);
|
||||
w.push(&mut f, 4, 2);
|
||||
for i in 0..functions.len() {
|
||||
let last = i + 1 == functions.len();
|
||||
w.push(&mut f, 4, 0x1234_5670 | u64::from(last));
|
||||
}
|
||||
// .dynamic
|
||||
align(&mut f, 8);
|
||||
let dyn_off = f.len();
|
||||
let mut dynv: Vec<(u64, u64)> = vec![(5, dynstr_off as u64), (6, dynsym_off as u64), (10, dynstr_size as u64), (11, sym_size)];
|
||||
match table {
|
||||
SymbolTable::Sections | SymbolTable::DynamicHash => dynv.push((4, hash_off as u64)),
|
||||
SymbolTable::DynamicGnuHash => dynv.push((0x6fff_fef5, gnu_off as u64)),
|
||||
}
|
||||
dynv.push((0, 0));
|
||||
for (t, v) in dynv {
|
||||
w.push(&mut f, w.word(), t);
|
||||
w.push(&mut f, w.word(), v);
|
||||
}
|
||||
let dyn_size = (f.len() - dyn_off) as u64;
|
||||
let total = f.len() as u64;
|
||||
// ELF header and two program headers: LOAD (read, execute) and DYNAMIC.
|
||||
f[0..4].copy_from_slice(b"\x7fELF");
|
||||
f[4] = if is64 { 2 } else { 1 };
|
||||
f[5] = if big { 2 } else { 1 };
|
||||
f[6] = 1;
|
||||
let machine = match (is64, big) {
|
||||
(true, false) => 62,
|
||||
(false, false) => 3,
|
||||
(false, true) => 8,
|
||||
(true, true) => 21,
|
||||
};
|
||||
w.put(&mut f, 16, 2, 3);
|
||||
w.put(&mut f, 18, 2, machine);
|
||||
w.put(&mut f, 20, 4, 1);
|
||||
let (phoff, phent, ehsize) = if is64 { (64, 56, 64) } else { (52, 32, 52) };
|
||||
if is64 {
|
||||
w.put(&mut f, 32, 8, phoff);
|
||||
w.put(&mut f, 52, 2, ehsize);
|
||||
w.put(&mut f, 54, 2, phent);
|
||||
w.put(&mut f, 56, 2, 2);
|
||||
w.put(&mut f, 58, 2, 64);
|
||||
} else {
|
||||
w.put(&mut f, 28, 4, phoff);
|
||||
w.put(&mut f, 40, 2, ehsize);
|
||||
w.put(&mut f, 42, 2, phent);
|
||||
w.put(&mut f, 44, 2, 2);
|
||||
w.put(&mut f, 46, 2, 40);
|
||||
}
|
||||
for (i, (ty, flags, off, size)) in [(1u64, 5u64, 0u64, total), (2, 6, dyn_off as u64, dyn_size)].into_iter().enumerate() {
|
||||
let p = phoff as usize + i * phent as usize;
|
||||
w.put(&mut f, p, 4, ty);
|
||||
if is64 {
|
||||
w.put(&mut f, p + 4, 4, flags);
|
||||
for (d, x) in [(8, off), (16, off), (24, off), (32, size), (40, size), (48, 8)] {
|
||||
w.put(&mut f, p + d, 8, x);
|
||||
}
|
||||
} else {
|
||||
for (d, x) in [(4, off), (8, off), (12, off), (16, size), (20, size), (24, flags), (28, 4)] {
|
||||
w.put(&mut f, p + d, 4, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
@@ -171,6 +171,11 @@ Owner: open · Status: done (branch chore/r0.5-cleanups)
|
||||
- [x] `macho.rs` `method_list`: `list + 8 + i * entsize` adds to a pointer read from the file, so a
|
||||
segment that ends at `u64::MAX` overflowed it (found by a lint scan; now reproduced by a
|
||||
regression test, and the entry addresses are checked)
|
||||
- [x] ELF files whose section headers were removed (common in NAS and router firmware, also big-endian
|
||||
MIPS) show their dynamic symbols: `elf::dynamic_table` reads `DT_SYMTAB`/`DT_STRTAB` and the count
|
||||
from `DT_HASH` or `DT_GNU_HASH` (moved out of `dart.rs`), `Binary` symbols, imports and exports are
|
||||
filled from it, `elf::dynamic_symbols` reads versions through `DT_VERSYM`/`DT_VERDEF`/`DT_VERNEED`,
|
||||
and the prologue scan walks executable segments (#223)
|
||||
|
||||
### R0.8 Tests never touch the real machine
|
||||
Owner: open · Status: done, except protecting `main` (a maintainer setting)
|
||||
|
||||
Reference in New Issue
Block a user