From f5fb2f6a127c108ee6adc900e2a380536b3bff61 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 24 Dec 2025 20:44:23 +0000 Subject: [PATCH 1/5] feat: allow finding gadgets in multiple files --- crackers/src/bin/crackers/main.rs | 1 + crackers/src/config/error.rs | 2 +- crackers/src/gadget/library/builder.rs | 148 ++++++++++++++++++++++++- crackers/src/gadget/library/mod.rs | 53 +++++++-- 4 files changed, 188 insertions(+), 16 deletions(-) diff --git a/crackers/src/bin/crackers/main.rs b/crackers/src/bin/crackers/main.rs index 48aa45e..df3b621 100644 --- a/crackers/src/bin/crackers/main.rs +++ b/crackers/src/bin/crackers/main.rs @@ -146,6 +146,7 @@ fn new(path: PathBuf, library: Option) -> anyhow::Result<()> { path: library_path, sample_size: None, base_address: None, + loaded_libraries: None, }, sleigh: SleighConfig { ghidra_path: "/Applications/ghidra".to_string(), diff --git a/crackers/src/config/error.rs b/crackers/src/config/error.rs index 32cd40c..fb408ee 100644 --- a/crackers/src/config/error.rs +++ b/crackers/src/config/error.rs @@ -6,7 +6,7 @@ use thiserror::Error; pub enum CrackersConfigError { #[error("Invalid log level")] InvalidLogLevel, - #[error("An error reading a file referenced from the config")] + #[error("An error reading a file referenced from the config: {0}")] Io(#[from] std::io::Error), #[error("An error parsing a file with gimli object: {0}")] Gimli(#[from] object::Error), diff --git a/crackers/src/gadget/library/builder.rs b/crackers/src/gadget/library/builder.rs index 356bf4e..1a478e1 100644 --- a/crackers/src/gadget/library/builder.rs +++ b/crackers/src/gadget/library/builder.rs @@ -11,6 +11,25 @@ use crate::config::error::CrackersConfigError; use crate::config::object::load_sleigh; use crate::config::sleigh::SleighConfig; use crate::gadget::library::GadgetLibrary; +use tracing::{Level, event}; + +const LIB_ALIGNMENT: u64 = 0x4000; // 16 KiB alignment for loaded libraries +const LIB_GAP: u64 = 0x1000; // small gap between libraries when placing + +fn align_up(x: u64, align: u64) -> u64 { + if align == 0 { + return x; + } + ((x + align - 1) / align) * align +} + +#[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)] +#[builder(default)] +#[cfg_attr(feature = "pyo3", pyclass)] +pub struct LoadedLibraryConfig { + pub path: String, + pub base_address: Option, +} #[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)] #[builder(default)] @@ -22,15 +41,104 @@ pub struct GadgetLibraryConfig { pub path: String, pub sample_size: Option, pub base_address: Option, + /// Additional libraries to load alongside the primary one. Each entry may + /// optionally specify a base address. If no base address is provided the + /// builder will attempt to place the library in an address region that does + /// not conflict with the main library or previously placed libraries. + pub loaded_libraries: Option>, } impl GadgetLibraryConfig { pub fn build(&self, sleigh: &SleighConfig) -> Result { let mut library_sleigh = load_sleigh(&self.path, sleigh)?; if let Some(addr) = self.base_address { - library_sleigh.set_base_address(addr) + let aligned = align_up(addr, LIB_ALIGNMENT); + if aligned != addr { + event!( + Level::WARN, + "Main library base address {:#x} is not {:#x}-aligned; aligning to {:#x}", + addr, + LIB_ALIGNMENT, + aligned + ); + } + library_sleigh.set_base_address(aligned) } - GadgetLibrary::build_from_image(library_sleigh, self).map_err(CrackersConfigError::Sleigh) + + // Prepare a vector of sleigh contexts (main + any additional libraries) + // Start with the primary library context. + let mut sleighs = vec![library_sleigh]; + + // If there are additional libraries to load, load them and + // assign base addresses so they do not conflict with the main + // library or each other. + if let Some(loaded) = &self.loaded_libraries { + // Collect occupied ranges from the main library (first entry in `sleighs`) + let mut occupied: Vec<(u64, u64)> = sleighs[0] + .get_sections() + .map(|s| { + let start = s.base_address as u64; + let end = start + s.data.len() as u64; + (start, end) + }) + .collect(); + + let mut current_max: u64 = occupied.iter().map(|(_, e)| *e).max().unwrap_or(0); + + for cfg in loaded { + let mut other = load_sleigh(&cfg.path, sleigh)?; + // Use module-level alignment constants + if let Some(addr) = cfg.base_address { + // If user provided a base address, ensure it is aligned to LIB_ALIGNMENT. + let aligned = align_up(addr, LIB_ALIGNMENT); + if aligned != addr { + event!( + Level::WARN, + "Provided base address {:#x} for '{}' is not {:#x}-aligned; adjusting to {:#x}", + addr, + cfg.path, + LIB_ALIGNMENT, + aligned + ); + } + other.set_base_address(aligned); + } else { + // Place the library after the current known max address with a small gap, + // then align up to LIB_ALIGNMENT to guarantee alignment. + let base_hint = if current_max == 0 { + LIB_GAP + } else { + current_max + LIB_GAP + }; + let candidate = align_up(base_hint, LIB_ALIGNMENT); + event!( + Level::INFO, + "Auto-placing '{}' at aligned address {:#x} (base hint {:#x})", + cfg.path, + candidate, + base_hint + ); + other.set_base_address(candidate); + } + + // Update occupied ranges and current_max with this library's sections + for s in other.get_sections() { + let start = s.base_address as u64; + let end = start + s.data.len() as u64; + occupied.push((start, end)); + if end > current_max { + current_max = end; + } + } + + // Keep the loaded context so we can pass all contexts to the + // gadget library builder. + sleighs.push(other); + } + } + + // Build gadget library from all provided sleigh contexts. + GadgetLibrary::build_from_image(sleighs, self).map_err(CrackersConfigError::Sleigh) } } @@ -69,11 +177,29 @@ fn default_blacklist() -> HashSet { ]) } -/** +#[cfg(feature = "pyo3")] +#[pymethods] +impl LoadedLibraryConfig { + #[getter] + pub fn get_path(&self) -> &str { + self.path.as_str() + } + + #[setter] + pub fn set_path(&mut self, p: String) { + self.path = p; + } -pub sample_size: Option, -pub base_address: Option, -*/ + #[getter] + pub fn get_base_address(&self) -> Option { + self.base_address + } + + #[setter] + pub fn set_base_address(&mut self, a: Option) { + self.base_address = a; + } +} #[cfg(feature = "pyo3")] #[pymethods] @@ -117,4 +243,14 @@ impl GadgetLibraryConfig { pub fn set_base_address(&mut self, l: Option) { self.base_address = l; } + + #[getter] + pub fn get_loaded_libraries(&self) -> Option> { + self.loaded_libraries.clone() + } + + #[setter] + pub fn set_loaded_libraries(&mut self, l: Option>) { + self.loaded_libraries = l; + } } diff --git a/crackers/src/gadget/library/mod.rs b/crackers/src/gadget/library/mod.rs index b3ff9fc..deef160 100644 --- a/crackers/src/gadget/library/mod.rs +++ b/crackers/src/gadget/library/mod.rs @@ -47,27 +47,34 @@ impl GadgetLibrary { TraceCandidateIterator::new(info, r, trace.to_vec()) } pub(super) fn build_from_image( - sleigh: LoadedSleighContext, + sleighs: Vec, builder: &GadgetLibraryConfig, ) -> Result { + // We expect at least one sleigh (the primary library) to be provided. + // Use the first sleigh's arch info / language id as the library-wide info. + let mut iter = sleighs.into_iter(); + let first = iter.next().unwrap(); let mut lib: GadgetLibrary = GadgetLibrary { gadgets: vec![], - arch_info: sleigh.arch_info().clone(), - language_id: sleigh.get_language_id().to_string(), + arch_info: first.arch_info().clone(), + language_id: first.get_language_id().to_string(), }; - event!(Level::INFO, "Loading gadgets from sleigh"); - for section in sleigh.get_sections().filter(|s| s.perms.exec) { + + event!(Level::INFO, "Loading gadgets from sleighs"); + + // process the first sleigh + for section in first.get_sections().filter(|s| s.perms.exec) { let start = section.base_address as u64; let end = start + section.data.len() as u64; let mut curr = start; while curr < end { let instrs: Vec = - sleigh.read(curr, builder.max_gadget_length).collect(); + first.read(curr, builder.max_gadget_length).collect(); if let Some(i) = instrs.iter().position(|b| b.terminates_basic_block()) { let gadget = Gadget { - code_space_idx: sleigh.arch_info().default_code_space_index(), - spaces: sleigh.arch_info().spaces().to_vec(), + code_space_idx: first.arch_info().default_code_space_index(), + spaces: first.arch_info().spaces().to_vec(), instructions: instrs[0..=i].to_vec(), }; if !gadget.has_blacklisted_op(&builder.operation_blacklist) { @@ -78,6 +85,33 @@ impl GadgetLibrary { } event!(Level::INFO, "Found {} gadgets...", lib.gadgets.len()); } + + // process remaining sleighs (additional loaded libraries) + for sleigh in iter { + for section in sleigh.get_sections().filter(|s| s.perms.exec) { + let start = section.base_address as u64; + let end = start + section.data.len() as u64; + let mut curr = start; + + while curr < end { + let instrs: Vec = + sleigh.read(curr, builder.max_gadget_length).collect(); + if let Some(i) = instrs.iter().position(|b| b.terminates_basic_block()) { + let gadget = Gadget { + code_space_idx: sleigh.arch_info().default_code_space_index(), + spaces: sleigh.arch_info().spaces().to_vec(), + instructions: instrs[0..=i].to_vec(), + }; + if !gadget.has_blacklisted_op(&builder.operation_blacklist) { + lib.gadgets.push(gadget); + } + } + curr += 1 + } + event!(Level::INFO, "Found {} gadgets...", lib.gadgets.len()); + } + } + Ok(lib) } } @@ -103,6 +137,7 @@ mod tests { let sleigh = builder.build("x86:LE:64:default").unwrap(); let bin_sleigh = sleigh.initialize_with_image(file).unwrap(); let _lib = - GadgetLibrary::build_from_image(bin_sleigh, &GadgetLibraryConfig::default()).unwrap(); + GadgetLibrary::build_from_image(vec![bin_sleigh], &GadgetLibraryConfig::default()) + .unwrap(); } } From fbc983c07e52a18a289093ad026a1e08c35bd3fa Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 25 Dec 2025 11:45:32 +0000 Subject: [PATCH 2/5] ruff fmt --- crackers_python/crackers/config/library.py | 17 +++++++++++++++++ crackers_python/crackers/crackers.pyi | 22 +++++++++++++--------- crackers_python/gh_actions_setup.py | 5 ++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/crackers_python/crackers/config/library.py b/crackers_python/crackers/config/library.py index 83716cf..53d480e 100644 --- a/crackers_python/crackers/config/library.py +++ b/crackers_python/crackers/config/library.py @@ -1,6 +1,21 @@ +from typing import List + from pydantic import BaseModel +class LoadedLibraryConfig(BaseModel): + """ + Represents an additional library to load alongside the main library. + + Attributes: + path (str): Filesystem path of the target binary. + base_address (int | None): Optional base address for loading the library (may be adjusted/aligned on the Rust side). + """ + + path: str + base_address: int | None + + class LibraryConfig(BaseModel): """ Configuration for a binary library used in analysis or exploitation. @@ -10,9 +25,11 @@ class LibraryConfig(BaseModel): path (str): Filesystem path of the target binary. sample_size (int | None): Maximum number of gadgets to randomly sample (None to use all gadgets). base_address (int | None): Base address for loading the library, or None if not specified. + loaded_libraries (list[LoadedLibraryConfig] | None): Optional additional libraries to load alongside the primary one. """ max_gadget_length: int path: str sample_size: int | None base_address: int | None + loaded_libraries: list[LoadedLibraryConfig] | None diff --git a/crackers_python/crackers/crackers.pyi b/crackers_python/crackers/crackers.pyi index ff0d508..dd49485 100644 --- a/crackers_python/crackers/crackers.pyi +++ b/crackers_python/crackers/crackers.pyi @@ -1,4 +1,4 @@ -from typing import Callable, Iterable, Optional, Union +from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union from z3 import z3 # type: ignore @@ -65,11 +65,16 @@ class CrackersLogLevel: Trace: int Warn: int +class LoadedLibraryConfig: + path: str + base_address: Optional[int] + class GadgetLibraryConfig: max_gadget_length: int path: str sample_size: Optional[int] base_address: Optional[int] + loaded_libraries: Optional[List[LoadedLibraryConfig]] class MemoryEqualityConstraint: space: str @@ -91,7 +96,7 @@ class PointerRangeConstraints: class SleighConfig: ghidra_path: str -# New: represent the two possible shapes of the specification +# Represent the two possible shapes of the specification class BinaryFileSpecification: """ Represents the binary-file variant of the specification. @@ -110,7 +115,7 @@ class RawPcodeSpecification: raw_pcode: str -# SpecificationConfig is now a discriminated union of the two variants above. +# SpecificationConfig is a discriminated union of the two variants above. SpecificationConfig = Union[BinaryFileSpecification, RawPcodeSpecification] class StateEqualityConstraint: @@ -137,7 +142,7 @@ class SelectionFailure: class PythonDecisionResult_Unsat(DecisionResult): _0: SelectionFailure - pass + __match_args__ = ("_0",) class DecisionResult: AssignmentFound: PythonDecisionResult_AssignmentFound @@ -151,8 +156,7 @@ StateConstraintGenerator = Callable[[State, int], z3.BoolRef] TransitionConstraintGenerator = Callable[[ModeledBlock], z3.BoolRef] class SynthesisParams: - def run(self) -> "DecisionResultType": ... - def add_precondition(self, fn: StateConstraintGenerator): ... - def add_postcondition(self, fn: StateConstraintGenerator): ... - def add_transition_constraint(self, fn: TransitionConstraintGenerator): ... - pass + def run(self) -> DecisionResultType: ... + def add_precondition(self, fn: StateConstraintGenerator) -> None: ... + def add_postcondition(self, fn: StateConstraintGenerator) -> None: ... + def add_transition_constraint(self, fn: TransitionConstraintGenerator) -> None: ... diff --git a/crackers_python/gh_actions_setup.py b/crackers_python/gh_actions_setup.py index 5877777..ea4cd66 100644 --- a/crackers_python/gh_actions_setup.py +++ b/crackers_python/gh_actions_setup.py @@ -69,7 +69,10 @@ def install_z3_glibc(target_platform): github_token = os.environ.get("READ_ONLY_GITHUB_TOKEN") if github_token: request.add_header("Authorization", f"Bearer {github_token}") - print("Using GitHub PAT from READ_ONLY_GITHUB_TOKEN for API authentication.", file=sys.stderr) + print( + "Using GitHub PAT from READ_ONLY_GITHUB_TOKEN for API authentication.", + file=sys.stderr, + ) with urllib.request.urlopen(request) as response: data = json.loads(response.read().decode()) From 0bdbd7a17112c3e657a42981324e0fe5f00c963d Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 25 Dec 2025 12:01:14 +0000 Subject: [PATCH 3/5] ruff agani --- crackers_python/crackers/config/library.py | 2 -- crackers_python/crackers/crackers.pyi | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crackers_python/crackers/config/library.py b/crackers_python/crackers/config/library.py index 53d480e..eb1000f 100644 --- a/crackers_python/crackers/config/library.py +++ b/crackers_python/crackers/config/library.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import BaseModel diff --git a/crackers_python/crackers/crackers.pyi b/crackers_python/crackers/crackers.pyi index dd49485..010cb82 100644 --- a/crackers_python/crackers/crackers.pyi +++ b/crackers_python/crackers/crackers.pyi @@ -1,4 +1,4 @@ -from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union +from typing import Callable, Iterable, List, Optional, Union from z3 import z3 # type: ignore From b7bab242b9444d7761bf2b26f52cd32e2638688f Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 25 Dec 2025 12:02:38 +0000 Subject: [PATCH 4/5] clippy --- crackers/src/gadget/library/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crackers/src/gadget/library/builder.rs b/crackers/src/gadget/library/builder.rs index 1a478e1..c6edc23 100644 --- a/crackers/src/gadget/library/builder.rs +++ b/crackers/src/gadget/library/builder.rs @@ -20,7 +20,7 @@ fn align_up(x: u64, align: u64) -> u64 { if align == 0 { return x; } - ((x + align - 1) / align) * align + x.div_ceil(align) * align } #[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)] From 24b50891b510b00877f7665afa13c9c99f3c326f Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 25 Dec 2025 12:24:50 +0000 Subject: [PATCH 5/5] Set default None for loaded_libraries in LibraryConfig --- crackers_python/crackers/config/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crackers_python/crackers/config/library.py b/crackers_python/crackers/config/library.py index eb1000f..8825516 100644 --- a/crackers_python/crackers/config/library.py +++ b/crackers_python/crackers/config/library.py @@ -30,4 +30,4 @@ class LibraryConfig(BaseModel): path: str sample_size: int | None base_address: int | None - loaded_libraries: list[LoadedLibraryConfig] | None + loaded_libraries: list[LoadedLibraryConfig] | None = None