Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crackers/src/bin/crackers/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ fn new(path: PathBuf, library: Option<PathBuf>) -> anyhow::Result<()> {
path: library_path,
sample_size: None,
base_address: None,
loaded_libraries: None,
},
sleigh: SleighConfig {
ghidra_path: "/Applications/ghidra".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion crackers/src/config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
148 changes: 142 additions & 6 deletions crackers/src/gadget/library/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.div_ceil(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<u64>,
}

#[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)]
#[builder(default)]
Expand All @@ -22,15 +41,104 @@ pub struct GadgetLibraryConfig {
pub path: String,
pub sample_size: Option<usize>,
pub base_address: Option<u64>,
/// 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<Vec<LoadedLibraryConfig>>,
}

impl GadgetLibraryConfig {
pub fn build(&self, sleigh: &SleighConfig) -> Result<GadgetLibrary, CrackersConfigError> {
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)
}
}

Expand Down Expand Up @@ -69,11 +177,29 @@ fn default_blacklist() -> HashSet<OpCode> {
])
}

/**
#[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<usize>,
pub base_address: Option<u64>,
*/
#[getter]
pub fn get_base_address(&self) -> Option<u64> {
self.base_address
}

#[setter]
pub fn set_base_address(&mut self, a: Option<u64>) {
self.base_address = a;
}
}

#[cfg(feature = "pyo3")]
#[pymethods]
Expand Down Expand Up @@ -117,4 +243,14 @@ impl GadgetLibraryConfig {
pub fn set_base_address(&mut self, l: Option<u64>) {
self.base_address = l;
}

#[getter]
pub fn get_loaded_libraries(&self) -> Option<Vec<LoadedLibraryConfig>> {
self.loaded_libraries.clone()
}

#[setter]
pub fn set_loaded_libraries(&mut self, l: Option<Vec<LoadedLibraryConfig>>) {
self.loaded_libraries = l;
}
}
53 changes: 44 additions & 9 deletions crackers/src/gadget/library/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,34 @@ impl GadgetLibrary {
TraceCandidateIterator::new(info, r, trace.to_vec())
}
pub(super) fn build_from_image(
sleigh: LoadedSleighContext,
sleighs: Vec<LoadedSleighContext>,
builder: &GadgetLibraryConfig,
) -> Result<Self, JingleError> {
// 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<Instruction> =
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) {
Expand All @@ -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<Instruction> =
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)
}
}
Expand All @@ -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();
}
}
15 changes: 15 additions & 0 deletions crackers_python/crackers/config/library.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
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.
Expand All @@ -10,9 +23,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 = None
22 changes: 13 additions & 9 deletions crackers_python/crackers/crackers.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Iterable, Optional, Union
from typing import Callable, Iterable, List, Optional, Union

from z3 import z3 # type: ignore

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -137,7 +142,7 @@ class SelectionFailure:

class PythonDecisionResult_Unsat(DecisionResult):
_0: SelectionFailure
pass
__match_args__ = ("_0",)

class DecisionResult:
AssignmentFound: PythonDecisionResult_AssignmentFound
Expand All @@ -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: ...
5 changes: 4 additions & 1 deletion crackers_python/gh_actions_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down