use crate::error::{Error, Result};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct AslrCalculator {
known_offsets: HashMap<String, u64>,
bases: HashMap<String, u64>,
}
impl Default for AslrCalculator {
fn default() -> Self {
Self::new()
}
}
impl AslrCalculator {
pub fn new() -> Self {
Self {
known_offsets: HashMap::new(),
bases: HashMap::new(),
}
}
pub fn calc_base(&mut self, region: &str, leaked_addr: u64, offset: u64) -> u64 {
let base = leaked_addr.wrapping_sub(offset);
self.bases.insert(region.to_string(), base);
base
}
pub fn get_base(&self, region: &str) -> Option<u64> {
self.bases.get(region).copied()
}
pub fn calc_addr(&self, region: &str, offset: u64) -> Option<u64> {
self.bases.get(region).map(|base| base.wrapping_add(offset))
}
pub fn add_offset(&mut self, name: &str, offset: u64) {
self.known_offsets.insert(name.to_string(), offset);
}
pub fn resolve(&self, region: &str, name: &str) -> Option<u64> {
let base = self.bases.get(region)?;
let offset = self.known_offsets.get(name)?;
Some(base.wrapping_add(*offset))
}
pub fn all_bases(&self) -> &HashMap<String, u64> {
&self.bases
}
pub fn validate_leak(addr: u64) -> bool {
addr > 0x1000 && addr < 0x0000_8000_0000_0000
}
pub fn is_page_aligned(addr: u64) -> bool {
addr & 0xFFF == 0
}
pub fn page_offset(addr: u64) -> u64 {
addr & 0xFFF
}
}
#[derive(Debug, Clone)]
pub struct PartialOverwrite {
pub overwrite_bytes: usize,
pub payload: Vec<u8>,
pub random_bits: u32,
pub success_probability: f64,
pub expected_attempts: u64,
}
pub fn partial_overwrite(target_addr: u64, overwrite_bytes: usize) -> PartialOverwrite {
assert!((1..=8).contains(&overwrite_bytes));
let mask = if overwrite_bytes >= 8 {
u64::MAX
} else {
(1u64 << (overwrite_bytes * 8)) - 1
};
let payload_value = target_addr & mask;
let payload = payload_value.to_le_bytes()[..overwrite_bytes].to_vec();
let controlled_bits = (overwrite_bytes * 8) as u32;
let random_bits = controlled_bits.saturating_sub(12);
let success_probability = if random_bits == 0 {
1.0
} else {
1.0 / (1u64 << random_bits) as f64
};
let expected_attempts = if random_bits == 0 {
1
} else {
1u64 << random_bits
};
PartialOverwrite {
overwrite_bytes,
payload,
random_bits,
success_probability,
expected_attempts,
}
}
pub fn brute_force_bases(partial_leak: u64, n_bytes: usize, offset: u64) -> Vec<u64> {
assert!((1..=6).contains(&n_bytes));
let known_mask = (1u64 << (n_bytes * 8)) - 1;
let known_low = partial_leak & known_mask;
let step = 1u64 << (n_bytes * 8);
let mut bases = Vec::new();
let mut addr = known_low;
while addr < 0x0000_8000_0000_0000 {
let base = addr.wrapping_sub(offset);
if AslrCalculator::is_page_aligned(base) && base < 0x0000_8000_0000_0000 {
bases.push(base);
}
addr = match addr.checked_add(step) {
Some(a) => a,
None => break,
};
}
bases
}
pub fn base_from_leak(leaked_func: u64, func_offset: u64) -> u64 {
leaked_func.wrapping_sub(func_offset)
}
#[derive(Debug, Clone)]
pub struct LibcVersion {
pub id: String,
pub build_id: String,
pub symbols: HashMap<String, u64>,
}
#[derive(Default)]
pub struct LibcDb {
versions: Vec<LibcVersion>,
}
impl LibcDb {
pub fn new() -> Self {
Self::default()
}
pub fn add_version(&mut self, version: LibcVersion) {
self.versions.push(version);
}
pub fn lookup_by_build_id(&self, build_id: &str) -> Option<&LibcVersion> {
self.versions.iter().find(|v| v.build_id == build_id)
}
pub fn identify_by_leak(&self, symbol_name: &str, leaked_addr: u64) -> Vec<&LibcVersion> {
let low12 = leaked_addr & 0xFFF;
self.versions
.iter()
.filter(|v| {
v.symbols
.get(symbol_name)
.map(|off| off & 0xFFF == low12)
.unwrap_or(false)
})
.collect()
}
pub fn len(&self) -> usize {
self.versions.len()
}
pub fn is_empty(&self) -> bool {
self.versions.is_empty()
}
pub fn extract_offsets(data: &[u8]) -> Result<HashMap<String, u64>> {
let elf =
goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;
let mut offsets = HashMap::new();
for sym in &elf.dynsyms {
if sym.st_value != 0 {
if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
if !name.is_empty() {
offsets.insert(name.to_string(), sym.st_value);
}
}
}
}
Ok(offsets)
}
pub fn extract_build_id(data: &[u8]) -> Result<String> {
let elf =
goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;
for sh in &elf.section_headers {
let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
if name == ".note.gnu.build-id" {
let offset = sh.sh_offset as usize;
let size = sh.sh_size as usize;
if offset + size > data.len() || size < 16 {
continue;
}
let note_data = &data[offset..offset + size];
let namesz = u32::from_le_bytes(note_data[0..4].try_into().unwrap()) as usize;
let descsz = u32::from_le_bytes(note_data[4..8].try_into().unwrap()) as usize;
let name_aligned = (namesz + 3) & !3;
let desc_start = 12 + name_aligned;
if desc_start + descsz <= note_data.len() {
let build_id = ¬e_data[desc_start..desc_start + descsz];
return Ok(build_id.iter().map(|b| format!("{:02x}", b)).collect());
}
}
}
Err(Error::Other("BuildID not found".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calc_base_simple() {
let mut calc = AslrCalculator::new();
let base = calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
assert_eq!(base, 0x7f0000100000);
assert!(AslrCalculator::is_page_aligned(base));
}
#[test]
fn calc_addr_roundtrip() {
let mut calc = AslrCalculator::new();
calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
let addr = calc.calc_addr("libc", 0x50d70).unwrap();
assert_eq!(addr, 0x7f0000100000 + 0x50d70);
}
#[test]
fn resolve_symbol() {
let mut calc = AslrCalculator::new();
calc.calc_base("libc", 0x7f0000100aa0, 0xaa0);
calc.add_offset("system", 0x50d70);
let addr = calc.resolve("libc", "system").unwrap();
assert_eq!(addr, 0x7f0000150d70);
}
#[test]
fn resolve_missing_returns_none() {
let calc = AslrCalculator::new();
assert!(calc.resolve("libc", "system").is_none());
}
#[test]
fn validate_leak_values() {
assert!(!AslrCalculator::validate_leak(0));
assert!(!AslrCalculator::validate_leak(0x100));
assert!(AslrCalculator::validate_leak(0x400000));
assert!(AslrCalculator::validate_leak(0x7f0000000000));
assert!(!AslrCalculator::validate_leak(0xffff800000000000)); }
#[test]
fn page_alignment_check() {
assert!(AslrCalculator::is_page_aligned(0x400000));
assert!(AslrCalculator::is_page_aligned(0x7f0000100000));
assert!(!AslrCalculator::is_page_aligned(0x400001));
assert!(!AslrCalculator::is_page_aligned(0x400aa0));
}
#[test]
fn page_offset_extraction() {
assert_eq!(AslrCalculator::page_offset(0x7f00001050d0), 0x0d0);
assert_eq!(AslrCalculator::page_offset(0x400000), 0);
}
#[test]
fn partial_overwrite_1byte() {
let po = partial_overwrite(0x7f0000100060, 1);
assert_eq!(po.payload, vec![0x60]);
assert_eq!(po.random_bits, 0); assert_eq!(po.success_probability, 1.0);
assert_eq!(po.expected_attempts, 1);
}
#[test]
fn partial_overwrite_2byte() {
let po = partial_overwrite(0x7f0000101060, 2);
assert_eq!(po.payload, vec![0x60, 0x10]); assert_eq!(po.random_bits, 4); assert_eq!(po.expected_attempts, 16);
}
#[test]
fn brute_force_bases_small_leak() {
let bases = brute_force_bases(0x0aa0, 2, 0x80aa0);
for base in &bases {
assert!(AslrCalculator::is_page_aligned(*base));
}
assert!(!bases.is_empty());
}
#[test]
fn base_from_leak_convenience() {
let base = base_from_leak(0x7f00001050d0, 0x50d0);
assert_eq!(base, 0x7f0000100000);
}
#[test]
fn libc_db_identify_by_leak() {
let mut db = LibcDb::new();
let mut syms = HashMap::new();
syms.insert("puts".to_string(), 0x80aa0u64);
syms.insert("system".to_string(), 0x50d70u64);
db.add_version(LibcVersion {
id: "test-libc".into(),
build_id: "abc123".into(),
symbols: syms,
});
let matches = db.identify_by_leak("puts", 0x7f00001800aa0);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].id, "test-libc");
let matches = db.identify_by_leak("puts", 0x7f0000180bbb);
assert!(matches.is_empty());
}
#[test]
fn libc_db_build_id_lookup() {
let mut db = LibcDb::new();
db.add_version(LibcVersion {
id: "test".into(),
build_id: "deadbeef".into(),
symbols: HashMap::new(),
});
assert!(db.lookup_by_build_id("deadbeef").is_some());
assert!(db.lookup_by_build_id("00000000").is_none());
}
}