summaryrefslogtreecommitdiff
path: root/plugins/workflow_objc/src/activities
diff options
context:
space:
mode:
authorMark Rowe <mark@vector35.com>2025-08-11 10:42:07 -0700
committerMark Rowe <mark@vector35.com>2025-08-27 19:15:49 -0700
commit2303f75b080f6dd0c9a5c669a71f64ce830f5650 (patch)
treea49d80ea0a8131a50c16f85e767722679ff08c85 /plugins/workflow_objc/src/activities
parentb302d7ba796f41b1102ee61feed8b8e212299997 (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/activities')
-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
5 files changed, 376 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(),
+ })
+ }
+ }
+}