diff --git a/Cargo.lock b/Cargo.lock index e9b6202ee8..c66dec847c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3472,6 +3472,18 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "workflow_objc" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "dashmap", + "log", + "once_cell", + "thiserror 2.0.12", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 2f56c5c55d..2811850a56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ members = [ "plugins/idb_import", "plugins/pdb-ng", "plugins/pdb-ng/demo", - "plugins/warp", "plugins/svd", - "plugins/svd/demo" + "plugins/svd/demo", + "plugins/warp", + "plugins/workflow_objc", ] [workspace.dependencies] diff --git a/README.md b/README.md index ae8db7657b..d5244d7d66 100644 --- a/README.md +++ b/README.md @@ -124,18 +124,18 @@ This repository contains all of our Binary View Type plugins available here: ## Other Plugins -* [SVD Loader](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/svd/) +* [Objective-C](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/workflow_objc/) * [RTTI Analysis](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/rtti/) -* [WARP Integration](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/warp/) * [Stack Render Layer](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/stack_render_layer/) +* [SVD Loader](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/svd/) * [Triage View](https://github.com/Vector35/binaryninja-api/tree/dev/examples/triage/) +* [WARP Integration](https://github.com/Vector35/binaryninja-api/tree/dev/plugins/warp/) ## Related Repositories -In addition to this main API repository being open source Vector35 also has open sourced the Debugger and the Objective-C plugins open source as well: +In addition to this main API repository being open source Vector35 also has open sourced the Debugger plugin as well: * [Debugger](https://github.com/Vector35/debugger) -* [workflow_objc](https://github.com/Vector35/workflow_objc) ## Licensing diff --git a/docs/guide/objectivec.md b/docs/guide/objectivec.md index 5fef33bc11..3c4414b9a2 100644 --- a/docs/guide/objectivec.md +++ b/docs/guide/objectivec.md @@ -1,35 +1,40 @@ # Objective-C -Binary Ninja ships with [an additional plugin](https://github.com/Vector35/workflow_objc) for assisting with Objective-C analysis. +Binary Ninja ships with built-in functionality for assisting with Objective-C analysis. A brief summary of the features offered is as follows: -- **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. - - **Name and Type Recovery.** Using runtime information embedded in the - binary, Binary Ninja can automatically apply names and type information to + binary, Binary Ninja automatically applies names and type information to Objective-C functions. - **Structure Markup.** Data variables are automatically created for Objective-C structures such as classes and method lists to enable easy navigation. -- **Data Renderers.** Formatting of Objective-C types such as tagged and/or - (image-)relative pointers is improved via custom data renderers. +- **String Literal Handling.** Data variables are automatically created for all + `CFString` or `NSString` instances present in the binary. -- **CFString Handling.** Data variables are automatically created for all - `CFString` instances present in the binary. +- **Automatic Call Type Adjustments.** Binary Ninja automatically infers the number of arguments and their names + for individual calls to `objc_msgSend` and `objc_msgSendSuper2`. Argument names are derived from the selector + components, and argument types are inferred in limited cases. -## Usage +- **Pseudo Objective-C Language.** Decompiled code can be displayed using a _Pseudo Objective-C_ + language syntax. This renders `objc_msgSend` and other Objective-C runtime calls using the + `[receiver message:argument other:argument2]` syntax found in Objective-C source code. + Additionally, literals such as `CFString` or `NSString` are displayed inline as `@"string"`. -If you have an Objective-C binary opening in Binary Ninja should automatically process -the information. This is handled by the view, e.g. MACH-O & Shared Cache. +- **Direct Call Rewriting.** Calls to `objc_msgSend` can be rewritten to be direct calls to + the first known method implementation for that selector. + + This is disabled by default as it will give potentially confusing results for any selector + that has more than one implementation or for common selector names. That said, some users may + still find it to be useful. It can be enabled via `analysis.objectiveC.resolveDynamicDispatch` + setting. + +## Usage -### Workflow +Objective-C metadata will be automatically processed when you open a Mach-O or DYLD shared cache binary in Binary Ninja. -To utilize function call cleanup, the Objective-C workflow must be chosen when loading a binary for analysis. +The Pseudo Objective-C Language representation is available via the language pop-up menu at the top of Linear and Graph views: -![](../img/objc-workflow-selected.png) +![](../img/pseudo-objc-menu-item.png) -This will automatically apply structure analysis as the binary is analyzed and -also translate `objc_msgSend` calls to direct method calls, where possible. diff --git a/docs/img/objc-workflow-selected.png b/docs/img/objc-workflow-selected.png deleted file mode 100644 index 14d4c4ac14..0000000000 Binary files a/docs/img/objc-workflow-selected.png and /dev/null differ diff --git a/docs/img/pseudo-objc-menu-item.png b/docs/img/pseudo-objc-menu-item.png new file mode 100644 index 0000000000..e0db6c4a29 Binary files /dev/null and b/docs/img/pseudo-objc-menu-item.png differ diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs index ee778062a2..7a2bdcff03 100644 --- a/plugins/warp/src/plugin.rs +++ b/plugins/warp/src/plugin.rs @@ -12,9 +12,9 @@ use binaryninja::background_task::BackgroundTask; use binaryninja::command::{ register_command, register_command_for_function, register_command_for_project, }; +use binaryninja::is_ui_enabled; use binaryninja::logger::Logger; use binaryninja::settings::Settings; -use binaryninja::{add_optional_plugin_dependency, is_ui_enabled}; use log::LevelFilter; use reqwest::StatusCode; @@ -200,9 +200,3 @@ pub extern "C" fn CorePluginInit() -> bool { true } - -#[unsafe(no_mangle)] -pub extern "C" fn CorePluginDependencies() { - // TODO: Remove this once the objectivec workflow is registered on the meta workflow. - add_optional_plugin_dependency("workflow_objc"); -} diff --git a/plugins/workflow_objc/.clang-format b/plugins/workflow_objc/.clang-format deleted file mode 100644 index d969ca934a..0000000000 --- a/plugins/workflow_objc/.clang-format +++ /dev/null @@ -1,4 +0,0 @@ ---- -BasedOnStyle: WebKit -... - diff --git a/plugins/workflow_objc/.gitignore b/plugins/workflow_objc/.gitignore deleted file mode 100644 index aa45eef04a..0000000000 --- a/plugins/workflow_objc/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -build/ - -cmake-build-*/ -.idea/ - -.cache/ -compile_commands.json diff --git a/plugins/workflow_objc/BinaryNinja.h b/plugins/workflow_objc/BinaryNinja.h deleted file mode 100644 index 517346eb1c..0000000000 --- a/plugins/workflow_objc/BinaryNinja.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - -using AnalysisContextRef = BinaryNinja::Ref; -using BinaryViewRef = BinaryNinja::Ref; -using LLILFunctionRef = BinaryNinja::Ref; -using SymbolRef = BinaryNinja::Ref; -using TypeRef = BinaryNinja::Ref; - -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 index a16a55198a..69127ece05 100644 --- a/plugins/workflow_objc/CMakeLists.txt +++ b/plugins/workflow_objc/CMakeLists.txt @@ -1,47 +1,106 @@ -cmake_minimum_required(VERSION 3.14 FATAL_ERROR) +cmake_minimum_required(VERSION 3.15 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() +if(NOT BN_API_BUILD_EXAMPLES AND NOT BN_INTERNAL_BUILD) + if(NOT BN_API_PATH) + # If we have not already defined the API source directory try and find it. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + # List of paths to search for the clone of the api + HINTS ../../.. ../../binaryninja/api/ binaryninjaapi binaryninja-api $ENV{BN_API_PATH} + REQUIRED + ) + endif() + set(CARGO_STABLE_VERSION 1.83.0) + add_subdirectory(${BN_API_PATH} binaryninjaapi) endif() -if(NOT BN_INTERNAL_BUILD) - set(HEADLESS ON CACHE BOOL "") - add_subdirectory(${BN_API_PATH} ${PROJECT_BINARY_DIR}/api) + +file(GLOB_RECURSE PLUGIN_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/Cargo.toml + ${PROJECT_SOURCE_DIR}/src/*.rs) + +file(GLOB_RECURSE API_SOURCES CONFIGURE_DEPENDS + ${PROJECT_SOURCE_DIR}/../../binaryninjacore.h + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/build.rs + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/Cargo.toml + ${PROJECT_SOURCE_DIR}/../../rust/binaryninjacore-sys/src/*.rs + ${PROJECT_SOURCE_DIR}/../../rust/Cargo.toml + ${PROJECT_SOURCE_DIR}/../../rust/src/*/*.rs) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/debug) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target) +else() + set(TARGET_DIR ${PROJECT_BINARY_DIR}/target/release) + set(CARGO_OPTS --target-dir=${PROJECT_BINARY_DIR}/target --release) + set(OUTPUT_PDB_NAME ${CMAKE_SHARED_LIBRARY_PREFIX}workflow_objc.pdb) 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_20 c_std_99) -plugin_rpath(workflow_objc) - -# 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") +set(OUTPUT_FILE ${CMAKE_STATIC_LIBRARY_PREFIX}workflow_objc${CMAKE_SHARED_LIBRARY_SUFFIX}) +set(PLUGIN_PATH ${TARGET_DIR}/${OUTPUT_FILE}) + +add_custom_target(workflow_objc ALL DEPENDS ${PLUGIN_PATH}) +add_dependencies(workflow_objc binaryninjaapi) + +find_program(RUSTUP_PATH rustup REQUIRED HINTS ~/.cargo/bin) +if(CARGO_API_VERSION) + set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_API_VERSION} cargo build) +else() + set(RUSTUP_COMMAND ${RUSTUP_PATH} run ${CARGO_STABLE_VERSION} cargo build) 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}) +if(APPLE) + if(UNIVERSAL) + if(CMAKE_BUILD_TYPE MATCHES Debug) + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/debug/${OUTPUT_FILE}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/debug/${OUTPUT_FILE}) + else() + set(AARCH64_LIB_PATH ${PROJECT_BINARY_DIR}/target/aarch64-apple-darwin/release/${OUTPUT_FILE}) + set(X86_64_LIB_PATH ${PROJECT_BINARY_DIR}/target/x86_64-apple-darwin/release/${OUTPUT_FILE}) + endif() + + add_custom_command( + OUTPUT ${PLUGIN_PATH} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BN_CORE_OUTPUT_DIR} + ${RUSTUP_COMMAND} --target=aarch64-apple-darwin ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E env + MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BN_CORE_OUTPUT_DIR} + ${RUSTUP_COMMAND} --target=x86_64-apple-darwin ${CARGO_OPTS} + COMMAND mkdir -p ${TARGET_DIR} + COMMAND lipo -create ${AARCH64_LIB_PATH} ${X86_64_LIB_PATH} -output ${PLUGIN_PATH} + COMMAND ${CMAKE_COMMAND} -E copy ${PLUGIN_PATH} ${BN_CORE_PLUGIN_DIR} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES}) + else() + if(CMAKE_BUILD_TYPE MATCHES Debug) + set(LIB_PATH ${PROJECT_BINARY_DIR}/target/debug/${OUTPUT_FILE}) + else() + set(LIB_PATH ${PROJECT_BINARY_DIR}/target/release/${OUTPUT_FILE}) + endif() + + add_custom_command( + OUTPUT ${PLUGIN_PATH} + COMMAND ${CMAKE_COMMAND} -E env MACOSX_DEPLOYMENT_TARGET=10.14 BINARYNINJADIR=${BN_CORE_OUTPUT_DIR} ${RUSTUP_COMMAND} ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E copy ${PLUGIN_PATH} ${BN_CORE_PLUGIN_DIR} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES}) + endif() +elseif(WIN32) + add_custom_command( + OUTPUT ${PLUGIN_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BN_CORE_OUTPUT_DIR} ${RUSTUP_COMMAND} ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E copy ${PLUGIN_PATH} ${BN_CORE_PLUGIN_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${TARGET_DIR}/${OUTPUT_PDB_NAME} ${BN_CORE_PLUGIN_DIR} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES}) else() - bn_install_plugin(workflow_objc) + add_custom_command( + OUTPUT ${PLUGIN_PATH} + COMMAND ${CMAKE_COMMAND} -E env BINARYNINJADIR=${BN_CORE_OUTPUT_DIR} ${RUSTUP_COMMAND} ${CARGO_OPTS} + COMMAND ${CMAKE_COMMAND} -E copy ${PLUGIN_PATH} ${BN_CORE_PLUGIN_DIR} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + DEPENDS ${PLUGIN_SOURCES} ${API_SOURCES}) endif() diff --git a/plugins/workflow_objc/CONTRIBUTING.md b/plugins/workflow_objc/CONTRIBUTING.md deleted file mode 100644 index 77539dffd7..0000000000 --- a/plugins/workflow_objc/CONTRIBUTING.md +++ /dev/null @@ -1,26 +0,0 @@ -# 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 -``` - -- 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/Cargo.toml b/plugins/workflow_objc/Cargo.toml new file mode 100644 index 0000000000..09a2d3a725 --- /dev/null +++ b/plugins/workflow_objc/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "workflow_objc" +version = "0.1.0" +edition = "2021" +license = "BSD-3-Clause" + +[lib] +crate-type = ["staticlib", "cdylib"] + +[dependencies] +binaryninja = { workspace = true } +binaryninjacore-sys.workspace = true +log = "0.4" +dashmap = { version = "6.1", features = ["rayon"]} +once_cell = "1.20" +thiserror = "2.0" diff --git a/plugins/workflow_objc/Constants.h b/plugins/workflow_objc/Constants.h deleted file mode 100644 index 18cfb463c3..0000000000 --- a/plugins/workflow_objc/Constants.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6aff3670e7..0000000000 --- a/plugins/workflow_objc/GlobalState.cpp +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 -#include -#include - -static std::unordered_map g_messageHandlers; -static std::shared_mutex g_messageHandlerLock; -static std::unordered_map g_viewInfos; -static std::shared_mutex g_viewInfoLock; -static std::set g_ignoredViews; - - -MessageHandler* GlobalState::messageHandler(BinaryViewRef bv) -{ - std::unique_lock 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 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 lock(g_viewInfoLock); - SharedAnalysisInfo info = std::make_shared(); - 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 deleted file mode 100644 index 38aeb38408..0000000000 --- a/plugins/workflow_objc/GlobalState.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 -#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 objcStubsStartEnd; - std::unordered_map> selRefToImp; - std::unordered_map> selToImp; -}; - -typedef std::shared_ptr 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 deleted file mode 100644 index 2e3b2c3f24..0000000000 --- a/plugins/workflow_objc/LICENSE.txt +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 5c3422b257..0000000000 --- a/plugins/workflow_objc/MessageHandler.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "MessageHandler.h" - -using namespace BinaryNinja; - -MessageHandler::MessageHandler(Ref data) -{ - m_msgSendFunctions = findMsgSendFunctions(data); -} - -std::set MessageHandler::findMsgSendFunctions(BinaryNinja::Ref data) -{ - std::set 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, Ref 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 deleted file mode 100644 index 8826b09422..0000000000 --- a/plugins/workflow_objc/MessageHandler.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -class MessageHandler { - - std::set m_msgSendFunctions; - static std::set findMsgSendFunctions(BinaryNinja::Ref data); - -public: - MessageHandler(BinaryNinja::Ref data); - - std::set 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 deleted file mode 100644 index 7e497b4ea6..0000000000 --- a/plugins/workflow_objc/Performance.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - -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 - static T elapsed(high_res_clock::time_point start) - { - auto end = high_res_clock::now(); - return std::chrono::duration_cast(end - start); - } -}; diff --git a/plugins/workflow_objc/Plugin.cpp b/plugins/workflow_objc/Plugin.cpp deleted file mode 100644 index 6323e1a66f..0000000000 --- a/plugins/workflow_objc/Plugin.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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> targets = { - BinaryNinja::Architecture::GetByName("aarch64"), - BinaryNinja::Architecture::GetByName("x86_64") - }; - - BinaryNinja::LogRegistry::CreateLogger(PluginLoggerName); - - auto settings = BinaryNinja::Settings::Instance(); - settings->RegisterSetting("analysis.objectiveC.resolveDynamicDispatch", - R"({ - "title" : "Resolve Dynamic Dispatch Calls", - "type" : "boolean", - "default" : false, - "aliases": ["core.function.objectiveC.assumeMessageSendTarget", "core.function.objectiveC.rewriteMessageSendTarget"], - "description" : "Replaces objc_msgSend calls with direct calls to the first found implementation when the target method is visible. May produce false positives when multiple classes implement the same selector or when selectors conflict with system framework methods." - })"); - - return true; -} -} diff --git a/plugins/workflow_objc/README.md b/plugins/workflow_objc/README.md index 1c1d47f39f..9de7b89e27 100644 --- a/plugins/workflow_objc/README.md +++ b/plugins/workflow_objc/README.md @@ -5,15 +5,23 @@ 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. +1. Automatic inlining of the `objc_msgSend$foo:bar:` selector stub functions. +2. Automatic call type adjustments for calls to `objc_msgSend` and `objc_msgSendSuper2`. + Call types are adjusted at each call site to set the number of arguments that are expected + based on the selector. Argument names are derived from the selector components, and argument + types are inferred in limited cases. +3. Direct call rewriting. Calls to `objc_msgSend` can be rewritten to be direct calls to + the first known method implementation for that selector. This is disabled by default + as it will give potentially confusing results for any selector that has more than one + implementation or for common selector names. That said, some users may still find it to + be useful. It can be enabled via the `analysis.objectiveC.resolveDynamicDispatch` + setting. 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. +Please file issues at https://github.com/Vector35/binaryninja-api/issues. ## Building @@ -21,17 +29,18 @@ 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 clone https://github.com/Vector35/binaryninja-api.git && cd binaryninja-api git submodule update --init --recursive -cmake -S . -B build -GNinja +cmake -S plugins/workflow_objc -B build -G Ninja 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: +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 original Objective Ninja license are as follows: ``` Copyright (c) 2022-2023 Jon Palmisciano diff --git a/plugins/workflow_objc/Workflow.cpp b/plugins/workflow_objc/Workflow.cpp deleted file mode 100644 index fb65f29a9c..0000000000 --- a/plugins/workflow_objc/Workflow.cpp +++ /dev/null @@ -1,316 +0,0 @@ -/* - * 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 - -#include -#include "binaryninjaapi.h" - -static std::mutex g_initialAnalysisMutex; - -using SectionRef = BinaryNinja::Ref; -using SymbolRef = BinaryNinja::Ref; - -namespace { - -std::vector splitSelector(const std::string& selector) { - std::vector 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 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 With: and For: - 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 generateArgumentNames(const std::vector& components) { - std::vector 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) -{ - auto function = ssa->GetFunction(); - const auto bv = function->GetView(); - const auto llil = ssa->GetNonSSAForm(); - const auto insn = ssa->GetInstruction(insnIndex); - const auto params = insn.GetParameterExprs(); - - // 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(); - rawSelector = ssa->GetSSARegisterValue(selectorRegister).value; - } - else if (params[0].operation == LLIL_SEPARATE_PARAM_LIST_SSA) - { - if (params[0].GetParameterExprs().size() == 0) - { - return false; - } - const auto selectorRegister = params[0].GetParameterExprs()[1].GetSourceSSARegister(); - 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 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 selectorComponents = splitSelector(selector); - std::vector 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); - function->SetAutoCallTypeAdjustment(function->GetArchitecture(), insn.address, {funcType, BN_DEFAULT_CONFIDENCE}); - // -- - - if (!BinaryNinja::Settings::Instance()->Get("analysis.objectiveC.resolveDynamicDispatch", function)) - 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 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 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. - switch (llilInsn.operation) { - case LLIL_CALL: { - auto callDestExpr = llilInsn.GetDestExpr(); - callDestExpr.Replace(llil->ConstPointer(callDestExpr.size, implAddress, callDestExpr)); - llilInsn.Replace(llil->Call(callDestExpr.exprIndex, llilInsn)); - break; - } - case LLIL_TAILCALL: { - auto callDestExpr = llilInsn.GetDestExpr(); - callDestExpr.Replace(llil->ConstPointer(callDestExpr.size, implAddress, callDestExpr)); - llilInsn.Replace(llil->TailCall(callDestExpr.exprIndex, llilInsn)); - break; - } - default: - const auto log = BinaryNinja::LogRegistry::GetLogger(PluginLoggerName); - log->LogDebugF("Unexpected LLIL operation {} for objc_msgSend call at {:#0x}", llilInsn.operation, llilInsn.address); - return false; - } - - 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 || insn.operation == LLIL_TAILCALL_SSA) - { - // Filter out calls that aren't to `objc_msgSend`. - auto callExpr = insn.GetDestExpr(); - auto callTarget = callExpr.GetValue().value; - bool isMessageSend = messageHandler->isMessageSend(callTarget); - if (auto symbol = bv->GetSymbolByAddress(callTarget)) - 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::Get("core.function.baseAnalysis")->Clone("core.function.objectiveC"); - wf->RegisterActivity(new BinaryNinja::Activity( - ActivityID::ResolveMethodCalls, &Workflow::inlineMethodCalls)); - wf->InsertAfter("core.function.translateTailCalls", ActivityID::ResolveMethodCalls); - - BinaryNinja::Workflow::RegisterWorkflow(wf, WorkflowInfo); -} diff --git a/plugins/workflow_objc/Workflow.h b/plugins/workflow_objc/Workflow.h deleted file mode 100644 index 4479a8a740..0000000000 --- a/plugins/workflow_objc/Workflow.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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(); -}; diff --git a/plugins/workflow_objc/build.rs b/plugins/workflow_objc/build.rs new file mode 100644 index 0000000000..ed6cec7d27 --- /dev/null +++ b/plugins/workflow_objc/build.rs @@ -0,0 +1,15 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(not(target_os = "windows"))] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } +} diff --git a/plugins/workflow_objc/src/activities/inline_stubs.rs b/plugins/workflow_objc/src/activities/inline_stubs.rs new file mode 100644 index 0000000000..daae9baaa8 --- /dev/null +++ b/plugins/workflow_objc/src/activities/inline_stubs.rs @@ -0,0 +1,26 @@ +use binaryninja::workflow::AnalysisContext; + +use crate::{metadata::GlobalState, Error}; + +// If this function is within a section known to contain Objective-C stubs, +// mark it as being inlined during analysis. +pub fn process(ac: &AnalysisContext) -> Result<(), Error> { + let view = ac.view(); + if GlobalState::should_ignore_view(&view) { + return Ok(()); + } + + let func = ac.function(); + let Some(objc_stubs) = GlobalState::analysis_info(&view) + .as_ref() + .and_then(|info| info.objc_stubs.clone()) + else { + return Ok(()); + }; + + if objc_stubs.contains(&func.start()) { + func.set_auto_inline_during_analysis(true); + } + + Ok(()) +} diff --git a/plugins/workflow_objc/src/activities/mod.rs b/plugins/workflow_objc/src/activities/mod.rs new file mode 100644 index 0000000000..1134690014 --- /dev/null +++ b/plugins/workflow_objc/src/activities/mod.rs @@ -0,0 +1,2 @@ +pub mod inline_stubs; +pub mod objc_msg_send_calls; diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs new file mode 100644 index 0000000000..239b606c0e --- /dev/null +++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs @@ -0,0 +1,164 @@ +use binaryninja::{ + binary_view::{BinaryView, BinaryViewExt as _}, + function::Function, + low_level_il::{ + expression::{ExpressionHandler as _, LowLevelILExpressionKind}, + function::{LowLevelILFunction, Mutable, NonSSA, SSA}, + instruction::{InstructionHandler as _, LowLevelILInstruction, LowLevelILInstructionKind}, + operation::{CallSsa, Operation}, + }, + variable::PossibleValueSet, + workflow::AnalysisContext, +}; + +use crate::{ + metadata::{GlobalState, Selector}, + Error, +}; + +mod adjust_call_type; +mod rewrite_to_direct_call; + +// Apply all transformations that are specific to calls to `objc_msgSend` and `objc_msgSendSuper2` +// At present these are: +// 1. Call type adjustments +// 2. Rewriting to direct calls, if enabled. +pub fn process(ac: &AnalysisContext) -> Result<(), Error> { + let bv = ac.view(); + if GlobalState::should_ignore_view(&bv) { + return Ok(()); + } + + let func_start = ac.function().start(); + let Some(llil) = (unsafe { ac.llil_function() }) else { + return Err(Error::MissingLowLevelIL { func_start }); + }; + + let Some(ssa) = llil.ssa_form() else { + return Err(Error::MissingSsaForm { func_start }); + }; + + let func = ac.function(); + let mut function_changed = false; + for block in ssa.basic_blocks().iter() { + for insn in block.iter() { + match process_instruction(&bv, &func, &llil, &ssa, &insn) { + Ok(true) => function_changed = true, + Ok(_) => {} + Err(err) => { + log::error!( + "Error processing instruction at {:#x}: {}", + insn.address(), + err + ); + continue; + } + } + } + } + + if function_changed { + // Regenerate SSA form after modifications + llil.generate_ssa_form(); + } + Ok(()) +} + +// Process a single instruction, looking for calls to `objc_msgSend` or `objc_msgSendSuper2` +// Returns `Ok(false)` if the instruction is not a relevant call or if the function was not +// modified. Returns `Ok(true)` to indicate that the function was modified. +fn process_instruction( + bv: &BinaryView, + func: &Function, + llil: &LowLevelILFunction, + ssa: &LowLevelILFunction, + insn: &LowLevelILInstruction, +) -> Result { + let call_op = match insn.kind() { + LowLevelILInstructionKind::CallSsa(op) => op, + LowLevelILInstructionKind::TailCallSsa(op) => op, + _ => return Ok(false), + }; + + let target = call_op.target(); + let target_values = target.possible_values(); + + // Check if the target is a constant pointer to objc_msgSend + let call_target = match target_values { + PossibleValueSet::ConstantValue { value } + | PossibleValueSet::ConstantPointerValue { value } + | PossibleValueSet::ImportedAddressValue { value } => value as u64, + _ => return Ok(false), + }; + + let Some(message_send_type) = call_target_type(bv, call_target) else { + return Ok(false); + }; + + let Some(selector) = selector_from_call(bv, ssa, &call_op) else { + return Ok(false); + }; + + let mut function_changed = false; + if adjust_call_type::process_call(bv, func, insn, &selector, message_send_type).is_ok() { + function_changed = true; + } + + if rewrite_to_direct_call::process_call(bv, llil, insn, &selector, message_send_type).is_ok() { + function_changed = true; + } + + Ok(function_changed) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum MessageSendType { + Normal, + Super, +} + +fn call_target_type(bv: &BinaryView, call_target: u64) -> Option { + let name = bv + .symbol_by_address(call_target) + .map(|s| s.raw_name().to_string_lossy().into_owned())?; + + // Strip the `j_` prefix that the shared cache adds to the names of stub functions + let name = name.strip_prefix("j_").unwrap_or(&name); + + if name == "_objc_msgSend" { + Some(MessageSendType::Normal) + } else if name == "_objc_msgSendSuper" || name == "_objc_msgSendSuper2" { + Some(MessageSendType::Super) + } else { + None + } +} + +fn selector_from_call( + bv: &BinaryView, + ssa: &LowLevelILFunction, + call_op: &Operation, +) -> Option { + let LowLevelILExpressionKind::CallParamSsa(params) = &call_op.param_expr().kind() else { + return None; + }; + + let param_exprs = params.param_exprs(); + let param_exprs = + if let LowLevelILExpressionKind::SeparateParamListSsa(params) = ¶m_exprs[0].kind() { + params.param_exprs() + } else { + param_exprs + }; + + let LowLevelILExpressionKind::RegSsa(reg) = param_exprs.get(1)?.kind() else { + return None; + }; + + let raw_selector = ssa.get_ssa_register_value(®.source_reg())?.value as u64; + if raw_selector == 0 { + return None; + } + + Selector::from_address(bv, raw_selector).ok() +} diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs new file mode 100644 index 0000000000..21a3d35fd6 --- /dev/null +++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls/adjust_call_type.rs @@ -0,0 +1,123 @@ +use binaryninja::{ + binary_view::{BinaryView, BinaryViewBase as _, BinaryViewExt}, + confidence::Conf, + function::Function, + low_level_il::{ + function::{Mutable, SSA}, + instruction::LowLevelILInstruction, + }, + rc::Ref, + types::{FunctionParameter, Type}, +}; + +use super::MessageSendType; +use crate::{metadata::Selector, Error}; + +const HEURISTIC_CONFIDENCE: u8 = 192; + +fn named_type(bv: &BinaryView, name: &str) -> Option> { + bv.type_by_name(name) + .map(|t| Type::named_type_from_type(name, &t)) +} + +pub fn process_call( + bv: &BinaryView, + func: &Function, + insn: &LowLevelILInstruction, + selector: &Selector, + message_send_type: MessageSendType, +) -> Result<(), Error> { + let arch = func.arch(); + let id = named_type(bv, "id").unwrap_or_else(|| Type::pointer(&arch, &Type::void())); + let (receiver_type, receiver_name) = match message_send_type { + MessageSendType::Normal => (id.clone(), "self"), + MessageSendType::Super => ( + Type::pointer( + &arch, + &named_type(bv, "objc_super").unwrap_or_else(Type::void), + ), + "super", + ), + }; + let sel = named_type(bv, "SEL").unwrap_or_else(|| Type::pointer(&arch, &Type::char())); + + // TODO: Infer return type based on receiver type / selector. + let return_type = id.clone(); + + let mut params = vec![ + FunctionParameter::new(receiver_type, receiver_name.to_string(), None), + FunctionParameter::new(sel, "sel".to_string(), None), + ]; + + let argument_labels = selector.argument_labels(); + let mut argument_names = generate_argument_names(&argument_labels); + + // Pad out argument names if necessary + for i in argument_names.len()..argument_labels.len() { + argument_names.push(format!("arg{i}")); + } + + // Create types for all arguments. For now they're all signed integers of the platform word size. + let arg_type = Type::int(bv.address_size(), true); + params.extend( + argument_names + .into_iter() + .map(|name| FunctionParameter::new(arg_type.clone(), name, None)), + ); + + let func_type = Type::function(&return_type, params, false); + func.set_auto_call_type_adjustment( + insn.address(), + Conf::new(func_type, HEURISTIC_CONFIDENCE).as_ref(), + Some(arch), + ); + + Ok(()) +} + +fn selector_label_without_prefix(prefix: &str, label: &str) -> Option { + if label.len() <= prefix.len() || !label.starts_with(prefix) { + return None; + } + + let after_prefix = &label[prefix.len()..]; + + // If the character after the prefix is lowercase, the label may be something like "settings" + // in which case "set" should not be considered a prefix. + let (first, rest) = after_prefix.split_at_checked(1)?; + if first.chars().next()?.is_lowercase() { + return None; + } + + // Lowercase the first character if the second character is not also uppercase. + // This ensures we leave initialisms such as `URL` alone. + let (second, rest) = rest.split_at_checked(1)?; + Some(match second.chars().next() { + Some(c) if c.is_lowercase() => { + format!("{}{}{}", first.to_lowercase(), second, rest) + } + _ => after_prefix.to_string(), + }) +} + +fn argument_name_from_selector_label(label: &str) -> String { + // TODO: Handle other common patterns such as With: and For: + let prefixes = [ + "initWith", "with", "and", "using", "set", "read", "to", "for", + ]; + + for prefix in prefixes { + if let Some(arg_name) = selector_label_without_prefix(prefix, label) { + return arg_name; + } + } + + label.to_owned() +} + +fn generate_argument_names(labels: &[String]) -> Vec { + labels + .iter() + .map(|label| argument_name_from_selector_label(label)) + .collect() +} diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs new file mode 100644 index 0000000000..0ddf294561 --- /dev/null +++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls/rewrite_to_direct_call.rs @@ -0,0 +1,61 @@ +use binaryninja::{ + binary_view::BinaryView, + low_level_il::{ + function::{LowLevelILFunction, Mutable, NonSSA, SSA}, + instruction::{InstructionHandler as _, LowLevelILInstruction, LowLevelILInstructionKind}, + }, +}; + +use super::MessageSendType; +use crate::{ + metadata::{GlobalState, Selector}, + Error, +}; + +// TODO: This always rewrites to the first known implementation of a given selector. +// This is Not Good as it will pick the wrong target for any common selector. +// In order to do better we need to consider receiver type information. +pub fn process_call( + bv: &BinaryView, + llil: &LowLevelILFunction, + insn: &LowLevelILInstruction, + selector: &Selector, + message_send_type: MessageSendType, +) -> Result<(), Error> { + if message_send_type == MessageSendType::Super { + return Ok(()); + } + + let Some(info) = + GlobalState::analysis_info(bv).filter(|info| info.should_rewrite_to_direct_calls) + else { + return Ok(()); + }; + + let Some(impl_address) = info.get_selector_impl(bv, selector.addr) else { + return Ok(()); + }; + + // Change the destination expression of the call instruction to point directly to + // the method implementation. + let llil_insn = insn.non_ssa_form(llil); + match llil_insn.kind() { + LowLevelILInstructionKind::Call(call_op) | LowLevelILInstructionKind::TailCall(call_op) => { + let dest_expr = call_op.target(); + unsafe { + llil.replace_expression(dest_expr.index, llil.const_ptr(impl_address)); + } + Ok(()) + } + _ => { + log::error!( + "Unexpected LLIL operation for objc_msgSend call at {:#x}", + insn.address() + ); + Err(Error::UnexpectedLlilOperation { + address: insn.address(), + expected: "Call or TailCall".to_string(), + }) + } + } +} diff --git a/plugins/workflow_objc/src/error.rs b/plugins/workflow_objc/src/error.rs new file mode 100644 index 0000000000..7532822662 --- /dev/null +++ b/plugins/workflow_objc/src/error.rs @@ -0,0 +1,30 @@ +use thiserror::Error; + +/// A marker type for workflow registration errors +#[derive(Debug, Error)] +#[error("Failed to register workflow activity")] +pub struct WorkflowRegistrationError; + +impl From<()> for WorkflowRegistrationError { + fn from(_: ()) -> Self { + WorkflowRegistrationError + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Unable to retrieve low-level IL for function at {func_start:#x}")] + MissingLowLevelIL { func_start: u64 }, + + #[error("Unable to retrieve low-level SSA IL for function at {func_start:#x}")] + MissingSsaForm { func_start: u64 }, + + #[error("Unexpected LLIL operation at address {address:#x} (expected {expected})")] + UnexpectedLlilOperation { address: u64, expected: String }, + + #[error("Invalid selector at address {address:#x}")] + InvalidSelector { address: u64 }, + + #[error(transparent)] + WorkflowRegistrationFailed(#[from] WorkflowRegistrationError), +} diff --git a/plugins/workflow_objc/src/lib.rs b/plugins/workflow_objc/src/lib.rs new file mode 100644 index 0000000000..d877301ae8 --- /dev/null +++ b/plugins/workflow_objc/src/lib.rs @@ -0,0 +1,48 @@ +use binaryninja::{add_optional_plugin_dependency, logger::Logger, settings::Settings}; + +mod activities; +mod error; +mod metadata; +mod workflow; + +pub use error::Error; +use metadata::GlobalState; + +use log::LevelFilter; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginDependencies() { + add_optional_plugin_dependency("arch_x86"); + add_optional_plugin_dependency("arch_armv7"); + add_optional_plugin_dependency("arch_arm64"); +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginInit() -> bool { + Logger::new("Plugin.Objective-C") + .with_level(LevelFilter::Debug) + .init(); + + if workflow::register_activities().is_err() { + log::warn!("Failed to register Objective-C workflow"); + return false; + }; + + let settings = Settings::new(); + settings.register_setting_json( + "analysis.objectiveC.resolveDynamicDispatch", + r#"{ + "title" : "Resolve Dynamic Dispatch Calls", + "type" : "boolean", + "default" : false, + "aliases": ["core.function.objectiveC.assumeMessageSendTarget", "core.function.objectiveC.rewriteMessageSendTarget"], + "description" : "Replaces objc_msgSend calls with direct calls to the first found implementation when the target method is visible. May produce false positives when multiple classes implement the same selector or when selectors conflict with system framework methods." + }"#, + ); + + GlobalState::register_cleanup(); + + true +} diff --git a/plugins/workflow_objc/src/metadata/global_state.rs b/plugins/workflow_objc/src/metadata/global_state.rs new file mode 100644 index 0000000000..6110445169 --- /dev/null +++ b/plugins/workflow_objc/src/metadata/global_state.rs @@ -0,0 +1,207 @@ +use binaryninja::{ + binary_view::{BinaryView, BinaryViewBase, BinaryViewExt}, + file_metadata::FileMetadata, + metadata::Metadata, + rc::Ref, + settings::{QueryOptions, Settings}, + ObjectDestructor, +}; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use std::{ + collections::{HashMap, HashSet}, + ops::Range, + sync::{Arc, RwLock}, +}; + +pub struct AnalysisInfo { + pub image_base: u64, + pub objc_stubs: Option>, + pub should_rewrite_to_direct_calls: bool, + selector_impls: RwLock, +} + +enum SelectorImplsState { + NotLoaded, + Loaded(Option), +} + +struct SelectorImplementations { + sel_ref_to_impl: HashMap>, + sel_to_impl: HashMap>, +} + +static VIEW_INFOS: Lazy>> = Lazy::new(DashMap::new); +static IGNORED_VIEWS: Lazy> = Lazy::new(DashMap::new); + +struct ObjectLifetimeObserver; + +impl ObjectDestructor for ObjectLifetimeObserver { + fn destruct_file_metadata(&self, metadata: &FileMetadata) { + let id = metadata.session_id(); + VIEW_INFOS.remove(&id); + IGNORED_VIEWS.remove(&id); + } +} + +static SUPPORTED_ARCHS: Lazy> = Lazy::new(|| { + let mut m = HashSet::new(); + m.insert("aarch64"); + m.insert("x86_64"); + m.insert("armv7"); + m.insert("thumb2"); + m +}); + +fn is_supported_arch(bv: &BinaryView) -> bool { + let arch_name = bv + .default_arch() + .map(|arch| arch.name()) + .unwrap_or_default(); + SUPPORTED_ARCHS.contains(&arch_name as &str) +} + +pub struct GlobalState; + +impl GlobalState { + pub fn register_cleanup() { + let observer = Box::leak(Box::new(ObjectLifetimeObserver)); + observer.register(); + } + + fn id(bv: &BinaryView) -> usize { + bv.file().session_id() + } + + pub fn analysis_info(bv: &BinaryView) -> Option> { + let id = Self::id(bv); + + if let Some(info) = VIEW_INFOS.get(&id) { + if bv.start() == info.image_base { + return Some(info.clone()); + } + } + + let info = Arc::new(AnalysisInfo::from_view(bv)?); + VIEW_INFOS.insert(id, info.clone()); + Some(info) + } + + pub fn should_ignore_view(bv: &BinaryView) -> bool { + if let Some(ignore) = IGNORED_VIEWS.get(&Self::id(bv)) { + return *ignore; + } + + let ignore = !(is_supported_arch(bv) && AnalysisInfo::has_metadata(bv)); + IGNORED_VIEWS.insert(Self::id(bv), ignore); + ignore + } +} + +impl AnalysisInfo { + fn from_view(bv: &BinaryView) -> Option { + let should_rewrite_to_direct_calls = Settings::new().get_bool_with_opts( + "analysis.objectiveC.resolveDynamicDispatch", + &mut QueryOptions::new_with_view(bv), + ); + let info = AnalysisInfo { + image_base: bv.start(), + objc_stubs: bv + .section_by_name("__objc_stubs") + .map(|section| section.start()..section.end()), + should_rewrite_to_direct_calls, + selector_impls: RwLock::new(SelectorImplsState::NotLoaded), + }; + if !Self::has_metadata(bv) { + return None; + } + Some(info) + } + + fn has_metadata(bv: &BinaryView) -> bool { + bv.query_metadata("Objective-C").is_some() + } + + pub fn get_selector_impl(&self, bv: &BinaryView, selector_addr: u64) -> Option { + let get = |impls: &SelectorImplementations| { + impls + .sel_ref_to_impl + .get(&selector_addr) + .or_else(|| impls.sel_to_impl.get(&selector_addr)) + .and_then(|v| v.first().copied()) + .filter(|&addr| addr != 0) + }; + + let cache = self.selector_impls.read().unwrap(); + match &*cache { + SelectorImplsState::Loaded(Some(impls)) => return get(impls), + SelectorImplsState::Loaded(None) => return None, + SelectorImplsState::NotLoaded => {} + } + drop(cache); + + let mut cache = self.selector_impls.write().unwrap(); + if let SelectorImplsState::NotLoaded = &*cache { + *cache = SelectorImplsState::Loaded(self.load_selector_impls(bv)); + } + + if let SelectorImplsState::Loaded(Some(impls)) = &*cache { + get(impls) + } else { + None + } + } + + fn load_selector_impls(&self, bv: &BinaryView) -> Option { + let Some(Ok(meta)) = bv.get_metadata::>>("Objective-C") + else { + return None; + }; + let version_meta = meta.get("version")?; + if version_meta.get_unsigned_integer()? != 1 { + log::error!( + "workflow_objc: Unexpected Objective-C metadata version. Expected 1, got {}.", + version_meta.get_unsigned_integer()? + ); + return None; + } + + let mut sel_ref_to_impl = HashMap::new(); + if let Some(sel_ref_to_impl_meta) = meta.get("selRefImplementations") { + if let Some(map) = Self::parse_selector_impls(sel_ref_to_impl_meta) { + sel_ref_to_impl = map; + } + } + + let mut sel_to_impl = HashMap::new(); + if let Some(sel_to_impl_meta) = meta.get("selImplementations") { + if let Some(map) = Self::parse_selector_impls(sel_to_impl_meta) { + sel_to_impl = map; + } + } + + Some(SelectorImplementations { + sel_ref_to_impl, + sel_to_impl, + }) + } + + fn parse_selector_impls(meta: &Metadata) -> Option>> { + let array = meta.get_array()?; + let mut result = HashMap::new(); + for item in &array { + let item = item.get_array()?; + if item.len() != 2 { + log::warn!( + "Expected selector implementation metadata to have 2 items, found {}", + item.len() + ); + return None; + } + let selector = item.get(0).get_unsigned_integer()?; + let impls_meta = item.get(1).get_unsigned_integer_list()?; + result.insert(selector, impls_meta); + } + Some(result) + } +} diff --git a/plugins/workflow_objc/src/metadata/mod.rs b/plugins/workflow_objc/src/metadata/mod.rs new file mode 100644 index 0000000000..58926eb2ab --- /dev/null +++ b/plugins/workflow_objc/src/metadata/mod.rs @@ -0,0 +1,5 @@ +mod global_state; +mod selector; + +pub use global_state::GlobalState; +pub use selector::Selector; diff --git a/plugins/workflow_objc/src/metadata/selector.rs b/plugins/workflow_objc/src/metadata/selector.rs new file mode 100644 index 0000000000..a48be14d9c --- /dev/null +++ b/plugins/workflow_objc/src/metadata/selector.rs @@ -0,0 +1,52 @@ +use crate::Error; +use binaryninja::binary_view::{BinaryView, BinaryViewBase as _, BinaryViewExt}; + +pub struct Selector { + pub name: String, + pub addr: u64, +} + +impl Selector { + pub fn from_address(bv: &BinaryView, addr: u64) -> Result { + let name = if bv.offset_valid(addr) { + // Read the selector name from the binary view + read_cstring(bv, addr, 500) + } else { + // Look for the `sel_` symbols that ObjCProcessor adds to represent selectors + // whose backing regions have not yet been loaded into the view. + bv.symbol_by_address(addr) + .and_then(|sym| sym.raw_name().to_str().ok().map(|name| name.to_owned())) + .filter(|name| name.starts_with("sel_")) + .map(|name| name["sel_".len()..].to_string()) + } + .ok_or(Error::InvalidSelector { address: addr })?; + Ok(Selector { name, addr }) + } + + pub fn argument_labels(&self) -> Vec { + if !self.name.contains(':') { + return vec![]; + } + + self.name + .split(':') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() + } +} + +// Read a null-terminated string from the view +fn read_cstring(bv: &BinaryView, address: u64, max_len: usize) -> Option { + let mut buffer = vec![0u8; max_len]; + let bytes_read = bv.read(&mut buffer, address); + if bytes_read == 0 { + return None; + } + + // Find the null terminator + let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(bytes_read); + buffer.truncate(null_pos); + + String::from_utf8(buffer).ok() +} diff --git a/plugins/workflow_objc/src/workflow.rs b/plugins/workflow_objc/src/workflow.rs new file mode 100644 index 0000000000..090a8e5e4e --- /dev/null +++ b/plugins/workflow_objc/src/workflow.rs @@ -0,0 +1,57 @@ +use binaryninja::workflow::{activity, Activity, AnalysisContext, Workflow}; + +use crate::{activities, error::WorkflowRegistrationError}; + +const WORKFLOW_INFO: &str = r#"{ + "title": "Objective-C", + "description": "Enhanced analysis for Objective-C code.", + "capabilities": [] +}"#; + +fn run( + func: impl Fn(&AnalysisContext) -> Result<(), E>, +) -> impl Fn(&AnalysisContext) { + move |ac| { + if let Err(err) = func(ac) { + log::debug!("Error occurred while running activity: {err:#x?}"); + } + } +} + +pub fn register_activities() -> Result<(), WorkflowRegistrationError> { + let workflow = + Workflow::cloned("core.function.metaAnalysis").ok_or(WorkflowRegistrationError)?; + + let objc_msg_send_calls_activity = Activity::new_with_action( + activity::Config::action( + "core.function.objectiveC.analyzeMessageSends", + "Obj-C: Analyze Message Sends", + "Analyze inline objc_msgSend calls, including applying call type adjustments and resolving to direct calls (if enabled)", + ).eligibility( + activity::Eligibility::auto().predicate( + activity::ViewType::in_(["Mach-O", "DSCView"]), + )), + run(activities::objc_msg_send_calls::process), + ); + + let inline_stubs_activity = Activity::new_with_action( + activity::Config::action( + "core.function.objectiveC.inlineStubs", + "Obj-C: Inline Message Send Stubs", + "Inline Objective-C selector stubs, such as _objc_msgSend$foo, into their callers", + ) + .eligibility( + activity::Eligibility::without_setting() + // The shared cache view does its own inlining of stub functions. + .predicate(activity::ViewType::in_(["Mach-O"])), + ), + run(activities::inline_stubs::process), + ); + + workflow + .activity_after(&inline_stubs_activity, "core.function.translateTailCalls")? + .activity_after(&objc_msg_send_calls_activity, &inline_stubs_activity.name())? + .register_with_config(WORKFLOW_INFO)?; + + Ok(()) +} diff --git a/rust/src/low_level_il/function.rs b/rust/src/low_level_il/function.rs index 1955c147be..65c3536a4c 100644 --- a/rust/src/low_level_il/function.rs +++ b/rust/src/low_level_il/function.rs @@ -18,11 +18,12 @@ use std::marker::PhantomData; use binaryninjacore_sys::*; -use crate::architecture::CoreArchitecture; +use crate::architecture::{CoreArchitecture, CoreFlag}; use crate::basic_block::BasicBlock; use crate::function::Function; use crate::low_level_il::block::LowLevelILBlock; use crate::rc::*; +use crate::variable::RegisterValue; use super::*; @@ -248,7 +249,7 @@ impl Ref> { } } -impl Ref> { +impl LowLevelILFunction { /// Return a vector of all instructions that use the given SSA register. #[must_use] pub fn get_ssa_register_uses( @@ -297,6 +298,37 @@ impl Ref> { }; self.instruction_from_index(LowLevelInstructionIndex(instr_idx)) } + + /// Returns the value of the given SSA register. + #[must_use] + pub fn get_ssa_register_value( + &self, + reg: &LowLevelILSSARegisterKind, + ) -> Option { + let register_id = match reg { + LowLevelILSSARegisterKind::Full { kind, .. } => kind.id(), + LowLevelILSSARegisterKind::Partial { partial_reg, .. } => partial_reg.id(), + }; + let value = unsafe { + BNGetLowLevelILSSARegisterValue(self.handle, register_id.into(), reg.version() as usize) + }; + if value.state == BNRegisterValueType::UndeterminedValue { + return None; + } + Some(value.into()) + } + + /// Returns the value of the given SSA flag. + #[must_use] + pub fn get_ssa_flag_value(&self, flag: &LowLevelILSSAFlag) -> Option { + let value = unsafe { + BNGetLowLevelILSSAFlagValue(self.handle, flag.flag.id().0, flag.version as usize) + }; + if value.state == BNRegisterValueType::UndeterminedValue { + return None; + } + Some(value.into()) + } } impl ToOwned for LowLevelILFunction diff --git a/rust/src/low_level_il/lifting.rs b/rust/src/low_level_il/lifting.rs index f2850776cf..a51cd3d5f2 100644 --- a/rust/src/low_level_il/lifting.rs +++ b/rust/src/low_level_il/lifting.rs @@ -1048,6 +1048,7 @@ impl LowLevelILMutableFunction { no_arg_lifter!(bp, LLIL_BP, VoidExpr); unsized_unary_op_lifter!(call, LLIL_CALL, VoidExpr); + unsized_unary_op_lifter!(tailcall, LLIL_TAILCALL, VoidExpr); unsized_unary_op_lifter!(ret, LLIL_RET, VoidExpr); unsized_unary_op_lifter!(jump, LLIL_JUMP, VoidExpr); // TODO: LLIL_JUMP_TO diff --git a/rust/src/variable.rs b/rust/src/variable.rs index 7e32727a3b..fc6febfb67 100644 --- a/rust/src/variable.rs +++ b/rust/src/variable.rs @@ -654,7 +654,9 @@ pub enum PossibleValueSet { value: i64, }, ReturnAddressValue, - ImportedAddressValue, + ImportedAddressValue { + value: i64, + }, SignedRangeValue { value: i64, ranges: Vec>, @@ -708,7 +710,9 @@ impl PossibleValueSet { }, RegisterValueType::StackFrameOffset => Self::StackFrameOffset { value: value.value }, RegisterValueType::ReturnAddressValue => Self::ReturnAddressValue, - RegisterValueType::ImportedAddressValue => Self::ImportedAddressValue, + RegisterValueType::ImportedAddressValue => { + Self::ImportedAddressValue { value: value.value } + } RegisterValueType::SignedRangeValue => { let raw_ranges = unsafe { std::slice::from_raw_parts(value.ranges, value.count) }; Self::SignedRangeValue { @@ -791,7 +795,9 @@ impl PossibleValueSet { raw.value = value; } PossibleValueSet::ReturnAddressValue => {} - PossibleValueSet::ImportedAddressValue => {} + PossibleValueSet::ImportedAddressValue { value } => { + raw.value = value; + } PossibleValueSet::SignedRangeValue { value, ranges } => { let boxed_raw_ranges: Box<[BNValueRange]> = ranges.into_iter().map(BNValueRange::from).collect(); @@ -884,7 +890,9 @@ impl PossibleValueSet { } PossibleValueSet::StackFrameOffset { .. } => RegisterValueType::StackFrameOffset, PossibleValueSet::ReturnAddressValue => RegisterValueType::ReturnAddressValue, - PossibleValueSet::ImportedAddressValue => RegisterValueType::ImportedAddressValue, + PossibleValueSet::ImportedAddressValue { .. } => { + RegisterValueType::ImportedAddressValue + } PossibleValueSet::SignedRangeValue { .. } => RegisterValueType::SignedRangeValue, PossibleValueSet::UnsignedRangeValue { .. } => RegisterValueType::UnsignedRangeValue, PossibleValueSet::LookupTableValue { .. } => RegisterValueType::LookupTableValue, diff --git a/rust/src/workflow/activity.rs b/rust/src/workflow/activity.rs index 25f7bdf59c..a893e79830 100644 --- a/rust/src/workflow/activity.rs +++ b/rust/src/workflow/activity.rs @@ -287,6 +287,19 @@ pub struct Eligibility { } impl Eligibility { + /// Creates a new instance without an automatically generated boolean control setting. + /// The activity is eligible to run by default. + pub fn without_setting() -> Self { + Eligibility { + auto: None, + run_once: None, + run_once_per_session: None, + continuation: None, + predicates: vec![], + logical_operator: None, + } + } + /// Creates a new instance with an automatically generated boolean control setting and corresponding predicate. /// The setting is enabled by default. pub fn auto() -> Self { diff --git a/view/macho/machoview.cpp b/view/macho/machoview.cpp index 1da2bda798..1c65629af7 100644 --- a/view/macho/machoview.cpp +++ b/view/macho/machoview.cpp @@ -1845,6 +1845,17 @@ bool MachoView::InitializeHeader(MachOHeader& header, bool isMainHeader, uint64_ BinaryReader virtualReader(this); virtualReader.SetEndianness(m_endian); + if (m_file->IsBackedByDatabase(GetTypeName())) + { + // The `core.function.objectiveC` workflow has been removed in favor of the meta-analysis + // workflow. If the file is backed by a database, we need to update any references to the + // old workflow to refer to the meta-analysis workflow. + Ref analysisSettings = Settings::Instance(); + auto workflow = analysisSettings->Get("analysis.workflows.functionWorkflow", this); + if (workflow == "core.function.objectiveC") + analysisSettings->Set("analysis.workflows.functionWorkflow", "core.function.metaAnalysis", this); + } + bool parseObjCStructs = true; bool parseCFStrings = true; if (settings && settings->Contains("loader.macho.processObjectiveC")) @@ -1861,20 +1872,6 @@ bool MachoView::InitializeHeader(MachOHeader& header, bool isMainHeader, uint64_ { objcProcessor = std::make_unique(this); } - if (parseObjCStructs) - { - if (!settings) // Add our defaults - { - Ref programSettings = Settings::Instance(); - if (programSettings->Contains("corePlugins.workflows.objc")) - { - if (programSettings->Get("corePlugins.workflows.objc")) - { - programSettings->Set("analysis.workflows.functionWorkflow", "core.function.objectiveC", this); - } - } - } - } // parse thread starts section if available bool rebaseThreadStarts = false; @@ -4133,14 +4130,6 @@ Ref MachoViewType::GetLoadSettingsForData(BinaryView* data) "default" : true, "description" : "Processes Objective-C structures, applying method names and types from encoded metadata" })"); - Ref programSettings = Settings::Instance(); - if (programSettings->Contains("corePlugins.workflows.objc")) - { - if (programSettings->Get("corePlugins.workflows.objc")) - { - programSettings->Set("analysis.workflows.functionWorkflow", "core.function.objectiveC", viewRef); - } - } } if (viewRef->GetSectionByName("__cfstring")) { diff --git a/view/sharedcache/workflow/ObjCActivity.cpp b/view/sharedcache/workflow/ObjCActivity.cpp deleted file mode 100644 index f3efbccd10..0000000000 --- a/view/sharedcache/workflow/ObjCActivity.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "ObjCActivity.h" -#include "lowlevelilinstruction.h" - -// TODO: Consolidate this with the Obj-C workflow at some point https://github.com/Vector35/workflow_objc - -using namespace BinaryNinja; - -void ObjCActivity::Register(Workflow &workflow) -{ - workflow.RegisterActivity(new Activity(R"({ - "name": "core.analysis.sharedCache.objc.adjustCallType", - "eligibility": { - "predicates": [ - { - "type": "viewType", - "operator": "in", - "value": [ - "DSCView" - ] - } - ] - } - })", &AdjustCallType)); - workflow.Insert("core.function.analyzeTailCalls", "core.analysis.sharedCache.objc.adjustCallType"); -} - -std::vector splitSelector(const std::string& selector) { - std::vector components; - std::istringstream stream(selector); - std::string component; - - while (std::getline(stream, component, ':')) { - if (!component.empty()) { - components.push_back(component); - } - } - - return components; -} - -std::vector generateArgumentNames(const std::vector& components) { - std::vector 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(argumentName); - } - - return argumentNames; -} - -void ObjCActivity::AdjustCallType(Ref ctx) -{ - const auto func = ctx->GetFunction(); - const auto arch = func->GetArchitecture(); - const auto bv = func->GetView(); - const auto baseAddr = bv->GetStart(); - - const auto llil = ctx->GetLowLevelILFunction(); - if (!llil) { - return; - } - const auto ssa = llil->GetSSAForm(); - if (!ssa) { - return; - } - - const auto rewriteIfEligible = [bv, ssa, baseAddr](size_t insnIndex) { - auto insn = ssa->GetInstruction(insnIndex); - if (insn.operation != LLIL_CALL_SSA && insn.operation != LLIL_TAILCALL_SSA) - return; - - enum class MessageSendType { - Normal, - Super, - }; - - MessageSendType messageSendType = MessageSendType::Normal; - // Filter out calls that aren't to `objc_msgSend`, `objc_msgSendSuper`, or `objc_msgSendSuper2`. - auto callExpr = insn.GetDestExpr(); - if (auto symbol = bv->GetSymbolByAddress(callExpr.GetValue().value)) - { - std::string_view symbolName = symbol->GetRawNameRef(); - if (symbolName == "_objc_msgSend") - messageSendType = MessageSendType::Normal; - else if (symbolName == "_objc_msgSendSuper2" || symbolName == "_objc_msgSendSuper") - messageSendType = MessageSendType::Super; - else - return; - } - - const auto params = insn.GetParameterExprs(); - // 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; - uint64_t rawSelector = 0; - if (params[1].operation == LLIL_REG_SSA) - { - const auto selectorRegister = params[1].GetSourceSSARegister(); - rawSelector = ssa->GetSSARegisterValue(selectorRegister).value; - } - else if (params[0].operation == LLIL_SEPARATE_PARAM_LIST_SSA) - { - if (params[0].GetParameterExprs().size() == 0) - return; - const auto selectorRegister = params[0].GetParameterExprs()[1].GetSourceSSARegister(); - rawSelector = ssa->GetSSARegisterValue(selectorRegister).value; - } - - // Skip if we don't have a - if (!rawSelector || rawSelector < baseAddr) - return; - - std::string selector; - if (bv->IsValidOffset(rawSelector)) { - BinaryReader reader(bv); - reader.Seek(rawSelector); - selector = reader.ReadCString(500); - } else { - // Look for the `sel_` symbols that ObjCProcessor adds to represent selectors - // whose backing regions have not yet been loaded into the view. - constexpr std::string_view SelectorPrefix = "sel_"; - - auto symbol = bv->GetSymbolByAddress(rawSelector); - if (!symbol) - return; - - std::string_view name = symbol->GetRawNameRef(); - if (name.find(SelectorPrefix) != 0) - return; - - selector = name.substr(SelectorPrefix.length()); - } - - // -- Do callsite override - auto additionalArgumentCount = std::count(selector.begin(), selector.end(), ':'); - - auto retType = bv->GetTypeByName({ "id" }); - if (!retType) - retType = Type::PointerType(ssa->GetArchitecture(), Type::VoidType()); - - std::vector callTypeParams; - auto cc = bv->GetDefaultPlatform()->GetDefaultCallingConvention(); - - if (messageSendType == MessageSendType::Normal) - callTypeParams.emplace_back("self", retType, true, Variable()); - else - { - auto superType = bv->GetTypeByName({ "objc_super" }); - if (!superType) - superType = Type::PointerType(ssa->GetArchitecture(), Type::VoidType()); - callTypeParams.emplace_back("super", Type::PointerType(ssa->GetArchitecture(), superType), true, Variable()); - } - - auto selType = bv->GetTypeByName({ "SEL" }); - if (!selType) - selType = Type::PointerType(ssa->GetArchitecture(), Type::IntegerType(1, true)); - callTypeParams.emplace_back("sel", selType, true, Variable()); - - std::vector selectorComponents = splitSelector(selector); - std::vector argumentNames = generateArgumentNames(selectorComponents); - - for (size_t i = 0; i < additionalArgumentCount; i++) - { - auto argType = Type::IntegerType(bv->GetAddressSize(), true); - if (argumentNames.size() > i && !argumentNames[i].empty()) - callTypeParams.emplace_back(argumentNames[i], argType, true, Variable()); - else - callTypeParams.emplace_back("arg" + std::to_string(i), argType, true, Variable()); - } - - auto funcType = Type::FunctionType(retType, cc, callTypeParams); - ssa->GetFunction()->SetAutoCallTypeAdjustment(ssa->GetFunction()->GetArchitecture(), insn.address, {funcType, BN_DEFAULT_CONFIDENCE}); - // -- - }; - - for (const auto& block : ssa->GetBasicBlocks()) - for (size_t i = block->GetStart(), end = block->GetEnd(); i < end; ++i) - rewriteIfEligible(i); -} - diff --git a/view/sharedcache/workflow/ObjCActivity.h b/view/sharedcache/workflow/ObjCActivity.h deleted file mode 100644 index 05a0d1b88d..0000000000 --- a/view/sharedcache/workflow/ObjCActivity.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include "binaryninjaapi.h" - -class ObjCActivity -{ - static void AdjustCallType(BinaryNinja::Ref ctx); -public: - static void Register(BinaryNinja::Workflow& workflow); -}; \ No newline at end of file diff --git a/view/sharedcache/workflow/SharedCacheWorkflow.cpp b/view/sharedcache/workflow/SharedCacheWorkflow.cpp index 3844516f6b..dcdf6c239f 100644 --- a/view/sharedcache/workflow/SharedCacheWorkflow.cpp +++ b/view/sharedcache/workflow/SharedCacheWorkflow.cpp @@ -15,8 +15,6 @@ #include "thread" #include -#include "ObjCActivity.h" - using namespace BinaryNinja; using namespace SharedCacheAPI; @@ -373,7 +371,6 @@ void SharedCacheWorkflow::Register() Ref workflow = Workflow::Get("core.function.metaAnalysis")->Clone("core.function.metaAnalysis"); // Register and insert activities here. - ObjCActivity::Register(*workflow); workflow->RegisterActivity(new Activity(R"({ "name": "core.analysis.sharedCache.analysis", "eligibility": {