diff options
| author | Mark Rowe <mark@vector35.com> | 2025-08-11 10:42:07 -0700 |
|---|---|---|
| committer | Mark Rowe <mark@vector35.com> | 2025-08-27 19:15:49 -0700 |
| commit | 2303f75b080f6dd0c9a5c669a71f64ce830f5650 (patch) | |
| tree | a49d80ea0a8131a50c16f85e767722679ff08c85 /plugins/workflow_objc/src/metadata | |
| parent | b302d7ba796f41b1102ee61feed8b8e212299997 (diff) | |
Rewrite Obj-C workflow in Rust
This is functionally equivalent to the previous workflow_objc, with the
following changes:
1. It mutates the `core.function.metaAnalysis` workflow rather than
registering a new named workflow. The activities now all check for
the presence of the Objective-C metadata added by `ObjCProcessor` to
determine whether they should do work, rather than relying on
`MachoView` to override the function workflow when Objective-C
metadata is present. This fixes
https://github.com/Vector35/binaryninja-api/issues/6779.
2. The auto-inlining of `objc_msgSend` selector stub functions is
performed in a separate activity from the processing of
`objc_msgSend` call sites. The selector stub inlining activity is
configured so that it does not run in `DSCView` as the shared cache
needs different behavior for stub functions more generally that
`SharedCacheWorkflow` already provides.
3. The way that types like `id` and `SEL` are referenced is fixed so
that they show up as `id` rather than `objc_struct*`.
This also replaces the Objective-C portion of the shared cache's
workflow, and incorporates several bug fixes that had been applied to it
but not the standalone Objective-C workflow.
Diffstat (limited to 'plugins/workflow_objc/src/metadata')
| -rw-r--r-- | plugins/workflow_objc/src/metadata/global_state.rs | 207 | ||||
| -rw-r--r-- | plugins/workflow_objc/src/metadata/mod.rs | 5 | ||||
| -rw-r--r-- | plugins/workflow_objc/src/metadata/selector.rs | 52 |
3 files changed, 264 insertions, 0 deletions
diff --git a/plugins/workflow_objc/src/metadata/global_state.rs b/plugins/workflow_objc/src/metadata/global_state.rs new file mode 100644 index 00000000..61104451 --- /dev/null +++ b/plugins/workflow_objc/src/metadata/global_state.rs @@ -0,0 +1,207 @@ +use binaryninja::{ + binary_view::{BinaryView, BinaryViewBase, BinaryViewExt}, + file_metadata::FileMetadata, + metadata::Metadata, + rc::Ref, + settings::{QueryOptions, Settings}, + ObjectDestructor, +}; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use std::{ + collections::{HashMap, HashSet}, + ops::Range, + sync::{Arc, RwLock}, +}; + +pub struct AnalysisInfo { + pub image_base: u64, + pub objc_stubs: Option<Range<u64>>, + pub should_rewrite_to_direct_calls: bool, + selector_impls: RwLock<SelectorImplsState>, +} + +enum SelectorImplsState { + NotLoaded, + Loaded(Option<SelectorImplementations>), +} + +struct SelectorImplementations { + sel_ref_to_impl: HashMap<u64, Vec<u64>>, + sel_to_impl: HashMap<u64, Vec<u64>>, +} + +static VIEW_INFOS: Lazy<DashMap<usize, Arc<AnalysisInfo>>> = Lazy::new(DashMap::new); +static IGNORED_VIEWS: Lazy<DashMap<usize, bool>> = Lazy::new(DashMap::new); + +struct ObjectLifetimeObserver; + +impl ObjectDestructor for ObjectLifetimeObserver { + fn destruct_file_metadata(&self, metadata: &FileMetadata) { + let id = metadata.session_id(); + VIEW_INFOS.remove(&id); + IGNORED_VIEWS.remove(&id); + } +} + +static SUPPORTED_ARCHS: Lazy<HashSet<&'static str>> = Lazy::new(|| { + let mut m = HashSet::new(); + m.insert("aarch64"); + m.insert("x86_64"); + m.insert("armv7"); + m.insert("thumb2"); + m +}); + +fn is_supported_arch(bv: &BinaryView) -> bool { + let arch_name = bv + .default_arch() + .map(|arch| arch.name()) + .unwrap_or_default(); + SUPPORTED_ARCHS.contains(&arch_name as &str) +} + +pub struct GlobalState; + +impl GlobalState { + pub fn register_cleanup() { + let observer = Box::leak(Box::new(ObjectLifetimeObserver)); + observer.register(); + } + + fn id(bv: &BinaryView) -> usize { + bv.file().session_id() + } + + pub fn analysis_info(bv: &BinaryView) -> Option<Arc<AnalysisInfo>> { + let id = Self::id(bv); + + if let Some(info) = VIEW_INFOS.get(&id) { + if bv.start() == info.image_base { + return Some(info.clone()); + } + } + + let info = Arc::new(AnalysisInfo::from_view(bv)?); + VIEW_INFOS.insert(id, info.clone()); + Some(info) + } + + pub fn should_ignore_view(bv: &BinaryView) -> bool { + if let Some(ignore) = IGNORED_VIEWS.get(&Self::id(bv)) { + return *ignore; + } + + let ignore = !(is_supported_arch(bv) && AnalysisInfo::has_metadata(bv)); + IGNORED_VIEWS.insert(Self::id(bv), ignore); + ignore + } +} + +impl AnalysisInfo { + fn from_view(bv: &BinaryView) -> Option<Self> { + let should_rewrite_to_direct_calls = Settings::new().get_bool_with_opts( + "analysis.objectiveC.resolveDynamicDispatch", + &mut QueryOptions::new_with_view(bv), + ); + let info = AnalysisInfo { + image_base: bv.start(), + objc_stubs: bv + .section_by_name("__objc_stubs") + .map(|section| section.start()..section.end()), + should_rewrite_to_direct_calls, + selector_impls: RwLock::new(SelectorImplsState::NotLoaded), + }; + if !Self::has_metadata(bv) { + return None; + } + Some(info) + } + + fn has_metadata(bv: &BinaryView) -> bool { + bv.query_metadata("Objective-C").is_some() + } + + pub fn get_selector_impl(&self, bv: &BinaryView, selector_addr: u64) -> Option<u64> { + let get = |impls: &SelectorImplementations| { + impls + .sel_ref_to_impl + .get(&selector_addr) + .or_else(|| impls.sel_to_impl.get(&selector_addr)) + .and_then(|v| v.first().copied()) + .filter(|&addr| addr != 0) + }; + + let cache = self.selector_impls.read().unwrap(); + match &*cache { + SelectorImplsState::Loaded(Some(impls)) => return get(impls), + SelectorImplsState::Loaded(None) => return None, + SelectorImplsState::NotLoaded => {} + } + drop(cache); + + let mut cache = self.selector_impls.write().unwrap(); + if let SelectorImplsState::NotLoaded = &*cache { + *cache = SelectorImplsState::Loaded(self.load_selector_impls(bv)); + } + + if let SelectorImplsState::Loaded(Some(impls)) = &*cache { + get(impls) + } else { + None + } + } + + fn load_selector_impls(&self, bv: &BinaryView) -> Option<SelectorImplementations> { + let Some(Ok(meta)) = bv.get_metadata::<HashMap<String, Ref<Metadata>>>("Objective-C") + else { + return None; + }; + let version_meta = meta.get("version")?; + if version_meta.get_unsigned_integer()? != 1 { + log::error!( + "workflow_objc: Unexpected Objective-C metadata version. Expected 1, got {}.", + version_meta.get_unsigned_integer()? + ); + return None; + } + + let mut sel_ref_to_impl = HashMap::new(); + if let Some(sel_ref_to_impl_meta) = meta.get("selRefImplementations") { + if let Some(map) = Self::parse_selector_impls(sel_ref_to_impl_meta) { + sel_ref_to_impl = map; + } + } + + let mut sel_to_impl = HashMap::new(); + if let Some(sel_to_impl_meta) = meta.get("selImplementations") { + if let Some(map) = Self::parse_selector_impls(sel_to_impl_meta) { + sel_to_impl = map; + } + } + + Some(SelectorImplementations { + sel_ref_to_impl, + sel_to_impl, + }) + } + + fn parse_selector_impls(meta: &Metadata) -> Option<HashMap<u64, Vec<u64>>> { + let array = meta.get_array()?; + let mut result = HashMap::new(); + for item in &array { + let item = item.get_array()?; + if item.len() != 2 { + log::warn!( + "Expected selector implementation metadata to have 2 items, found {}", + item.len() + ); + return None; + } + let selector = item.get(0).get_unsigned_integer()?; + let impls_meta = item.get(1).get_unsigned_integer_list()?; + result.insert(selector, impls_meta); + } + Some(result) + } +} diff --git a/plugins/workflow_objc/src/metadata/mod.rs b/plugins/workflow_objc/src/metadata/mod.rs new file mode 100644 index 00000000..58926eb2 --- /dev/null +++ b/plugins/workflow_objc/src/metadata/mod.rs @@ -0,0 +1,5 @@ +mod global_state; +mod selector; + +pub use global_state::GlobalState; +pub use selector::Selector; diff --git a/plugins/workflow_objc/src/metadata/selector.rs b/plugins/workflow_objc/src/metadata/selector.rs new file mode 100644 index 00000000..a48be14d --- /dev/null +++ b/plugins/workflow_objc/src/metadata/selector.rs @@ -0,0 +1,52 @@ +use crate::Error; +use binaryninja::binary_view::{BinaryView, BinaryViewBase as _, BinaryViewExt}; + +pub struct Selector { + pub name: String, + pub addr: u64, +} + +impl Selector { + pub fn from_address(bv: &BinaryView, addr: u64) -> Result<Self, Error> { + let name = if bv.offset_valid(addr) { + // Read the selector name from the binary view + read_cstring(bv, addr, 500) + } else { + // Look for the `sel_` symbols that ObjCProcessor adds to represent selectors + // whose backing regions have not yet been loaded into the view. + bv.symbol_by_address(addr) + .and_then(|sym| sym.raw_name().to_str().ok().map(|name| name.to_owned())) + .filter(|name| name.starts_with("sel_")) + .map(|name| name["sel_".len()..].to_string()) + } + .ok_or(Error::InvalidSelector { address: addr })?; + Ok(Selector { name, addr }) + } + + pub fn argument_labels(&self) -> Vec<String> { + if !self.name.contains(':') { + return vec![]; + } + + self.name + .split(':') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() + } +} + +// Read a null-terminated string from the view +fn read_cstring(bv: &BinaryView, address: u64, max_len: usize) -> Option<String> { + let mut buffer = vec![0u8; max_len]; + let bytes_read = bv.read(&mut buffer, address); + if bytes_read == 0 { + return None; + } + + // Find the null terminator + let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(bytes_read); + buffer.truncate(null_pos); + + String::from_utf8(buffer).ok() +} |
