diff options
| author | kat <kat@vector35.com> | 2025-06-20 16:17:01 -0400 |
|---|---|---|
| committer | kat <kat@vector35.com> | 2025-06-23 10:40:55 -0400 |
| commit | 76215b86ff629da58aa94d4cf4093d2e67017412 (patch) | |
| tree | 06af88506096351e99053ab5ed9901ff892801f3 /plugins/workflow_objc | |
| parent | 17b2cb59846073fdbc08235555fd68cdb6852c42 (diff) | |
Move workflow_objc to API repo, remove CFString & relptr renderers
Diffstat (limited to 'plugins/workflow_objc')
| -rw-r--r-- | plugins/workflow_objc/.clang-format | 4 | ||||
| -rw-r--r-- | plugins/workflow_objc/.gitignore | 7 | ||||
| -rw-r--r-- | plugins/workflow_objc/BinaryNinja.h | 21 | ||||
| -rw-r--r-- | plugins/workflow_objc/CMakeLists.txt | 46 | ||||
| -rw-r--r-- | plugins/workflow_objc/CONTRIBUTING.md | 26 | ||||
| -rw-r--r-- | plugins/workflow_objc/Constants.h | 10 | ||||
| -rw-r--r-- | plugins/workflow_objc/GlobalState.cpp | 102 | ||||
| -rw-r--r-- | plugins/workflow_objc/GlobalState.h | 79 | ||||
| -rw-r--r-- | plugins/workflow_objc/LICENSE.txt | 26 | ||||
| -rw-r--r-- | plugins/workflow_objc/MessageHandler.cpp | 63 | ||||
| -rw-r--r-- | plugins/workflow_objc/MessageHandler.h | 16 | ||||
| -rw-r--r-- | plugins/workflow_objc/Performance.h | 38 | ||||
| -rw-r--r-- | plugins/workflow_objc/Plugin.cpp | 44 | ||||
| -rw-r--r-- | plugins/workflow_objc/README.md | 63 | ||||
| -rw-r--r-- | plugins/workflow_objc/Workflow.cpp | 299 | ||||
| -rw-r--r-- | plugins/workflow_objc/Workflow.h | 48 |
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(); +}; |
