diff --git a/objectivec/objc.cpp b/objectivec/objc.cpp index d3ab8525fe..814e4c0803 100644 --- a/objectivec/objc.cpp +++ b/objectivec/objc.cpp @@ -918,8 +918,11 @@ void ObjCProcessor::LoadProtocols(ObjCReader* reader, Ref
listSection) } } -void ObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth) +void ObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase) { + // `typesAreOffsetsFromSelectorBase` is only relevant for shared caches + (void)typesAreOffsetsFromSelectorBase; + uint64_t offset = reader->GetOffset(); meth.name = offset + reader->ReadS32(); @@ -980,15 +983,16 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri uint64_t pointerSize = m_data->GetAddressSize(); bool relativeOffsets = (head.entsizeAndFlags & 0xFFFF0000) & 0x80000000; bool directSelectors = (head.entsizeAndFlags & 0xFFFF0000) & 0x40000000; + bool typesAreOffsetsFromSelectorBase = (head.entsizeAndFlags & 0xFFFF0000) & 0x20000000; auto methodSize = relativeOffsets ? 12 : pointerSize * 3; DefineObjCSymbol(DataSymbol, m_typeNames.methodList, "method_list_" + std::string(name), start, true); for (unsigned i = 0; i < head.count; i++) { + auto cursor = start + sizeof(method_list_t) + (i * methodSize); try { Method method; - auto cursor = start + sizeof(method_list_t) + (i * methodSize); reader->Seek(cursor); method_t meth; // workflow_objc support @@ -997,7 +1001,7 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri // -- if (relativeOffsets) { - GetRelativeMethod(reader, meth); + GetRelativeMethod(reader, meth, typesAreOffsetsFromSelectorBase); } else { @@ -1050,8 +1054,12 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri m_selRefToImplementations[selRefAddr].push_back(meth.imp); // -- - DefineObjCSymbol(DataSymbol, relativeOffsets ? m_typeNames.methodEntry : m_typeNames.method, - "method_" + method.name, cursor, true); + QualifiedName methodTypeName = m_typeNames.method; + if (relativeOffsets) + methodTypeName = typesAreOffsetsFromSelectorBase && !m_typeNames.methodEntryTypeOffsets.IsEmpty() + ? m_typeNames.methodEntryTypeOffsets + : m_typeNames.methodEntry; + DefineObjCSymbol(DataSymbol, methodTypeName, "method_" + method.name, cursor, true); method.imp = meth.imp; cls.methodList[cursor] = method; m_localMethods[cursor] = method; @@ -1061,10 +1069,14 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri if (selRefAddr) m_data->AddDataReference(selRefAddr, meth.imp); } + catch (const std::exception& ex) + { + m_logger->LogErrorF( + "Failed to process a method at offset {:#x} in method list \"{}\": {}", cursor, name, ex.what()); + } catch (...) { - m_logger->LogError( - "Failed to process a method at offset 0x%llx", start + sizeof(method_list_t) + (i * methodSize)); + m_logger->LogErrorF("Failed to process a method at offset {:#x} in method list \"{}\"", cursor, name); } } } @@ -1512,6 +1524,23 @@ void ObjCProcessor::ProcessObjCData() auto type = finalizeStructureBuilder(m_data, methodEntry, "objc_method_entry_t"); m_typeNames.methodEntry = type.first; + // Shared caches built with type offsets store the `types` field as an offset from the same base address as + // relative selectors. That base address is only known for shared caches, so the struct is only defined for them. + if (relativeSelectorBaseOffset) + { + auto relativeTypesPtrName = defineTypedef(m_data, {"rel_types"}, + TypeBuilder::PointerType(4, Type::PointerType(addrSize, Type::IntegerType(1, false))) + .SetPointerBase(RelativeToConstantPointerBaseType, relativeSelectorBaseOffset) + .Finalize()); + + StructureBuilder methodEntryTypeOffsets; + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeSelectorPtrName), "name"); + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeTypesPtrName), "types"); + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeIMPPtrName), "imp"); + type = finalizeStructureBuilder(m_data, methodEntryTypeOffsets, "objc_method_entry_type_offsets_t"); + m_typeNames.methodEntryTypeOffsets = type.first; + } + StructureBuilder method; method.AddMember(Type::PointerType(addrSize, Type::IntegerType(1, true)), "name"); method.AddMember(Type::PointerType(addrSize, Type::IntegerType(1, true)), "types"); diff --git a/objectivec/objc.h b/objectivec/objc.h index e0b2167e4d..991d1daac3 100644 --- a/objectivec/objc.h +++ b/objectivec/objc.h @@ -268,6 +268,7 @@ namespace BinaryNinja { QualifiedName imageInfoSwiftVersion; QualifiedName imageInfo; QualifiedName methodEntry; + QualifiedName methodEntryTypeOffsets; QualifiedName method; QualifiedName methodList; QualifiedName classRO; @@ -337,7 +338,7 @@ namespace BinaryNinja { Ref m_logger; virtual uint64_t GetObjCRelativeMethodBaseAddress(ObjCReader* reader); - virtual void GetRelativeMethod(ObjCReader* reader, method_t& meth); + virtual void GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase); virtual std::shared_ptr GetReader() = 0; // Because an objective-c processor might have access to other non-view symbols that we want to retrieve. // By default, this will just get symbol at the address in the view. diff --git a/plugins/workflow_objc/src/activities/mod.rs b/plugins/workflow_objc/src/activities/mod.rs index b418c1a655..2ff180710c 100644 --- a/plugins/workflow_objc/src/activities/mod.rs +++ b/plugins/workflow_objc/src/activities/mod.rs @@ -1,5 +1,6 @@ pub mod alloc_init; pub mod inline_stubs; +pub mod name_stubs; pub mod objc_msg_send_calls; pub mod remove_memory_management; pub mod super_init; diff --git a/plugins/workflow_objc/src/activities/name_stubs.rs b/plugins/workflow_objc/src/activities/name_stubs.rs new file mode 100644 index 0000000000..f39d6aa0e1 --- /dev/null +++ b/plugins/workflow_objc/src/activities/name_stubs.rs @@ -0,0 +1,99 @@ +use binaryninja::{ + low_level_il::instruction::{InstructionHandler as _, LowLevelILInstructionKind}, + symbol::{Symbol, SymbolType}, + variable::PossibleValueSet, + workflow::AnalysisContext, +}; + +use crate::{ + activities::objc_msg_send_calls::{call_target_type, selector_from_call, MessageSendType}, + error::ILLevel, + metadata::GlobalState, + Error, +}; + +// Reconstruct names for Objective-C selector stubs, e.g. `_objc_msgSend$length`. +// The compiler emits one of these stubs per selector. Each one does nothing but load the selector and tail-call +// `objc_msgSend`. +pub fn process(ac: &AnalysisContext) -> Result<(), Error> { + let view = ac.view(); + if GlobalState::should_ignore_view(&view) { + return Ok(()); + } + + let func = ac.function(); + let func_start = func.start(); + + // Don't override a name the user has assigned to this function. + if let Some(symbol) = view.symbol_by_address(func_start) { + if !symbol.auto_defined() { + return Ok(()); + } + } + + // Bail out early for functions that do not look like a stub: a single basic block of a few instructions. + const MAX_STUB_SIZE: u64 = 32; + if func.highest_address().saturating_sub(func_start) > MAX_STUB_SIZE + || func.basic_blocks().len() != 1 + { + return Ok(()); + } + + let Some(llil) = (unsafe { ac.llil_function() }) else { + return Err(Error::MissingIL { + level: ILLevel::Low, + func_start, + }); + }; + let Some(ssa) = llil.ssa_form() else { + return Err(Error::MissingSsaForm { + level: ILLevel::Low, + func_start, + }); + }; + + // The tail call terminates the stub's single basic block, so it can only be the last instruction. + let blocks = ssa.basic_blocks(); + let Some(block) = blocks.iter().next() else { + return Ok(()); + }; + let Some(insn) = block.iter().last() else { + return Ok(()); + }; + let LowLevelILInstructionKind::TailCallSsa(call_op) = insn.kind() else { + return Ok(()); + }; + + // A selector stub does nothing besides load a selector and tail-call objc_msgSend. + // Reject any function that performs a regular call or memory write before the tail call. + if block.iter().any(|insn| { + matches!( + insn.kind(), + LowLevelILInstructionKind::CallSsa(_) + | LowLevelILInstructionKind::Store(_) + | LowLevelILInstructionKind::StoreSsa(_) + ) + }) { + return Ok(()); + } + + let call_target = match call_op.target().possible_values() { + PossibleValueSet::ConstantValue { value } + | PossibleValueSet::ConstantPointerValue { value } + | PossibleValueSet::ImportedAddressValue { value } => value as u64, + _ => return Ok(()), + }; + + if call_target_type(&view, call_target) != Some(MessageSendType::Normal) { + return Ok(()); + } + let Some(selector) = selector_from_call(&view, &ssa, &call_op) else { + return Ok(()); + }; + + let name = format!("_objc_msgSend${}", selector.name); + let symbol = Symbol::builder(SymbolType::Function, &name, func_start).create(); + view.define_auto_symbol(&symbol); + + Ok(()) +} diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs index 41fc38c065..1d9c580ef4 100644 --- a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs +++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs @@ -119,12 +119,12 @@ fn process_instruction( } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum MessageSendType { +pub(crate) enum MessageSendType { Normal, Super, } -fn call_target_type(bv: &BinaryView, call_target: u64) -> Option { +pub(crate) fn call_target_type(bv: &BinaryView, call_target: u64) -> Option { let name = bv .symbol_by_address(call_target) .map(|s| s.raw_name().to_string_lossy().into_owned())?; @@ -141,7 +141,7 @@ fn call_target_type(bv: &BinaryView, call_target: u64) -> Option, call_op: &Operation, diff --git a/plugins/workflow_objc/src/workflow.rs b/plugins/workflow_objc/src/workflow.rs index 8c52e0f48e..be8c1be16b 100644 --- a/plugins/workflow_objc/src/workflow.rs +++ b/plugins/workflow_objc/src/workflow.rs @@ -37,6 +37,19 @@ pub fn register_activities() -> Result<(), WorkflowRegistrationError> { run(activities::objc_msg_send_calls::process), ); + let name_stubs_activity = Activity::new_with_action( + activity::Config::action( + "core.function.objectiveC.nameSelectorStubs", + "Obj-C: Rename Message Send Stubs", + "Reconstruct names for Objective-C selector stubs, such as _objc_msgSend$foo, that have no symbol table entry.", + ) + .eligibility( + activity::Eligibility::auto().predicate( + activity::ViewType::in_(["Mach-O", "DSCView"]), + )), + run(activities::name_stubs::process), + ); + let inline_stubs_activity = Activity::new_with_action( activity::Config::action( "core.function.objectiveC.inlineStubs", @@ -93,7 +106,8 @@ pub fn register_activities() -> Result<(), WorkflowRegistrationError> { ); workflow - .activity_after(&inline_stubs_activity, "core.function.translateTailCalls")? + .activity_after(&name_stubs_activity, "core.function.translateTailCalls")? + .activity_after(&inline_stubs_activity, &name_stubs_activity.name())? .activity_after(&objc_msg_send_calls_activity, &inline_stubs_activity.name())? .activity_before( &remove_memory_management_activity, diff --git a/view/sharedcache/core/MachO.cpp b/view/sharedcache/core/MachO.cpp index 7afcbd8d0a..8393b0eeef 100644 --- a/view/sharedcache/core/MachO.cpp +++ b/view/sharedcache/core/MachO.cpp @@ -531,8 +531,16 @@ std::vector SharedCacheMachOHeader::ReadSymbolTable(VirtualMemory& { if (!flags.has_value()) { - // TODO: where logger? - LogErrorF("Symbol {:?} at address {:#x} is not in any section", symbolName.c_str(), symbolAddress); + // In iOS / macOS 27 shared caches, sections such as __objc_stubs are coalesced out of + // individual dylibs, leaving a zero-size section whose symbols no longer point at + // anything in this image. These are not an error. + bool coalescedSection = nlist.n_sect > 0 && (size_t)(nlist.n_sect - 1) < sections.size() + && sections[nlist.n_sect - 1].size == 0; + if (!coalescedSection) + { + // TODO: where logger? + LogErrorF("Symbol {:?} at address {:#x} is not in any section", symbolName, symbolAddress); + } continue; } diff --git a/view/sharedcache/core/ObjC.cpp b/view/sharedcache/core/ObjC.cpp index b411ed8752..64fecb0818 100644 --- a/view/sharedcache/core/ObjC.cpp +++ b/view/sharedcache/core/ObjC.cpp @@ -92,21 +92,30 @@ std::shared_ptr SharedCacheObjCProcessor::GetReader() return std::make_shared(reader); } -void SharedCacheObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth) +void SharedCacheObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase) { if (m_customRelativeMethodSelectorBase.has_value()) { meth.name = m_customRelativeMethodSelectorBase.value() + reader->ReadS32(); - uint64_t offset = reader->GetOffset(); - meth.types = offset + reader->ReadS32(); + // `typesAreOffsetsFromSelectorBase` indicates that `types` is an unsigned offset into the cache's + // deduplicated string pool, relative to the same base address as relative method selectors. + if (typesAreOffsetsFromSelectorBase) + { + meth.types = m_customRelativeMethodSelectorBase.value() + reader->Read32(); + } + else + { + uint64_t offset = reader->GetOffset(); + meth.types = offset + reader->ReadS32(); + } - offset += sizeof(int32_t); + uint64_t offset = reader->GetOffset(); meth.imp = offset + reader->ReadS32(); } else { - ObjCProcessor::GetRelativeMethod(reader, meth); + ObjCProcessor::GetRelativeMethod(reader, meth, typesAreOffsetsFromSelectorBase); } } diff --git a/view/sharedcache/core/ObjC.h b/view/sharedcache/core/ObjC.h index e8c47e70fe..13be2ad9b5 100644 --- a/view/sharedcache/core/ObjC.h +++ b/view/sharedcache/core/ObjC.h @@ -78,7 +78,8 @@ namespace DSCObjC { std::shared_ptr GetReader() override; - void GetRelativeMethod(BinaryNinja::ObjCReader* reader, BinaryNinja::method_t& meth) override; + void GetRelativeMethod(BinaryNinja::ObjCReader* reader, BinaryNinja::method_t& meth, + bool typesAreOffsetsFromSelectorBase) override; BinaryNinja::Ref GetSymbol(uint64_t address) override; diff --git a/view/sharedcache/workflow/SharedCacheWorkflow.cpp b/view/sharedcache/workflow/SharedCacheWorkflow.cpp index 2b9cd28bbc..ff42e0c5bd 100644 --- a/view/sharedcache/workflow/SharedCacheWorkflow.cpp +++ b/view/sharedcache/workflow/SharedCacheWorkflow.cpp @@ -99,9 +99,17 @@ void IdentifyStub(BinaryView& view, const SharedCacheController& controller, uin // Ref selectedType = demangledType; Ref selectedType = nullptr; if (const auto image = controller.GetImageContaining(symbolAddr)) - if (auto typeLib = TypeLibraryFromName(view, image->name)) + { + // The objc_msgSend trampolines live in their own libobjcMsgSend* dylibs which have no type + // library. Look in libobjc.A.dylib instead. + std::string typeLibName = image->name; + if (typeLibName.rfind("/usr/lib/objc/libobjcMsgSend", 0) == 0) + typeLibName = "/usr/lib/libobjc.A.dylib"; + + if (auto typeLib = TypeLibraryFromName(view, typeLibName)) if (Ref libraryType = view.ImportTypeLibraryObject(typeLib, {symbol->name}); libraryType) selectedType = libraryType; + } if (selectedType != nullptr) targetFunc->ApplyAutoDiscoveredType(selectedType); @@ -112,7 +120,18 @@ void IdentifyStub(BinaryView& view, const SharedCacheController& controller, uin view.DefineAutoSymbol(bnSymbol); } -void AnalyzeStubFunction(Ref func, Ref mlil, SharedCacheController& controller, bool loadImage) +// Controls which images AnalyzeStubFunction may auto-load to resolve a stub's jump target. +enum class StubImageLoading +{ + // Do not auto-load any target image. + None, + // Only the dedicated objc_msgSend libraries introduced in macOS 27. + ObjCMsgSendOnly, + // Any directly referenced image. + AnyReferenced, +}; + +void AnalyzeStubFunction(Ref func, Ref mlil, SharedCacheController& controller, StubImageLoading imageLoading) { // 1. Identify the load target and load the region, resolving the load to a const pointer. // 2. We _should_ have a proper call now to the appropriate external function (external to the current image) @@ -138,6 +157,20 @@ void AnalyzeStubFunction(Ref func, Ref mlil, Sh const auto image = controller.GetImageContaining(imageAddr); if (!image.has_value() || controller.IsImageLoaded(*image)) return false; + + const bool isLibobjcMsgSend = image->name.rfind("/usr/lib/objc/libobjcMsgSend", 0) == 0; + if (imageLoading == StubImageLoading::ObjCMsgSendOnly && !isLibobjcMsgSend) + return false; + + if (isLibobjcMsgSend) + { + // The selectors referenced by `objc_msgSend` stubs still live in libobjc.A.dylib. + // Load it too so they are resolved to strings rather than `sel_` symbols. + auto libobjc = controller.GetImageWithName("/usr/lib/libobjc.A.dylib"); + if (libobjc && !controller.IsImageLoaded(*libobjc)) + controller.ApplyImage(*view, *libobjc); + } + return controller.ApplyImage(*view, *image); }; @@ -145,8 +178,7 @@ void AnalyzeStubFunction(Ref func, Ref mlil, Sh // Skip if already loaded. if (view->IsValidOffset(targetAddr)) return false; - // If the stub function is allowed to load images (for inlining) - if (loadImage && loadTargetImage(targetAddr)) + if (imageLoading != StubImageLoading::None && loadTargetImage(targetAddr)) return true; return loadStubIslandRegion(targetAddr); }; @@ -358,10 +390,12 @@ void AnalyzeFunction(Ref ctx) AnalyzeStandardFunction(func, mlilSsa, *controller); break; case StubFunction: - AnalyzeStubFunction(func, mlilSsa, *controller, false); + AnalyzeStubFunction(func, mlilSsa, *controller, + workflowState->autoLoadObjCStubRequirements ? StubImageLoading::ObjCMsgSendOnly : StubImageLoading::None); break; case ObjCStubFunction: - AnalyzeStubFunction(func, mlilSsa, *controller, workflowState->autoLoadObjCStubRequirements); + AnalyzeStubFunction(func, mlilSsa, *controller, + workflowState->autoLoadObjCStubRequirements ? StubImageLoading::AnyReferenced : StubImageLoading::None); break; } }