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/activities | |
| 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/activities')
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) = ¶m_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(®.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(), + }) + } + } +} |
