Skip to content

Commit d1d79b6

Browse files
authored
feat: search for gadgets in multiple libraries (#98)
* feat: allow finding gadgets in multiple files * ruff fmt * ruff agani * clippy * Set default None for loaded_libraries in LibraryConfig
1 parent 971cdc1 commit d1d79b6

File tree

7 files changed

+220
-26
lines changed

7 files changed

+220
-26
lines changed

crackers/src/bin/crackers/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ fn new(path: PathBuf, library: Option<PathBuf>) -> anyhow::Result<()> {
146146
path: library_path,
147147
sample_size: None,
148148
base_address: None,
149+
loaded_libraries: None,
149150
},
150151
sleigh: SleighConfig {
151152
ghidra_path: "/Applications/ghidra".to_string(),

crackers/src/config/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use thiserror::Error;
66
pub enum CrackersConfigError {
77
#[error("Invalid log level")]
88
InvalidLogLevel,
9-
#[error("An error reading a file referenced from the config")]
9+
#[error("An error reading a file referenced from the config: {0}")]
1010
Io(#[from] std::io::Error),
1111
#[error("An error parsing a file with gimli object: {0}")]
1212
Gimli(#[from] object::Error),

crackers/src/gadget/library/builder.rs

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ use crate::config::error::CrackersConfigError;
1111
use crate::config::object::load_sleigh;
1212
use crate::config::sleigh::SleighConfig;
1313
use crate::gadget::library::GadgetLibrary;
14+
use tracing::{Level, event};
15+
16+
const LIB_ALIGNMENT: u64 = 0x4000; // 16 KiB alignment for loaded libraries
17+
const LIB_GAP: u64 = 0x1000; // small gap between libraries when placing
18+
19+
fn align_up(x: u64, align: u64) -> u64 {
20+
if align == 0 {
21+
return x;
22+
}
23+
x.div_ceil(align) * align
24+
}
25+
26+
#[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)]
27+
#[builder(default)]
28+
#[cfg_attr(feature = "pyo3", pyclass)]
29+
pub struct LoadedLibraryConfig {
30+
pub path: String,
31+
pub base_address: Option<u64>,
32+
}
1433

1534
#[derive(Clone, Debug, Default, Builder, Deserialize, Serialize)]
1635
#[builder(default)]
@@ -22,15 +41,104 @@ pub struct GadgetLibraryConfig {
2241
pub path: String,
2342
pub sample_size: Option<usize>,
2443
pub base_address: Option<u64>,
44+
/// Additional libraries to load alongside the primary one. Each entry may
45+
/// optionally specify a base address. If no base address is provided the
46+
/// builder will attempt to place the library in an address region that does
47+
/// not conflict with the main library or previously placed libraries.
48+
pub loaded_libraries: Option<Vec<LoadedLibraryConfig>>,
2549
}
2650

2751
impl GadgetLibraryConfig {
2852
pub fn build(&self, sleigh: &SleighConfig) -> Result<GadgetLibrary, CrackersConfigError> {
2953
let mut library_sleigh = load_sleigh(&self.path, sleigh)?;
3054
if let Some(addr) = self.base_address {
31-
library_sleigh.set_base_address(addr)
55+
let aligned = align_up(addr, LIB_ALIGNMENT);
56+
if aligned != addr {
57+
event!(
58+
Level::WARN,
59+
"Main library base address {:#x} is not {:#x}-aligned; aligning to {:#x}",
60+
addr,
61+
LIB_ALIGNMENT,
62+
aligned
63+
);
64+
}
65+
library_sleigh.set_base_address(aligned)
3266
}
33-
GadgetLibrary::build_from_image(library_sleigh, self).map_err(CrackersConfigError::Sleigh)
67+
68+
// Prepare a vector of sleigh contexts (main + any additional libraries)
69+
// Start with the primary library context.
70+
let mut sleighs = vec![library_sleigh];
71+
72+
// If there are additional libraries to load, load them and
73+
// assign base addresses so they do not conflict with the main
74+
// library or each other.
75+
if let Some(loaded) = &self.loaded_libraries {
76+
// Collect occupied ranges from the main library (first entry in `sleighs`)
77+
let mut occupied: Vec<(u64, u64)> = sleighs[0]
78+
.get_sections()
79+
.map(|s| {
80+
let start = s.base_address as u64;
81+
let end = start + s.data.len() as u64;
82+
(start, end)
83+
})
84+
.collect();
85+
86+
let mut current_max: u64 = occupied.iter().map(|(_, e)| *e).max().unwrap_or(0);
87+
88+
for cfg in loaded {
89+
let mut other = load_sleigh(&cfg.path, sleigh)?;
90+
// Use module-level alignment constants
91+
if let Some(addr) = cfg.base_address {
92+
// If user provided a base address, ensure it is aligned to LIB_ALIGNMENT.
93+
let aligned = align_up(addr, LIB_ALIGNMENT);
94+
if aligned != addr {
95+
event!(
96+
Level::WARN,
97+
"Provided base address {:#x} for '{}' is not {:#x}-aligned; adjusting to {:#x}",
98+
addr,
99+
cfg.path,
100+
LIB_ALIGNMENT,
101+
aligned
102+
);
103+
}
104+
other.set_base_address(aligned);
105+
} else {
106+
// Place the library after the current known max address with a small gap,
107+
// then align up to LIB_ALIGNMENT to guarantee alignment.
108+
let base_hint = if current_max == 0 {
109+
LIB_GAP
110+
} else {
111+
current_max + LIB_GAP
112+
};
113+
let candidate = align_up(base_hint, LIB_ALIGNMENT);
114+
event!(
115+
Level::INFO,
116+
"Auto-placing '{}' at aligned address {:#x} (base hint {:#x})",
117+
cfg.path,
118+
candidate,
119+
base_hint
120+
);
121+
other.set_base_address(candidate);
122+
}
123+
124+
// Update occupied ranges and current_max with this library's sections
125+
for s in other.get_sections() {
126+
let start = s.base_address as u64;
127+
let end = start + s.data.len() as u64;
128+
occupied.push((start, end));
129+
if end > current_max {
130+
current_max = end;
131+
}
132+
}
133+
134+
// Keep the loaded context so we can pass all contexts to the
135+
// gadget library builder.
136+
sleighs.push(other);
137+
}
138+
}
139+
140+
// Build gadget library from all provided sleigh contexts.
141+
GadgetLibrary::build_from_image(sleighs, self).map_err(CrackersConfigError::Sleigh)
34142
}
35143
}
36144

@@ -69,11 +177,29 @@ fn default_blacklist() -> HashSet<OpCode> {
69177
])
70178
}
71179

72-
/**
180+
#[cfg(feature = "pyo3")]
181+
#[pymethods]
182+
impl LoadedLibraryConfig {
183+
#[getter]
184+
pub fn get_path(&self) -> &str {
185+
self.path.as_str()
186+
}
187+
188+
#[setter]
189+
pub fn set_path(&mut self, p: String) {
190+
self.path = p;
191+
}
73192

74-
pub sample_size: Option<usize>,
75-
pub base_address: Option<u64>,
76-
*/
193+
#[getter]
194+
pub fn get_base_address(&self) -> Option<u64> {
195+
self.base_address
196+
}
197+
198+
#[setter]
199+
pub fn set_base_address(&mut self, a: Option<u64>) {
200+
self.base_address = a;
201+
}
202+
}
77203

78204
#[cfg(feature = "pyo3")]
79205
#[pymethods]
@@ -117,4 +243,14 @@ impl GadgetLibraryConfig {
117243
pub fn set_base_address(&mut self, l: Option<u64>) {
118244
self.base_address = l;
119245
}
246+
247+
#[getter]
248+
pub fn get_loaded_libraries(&self) -> Option<Vec<LoadedLibraryConfig>> {
249+
self.loaded_libraries.clone()
250+
}
251+
252+
#[setter]
253+
pub fn set_loaded_libraries(&mut self, l: Option<Vec<LoadedLibraryConfig>>) {
254+
self.loaded_libraries = l;
255+
}
120256
}

crackers/src/gadget/library/mod.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,34 @@ impl GadgetLibrary {
4747
TraceCandidateIterator::new(info, r, trace.to_vec())
4848
}
4949
pub(super) fn build_from_image(
50-
sleigh: LoadedSleighContext,
50+
sleighs: Vec<LoadedSleighContext>,
5151
builder: &GadgetLibraryConfig,
5252
) -> Result<Self, JingleError> {
53+
// We expect at least one sleigh (the primary library) to be provided.
54+
// Use the first sleigh's arch info / language id as the library-wide info.
55+
let mut iter = sleighs.into_iter();
56+
let first = iter.next().unwrap();
5357
let mut lib: GadgetLibrary = GadgetLibrary {
5458
gadgets: vec![],
55-
arch_info: sleigh.arch_info().clone(),
56-
language_id: sleigh.get_language_id().to_string(),
59+
arch_info: first.arch_info().clone(),
60+
language_id: first.get_language_id().to_string(),
5761
};
58-
event!(Level::INFO, "Loading gadgets from sleigh");
59-
for section in sleigh.get_sections().filter(|s| s.perms.exec) {
62+
63+
event!(Level::INFO, "Loading gadgets from sleighs");
64+
65+
// process the first sleigh
66+
for section in first.get_sections().filter(|s| s.perms.exec) {
6067
let start = section.base_address as u64;
6168
let end = start + section.data.len() as u64;
6269
let mut curr = start;
6370

6471
while curr < end {
6572
let instrs: Vec<Instruction> =
66-
sleigh.read(curr, builder.max_gadget_length).collect();
73+
first.read(curr, builder.max_gadget_length).collect();
6774
if let Some(i) = instrs.iter().position(|b| b.terminates_basic_block()) {
6875
let gadget = Gadget {
69-
code_space_idx: sleigh.arch_info().default_code_space_index(),
70-
spaces: sleigh.arch_info().spaces().to_vec(),
76+
code_space_idx: first.arch_info().default_code_space_index(),
77+
spaces: first.arch_info().spaces().to_vec(),
7178
instructions: instrs[0..=i].to_vec(),
7279
};
7380
if !gadget.has_blacklisted_op(&builder.operation_blacklist) {
@@ -78,6 +85,33 @@ impl GadgetLibrary {
7885
}
7986
event!(Level::INFO, "Found {} gadgets...", lib.gadgets.len());
8087
}
88+
89+
// process remaining sleighs (additional loaded libraries)
90+
for sleigh in iter {
91+
for section in sleigh.get_sections().filter(|s| s.perms.exec) {
92+
let start = section.base_address as u64;
93+
let end = start + section.data.len() as u64;
94+
let mut curr = start;
95+
96+
while curr < end {
97+
let instrs: Vec<Instruction> =
98+
sleigh.read(curr, builder.max_gadget_length).collect();
99+
if let Some(i) = instrs.iter().position(|b| b.terminates_basic_block()) {
100+
let gadget = Gadget {
101+
code_space_idx: sleigh.arch_info().default_code_space_index(),
102+
spaces: sleigh.arch_info().spaces().to_vec(),
103+
instructions: instrs[0..=i].to_vec(),
104+
};
105+
if !gadget.has_blacklisted_op(&builder.operation_blacklist) {
106+
lib.gadgets.push(gadget);
107+
}
108+
}
109+
curr += 1
110+
}
111+
event!(Level::INFO, "Found {} gadgets...", lib.gadgets.len());
112+
}
113+
}
114+
81115
Ok(lib)
82116
}
83117
}
@@ -103,6 +137,7 @@ mod tests {
103137
let sleigh = builder.build("x86:LE:64:default").unwrap();
104138
let bin_sleigh = sleigh.initialize_with_image(file).unwrap();
105139
let _lib =
106-
GadgetLibrary::build_from_image(bin_sleigh, &GadgetLibraryConfig::default()).unwrap();
140+
GadgetLibrary::build_from_image(vec![bin_sleigh], &GadgetLibraryConfig::default())
141+
.unwrap();
107142
}
108143
}

crackers_python/crackers/config/library.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
from pydantic import BaseModel
22

33

4+
class LoadedLibraryConfig(BaseModel):
5+
"""
6+
Represents an additional library to load alongside the main library.
7+
8+
Attributes:
9+
path (str): Filesystem path of the target binary.
10+
base_address (int | None): Optional base address for loading the library (may be adjusted/aligned on the Rust side).
11+
"""
12+
13+
path: str
14+
base_address: int | None
15+
16+
417
class LibraryConfig(BaseModel):
518
"""
619
Configuration for a binary library used in analysis or exploitation.
@@ -10,9 +23,11 @@ class LibraryConfig(BaseModel):
1023
path (str): Filesystem path of the target binary.
1124
sample_size (int | None): Maximum number of gadgets to randomly sample (None to use all gadgets).
1225
base_address (int | None): Base address for loading the library, or None if not specified.
26+
loaded_libraries (list[LoadedLibraryConfig] | None): Optional additional libraries to load alongside the primary one.
1327
"""
1428

1529
max_gadget_length: int
1630
path: str
1731
sample_size: int | None
1832
base_address: int | None
33+
loaded_libraries: list[LoadedLibraryConfig] | None = None

crackers_python/crackers/crackers.pyi

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable, Iterable, Optional, Union
1+
from typing import Callable, Iterable, List, Optional, Union
22

33
from z3 import z3 # type: ignore
44

@@ -65,11 +65,16 @@ class CrackersLogLevel:
6565
Trace: int
6666
Warn: int
6767

68+
class LoadedLibraryConfig:
69+
path: str
70+
base_address: Optional[int]
71+
6872
class GadgetLibraryConfig:
6973
max_gadget_length: int
7074
path: str
7175
sample_size: Optional[int]
7276
base_address: Optional[int]
77+
loaded_libraries: Optional[List[LoadedLibraryConfig]]
7378

7479
class MemoryEqualityConstraint:
7580
space: str
@@ -91,7 +96,7 @@ class PointerRangeConstraints:
9196
class SleighConfig:
9297
ghidra_path: str
9398

94-
# New: represent the two possible shapes of the specification
99+
# Represent the two possible shapes of the specification
95100
class BinaryFileSpecification:
96101
"""
97102
Represents the binary-file variant of the specification.
@@ -110,7 +115,7 @@ class RawPcodeSpecification:
110115

111116
raw_pcode: str
112117

113-
# SpecificationConfig is now a discriminated union of the two variants above.
118+
# SpecificationConfig is a discriminated union of the two variants above.
114119
SpecificationConfig = Union[BinaryFileSpecification, RawPcodeSpecification]
115120

116121
class StateEqualityConstraint:
@@ -137,7 +142,7 @@ class SelectionFailure:
137142

138143
class PythonDecisionResult_Unsat(DecisionResult):
139144
_0: SelectionFailure
140-
pass
145+
__match_args__ = ("_0",)
141146

142147
class DecisionResult:
143148
AssignmentFound: PythonDecisionResult_AssignmentFound
@@ -151,8 +156,7 @@ StateConstraintGenerator = Callable[[State, int], z3.BoolRef]
151156
TransitionConstraintGenerator = Callable[[ModeledBlock], z3.BoolRef]
152157

153158
class SynthesisParams:
154-
def run(self) -> "DecisionResultType": ...
155-
def add_precondition(self, fn: StateConstraintGenerator): ...
156-
def add_postcondition(self, fn: StateConstraintGenerator): ...
157-
def add_transition_constraint(self, fn: TransitionConstraintGenerator): ...
158-
pass
159+
def run(self) -> DecisionResultType: ...
160+
def add_precondition(self, fn: StateConstraintGenerator) -> None: ...
161+
def add_postcondition(self, fn: StateConstraintGenerator) -> None: ...
162+
def add_transition_constraint(self, fn: TransitionConstraintGenerator) -> None: ...

crackers_python/gh_actions_setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ def install_z3_glibc(target_platform):
6969
github_token = os.environ.get("READ_ONLY_GITHUB_TOKEN")
7070
if github_token:
7171
request.add_header("Authorization", f"Bearer {github_token}")
72-
print("Using GitHub PAT from READ_ONLY_GITHUB_TOKEN for API authentication.", file=sys.stderr)
72+
print(
73+
"Using GitHub PAT from READ_ONLY_GITHUB_TOKEN for API authentication.",
74+
file=sys.stderr,
75+
)
7376

7477
with urllib.request.urlopen(request) as response:
7578
data = json.loads(response.read().decode())

0 commit comments

Comments
 (0)