Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions objectivec/objc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -918,8 +918,11 @@ void ObjCProcessor::LoadProtocols(ObjCReader* reader, Ref<Section> 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();

Expand Down Expand Up @@ -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
Expand All @@ -997,7 +1001,7 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri
// --
if (relativeOffsets)
{
GetRelativeMethod(reader, meth);
GetRelativeMethod(reader, meth, typesAreOffsetsFromSelectorBase);
}
else
{
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion objectivec/objc.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ namespace BinaryNinja {
QualifiedName imageInfoSwiftVersion;
QualifiedName imageInfo;
QualifiedName methodEntry;
QualifiedName methodEntryTypeOffsets;
QualifiedName method;
QualifiedName methodList;
QualifiedName classRO;
Expand Down Expand Up @@ -337,7 +338,7 @@ namespace BinaryNinja {
Ref<Logger> 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<ObjCReader> 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.
Expand Down
1 change: 1 addition & 0 deletions plugins/workflow_objc/src/activities/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
99 changes: 99 additions & 0 deletions plugins/workflow_objc/src/activities/name_stubs.rs
Original file line number Diff line number Diff line change
@@ -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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to update AnalysisContext::function to be an Option<Ref<Function>>, since it can be executed in the context of the binary view for a module level workflow.

This isn't an issue with your PR I just noticed it when reviewing.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also suppose to capture jumps as well? https://github.com/Vector35/binaryninja-api/pull/8261/changes#diff-153013bd9df2d3608be397874180b01d8e12bf3415cfe2cf26f001ab287d4120R47 I see this and it seems like it should? Not sure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code you link to deals with cross-image stub functions into an image that is not yet loaded. In that case the call will often be represented as a jump. This case is a little different.

objc_msgSend$stub functions always end with a call to objc_msgSend. AnalyzeStubFunction in SharedCacheWorkflow.cpp automatically loads libobjc (and/or the new /usr/lib/objc/libobjcMsgSendN.dylib) when it processes one of these stub functions, so any jump instruction that may have existed will have been converted to a tailcall.

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(())
}
6 changes: 3 additions & 3 deletions plugins/workflow_objc/src/activities/objc_msg_send_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MessageSendType> {
pub(crate) 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())?;
Expand All @@ -141,7 +141,7 @@ fn call_target_type(bv: &BinaryView, call_target: u64) -> Option<MessageSendType
}
}

fn selector_from_call(
pub(crate) fn selector_from_call(
bv: &BinaryView,
ssa: &LowLevelILFunction<Mutable, SSA>,
call_op: &Operation<Mutable, SSA, CallSsa>,
Expand Down
16 changes: 15 additions & 1 deletion plugins/workflow_objc/src/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions view/sharedcache/core/MachO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,16 @@ std::vector<CacheSymbol> 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;
}

Expand Down
19 changes: 14 additions & 5 deletions view/sharedcache/core/ObjC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,30 @@ std::shared_ptr<ObjCReader> SharedCacheObjCProcessor::GetReader()
return std::make_shared<SharedCacheObjCReader>(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);
}
}

Expand Down
3 changes: 2 additions & 1 deletion view/sharedcache/core/ObjC.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ namespace DSCObjC {

std::shared_ptr<BinaryNinja::ObjCReader> 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<BinaryNinja::Symbol> GetSymbol(uint64_t address) override;

Expand Down
Loading
Loading