summaryrefslogtreecommitdiff
path: root/plugins/workflow_objc/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/workflow_objc/src')
-rw-r--r--plugins/workflow_objc/src/activities/inline_stubs.rs26
-rw-r--r--plugins/workflow_objc/src/activities/mod.rs2
-rw-r--r--plugins/workflow_objc/src/activities/objc_msg_send_calls.rs164
-rw-r--r--plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs123
-rw-r--r--plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs61
-rw-r--r--plugins/workflow_objc/src/error.rs30
-rw-r--r--plugins/workflow_objc/src/lib.rs48
-rw-r--r--plugins/workflow_objc/src/metadata/global_state.rs207
-rw-r--r--plugins/workflow_objc/src/metadata/mod.rs5
-rw-r--r--plugins/workflow_objc/src/metadata/selector.rs52
-rw-r--r--plugins/workflow_objc/src/workflow.rs57
11 files changed, 775 insertions, 0 deletions
diff --git a/plugins/workflow_objc/src/activities/inline_stubs.rs b/plugins/workflow_objc/src/activities/inline_stubs.rs
new file mode 100644
index 00000000..daae9baa
--- /dev/null
+++ b/plugins/workflow_objc/src/activities/inline_stubs.rs
@@ -0,0 +1,26 @@
+use binaryninja::workflow::AnalysisContext;
+
+use crate::{metadata::GlobalState, Error};
+
+// If this function is within a section known to contain Objective-C stubs,
+// mark it as being inlined during analysis.
+pub fn process(ac: &AnalysisContext) -> Result<(), Error> {
+ let view = ac.view();
+ if GlobalState::should_ignore_view(&view) {
+ return Ok(());
+ }
+
+ let func = ac.function();
+ let Some(objc_stubs) = GlobalState::analysis_info(&view)
+ .as_ref()
+ .and_then(|info| info.objc_stubs.clone())
+ else {
+ return Ok(());
+ };
+
+ if objc_stubs.contains(&func.start()) {
+ func.set_auto_inline_during_analysis(true);
+ }
+
+ Ok(())
+}
diff --git a/plugins/workflow_objc/src/activities/mod.rs b/plugins/workflow_objc/src/activities/mod.rs
new file mode 100644
index 00000000..11346900
--- /dev/null
+++ b/plugins/workflow_objc/src/activities/mod.rs
@@ -0,0 +1,2 @@
+pub mod inline_stubs;
+pub mod objc_msg_send_calls;
diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs
new file mode 100644
index 00000000..239b606c
--- /dev/null
+++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs
@@ -0,0 +1,164 @@
+use binaryninja::{
+ binary_view::{BinaryView, BinaryViewExt as _},
+ function::Function,
+ low_level_il::{
+ expression::{ExpressionHandler as _, LowLevelILExpressionKind},
+ function::{LowLevelILFunction, Mutable, NonSSA, SSA},
+ instruction::{InstructionHandler as _, LowLevelILInstruction, LowLevelILInstructionKind},
+ operation::{CallSsa, Operation},
+ },
+ variable::PossibleValueSet,
+ workflow::AnalysisContext,
+};
+
+use crate::{
+ metadata::{GlobalState, Selector},
+ Error,
+};
+
+mod adjust_call_type;
+mod rewrite_to_direct_call;
+
+// Apply all transformations that are specific to calls to `objc_msgSend` and `objc_msgSendSuper2`
+// At present these are:
+// 1. Call type adjustments
+// 2. Rewriting to direct calls, if enabled.
+pub fn process(ac: &AnalysisContext) -> Result<(), Error> {
+ let bv = ac.view();
+ if GlobalState::should_ignore_view(&bv) {
+ return Ok(());
+ }
+
+ let func_start = ac.function().start();
+ let Some(llil) = (unsafe { ac.llil_function() }) else {
+ return Err(Error::MissingLowLevelIL { func_start });
+ };
+
+ let Some(ssa) = llil.ssa_form() else {
+ return Err(Error::MissingSsaForm { func_start });
+ };
+
+ let func = ac.function();
+ let mut function_changed = false;
+ for block in ssa.basic_blocks().iter() {
+ for insn in block.iter() {
+ match process_instruction(&bv, &func, &llil, &ssa, &insn) {
+ Ok(true) => function_changed = true,
+ Ok(_) => {}
+ Err(err) => {
+ log::error!(
+ "Error processing instruction at {:#x}: {}",
+ insn.address(),
+ err
+ );
+ continue;
+ }
+ }
+ }
+ }
+
+ if function_changed {
+ // Regenerate SSA form after modifications
+ llil.generate_ssa_form();
+ }
+ Ok(())
+}
+
+// Process a single instruction, looking for calls to `objc_msgSend` or `objc_msgSendSuper2`
+// Returns `Ok(false)` if the instruction is not a relevant call or if the function was not
+// modified. Returns `Ok(true)` to indicate that the function was modified.
+fn process_instruction(
+ bv: &BinaryView,
+ func: &Function,
+ llil: &LowLevelILFunction<Mutable, NonSSA>,
+ ssa: &LowLevelILFunction<Mutable, SSA>,
+ insn: &LowLevelILInstruction<Mutable, SSA>,
+) -> Result<bool, &'static str> {
+ let call_op = match insn.kind() {
+ LowLevelILInstructionKind::CallSsa(op) => op,
+ LowLevelILInstructionKind::TailCallSsa(op) => op,
+ _ => return Ok(false),
+ };
+
+ let target = call_op.target();
+ let target_values = target.possible_values();
+
+ // Check if the target is a constant pointer to objc_msgSend
+ let call_target = match target_values {
+ PossibleValueSet::ConstantValue { value }
+ | PossibleValueSet::ConstantPointerValue { value }
+ | PossibleValueSet::ImportedAddressValue { value } => value as u64,
+ _ => return Ok(false),
+ };
+
+ let Some(message_send_type) = call_target_type(bv, call_target) else {
+ return Ok(false);
+ };
+
+ let Some(selector) = selector_from_call(bv, ssa, &call_op) else {
+ return Ok(false);
+ };
+
+ let mut function_changed = false;
+ if adjust_call_type::process_call(bv, func, insn, &selector, message_send_type).is_ok() {
+ function_changed = true;
+ }
+
+ if rewrite_to_direct_call::process_call(bv, llil, insn, &selector, message_send_type).is_ok() {
+ function_changed = true;
+ }
+
+ Ok(function_changed)
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum MessageSendType {
+ Normal,
+ Super,
+}
+
+fn call_target_type(bv: &BinaryView, call_target: u64) -> Option<MessageSendType> {
+ let name = bv
+ .symbol_by_address(call_target)
+ .map(|s| s.raw_name().to_string_lossy().into_owned())?;
+
+ // Strip the `j_` prefix that the shared cache adds to the names of stub functions
+ let name = name.strip_prefix("j_").unwrap_or(&name);
+
+ if name == "_objc_msgSend" {
+ Some(MessageSendType::Normal)
+ } else if name == "_objc_msgSendSuper" || name == "_objc_msgSendSuper2" {
+ Some(MessageSendType::Super)
+ } else {
+ None
+ }
+}
+
+fn selector_from_call(
+ bv: &BinaryView,
+ ssa: &LowLevelILFunction<Mutable, SSA>,
+ call_op: &Operation<Mutable, SSA, CallSsa>,
+) -> Option<Selector> {
+ let LowLevelILExpressionKind::CallParamSsa(params) = &call_op.param_expr().kind() else {
+ return None;
+ };
+
+ let param_exprs = params.param_exprs();
+ let param_exprs =
+ if let LowLevelILExpressionKind::SeparateParamListSsa(params) = &param_exprs[0].kind() {
+ params.param_exprs()
+ } else {
+ param_exprs
+ };
+
+ let LowLevelILExpressionKind::RegSsa(reg) = param_exprs.get(1)?.kind() else {
+ return None;
+ };
+
+ let raw_selector = ssa.get_ssa_register_value(&reg.source_reg())?.value as u64;
+ if raw_selector == 0 {
+ return None;
+ }
+
+ Selector::from_address(bv, raw_selector).ok()
+}
diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs
new file mode 100644
index 00000000..21a3d35f
--- /dev/null
+++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs
@@ -0,0 +1,123 @@
+use binaryninja::{
+ binary_view::{BinaryView, BinaryViewBase as _, BinaryViewExt},
+ confidence::Conf,
+ function::Function,
+ low_level_il::{
+ function::{Mutable, SSA},
+ instruction::LowLevelILInstruction,
+ },
+ rc::Ref,
+ types::{FunctionParameter, Type},
+};
+
+use super::MessageSendType;
+use crate::{metadata::Selector, Error};
+
+const HEURISTIC_CONFIDENCE: u8 = 192;
+
+fn named_type(bv: &BinaryView, name: &str) -> Option<Ref<Type>> {
+ bv.type_by_name(name)
+ .map(|t| Type::named_type_from_type(name, &t))
+}
+
+pub fn process_call(
+ bv: &BinaryView,
+ func: &Function,
+ insn: &LowLevelILInstruction<Mutable, SSA>,
+ selector: &Selector,
+ message_send_type: MessageSendType,
+) -> Result<(), Error> {
+ let arch = func.arch();
+ let id = named_type(bv, "id").unwrap_or_else(|| Type::pointer(&arch, &Type::void()));
+ let (receiver_type, receiver_name) = match message_send_type {
+ MessageSendType::Normal => (id.clone(), "self"),
+ MessageSendType::Super => (
+ Type::pointer(
+ &arch,
+ &named_type(bv, "objc_super").unwrap_or_else(Type::void),
+ ),
+ "super",
+ ),
+ };
+ let sel = named_type(bv, "SEL").unwrap_or_else(|| Type::pointer(&arch, &Type::char()));
+
+ // TODO: Infer return type based on receiver type / selector.
+ let return_type = id.clone();
+
+ let mut params = vec![
+ FunctionParameter::new(receiver_type, receiver_name.to_string(), None),
+ FunctionParameter::new(sel, "sel".to_string(), None),
+ ];
+
+ let argument_labels = selector.argument_labels();
+ let mut argument_names = generate_argument_names(&argument_labels);
+
+ // Pad out argument names if necessary
+ for i in argument_names.len()..argument_labels.len() {
+ argument_names.push(format!("arg{i}"));
+ }
+
+ // Create types for all arguments. For now they're all signed integers of the platform word size.
+ let arg_type = Type::int(bv.address_size(), true);
+ params.extend(
+ argument_names
+ .into_iter()
+ .map(|name| FunctionParameter::new(arg_type.clone(), name, None)),
+ );
+
+ let func_type = Type::function(&return_type, params, false);
+ func.set_auto_call_type_adjustment(
+ insn.address(),
+ Conf::new(func_type, HEURISTIC_CONFIDENCE).as_ref(),
+ Some(arch),
+ );
+
+ Ok(())
+}
+
+fn selector_label_without_prefix(prefix: &str, label: &str) -> Option<String> {
+ if label.len() <= prefix.len() || !label.starts_with(prefix) {
+ return None;
+ }
+
+ let after_prefix = &label[prefix.len()..];
+
+ // If the character after the prefix is lowercase, the label may be something like "settings"
+ // in which case "set" should not be considered a prefix.
+ let (first, rest) = after_prefix.split_at_checked(1)?;
+ if first.chars().next()?.is_lowercase() {
+ return None;
+ }
+
+ // Lowercase the first character if the second character is not also uppercase.
+ // This ensures we leave initialisms such as `URL` alone.
+ let (second, rest) = rest.split_at_checked(1)?;
+ Some(match second.chars().next() {
+ Some(c) if c.is_lowercase() => {
+ format!("{}{}{}", first.to_lowercase(), second, rest)
+ }
+ _ => after_prefix.to_string(),
+ })
+}
+
+fn argument_name_from_selector_label(label: &str) -> String {
+ // TODO: Handle other common patterns such as <do some action>With<arg>: and <do some action>For<arg>:
+ let prefixes = [
+ "initWith", "with", "and", "using", "set", "read", "to", "for",
+ ];
+
+ for prefix in prefixes {
+ if let Some(arg_name) = selector_label_without_prefix(prefix, label) {
+ return arg_name;
+ }
+ }
+
+ label.to_owned()
+}
+
+fn generate_argument_names(labels: &[String]) -> Vec<String> {
+ labels
+ .iter()
+ .map(|label| argument_name_from_selector_label(label))
+ .collect()
+}
diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs
new file mode 100644
index 00000000..0ddf2945
--- /dev/null
+++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs
@@ -0,0 +1,61 @@
+use binaryninja::{
+ binary_view::BinaryView,
+ low_level_il::{
+ function::{LowLevelILFunction, Mutable, NonSSA, SSA},
+ instruction::{InstructionHandler as _, LowLevelILInstruction, LowLevelILInstructionKind},
+ },
+};
+
+use super::MessageSendType;
+use crate::{
+ metadata::{GlobalState, Selector},
+ Error,
+};
+
+// TODO: This always rewrites to the first known implementation of a given selector.
+// This is Not Good as it will pick the wrong target for any common selector.
+// In order to do better we need to consider receiver type information.
+pub fn process_call(
+ bv: &BinaryView,
+ llil: &LowLevelILFunction<Mutable, NonSSA>,
+ insn: &LowLevelILInstruction<Mutable, SSA>,
+ selector: &Selector,
+ message_send_type: MessageSendType,
+) -> Result<(), Error> {
+ if message_send_type == MessageSendType::Super {
+ return Ok(());
+ }
+
+ let Some(info) =
+ GlobalState::analysis_info(bv).filter(|info| info.should_rewrite_to_direct_calls)
+ else {
+ return Ok(());
+ };
+
+ let Some(impl_address) = info.get_selector_impl(bv, selector.addr) else {
+ return Ok(());
+ };
+
+ // Change the destination expression of the call instruction to point directly to
+ // the method implementation.
+ let llil_insn = insn.non_ssa_form(llil);
+ match llil_insn.kind() {
+ LowLevelILInstructionKind::Call(call_op) | LowLevelILInstructionKind::TailCall(call_op) => {
+ let dest_expr = call_op.target();
+ unsafe {
+ llil.replace_expression(dest_expr.index, llil.const_ptr(impl_address));
+ }
+ Ok(())
+ }
+ _ => {
+ log::error!(
+ "Unexpected LLIL operation for objc_msgSend call at {:#x}",
+ insn.address()
+ );
+ Err(Error::UnexpectedLlilOperation {
+ address: insn.address(),
+ expected: "Call or TailCall".to_string(),
+ })
+ }
+ }
+}
diff --git a/plugins/workflow_objc/src/error.rs b/plugins/workflow_objc/src/error.rs
new file mode 100644
index 00000000..75328226
--- /dev/null
+++ b/plugins/workflow_objc/src/error.rs
@@ -0,0 +1,30 @@
+use thiserror::Error;
+
+/// A marker type for workflow registration errors
+#[derive(Debug, Error)]
+#[error("Failed to register workflow activity")]
+pub struct WorkflowRegistrationError;
+
+impl From<()> for WorkflowRegistrationError {
+ fn from(_: ()) -> Self {
+ WorkflowRegistrationError
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Unable to retrieve low-level IL for function at {func_start:#x}")]
+ MissingLowLevelIL { func_start: u64 },
+
+ #[error("Unable to retrieve low-level SSA IL for function at {func_start:#x}")]
+ MissingSsaForm { func_start: u64 },
+
+ #[error("Unexpected LLIL operation at address {address:#x} (expected {expected})")]
+ UnexpectedLlilOperation { address: u64, expected: String },
+
+ #[error("Invalid selector at address {address:#x}")]
+ InvalidSelector { address: u64 },
+
+ #[error(transparent)]
+ WorkflowRegistrationFailed(#[from] WorkflowRegistrationError),
+}
diff --git a/plugins/workflow_objc/src/lib.rs b/plugins/workflow_objc/src/lib.rs
new file mode 100644
index 00000000..d877301a
--- /dev/null
+++ b/plugins/workflow_objc/src/lib.rs
@@ -0,0 +1,48 @@
+use binaryninja::{add_optional_plugin_dependency, logger::Logger, settings::Settings};
+
+mod activities;
+mod error;
+mod metadata;
+mod workflow;
+
+pub use error::Error;
+use metadata::GlobalState;
+
+use log::LevelFilter;
+
+#[no_mangle]
+#[allow(non_snake_case)]
+pub extern "C" fn CorePluginDependencies() {
+ add_optional_plugin_dependency("arch_x86");
+ add_optional_plugin_dependency("arch_armv7");
+ add_optional_plugin_dependency("arch_arm64");
+}
+
+#[no_mangle]
+#[allow(non_snake_case)]
+pub extern "C" fn CorePluginInit() -> bool {
+ Logger::new("Plugin.Objective-C")
+ .with_level(LevelFilter::Debug)
+ .init();
+
+ if workflow::register_activities().is_err() {
+ log::warn!("Failed to register Objective-C workflow");
+ return false;
+ };
+
+ let settings = Settings::new();
+ settings.register_setting_json(
+ "analysis.objectiveC.resolveDynamicDispatch",
+ r#"{
+ "title" : "Resolve Dynamic Dispatch Calls",
+ "type" : "boolean",
+ "default" : false,
+ "aliases": ["core.function.objectiveC.assumeMessageSendTarget", "core.function.objectiveC.rewriteMessageSendTarget"],
+ "description" : "Replaces objc_msgSend calls with direct calls to the first found implementation when the target method is visible. May produce false positives when multiple classes implement the same selector or when selectors conflict with system framework methods."
+ }"#,
+ );
+
+ GlobalState::register_cleanup();
+
+ true
+}
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()
+}
diff --git a/plugins/workflow_objc/src/workflow.rs b/plugins/workflow_objc/src/workflow.rs
new file mode 100644
index 00000000..090a8e5e
--- /dev/null
+++ b/plugins/workflow_objc/src/workflow.rs
@@ -0,0 +1,57 @@
+use binaryninja::workflow::{activity, Activity, AnalysisContext, Workflow};
+
+use crate::{activities, error::WorkflowRegistrationError};
+
+const WORKFLOW_INFO: &str = r#"{
+ "title": "Objective-C",
+ "description": "Enhanced analysis for Objective-C code.",
+ "capabilities": []
+}"#;
+
+fn run<E: std::fmt::Debug>(
+ func: impl Fn(&AnalysisContext) -> Result<(), E>,
+) -> impl Fn(&AnalysisContext) {
+ move |ac| {
+ if let Err(err) = func(ac) {
+ log::debug!("Error occurred while running activity: {err:#x?}");
+ }
+ }
+}
+
+pub fn register_activities() -> Result<(), WorkflowRegistrationError> {
+ let workflow =
+ Workflow::cloned("core.function.metaAnalysis").ok_or(WorkflowRegistrationError)?;
+
+ let objc_msg_send_calls_activity = Activity::new_with_action(
+ activity::Config::action(
+ "core.function.objectiveC.analyzeMessageSends",
+ "Obj-C: Analyze Message Sends",
+ "Analyze inline objc_msgSend calls, including applying call type adjustments and resolving to direct calls (if enabled)",
+ ).eligibility(
+ activity::Eligibility::auto().predicate(
+ activity::ViewType::in_(["Mach-O", "DSCView"]),
+ )),
+ run(activities::objc_msg_send_calls::process),
+ );
+
+ let inline_stubs_activity = Activity::new_with_action(
+ activity::Config::action(
+ "core.function.objectiveC.inlineStubs",
+ "Obj-C: Inline Message Send Stubs",
+ "Inline Objective-C selector stubs, such as _objc_msgSend$foo, into their callers",
+ )
+ .eligibility(
+ activity::Eligibility::without_setting()
+ // The shared cache view does its own inlining of stub functions.
+ .predicate(activity::ViewType::in_(["Mach-O"])),
+ ),
+ run(activities::inline_stubs::process),
+ );
+
+ workflow
+ .activity_after(&inline_stubs_activity, "core.function.translateTailCalls")?
+ .activity_after(&objc_msg_send_calls_activity, &inline_stubs_activity.name())?
+ .register_with_config(WORKFLOW_INFO)?;
+
+ Ok(())
+}