736 lines
30 KiB
Rust
736 lines
30 KiB
Rust
//! Minimal hand-rolled ELF64 reader for CS2 Linux `.so` files.
|
|
//!
|
|
//! Two jobs: (1) expose executable code for signature scanning, and (2) expose the metadata
|
|
//! the vtable/RTTI resolver needs — sections by name, dynamic symbols, and a relocation map
|
|
//! (vtable slots in `.data.rel.ro` are 0 on disk and supplied by `.rela.dyn` at load, so we
|
|
//! reconstruct their values here). No external ELF crate; the ELF64 layout is fixed.
|
|
|
|
use crate::sig::Pattern;
|
|
use anyhow::{Context, Result, ensure};
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
#[derive(Default)]
|
|
struct Sec {
|
|
typ: u32,
|
|
flags: u64,
|
|
addr: u64,
|
|
off: usize,
|
|
size: usize,
|
|
link: usize,
|
|
entsize: usize,
|
|
}
|
|
|
|
pub struct CodeImage {
|
|
data: Vec<u8>,
|
|
exec: Vec<(usize, u64, usize)>, // (file_off, vaddr, size) of executable sections
|
|
secs: Vec<Sec>,
|
|
sym_addr: HashMap<String, u64>, // symbol name -> vaddr
|
|
reloc: HashMap<u64, u64>, // vaddr slot -> resolved pointer value
|
|
reloc_by_val: HashMap<u64, Vec<u64>>, // pointer value -> slot vaddrs holding it
|
|
kind_at: HashMap<u64, KindTag>, // typeinfo vaddr -> its Itanium kind (by reloc symbol name)
|
|
}
|
|
|
|
// These read attacker-controlled offsets, so they are bounds- AND overflow-safe: an out-of-range read
|
|
// returns 0 (a truncated field is treated as zero, which downstream validity checks reject) rather
|
|
// than panicking. This alone removes the largest class of malformed-input panics.
|
|
fn u16le(b: &[u8], o: usize) -> u16 {
|
|
o.checked_add(2)
|
|
.and_then(|e| b.get(o..e))
|
|
.and_then(|s| s.try_into().ok())
|
|
.map_or(0, u16::from_le_bytes)
|
|
}
|
|
fn u32le(b: &[u8], o: usize) -> u32 {
|
|
o.checked_add(4)
|
|
.and_then(|e| b.get(o..e))
|
|
.and_then(|s| s.try_into().ok())
|
|
.map_or(0, u32::from_le_bytes)
|
|
}
|
|
fn u64le(b: &[u8], o: usize) -> u64 {
|
|
o.checked_add(8)
|
|
.and_then(|e| b.get(o..e))
|
|
.and_then(|s| s.try_into().ok())
|
|
.map_or(0, u64::from_le_bytes)
|
|
}
|
|
fn cstr(b: &[u8], o: usize) -> String {
|
|
let Some(sub) = b.get(o..) else {
|
|
return String::new();
|
|
};
|
|
let end = sub.iter().position(|&c| c == 0).unwrap_or(sub.len());
|
|
String::from_utf8_lossy(&sub[..end]).into_owned()
|
|
}
|
|
|
|
/// Byte width of a DWARF exception-handling pointer encoding (its low nibble is the value format).
|
|
/// Returns 0 for LEB128 / unsupported formats, which callers treat as "give up, use the fallback".
|
|
fn dw_ptr_size(enc: u8) -> usize {
|
|
match enc & 0x0f {
|
|
0x02 | 0x0a => 2, // udata2 / sdata2
|
|
0x03 | 0x0b => 4, // udata4 / sdata4
|
|
0x04 | 0x0c => 8, // udata8 / sdata8
|
|
0x00 => 8, // absptr (LP64)
|
|
_ => 0, // uleb128 / sleb128 / unknown
|
|
}
|
|
}
|
|
|
|
const SHF_WRITE: u64 = 0x1;
|
|
const SHF_EXECINSTR: u64 = 0x4;
|
|
const SHF_ALLOC: u64 = 0x2;
|
|
const SHT_NOBITS: u32 = 8;
|
|
const SHT_DYNSYM: u32 = 11;
|
|
const SHT_SYMTAB: u32 = 2;
|
|
const SHT_RELA: u32 = 4;
|
|
const R_X86_64_64: u32 = 1;
|
|
const R_X86_64_RELATIVE: u32 = 8;
|
|
const R_X86_64_GLOB_DAT: u32 = 6;
|
|
|
|
/// The Itanium `type_info` "kind" a typeinfo's `+0` field references. Recovered by the referenced SYMBOL
|
|
/// NAME rather than its pointer value, because when the C++ runtime is DYNAMICALLY linked (old Source-2
|
|
/// builds `DT_NEEDED libstdc++`) the three kind vtables are UND imports with value 0 — so their `+0` reloc
|
|
/// resolves offline to the same `0 + addend` for all three and can't be told apart (or found) by value.
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum KindTag {
|
|
Class, // __class_type_info — no bases
|
|
Si, // __si_class_type_info — one public base at offset 0
|
|
Vmi, // __vmi_class_type_info — multiple / virtual / non-public bases
|
|
}
|
|
|
|
/// Map a `_ZTVN10__cxxabiv1…` kind-vtable symbol name to its [`KindTag`]. The dynstr stores the undecorated
|
|
/// mangled name (symbol versioning lives in a separate table), so an exact match is correct.
|
|
fn kind_tag_of(sym: &str) -> Option<KindTag> {
|
|
match sym {
|
|
"_ZTVN10__cxxabiv117__class_type_infoE" => Some(KindTag::Class),
|
|
"_ZTVN10__cxxabiv120__si_class_type_infoE" => Some(KindTag::Si),
|
|
"_ZTVN10__cxxabiv121__vmi_class_type_infoE" => Some(KindTag::Vmi),
|
|
_ => None,
|
|
}
|
|
}
|
|
// Program-header type marking the `.eh_frame_hdr` FDE lookup table. The loader locates it this way,
|
|
// so we do too — no dependence on section names, which stripping can remove.
|
|
const PT_GNU_EH_FRAME: u32 = 0x6474_e550;
|
|
|
|
impl CodeImage {
|
|
pub fn load(path: &Path) -> Result<Self> {
|
|
let data = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
|
|
Self::from_bytes(data)
|
|
}
|
|
|
|
/// Parse an in-memory ELF64 image — the filesystem-free core of `load`. This is the fuzz/property
|
|
/// surface: it consumes fully attacker-controlled bytes (a Valve `.so`, or a fuzzer mutation) and
|
|
/// MUST return `Err` on any malformed input, never panic (no out-of-bounds index, no overflow).
|
|
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
|
|
ensure!(
|
|
data.len() > 64 && &data[0..4] == b"\x7fELF",
|
|
"not an ELF file"
|
|
);
|
|
ensure!(data[4] == 2, "only ELF64 is supported");
|
|
|
|
let shoff = u64le(&data, 40) as usize;
|
|
let shentsize = u16le(&data, 58) as usize;
|
|
let shnum = u16le(&data, 60) as usize;
|
|
ensure!(
|
|
shentsize >= 64,
|
|
"unexpected section header size {shentsize}"
|
|
);
|
|
// Section headers (matched by type/flags, never by name). Every field is attacker-controlled:
|
|
// compute the header offset with checked arithmetic and keep only sections whose file range and
|
|
// virtual range don't overflow / exceed the file. A malformed section becomes an inert empty
|
|
// placeholder (so `link` indices stay aligned and every downstream slice/address stays in
|
|
// bounds). A valid ELF skips none of this — its headers are all in range.
|
|
let mut secs: Vec<Sec> = Vec::with_capacity(shnum);
|
|
for i in 0..shnum {
|
|
let hdr_ok = i
|
|
.checked_mul(shentsize)
|
|
.and_then(|x| shoff.checked_add(x))
|
|
.filter(|&o| o.checked_add(64).is_some_and(|e| e <= data.len()));
|
|
let Some(o) = hdr_ok else {
|
|
secs.push(Sec::default());
|
|
continue;
|
|
};
|
|
let typ = u32le(&data, o + 4);
|
|
let off = u64le(&data, o + 24) as usize;
|
|
let size = u64le(&data, o + 32) as usize;
|
|
let addr = u64le(&data, o + 16);
|
|
let file_ok =
|
|
typ == SHT_NOBITS || off.checked_add(size).is_some_and(|e| e <= data.len());
|
|
let addr_ok = addr.checked_add(size as u64).is_some();
|
|
if file_ok && addr_ok {
|
|
secs.push(Sec {
|
|
typ,
|
|
flags: u64le(&data, o + 8),
|
|
addr,
|
|
off,
|
|
size,
|
|
link: u32le(&data, o + 40) as usize,
|
|
entsize: u64le(&data, o + 56) as usize,
|
|
});
|
|
} else {
|
|
secs.push(Sec::default());
|
|
}
|
|
}
|
|
|
|
let exec: Vec<(usize, u64, usize)> = secs
|
|
.iter()
|
|
.filter(|s| s.flags & SHF_EXECINSTR != 0 && s.typ != SHT_NOBITS)
|
|
.map(|s| (s.off, s.addr, s.size))
|
|
.collect();
|
|
ensure!(!exec.is_empty(), "no executable sections found");
|
|
|
|
// dynamic symbols (prefer .dynsym; fall back to .symtab if present)
|
|
let mut sym_addr = HashMap::new();
|
|
let mut sym_values: Vec<u64> = Vec::new();
|
|
// Every symbol's name, pushed in lockstep with `sym_values` so a reloc's `r_sym` index recovers the
|
|
// name even for UND (value-0) imports — the only way to identify the dynamically-linked kind vtables.
|
|
let mut sym_names: Vec<String> = Vec::new();
|
|
if let Some(symtab) = secs
|
|
.iter()
|
|
.find(|s| s.typ == SHT_DYNSYM)
|
|
.or_else(|| secs.iter().find(|s| s.typ == SHT_SYMTAB))
|
|
{
|
|
let str_off = secs.get(symtab.link).map_or(0, |s| s.off);
|
|
let n = if symtab.entsize >= 24 {
|
|
symtab.size / symtab.entsize
|
|
} else {
|
|
0
|
|
};
|
|
for i in 0..n {
|
|
let Some(o) = i
|
|
.checked_mul(symtab.entsize)
|
|
.and_then(|x| symtab.off.checked_add(x))
|
|
.filter(|&o| o.checked_add(24).is_some_and(|e| e <= data.len()))
|
|
else {
|
|
break;
|
|
};
|
|
let name = cstr(&data, str_off.wrapping_add(u32le(&data, o) as usize));
|
|
let value = u64le(&data, o + 8);
|
|
sym_values.push(value);
|
|
sym_names.push(name.clone()); // lockstep with sym_values, ALL symbols (incl. UND/value-0)
|
|
if !name.is_empty() && value != 0 {
|
|
sym_addr.entry(name.clone()).or_insert(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// relocations: reconstruct the as-loaded pointer values for .data.rel.ro etc.
|
|
let mut reloc = HashMap::new();
|
|
let mut reloc_by_val: HashMap<u64, Vec<u64>> = HashMap::new();
|
|
let mut kind_at: HashMap<u64, KindTag> = HashMap::new();
|
|
for s in secs.iter().filter(|s| s.typ == SHT_RELA) {
|
|
let n = if s.entsize >= 24 {
|
|
s.size / s.entsize
|
|
} else {
|
|
0
|
|
};
|
|
for i in 0..n {
|
|
let Some(o) = i
|
|
.checked_mul(s.entsize)
|
|
.and_then(|x| s.off.checked_add(x))
|
|
.filter(|&o| o.checked_add(24).is_some_and(|e| e <= data.len()))
|
|
else {
|
|
break;
|
|
};
|
|
let r_offset = u64le(&data, o);
|
|
let r_info = u64le(&data, o + 8);
|
|
let r_addend = u64le(&data, o + 16);
|
|
let r_type = (r_info & 0xffff_ffff) as u32;
|
|
let r_sym = (r_info >> 32) as usize;
|
|
// A typeinfo's `+0` field is a symbolic reloc against a `__cxxabiv1` kind vtable. Record the
|
|
// kind by the referenced symbol NAME (keyed by `r_offset` = the typeinfo's base vaddr), so a
|
|
// dynamically-linked runtime — where the value resolves to a useless `0 + 0x10` for all three
|
|
// kinds — is still classifiable. Recorded regardless of the value gate below.
|
|
if matches!(r_type, R_X86_64_64 | R_X86_64_GLOB_DAT)
|
|
&& let Some(tag) = sym_names.get(r_sym).and_then(|n| kind_tag_of(n))
|
|
{
|
|
kind_at.insert(r_offset, tag);
|
|
}
|
|
let val = match r_type {
|
|
R_X86_64_RELATIVE => r_addend,
|
|
R_X86_64_64 | R_X86_64_GLOB_DAT => sym_values
|
|
.get(r_sym)
|
|
.copied()
|
|
.unwrap_or(0)
|
|
.wrapping_add(r_addend),
|
|
_ => continue,
|
|
};
|
|
if val != 0 {
|
|
reloc.insert(r_offset, val);
|
|
reloc_by_val.entry(val).or_default().push(r_offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
data,
|
|
exec,
|
|
secs,
|
|
sym_addr,
|
|
reloc,
|
|
reloc_by_val,
|
|
kind_at,
|
|
})
|
|
}
|
|
|
|
/// Virtual addresses where `pat` matches inside any executable section.
|
|
pub fn find(&self, pat: &Pattern) -> Vec<u64> {
|
|
let mut hits = Vec::new();
|
|
for &(off, vaddr, size) in &self.exec {
|
|
let end = (off + size).min(self.data.len());
|
|
if off >= end {
|
|
continue;
|
|
}
|
|
for m in pat.find_all(&self.data[off..end]) {
|
|
hits.push(vaddr + m as u64);
|
|
}
|
|
}
|
|
hits
|
|
}
|
|
|
|
/// Executable bytes starting at virtual address `vaddr` (to the end of its section).
|
|
pub fn code_at(&self, vaddr: u64) -> Option<&[u8]> {
|
|
for &(off, sec_va, size) in &self.exec {
|
|
if vaddr >= sec_va && vaddr < sec_va + size as u64 {
|
|
let start = off + (vaddr - sec_va) as usize;
|
|
let end = (off + size).min(self.data.len());
|
|
if start < end {
|
|
return Some(&self.data[start..end]);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Executable bytes for the half-open virtual range `[start, end)` — used to disassemble a
|
|
/// single function from its known `.eh_frame` boundary (so linear decode can't misalign on data
|
|
/// between functions).
|
|
pub fn code_range(&self, start: u64, end: u64) -> Option<&[u8]> {
|
|
let all = self.code_at(start)?;
|
|
let len = end.checked_sub(start)? as usize;
|
|
Some(&all[..len.min(all.len())])
|
|
}
|
|
|
|
/// Is `vaddr` inside an executable section (i.e. plausibly a function pointer)?
|
|
pub fn is_code(&self, vaddr: u64) -> bool {
|
|
self.exec
|
|
.iter()
|
|
.any(|&(_, va, size)| vaddr >= va && vaddr < va + size as u64)
|
|
}
|
|
|
|
/// Executable sections as `(vaddr, bytes)` for linear disassembly.
|
|
pub fn exec_blocks(&self) -> Vec<(u64, &[u8])> {
|
|
self.exec
|
|
.iter()
|
|
.filter_map(|&(off, va, size)| {
|
|
let end = (off + size).min(self.data.len());
|
|
(off < end).then(|| (va, &self.data[off..end]))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Allocated, initialised, WRITABLE data sections as `(vaddr, byte_len)` — `.data` and
|
|
/// `.data.rel.ro`, where a module's static tables live. Returned as ranges rather than slices so
|
|
/// callers keep reading through [`read_ptr`](Self::read_ptr) and get the relocated pointer values
|
|
/// (a table of function pointers is relocation-driven; its raw file bytes are only incidentally
|
|
/// correct).
|
|
pub fn data_blocks(&self) -> Vec<(u64, usize)> {
|
|
self.secs
|
|
.iter()
|
|
.filter(|s| {
|
|
s.flags & SHF_ALLOC != 0
|
|
&& s.flags & SHF_WRITE != 0
|
|
&& s.flags & SHF_EXECINSTR == 0
|
|
&& s.typ != SHT_NOBITS
|
|
&& s.off + s.size <= self.data.len()
|
|
})
|
|
.map(|s| (s.addr, s.size))
|
|
.collect()
|
|
}
|
|
|
|
/// Relocation values that point into executable code — vtable slots and function pointers, i.e.
|
|
/// a large set of real function entry addresses obtained without disassembling anything.
|
|
pub fn code_pointer_targets(&self) -> Vec<u64> {
|
|
let mut out: Vec<u64> = self
|
|
.reloc
|
|
.values()
|
|
.copied()
|
|
.filter(|&v| self.is_code(v))
|
|
.collect();
|
|
out.sort_unstable();
|
|
out.dedup();
|
|
out
|
|
}
|
|
|
|
/// Raw allocated bytes at `vaddr`, up to `len`.
|
|
fn data_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> {
|
|
for s in &self.secs {
|
|
if s.flags & SHF_ALLOC != 0
|
|
&& s.typ != SHT_NOBITS
|
|
&& vaddr >= s.addr
|
|
&& vaddr < s.addr + s.size as u64
|
|
{
|
|
let start = s.off + (vaddr - s.addr) as usize;
|
|
if start + len <= self.data.len() {
|
|
return Some(&self.data[start..start + len]);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Read-only initialised data bytes at `vaddr` (i.e. `.rodata`): allocated, not writable, not
|
|
/// executable, not NOBITS. This is the build-invariant content — referenced strings, magic
|
|
/// constants — so a fingerprint over it survives recompiles, unlike writable/relocated data.
|
|
pub fn rodata_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> {
|
|
for s in &self.secs {
|
|
if s.flags & SHF_ALLOC != 0
|
|
&& s.flags & SHF_WRITE == 0
|
|
&& s.flags & SHF_EXECINSTR == 0
|
|
&& s.typ != SHT_NOBITS
|
|
&& vaddr >= s.addr
|
|
&& vaddr < s.addr + s.size as u64
|
|
{
|
|
let start = s.off + (vaddr - s.addr) as usize;
|
|
let end = (start + len).min(s.off + s.size).min(self.data.len());
|
|
if start < end {
|
|
return Some(&self.data[start..end]);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// The pointer value stored at `vaddr` — from the relocation map if relocated, else the
|
|
/// raw qword in the file.
|
|
pub fn read_ptr(&self, vaddr: u64) -> Option<u64> {
|
|
if let Some(&v) = self.reloc.get(&vaddr) {
|
|
return Some(v);
|
|
}
|
|
self.data_at(vaddr, 8).map(|b| u64le(b, 0))
|
|
}
|
|
|
|
/// Slot vaddrs whose (relocated) pointer value equals `target`.
|
|
pub fn ptrs_to(&self, target: u64) -> &[u64] {
|
|
self.reloc_by_val.get(&target).map_or(&[], |v| v.as_slice())
|
|
}
|
|
|
|
/// The Itanium kind of the typeinfo at `ti`, recovered from its `+0` reloc's SYMBOL NAME. `Some` when
|
|
/// the kind vtable is a named `__cxxabiv1` symbol (always so for a dynamically-linked runtime — the case
|
|
/// the value-based check can't handle); `None` for a statically-linked build, where the caller falls
|
|
/// back to comparing the resolved `+0` pointer against the in-image kind vtables.
|
|
pub fn kind_at(&self, ti: u64) -> Option<KindTag> {
|
|
self.kind_at.get(&ti).copied()
|
|
}
|
|
|
|
/// Iterate `(slot_vaddr, resolved_pointer)` over every relocation — the reloc-driven way to
|
|
/// sweep for vtables/typeinfos without brute-scanning section bytes.
|
|
pub fn reloc_slots(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
|
|
self.reloc.iter().map(|(&k, &v)| (k, v))
|
|
}
|
|
|
|
/// Raw signed qword at `vaddr` from file bytes — for non-relocated integers (e.g. an Itanium
|
|
/// vtable's offset-to-top), where `read_ptr`'s reloc lookup would be meaningless.
|
|
pub fn read_i64(&self, vaddr: u64) -> Option<i64> {
|
|
self.data_at(vaddr, 8)
|
|
.map(|b| i64::from_le_bytes(b[..8].try_into().unwrap()))
|
|
}
|
|
|
|
/// Raw `u32` at `vaddr` from file bytes (e.g. an Itanium `__vmi` typeinfo's base count).
|
|
pub fn read_u32(&self, vaddr: u64) -> Option<u32> {
|
|
self.data_at(vaddr, 4).map(|b| u32le(b, 0))
|
|
}
|
|
|
|
/// Raw `i32` at `vaddr` from file bytes (e.g. a schema field's inheritance offset).
|
|
pub fn read_i32(&self, vaddr: u64) -> Option<i32> {
|
|
self.data_at(vaddr, 4)
|
|
.map(|b| i32::from_le_bytes(b[..4].try_into().unwrap()))
|
|
}
|
|
|
|
/// Raw `u16` at `vaddr` from file bytes (e.g. a schema class's field count).
|
|
pub fn read_u16(&self, vaddr: u64) -> Option<u16> {
|
|
self.data_at(vaddr, 2).map(|b| u16le(b, 0))
|
|
}
|
|
|
|
/// Raw `u8` at `vaddr` from file bytes (e.g. a schema class's base count).
|
|
pub fn read_u8(&self, vaddr: u64) -> Option<u8> {
|
|
self.data_at(vaddr, 1).map(|b| b[0])
|
|
}
|
|
|
|
/// Is `vaddr` inside any allocated section (code or data)? The "does this pointer land in the
|
|
/// image" test RTTI validation needs.
|
|
pub fn contains(&self, vaddr: u64) -> bool {
|
|
self.secs
|
|
.iter()
|
|
.any(|s| s.flags & SHF_ALLOC != 0 && vaddr >= s.addr && vaddr < s.addr + s.size as u64)
|
|
}
|
|
|
|
pub fn symbol_addr(&self, name: &str) -> Option<u64> {
|
|
self.sym_addr.get(name).copied()
|
|
}
|
|
|
|
/// Virtual addresses of an exact byte string within allocated, non-executable sections
|
|
/// (used to find RTTI name strings in `.rodata`).
|
|
pub fn find_bytes(&self, needle: &[u8]) -> Vec<u64> {
|
|
let mut out = Vec::new();
|
|
if needle.is_empty() {
|
|
return out;
|
|
}
|
|
for s in &self.secs {
|
|
if s.flags & SHF_ALLOC == 0
|
|
|| s.typ == SHT_NOBITS
|
|
|| s.flags & SHF_EXECINSTR != 0
|
|
|| s.off + s.size > self.data.len()
|
|
{
|
|
continue;
|
|
}
|
|
let hay = &self.data[s.off..s.off + s.size];
|
|
let mut i = 0;
|
|
while let Some(p) = memchr::memmem::find(&hay[i..], needle) {
|
|
out.push(s.addr + (i + p) as u64);
|
|
i += p + 1;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The NUL-terminated string at `vaddr` in any allocated, initialised section — RTTI `_ZTS`
|
|
/// names, schema class/field names, referenced literals. Capped so a missing terminator (e.g.
|
|
/// a bogus pointer into a non-string section) can't run to the end of the file.
|
|
pub fn read_c_string(&self, vaddr: u64) -> Option<String> {
|
|
for s in &self.secs {
|
|
if s.flags & SHF_ALLOC != 0
|
|
&& s.typ != SHT_NOBITS
|
|
&& vaddr >= s.addr
|
|
&& vaddr < s.addr + s.size as u64
|
|
{
|
|
let start = s.off + (vaddr - s.addr) as usize;
|
|
let end = (s.off + s.size).min(self.data.len()).min(start + 4096);
|
|
if start >= end {
|
|
return None;
|
|
}
|
|
let rel = self.data[start..end].iter().position(|&c| c == 0)?;
|
|
return Some(String::from_utf8_lossy(&self.data[start..start + rel]).into_owned());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Virtual address + file offset of `.eh_frame_hdr`, via the PT_GNU_EH_FRAME program header.
|
|
fn eh_frame_hdr(&self) -> Option<(u64, usize)> {
|
|
let d = &self.data;
|
|
let phoff = u64le(d, 32) as usize;
|
|
let phentsize = u16le(d, 54) as usize;
|
|
let phnum = u16le(d, 56) as usize;
|
|
if phentsize < 56 {
|
|
return None;
|
|
}
|
|
for i in 0..phnum {
|
|
let Some(o) = i
|
|
.checked_mul(phentsize)
|
|
.and_then(|x| phoff.checked_add(x))
|
|
.filter(|&o| o.checked_add(56).is_some_and(|e| e <= d.len()))
|
|
else {
|
|
break;
|
|
};
|
|
if u32le(d, o) == PT_GNU_EH_FRAME {
|
|
return Some((u64le(d, o + 16), u64le(d, o + 8) as usize));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Decode a DWARF-encoded value at `field_va`, applying its pcrel/datarel base. Returns
|
|
/// `(value, byte_width)`. `datarel_base` is the `.eh_frame_hdr` vaddr (only used by datarel enc).
|
|
fn read_enc(&self, enc: u8, field_va: u64, datarel_base: u64) -> Option<(u64, usize)> {
|
|
let sz = dw_ptr_size(enc);
|
|
if sz == 0 {
|
|
return None;
|
|
}
|
|
let b = self.data_at(field_va, sz)?;
|
|
let raw = match sz {
|
|
2 => u16le(b, 0) as u64,
|
|
4 => u32le(b, 0) as u64,
|
|
_ => u64le(b, 0),
|
|
};
|
|
let base = match enc & 0x70 {
|
|
0x00 => 0, // absolute — no base (also how lengths/sizes are stored)
|
|
0x10 => field_va, // pcrel: relative to this field's own address
|
|
0x30 => datarel_base, // datarel: relative to `.eh_frame_hdr`
|
|
_ => return None,
|
|
};
|
|
let val = if matches!(enc & 0x0f, 0x0a..=0x0c) {
|
|
let s = match sz {
|
|
2 => raw as u16 as i16 as i64,
|
|
4 => raw as u32 as i32 as i64,
|
|
_ => raw as i64,
|
|
};
|
|
base.wrapping_add(s as u64)
|
|
} else {
|
|
base.wrapping_add(raw)
|
|
};
|
|
Some((val, sz))
|
|
}
|
|
|
|
/// Byte length of the LEB128 value at `va` (the value itself is unused here — we only skip it).
|
|
fn leb_len(&self, va: u64) -> Option<u64> {
|
|
let mut n = 0u64;
|
|
loop {
|
|
let byte = self.data_at(va.wrapping_add(n), 1)?[0];
|
|
n += 1;
|
|
if byte & 0x80 == 0 {
|
|
return Some(n);
|
|
}
|
|
if n >= 16 {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The FDE pointer encoding a CIE advertises — its `'R'` augmentation byte. Absptr (0) when the
|
|
/// CIE carries no `z`/`R` augmentation (then FDE addresses are absolute).
|
|
fn cie_fde_enc(&self, cie_va: u64) -> u8 {
|
|
let Some(head) = self.data_at(cie_va, 9) else {
|
|
return 0;
|
|
};
|
|
if u32le(head, 4) != 0 {
|
|
return 0; // CIE id field must be 0
|
|
}
|
|
let version = head[8];
|
|
let mut p = cie_va.wrapping_add(9);
|
|
let Some(aug) = self.read_c_string(p) else {
|
|
return 0;
|
|
};
|
|
p = p.wrapping_add(aug.len() as u64 + 1);
|
|
if !aug.starts_with('z') {
|
|
return 0;
|
|
}
|
|
// code_align (uleb), data_align (sleb), return-addr reg (uleb v>=3 else 1 byte), aug_len (uleb)
|
|
for _ in 0..2 {
|
|
match self.leb_len(p) {
|
|
Some(n) => p = p.wrapping_add(n),
|
|
None => return 0,
|
|
}
|
|
}
|
|
if version >= 3 {
|
|
match self.leb_len(p) {
|
|
Some(n) => p = p.wrapping_add(n),
|
|
None => return 0,
|
|
}
|
|
} else {
|
|
p = p.wrapping_add(1);
|
|
}
|
|
match self.leb_len(p) {
|
|
Some(n) => p = p.wrapping_add(n),
|
|
None => return 0,
|
|
}
|
|
// The augmentation letters after 'z' name the aug-data fields, in order.
|
|
for c in aug.bytes().skip(1) {
|
|
match c {
|
|
b'R' => return self.data_at(p, 1).map_or(0, |b| b[0]),
|
|
b'L' => p = p.wrapping_add(1),
|
|
b'P' => {
|
|
let Some(e) = self.data_at(p, 1).map(|b| b[0]) else {
|
|
return 0;
|
|
};
|
|
p = p.wrapping_add(1 + dw_ptr_size(e) as u64);
|
|
}
|
|
b'S' | b'B' | b'G' => {}
|
|
_ => return 0,
|
|
}
|
|
}
|
|
0
|
|
}
|
|
|
|
/// Parse the FDE at `fde_va` to `(pc_begin, pc_end)`, using its owning CIE's pointer encoding.
|
|
/// `enc_cache` memoises CIE encodings (nearly all FDEs share one CIE).
|
|
fn fde_range(&self, fde_va: u64, enc_cache: &mut HashMap<u64, u8>) -> Option<(u64, u64)> {
|
|
let head = self.data_at(fde_va, 8)?;
|
|
let len = u32le(head, 0);
|
|
if len == 0 || len == 0xffff_ffff {
|
|
return None; // terminator, or 64-bit DWARF (not emitted by the CS2 toolchain)
|
|
}
|
|
let cie_ptr = u32le(head, 4);
|
|
if cie_ptr == 0 {
|
|
return None; // a CIE, not an FDE
|
|
}
|
|
let cie_va = fde_va.wrapping_add(4).wrapping_sub(cie_ptr as u64);
|
|
let enc = *enc_cache
|
|
.entry(cie_va)
|
|
.or_insert_with(|| self.cie_fde_enc(cie_va));
|
|
let (pc_begin, sz) = self.read_enc(enc, fde_va.wrapping_add(8), 0)?;
|
|
// PC_range follows PC_begin at the same width; it's an absolute size (no base applied).
|
|
let rb = self.data_at(fde_va.wrapping_add(8).wrapping_add(sz as u64), sz)?;
|
|
let range = match sz {
|
|
2 => u16le(rb, 0) as u64,
|
|
4 => u32le(rb, 0) as u64,
|
|
_ => u64le(rb, 0),
|
|
};
|
|
Some((pc_begin, pc_begin.wrapping_add(range)))
|
|
}
|
|
|
|
/// Every function `.eh_frame` unwind data describes, as sorted `(start, end)` virtual-address
|
|
/// pairs. This enumerates far more functions than the dynamic symbol table exposes (stripped
|
|
/// internal functions still need unwind info), making it the completeness denominator for
|
|
/// coverage audits and the source of exact byte extents for the content-locator.
|
|
pub fn eh_frame_functions(&self) -> Vec<(u64, u64)> {
|
|
let Some((hdr_va, hdr_off)) = self.eh_frame_hdr() else {
|
|
return Vec::new();
|
|
};
|
|
let d = &self.data;
|
|
// header: version(1) + eh_frame_ptr_enc(1) + fde_count_enc(1) + table_enc(1)
|
|
let Some(hb) = hdr_off.checked_add(4).and_then(|e| d.get(hdr_off..e)) else {
|
|
return Vec::new();
|
|
};
|
|
if hb[0] != 1 {
|
|
return Vec::new();
|
|
}
|
|
let ptr_sz = dw_ptr_size(hb[1]); // eh_frame_ptr encoding (we skip the pointer)
|
|
let count_sz = dw_ptr_size(hb[2]);
|
|
let table_enc = hb[3];
|
|
let esz = dw_ptr_size(table_enc);
|
|
if ptr_sz == 0 || count_sz == 0 || esz == 0 {
|
|
return Vec::new();
|
|
}
|
|
let Some(table_off) = hdr_off
|
|
.checked_add(4)
|
|
.and_then(|x| x.checked_add(ptr_sz))
|
|
.and_then(|count_off| count_off.checked_add(count_sz).map(|t| (count_off, t)))
|
|
.filter(|&(_, t)| t <= d.len())
|
|
else {
|
|
return Vec::new();
|
|
};
|
|
let (count_off, table_off) = table_off;
|
|
// fde_count is attacker-controlled; each table entry is `2*esz` bytes, so a real count can't
|
|
// exceed the file. Cap it BEFORE any allocation — a crafted count would otherwise drive an
|
|
// out-of-memory abort.
|
|
let fde_count = match count_sz {
|
|
2 => u16le(d, count_off) as usize,
|
|
4 => u32le(d, count_off) as usize,
|
|
_ => u64le(d, count_off) as usize,
|
|
}
|
|
.min(d.len() / (2 * esz).max(1) + 1);
|
|
let entry_bytes = 2 * esz;
|
|
let table_va = hdr_va.wrapping_add(table_off.wrapping_sub(hdr_off) as u64);
|
|
let mut starts: Vec<(u64, u64)> = Vec::new(); // (fn start, fde vaddr)
|
|
for i in 0..fde_count {
|
|
let field_va = table_va.wrapping_add(i.wrapping_mul(entry_bytes) as u64);
|
|
let (Some((start, _)), Some((fde_va, _))) = (
|
|
self.read_enc(table_enc, field_va, hdr_va),
|
|
self.read_enc(table_enc, field_va.wrapping_add(esz as u64), hdr_va),
|
|
) else {
|
|
break;
|
|
};
|
|
starts.push((start, fde_va));
|
|
}
|
|
starts.sort_unstable_by_key(|&(s, _)| s);
|
|
let mut cache = HashMap::new();
|
|
let mut out = Vec::with_capacity(starts.len());
|
|
for (i, &(start, fde_va)) in starts.iter().enumerate() {
|
|
// Prefer the FDE's own extent; fall back to the next function's start (padding included).
|
|
let end = self
|
|
.fde_range(fde_va, &mut cache)
|
|
.map(|(_, e)| e)
|
|
.filter(|&e| e > start)
|
|
.unwrap_or_else(|| starts.get(i + 1).map_or(start, |&(s, _)| s));
|
|
out.push((start, end));
|
|
}
|
|
out
|
|
}
|
|
}
|