rnicro 0.3.0

A Linux x86_64 debugger and exploit development toolkit written in Rust
Documentation
//! ELF binary loading and symbol resolution.
//!
//! Corresponds to sdb's elf.hpp/cpp and book Ch.9 (ELF and DWARF).
//! Memory-maps the ELF binary and provides symbol lookup
//! for mapping addresses to function names.

use std::path::Path;

use memmap2::Mmap;
use object::{Object, ObjectSymbol, SymbolKind};

use crate::error::{Error, Result};
use crate::types::VirtAddr;

/// A GOT/PLT relocation entry parsed from the ELF.
#[derive(Debug, Clone)]
pub struct GotPltEntry {
    /// Address of the GOT slot.
    pub got_addr: u64,
    /// Symbol name from dynamic symbol table.
    pub name: String,
    /// PLT stub address (estimated from PLT section layout).
    pub plt_addr: Option<u64>,
}

/// Parse GOT/PLT relocation entries from ELF binary data.
///
/// Reads `.rela.plt` relocations to find each GOT slot and its
/// associated dynamic symbol. PLT addresses are estimated from
/// the `.plt` section layout (16 bytes per entry, first entry is PLT0).
pub fn parse_got_plt(data: &[u8]) -> Result<Vec<GotPltEntry>> {
    let elf =
        goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;

    // Find .plt section for PLT address estimation
    let plt_section = elf.section_headers.iter().find(|sh| {
        elf.shdr_strtab
            .get_at(sh.sh_name)
            .map(|name| name == ".plt")
            .unwrap_or(false)
    });

    let mut entries = Vec::new();

    for (i, reloc) in elf.pltrelocs.iter().enumerate() {
        let sym_idx = reloc.r_sym;
        let name = if let Some(sym) = elf.dynsyms.get(sym_idx) {
            elf.dynstrtab
                .get_at(sym.st_name)
                .unwrap_or("<unknown>")
                .to_string()
        } else {
            format!("<sym_{}>", sym_idx)
        };

        // PLT entry = .plt base + 16 (PLT0) + i * 16
        let plt_addr = plt_section.map(|sh| sh.sh_addr + 16 + (i as u64) * 16);

        entries.push(GotPltEntry {
            got_addr: reloc.r_offset,
            name,
            plt_addr,
        });
    }

    Ok(entries)
}

/// Parse all dynamic relocations (from .rela.dyn) to find GOT entries.
pub fn parse_got_dyn(data: &[u8]) -> Result<Vec<GotPltEntry>> {
    let elf =
        goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;

    let mut entries = Vec::new();

    for reloc in elf.dynrelas.iter() {
        let sym_idx = reloc.r_sym;
        if sym_idx == 0 {
            continue;
        }
        let name = if let Some(sym) = elf.dynsyms.get(sym_idx) {
            elf.dynstrtab
                .get_at(sym.st_name)
                .unwrap_or("<unknown>")
                .to_string()
        } else {
            continue;
        };
        if name.is_empty() {
            continue;
        }
        entries.push(GotPltEntry {
            got_addr: reloc.r_offset,
            name,
            plt_addr: None,
        });
    }

    Ok(entries)
}

/// Classification of ELF symbols.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolType {
    /// Code / function symbol.
    Function,
    /// Data / object symbol.
    Data,
    /// Other symbol types.
    Other,
}

/// A resolved symbol from the ELF file.
#[derive(Debug, Clone)]
pub struct Symbol {
    /// Symbol name (may be mangled).
    pub name: String,
    /// Virtual address of the symbol.
    pub addr: VirtAddr,
    /// Size of the symbol in bytes (0 if unknown).
    pub size: u64,
    /// Symbol classification.
    pub kind: SymbolType,
}

/// A loaded ELF binary with symbol table access.
pub struct ElfFile {
    _mmap: Mmap,
    symbols: Vec<Symbol>,
}

impl ElfFile {
    /// Load an ELF binary from disk via memory mapping.
    pub fn load(path: &Path) -> Result<Self> {
        let file = std::fs::File::open(path)
            .map_err(|e| Error::Other(format!("open ELF '{}': {}", path.display(), e)))?;
        let mmap =
            unsafe { Mmap::map(&file) }.map_err(|e| Error::Other(format!("mmap ELF: {}", e)))?;
        let obj =
            object::File::parse(&*mmap).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;

        let mut symbols = Vec::new();
        for sym in obj.symbols() {
            if let Ok(name) = sym.name() {
                if !name.is_empty() && sym.address() != 0 {
                    symbols.push(Symbol {
                        name: name.to_string(),
                        addr: VirtAddr(sym.address()),
                        size: sym.size(),
                        kind: match sym.kind() {
                            SymbolKind::Text => SymbolType::Function,
                            SymbolKind::Data => SymbolType::Data,
                            _ => SymbolType::Other,
                        },
                    });
                }
            }
        }

        // Also include dynamic symbols
        for sym in obj.dynamic_symbols() {
            if let Ok(name) = sym.name() {
                if !name.is_empty() && sym.address() != 0 {
                    symbols.push(Symbol {
                        name: name.to_string(),
                        addr: VirtAddr(sym.address()),
                        size: sym.size(),
                        kind: match sym.kind() {
                            SymbolKind::Text => SymbolType::Function,
                            SymbolKind::Data => SymbolType::Data,
                            _ => SymbolType::Other,
                        },
                    });
                }
            }
        }

        // Sort by address for efficient lookup
        symbols.sort_by_key(|s| s.addr);

        Ok(ElfFile {
            _mmap: mmap,
            symbols,
        })
    }

    /// Find a symbol by exact name match.
    pub fn find_symbol(&self, name: &str) -> Option<&Symbol> {
        self.symbols.iter().find(|s| s.name == name)
    }

    /// Find the symbol containing the given address.
    pub fn find_symbol_at(&self, addr: VirtAddr) -> Option<&Symbol> {
        self.symbols
            .iter()
            .filter(|s| s.size > 0)
            .find(|s| addr.addr() >= s.addr.addr() && addr.addr() < s.addr.addr() + s.size)
    }

    /// Iterate over all function symbols with nonzero size.
    pub fn functions(&self) -> impl Iterator<Item = &Symbol> {
        self.symbols
            .iter()
            .filter(|s| s.kind == SymbolType::Function && s.size > 0)
    }

    /// Get all symbols.
    pub fn symbols(&self) -> &[Symbol] {
        &self.symbols
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;

    /// Create a test ElfFile with pre-built symbol data (no real mmap needed).
    fn test_elf(symbols: Vec<Symbol>) -> ElfFile {
        // Create a temp file so we can mmap it
        let mut tmp = tempfile::NamedTempFile::new().unwrap();
        tmp.write_all(b"dummy").unwrap();
        tmp.flush().unwrap();
        let mmap = unsafe { Mmap::map(tmp.as_file()).unwrap() };
        ElfFile {
            _mmap: mmap,
            symbols,
        }
    }

    #[test]
    fn symbol_type_equality() {
        assert_eq!(SymbolType::Function, SymbolType::Function);
        assert_ne!(SymbolType::Function, SymbolType::Data);
    }

    #[test]
    fn symbol_sorting() {
        let syms = vec![
            Symbol {
                name: "b".into(),
                addr: VirtAddr(0x2000),
                size: 10,
                kind: SymbolType::Function,
            },
            Symbol {
                name: "a".into(),
                addr: VirtAddr(0x1000),
                size: 20,
                kind: SymbolType::Function,
            },
        ];
        let mut sorted = syms.clone();
        sorted.sort_by_key(|s| s.addr);
        assert_eq!(sorted[0].name, "a");
        assert_eq!(sorted[1].name, "b");
    }

    #[test]
    fn find_symbol_at_address() {
        let elf = test_elf(vec![
            Symbol {
                name: "func_a".into(),
                addr: VirtAddr(0x1000),
                size: 0x100,
                kind: SymbolType::Function,
            },
            Symbol {
                name: "func_b".into(),
                addr: VirtAddr(0x2000),
                size: 0x50,
                kind: SymbolType::Function,
            },
        ]);

        assert_eq!(
            elf.find_symbol_at(VirtAddr(0x1050)).map(|s| &s.name[..]),
            Some("func_a")
        );
        assert_eq!(
            elf.find_symbol_at(VirtAddr(0x2000)).map(|s| &s.name[..]),
            Some("func_b")
        );
        assert!(elf.find_symbol_at(VirtAddr(0x3000)).is_none());
        assert!(elf.find_symbol_at(VirtAddr(0x9999)).is_none());
    }

    #[test]
    fn find_symbol_by_name() {
        let elf = test_elf(vec![Symbol {
            name: "main".into(),
            addr: VirtAddr(0x401000),
            size: 100,
            kind: SymbolType::Function,
        }]);

        assert!(elf.find_symbol("main").is_some());
        assert!(elf.find_symbol("nonexistent").is_none());
    }

    #[test]
    fn functions_iterator() {
        let elf = test_elf(vec![
            Symbol {
                name: "func".into(),
                addr: VirtAddr(0x1000),
                size: 10,
                kind: SymbolType::Function,
            },
            Symbol {
                name: "global_var".into(),
                addr: VirtAddr(0x2000),
                size: 8,
                kind: SymbolType::Data,
            },
            Symbol {
                name: "zero_size_func".into(),
                addr: VirtAddr(0x3000),
                size: 0,
                kind: SymbolType::Function,
            },
        ]);

        let funcs: Vec<_> = elf.functions().collect();
        assert_eq!(funcs.len(), 1);
        assert_eq!(funcs[0].name, "func");
    }
}