summaryrefslogtreecommitdiff
path: root/plugins/workflow_objc
diff options
context:
space:
mode:
authorkat <kat@vector35.com>2025-06-20 16:17:01 -0400
committerkat <kat@vector35.com>2025-06-23 10:40:55 -0400
commit76215b86ff629da58aa94d4cf4093d2e67017412 (patch)
tree06af88506096351e99053ab5ed9901ff892801f3 /plugins/workflow_objc
parent17b2cb59846073fdbc08235555fd68cdb6852c42 (diff)
Move workflow_objc to API repo, remove CFString & relptr renderers
Diffstat (limited to 'plugins/workflow_objc')
-rw-r--r--plugins/workflow_objc/.clang-format4
-rw-r--r--plugins/workflow_objc/.gitignore7
-rw-r--r--plugins/workflow_objc/BinaryNinja.h21
-rw-r--r--plugins/workflow_objc/CMakeLists.txt46
-rw-r--r--plugins/workflow_objc/CONTRIBUTING.md26
-rw-r--r--plugins/workflow_objc/Constants.h10
-rw-r--r--plugins/workflow_objc/GlobalState.cpp102
-rw-r--r--plugins/workflow_objc/GlobalState.h79
-rw-r--r--plugins/workflow_objc/LICENSE.txt26
-rw-r--r--plugins/workflow_objc/MessageHandler.cpp63
-rw-r--r--plugins/workflow_objc/MessageHandler.h16
-rw-r--r--plugins/workflow_objc/Performance.h38
-rw-r--r--plugins/workflow_objc/Plugin.cpp44
-rw-r--r--plugins/workflow_objc/README.md63
-rw-r--r--plugins/workflow_objc/Workflow.cpp299
-rw-r--r--plugins/workflow_objc/Workflow.h48
16 files changed, 892 insertions, 0 deletions
diff --git a/plugins/workflow_objc/.clang-format b/plugins/workflow_objc/.clang-format
new file mode 100644
index 00000000..d969ca93
--- /dev/null
+++ b/plugins/workflow_objc/.clang-format
@@ -0,0 +1,4 @@
+---
+BasedOnStyle: WebKit
+...
+
diff --git a/plugins/workflow_objc/.gitignore b/plugins/workflow_objc/.gitignore
new file mode 100644
index 00000000..aa45eef0
--- /dev/null
+++ b/plugins/workflow_objc/.gitignore
@@ -0,0 +1,7 @@
+build/
+
+cmake-build-*/
+.idea/
+
+.cache/
+compile_commands.json
diff --git a/plugins/workflow_objc/BinaryNinja.h b/plugins/workflow_objc/BinaryNinja.h
new file mode 100644
index 00000000..517346eb
--- /dev/null
+++ b/plugins/workflow_objc/BinaryNinja.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#pragma once
+
+#include <binaryninjaapi.h>
+
+using AnalysisContextRef = BinaryNinja::Ref<BinaryNinja::AnalysisContext>;
+using BinaryViewRef = BinaryNinja::Ref<BinaryNinja::BinaryView>;
+using LLILFunctionRef = BinaryNinja::Ref<BinaryNinja::LowLevelILFunction>;
+using SymbolRef = BinaryNinja::Ref<BinaryNinja::Symbol>;
+using TypeRef = BinaryNinja::Ref<BinaryNinja::Type>;
+
+using BinaryViewPtr = BinaryNinja::BinaryView*;
+using TypePtr = BinaryNinja::Type*;
+
+using BinaryViewID = std::size_t;
diff --git a/plugins/workflow_objc/CMakeLists.txt b/plugins/workflow_objc/CMakeLists.txt
new file mode 100644
index 00000000..afb31972
--- /dev/null
+++ b/plugins/workflow_objc/CMakeLists.txt
@@ -0,0 +1,46 @@
+cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
+
+project(workflow_objc)
+
+
+if((NOT BN_API_PATH) AND (NOT BN_INTERNAL_BUILD))
+ set(BN_API_PATH $ENV{BN_API_PATH})
+ if(NOT BN_API_PATH)
+ message(FATAL_ERROR "Provide path to Binary Ninja API source in BN_API_PATH")
+ endif()
+endif()
+if(NOT BN_INTERNAL_BUILD)
+ set(HEADLESS ON CACHE BOOL "")
+ add_subdirectory(${BN_API_PATH} ${PROJECT_BINARY_DIR}/api)
+endif()
+
+# Binary Ninja plugin ----------------------------------------------------------
+
+set(PLUGIN_SOURCE
+ GlobalState.h
+ GlobalState.cpp
+ MessageHandler.cpp
+ MessageHandler.h
+ Plugin.cpp
+ Workflow.h
+ Workflow.cpp)
+
+add_library(workflow_objc SHARED ${PLUGIN_SOURCE})
+target_link_libraries(workflow_objc binaryninjaapi)
+target_compile_features(workflow_objc PRIVATE cxx_std_17 c_std_99)
+
+# Library targets linking against the Binary Ninja API need to be compiled with
+# position-independent code on Linux.
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+ target_compile_options(workflow_objc PRIVATE "-fPIC")
+endif()
+
+# Configure plugin output directory for internal builds, otherwise configure
+# plugin installation for public builds.
+if(BN_INTERNAL_BUILD)
+ set_target_properties(workflow_objc PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR}
+ RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR})
+else()
+ bn_install_plugin(workflow_objc)
+endif()
diff --git a/plugins/workflow_objc/CONTRIBUTING.md b/plugins/workflow_objc/CONTRIBUTING.md
new file mode 100644
index 00000000..77539dff
--- /dev/null
+++ b/plugins/workflow_objc/CONTRIBUTING.md
@@ -0,0 +1,26 @@
+# Contribution Guidelines
+
+Contributions in the form of issues and pull requests are welcome! See the
+sections below if you are contributing code.
+
+## Conventions
+
+Refer to the [WebKit Style Guide](https://webkit.org/code-style-guidelines/)
+when in doubt.
+
+## Formatting
+
+Let `clang-format` take care of it. The built-in WebKit style is used.
+
+```sh
+clang-format -i --style=WebKit <file>
+```
+
+- Split long lines when it improves readability. 80 columns is the preferred
+maximum line length, but use some judgement and don't split lines just because a
+semicolon exceeds the length limit, etc.
+
+## Testing
+
+If you are making changes to the core analysis library, run the test suite and
+ensure that there are no unexpected changes to analysis behavior.
diff --git a/plugins/workflow_objc/Constants.h b/plugins/workflow_objc/Constants.h
new file mode 100644
index 00000000..18cfb463
--- /dev/null
+++ b/plugins/workflow_objc/Constants.h
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#pragma once
+
+constexpr auto PluginLoggerName = "Plugin.Objective-C";
diff --git a/plugins/workflow_objc/GlobalState.cpp b/plugins/workflow_objc/GlobalState.cpp
new file mode 100644
index 00000000..6aff3670
--- /dev/null
+++ b/plugins/workflow_objc/GlobalState.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#include "GlobalState.h"
+
+#include <set>
+#include <shared_mutex>
+#include <unordered_map>
+
+static std::unordered_map<BinaryViewID, MessageHandler*> g_messageHandlers;
+static std::shared_mutex g_messageHandlerLock;
+static std::unordered_map<BinaryViewID, SharedAnalysisInfo> g_viewInfos;
+static std::shared_mutex g_viewInfoLock;
+static std::set<BinaryViewID> g_ignoredViews;
+
+
+MessageHandler* GlobalState::messageHandler(BinaryViewRef bv)
+{
+ std::unique_lock<std::shared_mutex> lock(g_messageHandlerLock);
+ auto messageHandler = g_messageHandlers.find(id(bv));
+ if (messageHandler != g_messageHandlers.end())
+ return messageHandler->second;
+ auto newMessageHandler = new MessageHandler(bv);
+ g_messageHandlers[id(bv)] = newMessageHandler;
+ return newMessageHandler;
+}
+
+BinaryViewID GlobalState::id(BinaryViewRef bv)
+{
+ return bv->GetFile()->GetSessionId();
+}
+
+void GlobalState::addIgnoredView(BinaryViewRef bv)
+{
+ g_ignoredViews.insert(id(std::move(bv)));
+}
+
+bool GlobalState::viewIsIgnored(BinaryViewRef bv)
+{
+ return g_ignoredViews.count(id(std::move(bv))) > 0;
+}
+
+SharedAnalysisInfo GlobalState::analysisInfo(BinaryViewRef data)
+{
+ {
+ std::shared_lock<std::shared_mutex> lock(g_viewInfoLock);
+ if (auto it = g_viewInfos.find(id(data)); it != g_viewInfos.end())
+ if (data->GetStart() == it->second->imageBase)
+ return it->second;
+ }
+
+ std::unique_lock<std::shared_mutex> lock(g_viewInfoLock);
+ SharedAnalysisInfo info = std::make_shared<AnalysisInfo>();
+ info->imageBase = data->GetStart();
+
+ if (auto objcStubs = data->GetSectionByName("__objc_stubs"))
+ {
+ info->objcStubsStartEnd = {objcStubs->GetStart(), objcStubs->GetEnd()};
+ info->hasObjcStubs = true;
+ }
+
+ auto meta = data->QueryMetadata("Objective-C");
+ if (!meta)
+ {
+ g_viewInfos[id(data)] = info;
+ return info;
+ }
+
+ auto metaKVS = meta->GetKeyValueStore();
+ if (metaKVS["version"]->GetUnsignedInteger() != 1)
+ {
+ BinaryNinja::LogError("workflow_objc: Invalid metadata version received!");
+ g_viewInfos[id(data)] = info;
+ return info;
+ }
+ for (const auto& selAndImps : metaKVS["selRefImplementations"]->GetArray())
+ info->selRefToImp[selAndImps->GetArray()[0]->GetUnsignedInteger()] = selAndImps->GetArray()[1]->GetUnsignedIntegerList();
+ for (const auto& selAndImps : metaKVS["selImplementations"]->GetArray())
+ info->selToImp[selAndImps->GetArray()[0]->GetUnsignedInteger()] = selAndImps->GetArray()[1]->GetUnsignedIntegerList();
+
+ g_viewInfos[id(data)] = info;
+ return info;
+}
+
+bool GlobalState::hasAnalysisInfo(BinaryViewRef data)
+{
+ return data->QueryMetadata("Objective-C") != nullptr;
+}
+
+bool GlobalState::hasFlag(BinaryViewRef bv, const std::string& flag)
+{
+ return bv->QueryMetadata(flag);
+}
+
+void GlobalState::setFlag(BinaryViewRef bv, const std::string& flag)
+{
+ bv->StoreMetadata(flag, new BinaryNinja::Metadata("YES"));
+}
diff --git a/plugins/workflow_objc/GlobalState.h b/plugins/workflow_objc/GlobalState.h
new file mode 100644
index 00000000..38aeb384
--- /dev/null
+++ b/plugins/workflow_objc/GlobalState.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#pragma once
+
+#include <condition_variable>
+#include "BinaryNinja.h"
+
+#include "MessageHandler.h"
+
+/**
+ * Namespace to hold metadata flag key constants.
+ */
+namespace Flag {
+
+constexpr auto DidRunWorkflow = "objectiveNinja.didRunWorkflow";
+constexpr auto DidRunStructureAnalysis = "objectiveNinja.didRunStructureAnalysis";
+
+}
+
+struct AnalysisInfo {
+ std::uint64_t imageBase;
+ bool hasObjcStubs = false;
+ std::pair<uint64_t, uint64_t> objcStubsStartEnd;
+ std::unordered_map<uint64_t, std::vector<uint64_t>> selRefToImp;
+ std::unordered_map<uint64_t, std::vector<uint64_t>> selToImp;
+};
+
+typedef std::shared_ptr<AnalysisInfo> SharedAnalysisInfo;
+
+/**
+ * Global state/storage interface.
+ */
+class GlobalState {
+ /**
+ * Get the ID for a view.
+ */
+ static BinaryViewID id(BinaryViewRef);
+
+public:
+ /**
+ * Get the analysis info for a view.
+ */
+ static SharedAnalysisInfo analysisInfo(BinaryViewRef);
+
+ /**
+ * Get ObjC Message Handler for a view
+ */
+ static MessageHandler* messageHandler(BinaryViewRef);
+
+ /**
+ * Check if analysis info exists for a view.
+ */
+ static bool hasAnalysisInfo(BinaryViewRef);
+
+ /**
+ * Add a view to the list of ignored views.
+ */
+ static void addIgnoredView(BinaryViewRef);
+
+ /**
+ * Check if a view is ignored.
+ */
+ static bool viewIsIgnored(BinaryViewRef);
+
+ /**
+ * Check if the a metadata flag is present for a view.
+ */
+ static bool hasFlag(BinaryViewRef, const std::string&);
+
+ /**
+ * Set a metadata flag for a view.
+ */
+ static void setFlag(BinaryViewRef, const std::string&);
+};
diff --git a/plugins/workflow_objc/LICENSE.txt b/plugins/workflow_objc/LICENSE.txt
new file mode 100644
index 00000000..2e3b2c3f
--- /dev/null
+++ b/plugins/workflow_objc/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2022-2023 Jon Palmisciano
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/workflow_objc/MessageHandler.cpp b/plugins/workflow_objc/MessageHandler.cpp
new file mode 100644
index 00000000..5c3422b2
--- /dev/null
+++ b/plugins/workflow_objc/MessageHandler.cpp
@@ -0,0 +1,63 @@
+#include "MessageHandler.h"
+
+using namespace BinaryNinja;
+
+MessageHandler::MessageHandler(Ref<BinaryView> data)
+{
+ m_msgSendFunctions = findMsgSendFunctions(data);
+}
+
+std::set<uint64_t> MessageHandler::findMsgSendFunctions(BinaryNinja::Ref<BinaryNinja::BinaryView> data)
+{
+ std::set<uint64_t> results;
+
+ const auto authStubsSection = data->GetSectionByName("__auth_stubs");
+ const auto stubsSection = data->GetSectionByName("__stubs");
+ const auto authGotSection = data->GetSectionByName("__auth_got");
+ const auto gotSection = data->GetSectionByName("__got");
+ const auto laSymbolPtrSection = data->GetSectionByName("__la_symbol_ptr");
+
+ // Shorthand to check if a symbol lies in a given section.
+ auto sectionContains = [](Ref<Section> section, Ref<Symbol> symbol) {
+ const auto start = section->GetStart();
+ const auto length = section->GetLength();
+ const auto address = symbol->GetAddress();
+
+ return (uint64_t)(address - start) <= length;
+ };
+
+ // There can be multiple `_objc_msgSend` symbols in the same binary; there
+ // may even be lots. Some of them are valid, others aren't. In order of
+ // preference, `_objc_msgSend` symbols in the following sections are
+ // preferred:
+ //
+ // 1. __auth_stubs
+ // 2. __stubs
+ // 3. __auth_got
+ // 4. __got
+ // ?. __la_symbol_ptr
+ //
+ // There is often an `_objc_msgSend` symbol that is a stub function, found
+ // in the `__stubs` section, which will come with an imported symbol of the
+ // same name in the `__got` section. Not all `__objc_msgSend` calls will be
+ // routed through the stub function, making it important to make note of
+ // both symbols' addresses. Furthermore, on ARM64, the `__auth{stubs,got}`
+ // sections are preferred over their unauthenticated counterparts.
+ const auto candidates = data->GetSymbolsByName("_objc_msgSend");
+ for (const auto& c : candidates) {
+ if ((authStubsSection && sectionContains(authStubsSection, c))
+ || (stubsSection && sectionContains(stubsSection, c))
+ || (authGotSection && sectionContains(authGotSection, c))
+ || (gotSection && sectionContains(gotSection, c))
+ || (laSymbolPtrSection && sectionContains(laSymbolPtrSection, c))) {
+ results.insert(c->GetAddress());
+ }
+ }
+
+ return results;
+}
+
+bool MessageHandler::isMessageSend(uint64_t functionAddress)
+{
+ return m_msgSendFunctions.count(functionAddress);
+}
diff --git a/plugins/workflow_objc/MessageHandler.h b/plugins/workflow_objc/MessageHandler.h
new file mode 100644
index 00000000..8826b094
--- /dev/null
+++ b/plugins/workflow_objc/MessageHandler.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <binaryninjaapi.h>
+
+class MessageHandler {
+
+ std::set<uint64_t> m_msgSendFunctions;
+ static std::set<uint64_t> findMsgSendFunctions(BinaryNinja::Ref<BinaryNinja::BinaryView> data);
+
+public:
+ MessageHandler(BinaryNinja::Ref<BinaryNinja::BinaryView> data);
+
+ std::set<uint64_t> getMessageSendFunctions() const { return m_msgSendFunctions; }
+ bool hasMessageSendFunctions() const { return m_msgSendFunctions.size() != 0; }
+ bool isMessageSend(uint64_t);
+};
diff --git a/plugins/workflow_objc/Performance.h b/plugins/workflow_objc/Performance.h
new file mode 100644
index 00000000..7e497b4e
--- /dev/null
+++ b/plugins/workflow_objc/Performance.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#pragma once
+
+#include <chrono>
+
+using high_res_clock = std::chrono::high_resolution_clock;
+
+/**
+ * Utilities for measuring performance.
+ */
+class Performance {
+public:
+ /**
+ * Get the current time.
+ */
+ static high_res_clock::time_point now()
+ {
+ return high_res_clock::now();
+ }
+
+ /**
+ * Get the current elapsed time from a given start time.
+ *
+ * Accepts a unit of measure template parameter for the result.
+ */
+ template <typename T>
+ static T elapsed(high_res_clock::time_point start)
+ {
+ auto end = high_res_clock::now();
+ return std::chrono::duration_cast<T>(end - start);
+ }
+};
diff --git a/plugins/workflow_objc/Plugin.cpp b/plugins/workflow_objc/Plugin.cpp
new file mode 100644
index 00000000..2773e10a
--- /dev/null
+++ b/plugins/workflow_objc/Plugin.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#include "Constants.h"
+#include "Workflow.h"
+
+extern "C" {
+
+BN_DECLARE_CORE_ABI_VERSION
+
+BINARYNINJAPLUGIN void CorePluginDependencies()
+{
+ BinaryNinja::AddOptionalPluginDependency("arch_x86");
+ BinaryNinja::AddOptionalPluginDependency("arch_armv7");
+ BinaryNinja::AddOptionalPluginDependency("arch_arm64");
+}
+
+BINARYNINJAPLUGIN bool CorePluginInit()
+{
+ Workflow::registerActivities();
+
+ std::vector<BinaryNinja::Ref<BinaryNinja::Architecture>> targets = {
+ BinaryNinja::Architecture::GetByName("aarch64"),
+ BinaryNinja::Architecture::GetByName("x86_64")
+ };
+
+ BinaryNinja::LogRegistry::CreateLogger(PluginLoggerName);
+
+ auto settings = BinaryNinja::Settings::Instance();
+ settings->RegisterSetting("core.function.objectiveC.rewriteMessageSendTarget",
+ R"({
+ "title" : "Rewrite objc_msgSend calls in IL",
+ "type" : "boolean",
+ "default" : false,
+ "description" : "Message sends of selectors with any visible implementation are replaced with a direct call to the first visible implementation. Note that this can produce false positives if the selector is implemented by more than one class, or shares a name with a method from a system framework."
+ })");
+
+ return true;
+}
+}
diff --git a/plugins/workflow_objc/README.md b/plugins/workflow_objc/README.md
new file mode 100644
index 00000000..1c1d47f3
--- /dev/null
+++ b/plugins/workflow_objc/README.md
@@ -0,0 +1,63 @@
+# Objective-C Workflow
+
+This is the Objective-C plugin that ships with Binary Ninja. It provides
+additional support for analyzing Objective-C binaries.
+
+The primary functionality offered by this plugin is:
+
+- **Function Call Cleanup.** When using the Objective-C workflow, calls to
+ `objc_msgSend` can be replaced with direct calls to the relevant function's
+ implementation.
+
+For more details and usage instructions, see the [user guide](https://dev-docs.binary.ninja/guide/objectivec.html).
+
+## Issues
+
+Issues for this repository have been disabled. Please file an issue for this repository at https://github.com/Vector35/binaryninja-api/issues. All previously existing issues for this repository have been transferred there as well.
+
+## Building
+
+This plugin can be built and installed separately from Binary Ninja via the
+following commands:
+
+```sh
+git clone https://github.com/Vector35/workflow_objc.git && cd workflow_objc
+git submodule update --init --recursive
+cmake -S . -B build -GNinja
+cmake --build build -t install
+```
+
+## Credits
+
+This plugin is a continuation of [Objective Ninja](https://github.com/jonpalmisc/ObjectiveNinja), originally made
+by [@jonpalmisc](https://twitter.com/jonpalmisc). The full terms of the
+Objective Ninja license are as follows:
+
+```
+Copyright (c) 2022-2023 Jon Palmisciano
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/plugins/workflow_objc/Workflow.cpp b/plugins/workflow_objc/Workflow.cpp
new file mode 100644
index 00000000..3be2a042
--- /dev/null
+++ b/plugins/workflow_objc/Workflow.cpp
@@ -0,0 +1,299 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#include "Workflow.h"
+
+#include "Constants.h"
+#include "GlobalState.h"
+#include "Performance.h"
+
+#include <lowlevelilinstruction.h>
+
+#include <queue>
+#include "binaryninjaapi.h"
+
+static std::mutex g_initialAnalysisMutex;
+
+using SectionRef = BinaryNinja::Ref<BinaryNinja::Section>;
+using SymbolRef = BinaryNinja::Ref<BinaryNinja::Symbol>;
+
+namespace {
+
+std::vector<std::string> splitSelector(const std::string& selector) {
+ std::vector<std::string> components;
+ std::istringstream stream(selector);
+ std::string component;
+
+ while (std::getline(stream, component, ':')) {
+ if (!component.empty()) {
+ components.push_back(component);
+ }
+ }
+
+ return components;
+}
+
+// Given a selector component such as `initWithPath' and a prefix of `initWith`, returns `path`.
+std::optional<std::string> SelectorComponentWithoutPrefix(std::string_view prefix, std::string_view component)
+{
+ if (component.size() <= prefix.size() || component.rfind(prefix.data(), 0) != 0
+ || !isupper(component[prefix.size()])) {
+ return std::nullopt;
+ }
+
+ std::string result(component.substr(prefix.size()));
+
+ // Lowercase the first character if the second character is not also uppercase.
+ // This ensures we leave initialisms such as `URL` alone.
+ if (result.size() > 1 && islower(result[1]))
+ result[0] = tolower(result[0]);
+
+ return result;
+}
+
+std::string ArgumentNameFromSelectorComponent(std::string component)
+{
+ // TODO: Handle other common patterns such as <do some action>With<arg>: and <do some action>For<arg>:
+ for (const auto& prefix : { "initWith", "with", "and", "using", "set", "read", "to", "for" }) {
+ if (auto argumentName = SelectorComponentWithoutPrefix(prefix, component); argumentName.has_value())
+ return std::move(*argumentName);
+ }
+
+ return component;
+}
+
+std::vector<std::string> generateArgumentNames(const std::vector<std::string>& components) {
+ std::vector<std::string> argumentNames;
+
+ for (const std::string& component : components) {
+ size_t startPos = component.find_last_of(" ");
+ std::string argumentName = (startPos == std::string::npos) ? component : component.substr(startPos + 1);
+ argumentNames.push_back(ArgumentNameFromSelectorComponent(std::move(argumentName)));
+ }
+
+ return argumentNames;
+}
+
+} // unnamed namespace
+
+bool Workflow::rewriteMethodCall(LLILFunctionRef ssa, size_t insnIndex)
+{
+ const auto bv = ssa->GetFunction()->GetView();
+ const auto llil = ssa->GetNonSSAForm();
+ const auto insn = ssa->GetInstruction(insnIndex);
+ const auto params = insn.GetParameterExprs<LLIL_CALL_SSA>();
+
+ // The second parameter passed to the objc_msgSend call is the address of
+ // either the selector reference or the method's name, which in both cases
+ // is dereferenced to retrieve a selector.
+ if (params.size() < 2)
+ return false;
+ uint64_t rawSelector = 0;
+ if (params[1].operation == LLIL_REG_SSA)
+ {
+ const auto selectorRegister = params[1].GetSourceSSARegister<LLIL_REG_SSA>();
+ rawSelector = ssa->GetSSARegisterValue(selectorRegister).value;
+ }
+ else if (params[0].operation == LLIL_SEPARATE_PARAM_LIST_SSA)
+ {
+ if (params[0].GetParameterExprs<LLIL_SEPARATE_PARAM_LIST_SSA>().size() == 0)
+ {
+ return false;
+ }
+ const auto selectorRegister = params[0].GetParameterExprs<LLIL_SEPARATE_PARAM_LIST_SSA>()[1].GetSourceSSARegister<LLIL_REG_SSA>();
+ rawSelector = ssa->GetSSARegisterValue(selectorRegister).value;
+ }
+ if (rawSelector == 0)
+ return false;
+
+ // -- Do callsite override
+ auto reader = BinaryNinja::BinaryReader(bv);
+ reader.Seek(rawSelector);
+ auto selector = reader.ReadCString(500);
+ auto additionalArgumentCount = std::count(selector.begin(), selector.end(), ':');
+
+ auto retType = bv->GetTypeByName({ "id" });
+ if (!retType)
+ retType = BinaryNinja::Type::PointerType(ssa->GetArchitecture(), BinaryNinja::Type::VoidType());
+
+ std::vector<BinaryNinja::FunctionParameter> callTypeParams;
+ auto cc = bv->GetDefaultPlatform()->GetDefaultCallingConvention();
+
+ callTypeParams.push_back({"self", retType, true, BinaryNinja::Variable()});
+
+ auto selType = bv->GetTypeByName({ "SEL" });
+ if (!selType)
+ selType = BinaryNinja::Type::PointerType(ssa->GetArchitecture(), BinaryNinja::Type::IntegerType(1, true));
+ callTypeParams.push_back({"sel", selType, true, BinaryNinja::Variable()});
+
+ std::vector<std::string> selectorComponents = splitSelector(selector);
+ std::vector<std::string> argumentNames = generateArgumentNames(selectorComponents);
+
+ for (size_t i = 0; i < additionalArgumentCount; i++)
+ {
+ auto argType = BinaryNinja::Type::IntegerType(bv->GetAddressSize(), true);
+ if (argumentNames.size() > i && !argumentNames[i].empty())
+ callTypeParams.push_back({argumentNames[i], argType, true, BinaryNinja::Variable()});
+ else
+ callTypeParams.push_back({"arg" + std::to_string(i), argType, true, BinaryNinja::Variable()});
+ }
+
+ auto funcType = BinaryNinja::Type::FunctionType(retType, cc, callTypeParams);
+ ssa->GetFunction()->SetAutoCallTypeAdjustment(ssa->GetFunction()->GetArchitecture(), insn.address, {funcType, BN_DEFAULT_CONFIDENCE});
+ // --
+
+ if (!BinaryNinja::Settings::Instance()->Get<bool>("core.function.objectiveC.rewriteMessageSendTarget", bv))
+ return false;
+
+ // Check the analysis info for a selector reference corresponding to the
+ // current selector. It is possible no such selector reference exists, for
+ // example, if the selector is for a method defined outside the current
+ // binary. If this is the case, there are no meaningful changes that can be
+ // made to the IL, and the operation should be aborted.
+
+ // k: also check direct selector value (x64 does this)
+ const auto info = GlobalState::analysisInfo(bv);
+ if (!info)
+ return false;
+
+ // Attempt to look up the implementation for the given selector, first by
+ // using the raw selector, then by the address of the selector reference. If
+ // the lookup fails in both cases, abort.
+ std::vector<uint64_t> imps;
+ if (const auto& it = info->selRefToImp.find(rawSelector); it != info->selRefToImp.end())
+ imps = it->second;
+ else if (const auto& iter = info->selToImp.find(rawSelector); iter != info->selToImp.end())
+ imps = iter->second;
+
+ if (imps.empty())
+ return false;
+
+ // k: This is the same behavior as before, however it is more apparent now by implementation
+ // that we are effectively just guessing which method this hits. This has _obvious_ drawbacks,
+ // but until we have more robust typing and objective-c type libraries, fixing this would
+ // make the objective-c workflow do effectively nothing.
+ uint64_t implAddress = imps[0];
+ if (!implAddress)
+ return false;
+
+ const auto llilIndex = ssa->GetNonSSAInstructionIndex(insnIndex);
+ auto llilInsn = llil->GetInstruction(llilIndex);
+
+ // Change the destination expression of the LLIL_CALL operation to point to
+ // the method implementation. This turns the "indirect call" piped through
+ // `objc_msgSend` and makes it a normal C-style function call.
+ auto callDestExpr = llilInsn.GetDestExpr<LLIL_CALL>();
+ callDestExpr.Replace(llil->ConstPointer(callDestExpr.size, implAddress, callDestExpr));
+ llilInsn.Replace(llil->Call(callDestExpr.exprIndex, llilInsn));
+
+ return true;
+}
+
+void Workflow::inlineMethodCalls(AnalysisContextRef ac)
+{
+ const auto func = ac->GetFunction();
+ const auto arch = func->GetArchitecture();
+ const auto bv = func->GetView();
+
+ if (GlobalState::viewIsIgnored(bv))
+ return;
+
+ const auto log = BinaryNinja::LogRegistry::GetLogger(PluginLoggerName);
+
+ // Ignore the view if it has an unsupported architecture.
+ //
+ // The reasoning for querying the default architecture here rather than the
+ // architecture of the function being analyzed is that the view needs to
+ // have a default architecture for the Objective-C runtime types to be
+ // defined successfully.
+ auto defaultArch = bv->GetDefaultArchitecture();
+ auto defaultArchName = defaultArch ? defaultArch->GetName() : "";
+ if (defaultArchName != "aarch64" && defaultArchName != "x86_64" && defaultArchName != "armv7" && defaultArchName != "thumb2") {
+ if (!defaultArch)
+ log->LogError("View must have a default architecture.");
+ else
+ log->LogError("Architecture '%s' is not supported", defaultArchName.c_str());
+
+ GlobalState::addIgnoredView(bv);
+ return;
+ }
+
+ if (auto info = GlobalState::analysisInfo(bv))
+ {
+ if (info->hasObjcStubs && func->GetStart() > info->objcStubsStartEnd.first && func->GetStart() < info->objcStubsStartEnd.second)
+ {
+ func->SetAutoInlinedDuringAnalysis({true, BN_FULL_CONFIDENCE});
+ // Do no further cleanup, this is a stub and it will be cleaned up after inlining
+ return;
+ }
+ }
+
+ auto messageHandler = GlobalState::messageHandler(bv);
+ if (!messageHandler->hasMessageSendFunctions()) {
+ //log->LogError("Cannot perform Objective-C IL cleanup; no objc_msgSend candidates found");
+ //GlobalState::addIgnoredView(bv);
+ //return;
+ }
+
+ const auto llil = ac->GetLowLevelILFunction();
+ if (!llil) {
+ // log->LogError("(Workflow) Failed to get LLIL for 0x%llx", func->GetStart());
+ return;
+ }
+ const auto ssa = llil->GetSSAForm();
+ if (!ssa) {
+ // log->LogError("(Workflow) Failed to get LLIL SSA form for 0x%llx", func->GetStart());
+ return;
+ }
+
+ const auto rewriteIfEligible = [bv, messageHandler, ssa](size_t insnIndex) {
+ auto insn = ssa->GetInstruction(insnIndex);
+
+ if (insn.operation == LLIL_CALL_SSA)
+ {
+ // Filter out calls that aren't to `objc_msgSend`.
+ auto callExpr = insn.GetDestExpr<LLIL_CALL_SSA>();
+ bool isMessageSend = messageHandler->isMessageSend(callExpr.GetValue().value);
+ if (auto symbol = bv->GetSymbolByAddress(callExpr.GetValue().value))
+ isMessageSend = isMessageSend || symbol->GetRawName() == "_objc_msgSend";
+ if (!isMessageSend)
+ return false;
+
+ return rewriteMethodCall(ssa, insnIndex);
+ }
+
+ return false;
+ };
+
+ bool isFunctionChanged = false;
+ for (const auto& block : ssa->GetBasicBlocks())
+ for (size_t i = block->GetStart(), end = block->GetEnd(); i < end; ++i)
+ if (rewriteIfEligible(i))
+ isFunctionChanged = true;
+
+ if (!isFunctionChanged)
+ return;
+
+ // Updates found, regenerate SSA form
+ llil->GenerateSSAForm();
+}
+
+static constexpr auto WorkflowInfo = R"({
+ "title": "Objective-C",
+ "description": "Enhanced analysis for Objective-C code.",
+ "capabilities": []
+})";
+
+void Workflow::registerActivities()
+{
+ const auto wf = BinaryNinja::Workflow::Instance("core.function.baseAnalysis")->Clone("core.function.objectiveC");
+ wf->RegisterActivity(new BinaryNinja::Activity(
+ ActivityID::ResolveMethodCalls, &Workflow::inlineMethodCalls));
+ wf->Insert("core.function.translateTailCalls", ActivityID::ResolveMethodCalls);
+
+ BinaryNinja::Workflow::RegisterWorkflow(wf, WorkflowInfo);
+}
diff --git a/plugins/workflow_objc/Workflow.h b/plugins/workflow_objc/Workflow.h
new file mode 100644
index 00000000..4479a8a7
--- /dev/null
+++ b/plugins/workflow_objc/Workflow.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved.
+ *
+ * Use of this source code is governed by the BSD 3-Clause license; the full
+ * terms of the license can be found in the LICENSE.txt file.
+ */
+
+#pragma once
+
+#include "BinaryNinja.h"
+
+/**
+ * Namespace to hold activity ID constants.
+ */
+namespace ActivityID {
+
+constexpr auto ResolveMethodCalls = "core.function.objectiveC.resolveMethodCalls";
+
+}
+
+/**
+ * Workflow-related procedures.
+ */
+class Workflow {
+
+ /**
+ * Attempt to rewrite the `objc_msgSend` call at `insnIndex` with a direct
+ * call to the requested method's implementation.
+ *
+ * @param insnIndex The index of the `LLIL_CALL` instruction to rewrite
+ */
+ static bool rewriteMethodCall(LLILFunctionRef, size_t insnIndex);
+
+public:
+ /**
+ * Attempt to inline all `objc_msgSend` calls in the given analysis context.
+ */
+ static void inlineMethodCalls(AnalysisContextRef);
+
+ /**
+ * Register the Objective Ninja workflow and all activities.
+ *
+ * This is named a bit strangely because `register` is a keyword in C++ and
+ * therefore an invalid method name, and I refuse to misspell it to appease
+ * the compiler and avoid the conflict.
+ */
+ static void registerActivities();
+};