├── src ├── ioshelper │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ └── utils.py │ ├── plugins │ │ ├── __init__.py │ │ ├── objc │ │ │ ├── __init__.py │ │ │ ├── objc_refcnt │ │ │ │ ├── __init__.py │ │ │ │ └── optimizer.py │ │ │ ├── objc_sugar │ │ │ │ ├── __init__.py │ │ │ │ └── objc_sugar.py │ │ │ ├── oslog │ │ │ │ ├── __init__.py │ │ │ │ ├── os_log.py │ │ │ │ ├── error_case_optimizer.py │ │ │ │ ├── log_enabled_optimizer.py │ │ │ │ └── log_macro_optimizer.py │ │ │ └── objc_ref │ │ │ │ ├── __init__.py │ │ │ │ └── objc_ref.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── range_condition │ │ │ │ ├── __init__.py │ │ │ │ └── range_condition.py │ │ │ ├── jump_to_string │ │ │ │ ├── __init__.py │ │ │ │ └── jump_to_string.py │ │ │ ├── outline │ │ │ │ ├── __init__.py │ │ │ │ └── outline.py │ │ │ ├── globals.py │ │ │ ├── segment_xrefs │ │ │ │ ├── __init__.py │ │ │ │ └── segment_xrefs.py │ │ │ ├── clang_blocks │ │ │ │ ├── __init__.py │ │ │ │ ├── utils.py │ │ │ │ ├── block.py │ │ │ │ ├── optimize_blocks_init.py │ │ │ │ └── block_arg_byref.py │ │ │ └── run_callback.py │ │ ├── kernelcache │ │ │ ├── __init__.py │ │ │ ├── generic_calls_fix │ │ │ │ ├── __init__.py │ │ │ │ └── generic_calls_fix.py │ │ │ ├── cpp_vtbl │ │ │ │ ├── __init__.py │ │ │ │ └── cpp_vtbl.py │ │ │ ├── obj_this │ │ │ │ ├── __init__.py │ │ │ │ └── obj_this.py │ │ │ ├── func_renamers │ │ │ │ ├── external.py │ │ │ │ ├── __init__.py │ │ │ │ ├── func_renamers.py │ │ │ │ ├── handlers.py │ │ │ │ └── pac_applier.py │ │ │ └── kalloc_type │ │ │ │ ├── __init__.py │ │ │ │ └── kalloc_type.py │ │ └── swift │ │ │ ├── swift_types │ │ │ └── __init__.py │ │ │ └── swift_strings │ │ │ ├── __init__.py │ │ │ ├── swift_string.py │ │ │ └── swift_string_fixup.py │ ├── ida_plugin.py │ └── core.py └── scripts │ └── fix_dyld_xrefs.py ├── vermin.ini ├── .idea ├── .gitignore ├── vcs.xml ├── misc.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml └── ida-objc-helper.iml ├── res ├── logo.png ├── jump_to_virtual_call.png └── jump_to_selector_xrefs.png ├── .github ├── actions │ └── setup-env │ │ └── action.yaml └── workflows │ ├── main.yml │ └── pypi.yaml ├── ida-plugin.json ├── ida_plugin_stub.py ├── pyproject.toml ├── .gitignore ├── uv.lock └── README.md /src/ioshelper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ioshelper/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vermin.ini: -------------------------------------------------------------------------------- 1 | [vermin] 2 | targets = 3.10- 3 | eval_annotations = yes -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavst/ida-ios-helper/HEAD/res/logo.png -------------------------------------------------------------------------------- /res/jump_to_virtual_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavst/ida-ios-helper/HEAD/res/jump_to_virtual_call.png -------------------------------------------------------------------------------- /res/jump_to_selector_xrefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavst/ida-ios-helper/HEAD/res/jump_to_selector_xrefs.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_refcnt/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["component"] 2 | 3 | from ioshelper.base.reloadable_plugin import OptimizersComponent 4 | 5 | from .optimizer import objc_calls_optimizer_t as optimizer 6 | 7 | component = OptimizersComponent.factory("Obj-C refcount optimizer", [optimizer]) 8 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/swift/swift_types/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["swift_types_component"] 2 | 3 | from ioshelper.base.reloadable_plugin import StartupScriptComponent 4 | 5 | from .swift_types import fix_swift_types 6 | 7 | swift_types_component = StartupScriptComponent.factory("SwiftTypes", [fix_swift_types]) 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/swift/swift_strings/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["swift_strings_component"] 2 | 3 | from ioshelper.base.reloadable_plugin import HexraysHookComponent 4 | 5 | from .swift_string_fixup import SwiftStringsHook 6 | 7 | swift_strings_component = HexraysHookComponent.factory("SwiftStrings", [SwiftStringsHook]) 8 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_sugar/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["objc_sugar_component"] 2 | 3 | from ioshelper.base.reloadable_plugin import HexraysHookComponent 4 | 5 | from .objc_sugar import objc_selector_hexrays_hooks_t 6 | 7 | objc_sugar_component = HexraysHookComponent.factory("ObjcSugar", [objc_selector_hexrays_hooks_t]) 8 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/range_condition/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["range_condition_optimizer_component"] 2 | 3 | from ioshelper.base.reloadable_plugin import HexraysHookComponent 4 | 5 | from .range_condition import range_condition_optimizer 6 | 7 | range_condition_optimizer_component = HexraysHookComponent.factory( 8 | "Range condition optimizer", [range_condition_optimizer] 9 | ) 10 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/generic_calls_fix/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["CAST_FUNCTION_NAMES", "generic_calls_fix_component"] 2 | 3 | from ioshelper.base.reloadable_plugin import OptimizersComponent 4 | 5 | from .generic_calls_fix import CAST_FUNCTIONS, generic_calls_fix_optimizer_t 6 | 7 | generic_calls_fix_component = OptimizersComponent.factory("Generic calls fixer", [generic_calls_fix_optimizer_t]) 8 | 9 | CAST_FUNCTION_NAMES = list(CAST_FUNCTIONS.values()) 10 | -------------------------------------------------------------------------------- /.github/actions/setup-env/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup Python environment 2 | description: Set up Python, install uv, sync dependencies 3 | runs: 4 | using: composite 5 | steps: 6 | 7 | - name: Install uv 8 | uses: astral-sh/setup-uv@v6 9 | with: 10 | python-version: 3.13 11 | activate-environment: true 12 | enable-cache: true 13 | - name: Sync project dependencies 14 | run: uv sync --locked --all-extras --dev 15 | shell: bash 16 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/oslog/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["component"] 2 | 3 | from ioshelper.base.reloadable_plugin import OptimizersComponent 4 | 5 | from .error_case_optimizer import log_error_case_optimizer_t 6 | from .log_enabled_optimizer import os_log_enabled_optimizer_t 7 | from .log_macro_optimizer import optimizer as log_macro_optimizer 8 | 9 | component = OptimizersComponent.factory( 10 | "os_log optimizer", [log_error_case_optimizer_t, log_macro_optimizer, os_log_enabled_optimizer_t] 11 | ) 12 | -------------------------------------------------------------------------------- /.idea/ida-objc-helper.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /ida-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDAMetadataDescriptorVersion": 1, 3 | "plugin": { 4 | "name": "iOSHelper", 5 | "description": "A plugin for IDA Pro 9.0+ to help with iOS code analysis.", 6 | "entryPoint": "ida_plugin_stub.py", 7 | "categories": ["decompilation"], 8 | "idaVersions": ">=9.0", 9 | "version": "1.0.19", 10 | "logoPath": "res/logo.png", 11 | "keywords": ["ios", "objc", "swift", "kernel-cache"], 12 | "license": "GPL-3.0", 13 | "pythonDependencies": ["ida-ios-helper==1.0.19"], 14 | "urls": {"repository": "https://github.com/yoavst/ida-ios-helper"}, 15 | "authors": [{"name": "Yoav Sternberg", "email": "yoav.sternberg@gmail.com"}] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ida_plugin_stub.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a stub file to be dropped in IDA plugins directory (usually ~/.idapro/plugins) 3 | You should install ida-ios-helper package globally in your python installation (When developing, use an editable install...) 4 | Make sure that this is the python version that IDA is using (otherwise you can switch with idapyswitch...) 5 | Then copy: 6 | - ida_plugin_stub.py to ~/idapro/plugins/ida_ios_helper/ida_plugin_stub.py 7 | - ida-plugin.json to ~/idapro/plugins/ida_ios_helper/ida_plugin.json 8 | """ 9 | 10 | # noinspection PyUnresolvedReferences 11 | __all__ = ["PLUGIN_ENTRY", "iOSHelperPlugin"] 12 | try: 13 | from ioshelper.ida_plugin import PLUGIN_ENTRY, iOSHelperPlugin 14 | except ImportError: 15 | print("[Error] Could not load ida-ios-helper plugin. ida-ios-helper Python package doesn't seem to be installed.") 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | check-python-compat: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | - name: Setup Environment 12 | uses: ./.github/actions/setup-env 13 | - name: Check for python version violation 14 | run: vermin --config-file vermin.ini --quiet --violations src/ 15 | 16 | ruff: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | - name: Setup Environment 22 | uses: ./.github/actions/setup-env 23 | - name: Ruff check 24 | uses: astral-sh/ruff-action@v3 25 | with: 26 | args: check 27 | - name: Ruff format check 28 | uses: astral-sh/ruff-action@v3 29 | with: 30 | args: format --check -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_ref/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["objc_xrefs_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | 6 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 7 | 8 | from .objc_ref import locate_selector_xrefs 9 | 10 | ACTION_ID = "ioshelper:show_objc_xrefs" 11 | 12 | objc_xrefs_component = UIActionsComponent.factory( 13 | "Show objc xrefs", 14 | [ 15 | lambda core: UIAction( 16 | ACTION_ID, 17 | idaapi.action_desc_t( 18 | ACTION_ID, "Show xrefs for current Obj-C method's selector", ShowObjcXrefsActionHandler(), "Ctrl+4" 19 | ), 20 | ) 21 | ], 22 | ) 23 | 24 | 25 | class ShowObjcXrefsActionHandler(ida_kernwin.action_handler_t): 26 | def activate(self, ctx): 27 | locate_selector_xrefs() 28 | return 0 29 | 30 | def update(self, ctx) -> int: 31 | return idaapi.AST_ENABLE_ALWAYS 32 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/jump_to_string/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["jump_to_string_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from ida_kernwin import action_handler_t 6 | 7 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 8 | 9 | from .jump_to_string import jump_to_string_ask 10 | 11 | ACTION_ID = "ioshelper:jump_to_string" 12 | 13 | jump_to_string_component = UIActionsComponent.factory( 14 | "Jump to function using a specific string", 15 | [ 16 | lambda core: UIAction( 17 | ACTION_ID, 18 | idaapi.action_desc_t(ACTION_ID, "Jump to function using a specific string", JumpToStringAction(), "Ctrl+S"), 19 | menu_location=UIAction.base_location(core), 20 | ) 21 | ], 22 | ) 23 | 24 | 25 | class JumpToStringAction(action_handler_t): 26 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 27 | jump_to_string_ask() 28 | return 0 29 | 30 | def update(self, ctx): 31 | return idaapi.AST_ENABLE_ALWAYS 32 | -------------------------------------------------------------------------------- /src/ioshelper/ida_plugin.py: -------------------------------------------------------------------------------- 1 | import ida_idaapi 2 | import idaapi 3 | from ida_idaapi import plugin_t 4 | 5 | from ioshelper.base.reloadable_plugin import PluginCore, ReloadablePlugin 6 | 7 | 8 | # noinspection PyPep8Naming 9 | class iOSHelperPlugin(ReloadablePlugin): 10 | flags = ida_idaapi.PLUGIN_HIDE 11 | wanted_name = "iOS helper" 12 | wanted_hotkey = "" 13 | comment = "Optimize iOS patterns in the code" 14 | help = "" 15 | 16 | def __init__(self): 17 | # Use lambda to plugin_core, so it could be fully reloaded from disk every time. 18 | # noinspection PyTypeChecker 19 | super().__init__("ioshelper", "ioshelper", plugin_core_wrapper_factory, extra_packages_to_reload=["idahelper"]) 20 | 21 | 22 | def plugin_core_wrapper_factory(*args, **kwargs) -> PluginCore: 23 | # Reload the module 24 | idaapi.require("ioshelper.core") 25 | # Bring the module into locals 26 | import ioshelper.core 27 | 28 | return ioshelper.core.plugin_core(*args, **kwargs) 29 | 30 | 31 | # noinspection PyPep8Naming 32 | def PLUGIN_ENTRY() -> plugin_t: 33 | return iOSHelperPlugin() 34 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/cpp_vtbl/__init__.py: -------------------------------------------------------------------------------- 1 | import ida_kernwin 2 | import idaapi 3 | from ida_kernwin import action_handler_t 4 | 5 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 6 | 7 | from .cpp_vtbl import get_vtable_call, show_vtable_xrefs 8 | 9 | ACTION_ID = "ioshelper:jump_to_vtbl_xrefs" 10 | 11 | jump_to_vtable_component = UIActionsComponent.factory( 12 | "Jump to VTables xrefs", 13 | [ 14 | lambda core: UIAction( 15 | ACTION_ID, 16 | idaapi.action_desc_t( 17 | ACTION_ID, 18 | "Jump to VTables xrefs", 19 | JumpToVtablesXrefs(), 20 | "Shift+X", 21 | ), 22 | dynamic_menu_add=lambda widget, popup: idaapi.get_widget_type(widget) == idaapi.BWN_PSEUDOCODE 23 | and get_vtable_call() is not None, 24 | ) 25 | ], 26 | ) 27 | 28 | 29 | class JumpToVtablesXrefs(action_handler_t): 30 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 31 | show_vtable_xrefs() 32 | return False 33 | 34 | def update(self, ctx): 35 | return idaapi.AST_ENABLE_ALWAYS 36 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/outline/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["mark_all_outline_functions", "mark_outline_functions_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from ida_kernwin import action_handler_t 6 | 7 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 8 | 9 | from .outline import mark_all_outline_functions 10 | 11 | ACTION_ID = "ioshelper:mark_outline_components" 12 | 13 | mark_outline_functions_component = UIActionsComponent.factory( 14 | "Locate all the outline functions and mark them as such", 15 | [ 16 | lambda core: UIAction( 17 | ACTION_ID, 18 | idaapi.action_desc_t( 19 | ACTION_ID, 20 | "Locate all the outline functions and mark them as such", 21 | MarkAllOutlineFunctionsAction(), 22 | ), 23 | menu_location=UIAction.base_location(core), 24 | ) 25 | ], 26 | ) 27 | 28 | 29 | class MarkAllOutlineFunctionsAction(action_handler_t): 30 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 31 | mark_all_outline_functions() 32 | return 0 33 | 34 | def update(self, ctx): 35 | return idaapi.AST_ENABLE_ALWAYS 36 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/globals.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from ioshelper.base.reloadable_plugin import Component, PluginCore 4 | 5 | 6 | def globals_component(_core: PluginCore) -> Component: 7 | class GlobalsComponent(Component): 8 | def __init__(self, core: PluginCore): 9 | super().__init__("globals", core) 10 | self.global_module = sys.modules["__main__"] 11 | 12 | from ioshelper.plugins.kernelcache.func_renamers import ( 13 | rename_function_by_arg, 14 | rename_function_by_callback, 15 | ) 16 | 17 | self.globals = { 18 | "rename_function_by_arg": rename_function_by_arg, 19 | "rename_function_by_callback": rename_function_by_callback, 20 | } 21 | 22 | def mount(self) -> bool: 23 | for global_name, global_value in self.globals.items(): 24 | setattr(self.global_module, global_name, global_value) 25 | return True 26 | 27 | def unmount(self): 28 | for global_name in self.globals: 29 | if hasattr(self.global_module, global_name): 30 | delattr(self.global_module, global_name) 31 | 32 | return GlobalsComponent(_core) 33 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_ref/objc_ref.py: -------------------------------------------------------------------------------- 1 | __all__ = ["locate_selector_xrefs"] 2 | 3 | import idautils 4 | import idc 5 | from idahelper.widgets import EAChoose 6 | 7 | # Taken from: https://github.com/doronz88/ida-scripts/blob/main/objc_hotkeys.py 8 | IGNORED_SECTIONS = ("__objc_const",) 9 | 10 | 11 | def get_name_for_ea(ea: int) -> str: 12 | func_name = idc.get_func_name(ea) 13 | func_address = idc.get_name_ea_simple(func_name) 14 | return func_name if ea == func_address else f"{func_name}+{ea - func_address:08x}" 15 | 16 | 17 | def locate_selector_xrefs() -> None: 18 | current_ea = idc.get_screen_ea() 19 | func_name = idc.get_func_name(current_ea) 20 | try: 21 | selector = func_name.split(" ")[1].split("]")[0] 22 | except IndexError: 23 | print("Failed to find current selector") 24 | return 25 | print(f"looking for references to: {selector}") 26 | 27 | items = [ 28 | (ea.frm, get_name_for_ea(ea.frm)) 29 | for ea in idautils.XrefsTo(idc.get_name_ea_simple(f"_objc_msgSend${selector}")) 30 | ] 31 | if items: 32 | for ea, name in items: 33 | print(f"0x{ea:08x} {name}") 34 | xrefs_choose = EAChoose(f"Xrefs to selector: {selector}", items, modal=True) 35 | xrefs_choose.show() 36 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/segment_xrefs/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["show_segment_xrefs_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from ida_kernwin import action_handler_t 6 | 7 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 8 | 9 | from .segment_xrefs import can_show_segment_xrefs, get_current_expr, show_segment_xrefs 10 | 11 | ACTION_ID = "ioshelper:show_segment_xrefs" 12 | 13 | show_segment_xrefs_component = UIActionsComponent.factory( 14 | "Show Xrefs inside segment", 15 | [ 16 | lambda core: UIAction( 17 | ACTION_ID, 18 | idaapi.action_desc_t( 19 | ACTION_ID, 20 | "Show Xrefs inside segment", 21 | ShowSegmentXrefsAction(), 22 | "Ctrl+Shift+X", 23 | ), 24 | dynamic_menu_add=lambda widget, popup: can_show_segment_xrefs(widget), 25 | ) 26 | ], 27 | ) 28 | 29 | 30 | class ShowSegmentXrefsAction(action_handler_t): 31 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 32 | expr = get_current_expr(ctx.widget) 33 | if expr is None: 34 | print("[Error] No expression found in the current context.") 35 | return False 36 | show_segment_xrefs(expr, func_ea=ctx.cur_func.start_ea) 37 | return False 38 | 39 | def update(self, ctx): 40 | return idaapi.AST_ENABLE_ALWAYS 41 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/obj_this/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["this_arg_fixer_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from ida_kernwin import action_handler_t 6 | from idahelper import functions, widgets 7 | 8 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 9 | 10 | from .obj_this import update_argument 11 | 12 | ACTION_ID = "ioshelper:this_arg_fixer" 13 | 14 | 15 | def dynamic_menu_add(widget, _popup) -> bool: 16 | if idaapi.get_widget_type(widget) not in (idaapi.BWN_DISASM, idaapi.BWN_PSEUDOCODE): 17 | return False 18 | current_ea = idaapi.get_screen_ea() 19 | return functions.is_in_function(current_ea) 20 | 21 | 22 | this_arg_fixer_component = UIActionsComponent.factory( 23 | "Convert first argument to this/self", 24 | [ 25 | lambda core: UIAction( 26 | ACTION_ID, 27 | idaapi.action_desc_t( 28 | ACTION_ID, 29 | "Update the first function argument to this/self and change its type", 30 | ThisArgFixerAction(), 31 | "Ctrl+T", 32 | ), 33 | dynamic_menu_add=dynamic_menu_add, 34 | ) 35 | ], 36 | ) 37 | 38 | 39 | class ThisArgFixerAction(action_handler_t): 40 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 41 | if ctx.cur_func is None: 42 | print("[Error] Not inside a function") 43 | return False 44 | 45 | if update_argument(ctx.cur_func) and ctx.widget is not None: 46 | widgets.refresh_widget(ctx.widget) 47 | return False 48 | 49 | def update(self, ctx): 50 | return idaapi.AST_ENABLE_ALWAYS 51 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/obj_this/obj_this.py: -------------------------------------------------------------------------------- 1 | __all__ = ["update_argument"] 2 | 3 | from ida_funcs import func_t 4 | from idahelper import cpp, memory, objc, tif 5 | 6 | 7 | def update_argument(func: func_t) -> bool: 8 | func_name = memory.name_from_ea(func.start_ea) 9 | if func_name is None: 10 | print("[Error] Failed to get function name") 11 | return False 12 | 13 | if objc.is_objc_method(func_name): 14 | if objc.is_objc_static_method(func_name): 15 | print("[Error] Static Obj-C method has no self") 16 | return False 17 | 18 | is_objc = True 19 | class_name = func_name.split(" ")[0][2:] 20 | else: 21 | # Try C++ 22 | is_objc = False 23 | class_name = cpp.demangle_class_only(memory.name_from_ea(func.start_ea)) 24 | if class_name is None: 25 | print("[Error] Failed to get class name in C++ mode") 26 | return False 27 | 28 | func_details = tif.get_func_details(func) 29 | if func_details is None: 30 | print("[Error] Failed to get function type info") 31 | return False 32 | 33 | if func_details.size() < 1: 34 | print("[Error] Function does not have enough arguments") 35 | return False 36 | 37 | # Change first argument name and type 38 | class_tinfo = tif.from_struct_name(class_name) 39 | if class_tinfo is None: 40 | print(f"[Error] Failed to get class type info for {class_name}") 41 | return False 42 | 43 | func_details[0].name = "self" if is_objc else "this" 44 | func_details[0].type = tif.pointer_of(class_tinfo) 45 | 46 | # Apply the changes 47 | new_tinfo = tif.from_func_details(func_details) 48 | if not tif.apply_tinfo_to_func(new_tinfo, func): 49 | print("[Error] Failed to apply new type info on function") 50 | return False 51 | 52 | print("Successfully updated first argument") 53 | return True 54 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/clang_blocks/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "clang_block_args_analyzer_component", 3 | "clang_block_optimizer_component", 4 | "run_objc_plugin_on_func", 5 | "try_add_block_arg_byref_to_func", 6 | ] 7 | 8 | import ida_kernwin 9 | import idaapi 10 | from idahelper import widgets 11 | 12 | from ioshelper.base.reloadable_plugin import HexraysHookComponent, UIAction, UIActionsComponent 13 | 14 | from .analyze_byref_args import try_add_block_arg_byref_to_func 15 | from .optimize_blocks_init import objc_blocks_optimizer_hooks_t 16 | from .utils import run_objc_plugin_on_func 17 | 18 | ACTION_ID = "ioshelper:restore_llvm_block_args_byref" 19 | 20 | clang_block_args_analyzer_component = UIActionsComponent.factory( 21 | "Clang Blocks - __block arguments", 22 | [ 23 | lambda core: UIAction( 24 | ACTION_ID, 25 | idaapi.action_desc_t( 26 | ACTION_ID, 27 | "Analyze stack-allocated blocks and their __block args (current function)", 28 | ClangBlockDetectByrefAction(), 29 | "Alt+Shift+s", 30 | ), 31 | menu_location=UIAction.base_location(core), 32 | ) 33 | ], 34 | ) 35 | 36 | clang_block_optimizer_component = HexraysHookComponent.factory( 37 | "Clang Blocks - optimizer", [objc_blocks_optimizer_hooks_t] 38 | ) 39 | 40 | 41 | class ClangBlockDetectByrefAction(ida_kernwin.action_handler_t): 42 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 43 | if ctx.cur_func is None: 44 | print("No function selected") 45 | return 0 46 | 47 | run_objc_plugin_on_func(ctx.cur_ea) 48 | widgets.refresh_pseudocode_widgets() 49 | if try_add_block_arg_byref_to_func(ctx.cur_func.start_ea): 50 | widgets.refresh_pseudocode_widgets() 51 | return 0 52 | 53 | def update(self, ctx) -> int: 54 | return idaapi.AST_ENABLE_ALWAYS 55 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/jump_to_string/jump_to_string.py: -------------------------------------------------------------------------------- 1 | from idahelper import functions, memory, strings, widgets, xrefs 2 | 3 | 4 | def find_matching_string(target_str: str) -> list[tuple[int, str]]: 5 | matches: list[tuple[int, str]] = [] 6 | for s in strings.strings(): 7 | if target_str in str(s): 8 | matches.append((s.ea, str(s))) # type: ignore # noqa: PGH003 9 | return matches 10 | 11 | 12 | def show_xrefs_to_string(ea: int): 13 | s_xrefs = xrefs.get_xrefs_to(ea) 14 | if not s_xrefs: 15 | print("No cross-references found.") 16 | elif len(s_xrefs) == 1: 17 | widgets.jump_to(s_xrefs[0]) 18 | else: 19 | print("Multiple xrefs to the string:") 20 | for xref in s_xrefs: 21 | print_xref(xref) 22 | 23 | 24 | def print_xref(ea: int, match: str | None = None): 25 | func_start = functions.get_start_of_function(ea) 26 | if func_start is None: 27 | print(f"{ea:X}") 28 | return 29 | 30 | func_name = memory.name_from_ea(func_start) or "" 31 | match_str = "" if match is None else f": {match}" 32 | 33 | print(f"{ea:X} at {func_name}+{ea - func_start:X}{match_str}") 34 | 35 | 36 | def jump_to_string_ask(): 37 | target_str = widgets.show_string_input("Enter substring to search in binary strings") 38 | if not target_str: 39 | return 40 | 41 | matches = find_matching_string(target_str) 42 | 43 | if not matches: 44 | print("[Warning] No matching strings found.") 45 | return 46 | 47 | # If there's only one result, or an exact match, show xrefs 48 | if len(matches) == 1 or any(s == target_str for _, s in matches): 49 | for ea, s in matches: 50 | if s == target_str or len(matches) == 1: 51 | show_xrefs_to_string(ea) 52 | return 53 | 54 | # Otherwise, let the user choose 55 | print("Multiple results for the string:") 56 | for ea, s in matches: 57 | print(f"{ea:X}: {s}") 58 | -------------------------------------------------------------------------------- /src/scripts/fix_dyld_xrefs.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | import ida_xref 4 | from ida_funcs import func_t 5 | from ida_ua import insn_t 6 | from idahelper import instructions, segments, xrefs 7 | 8 | stubs = [s for s in segments.get_segments() if "__stubs" in s.name or "__auth_stubs" in s.name] 9 | 10 | 11 | @lru_cache(1000) 12 | def is_stub_address(ea: int) -> bool: 13 | """Check if the given address is within any stub segment""" 14 | return any(stub.start_ea <= ea < stub.end_ea for stub in stubs) 15 | 16 | 17 | def fix_xrefs(): 18 | global_total_modified = 0 19 | segments_count = 0 20 | for seg in segments.get_segments("CODE"): 21 | segments_count += 1 22 | print(f"[Info] Processing segment {seg.name}...") 23 | total_modified = 0 24 | for func in seg.functions(): 25 | total_modified += handle_func(func) 26 | global_total_modified += total_modified 27 | print(f"[Info] Finished segment {seg.name}, total modified xrefs: {total_modified}") 28 | print(f"[Info] Finished fixing xrefs, added {global_total_modified} xrefs over {segments_count} segments.") 29 | 30 | 31 | def handle_func(func: func_t) -> int: 32 | total_modified = 0 33 | for insn in instructions.from_func(func): 34 | if insn.get_canon_mnem() in ("B", "BL"): 35 | total_modified += handle_bl_insn(insn) 36 | return total_modified 37 | 38 | 39 | def handle_bl_insn(insn: insn_t) -> bool: 40 | # Get the target of the BL instruction 41 | # noinspection PyPropertyAccess 42 | ea: int = insn.ea 43 | address: int = insn[0].addr 44 | 45 | # Check if the target function is a stub 46 | if not is_stub_address(address): 47 | return False 48 | 49 | # Check if there is xref from to the stub 50 | if ea in xrefs.code_xrefs_to(address): 51 | return False 52 | 53 | # Add code xref from the BL instruction to the stub 54 | insn.add_cref(address, 0, ida_xref.fl_CN | ida_xref.XREF_USER) 55 | return True 56 | 57 | 58 | if __name__ == "__main__": 59 | fix_xrefs() 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ida-ios-helper" 3 | dynamic = ['version'] 4 | description = "IDA Plugin for ease the reversing of iOS' usermode and kernelcache." 5 | authors = [ 6 | { name = "Yoav Sternberg", email = "yoav.sternberg@gmail.com" }, 7 | ] 8 | readme = "README.md" 9 | repository = "https://github.com/yoavst/ida-ios-helper" 10 | classifiers = [ 11 | "Programming Language :: Python :: 3", 12 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 13 | "Operating System :: OS Independent", 14 | ] 15 | 16 | requires-python = ">=3.10" 17 | dependencies = [ 18 | "idahelper==1.0.17", 19 | ] 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/yoavst/ida-ios-helper" 23 | Issues = "https://github.com/yoavst/ida-ios-helper/issues" 24 | 25 | [build-system] 26 | requires = ["hatchling", "hatch-vcs"] 27 | build-backend = "hatchling.build" 28 | 29 | [tool.hatch.version] 30 | source = "vcs" 31 | 32 | [tool.hatch.version.raw-options] 33 | version_scheme = "only-version" 34 | local_scheme = "no-local-version" 35 | 36 | [tool.hatch.build.targets.wheel] 37 | packages = ["src/ioshelper"] 38 | 39 | [dependency-groups] 40 | dev = [ 41 | "ruff>=0.12.2", 42 | "vermin>=1.6.0", 43 | ] 44 | 45 | [tool.ruff] 46 | target-version = "py310" 47 | line-length = 120 48 | fix = true 49 | 50 | [tool.ruff.lint] 51 | select = [ 52 | # flake8-2020 53 | "YTT", 54 | # flake8-bandit 55 | "S", 56 | # flake8-bugbear 57 | "B", 58 | # flake8-builtins 59 | "A", 60 | # flake8-comprehensions 61 | "C4", 62 | # flake8-debugger 63 | "T10", 64 | # flake8-simplify 65 | "SIM", 66 | # isort 67 | "I", 68 | # mccabe 69 | "C90", 70 | # pycodestyle 71 | "E", 72 | "W", 73 | # pyflakes 74 | "F", 75 | # pygrep-hooks 76 | "PGH", 77 | # pyupgrade 78 | "UP", 79 | # ruff 80 | "RUF", 81 | # tryceratops 82 | "TRY", 83 | ] 84 | ignore = [ 85 | # LineTooLong 86 | "E501", 87 | # DoNotAssignLambda 88 | "E731", 89 | # Asserts 90 | "S101", 91 | # Name for classes 92 | "N801", 93 | # Custom error classes 94 | "TRY003" 95 | ] 96 | 97 | [tool.ruff.format] 98 | preview = true 99 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/oslog/os_log.py: -------------------------------------------------------------------------------- 1 | __all__ = ["LogCallInfo", "LogCallParams", "get_call_info_for_name", "log_type_to_str"] 2 | 3 | import re 4 | from dataclasses import dataclass 5 | 6 | from ioshelper.base.utils import match_dict 7 | 8 | 9 | @dataclass 10 | class LogCallInfo: 11 | size_index: int 12 | buf_index: int 13 | format_index: int 14 | type_index: int 15 | name_index: int 16 | is_signpost: bool 17 | 18 | 19 | @dataclass 20 | class LogCallParams: 21 | log_type: int 22 | size: int 23 | stack_base_offset: int 24 | format_str_ea: int 25 | call_ea: int 26 | name_str_ea: int 27 | is_signpost: bool 28 | 29 | 30 | OS_LOG_IMPL_CALL_INFO = LogCallInfo( 31 | type_index=2, format_index=3, buf_index=4, size_index=5, name_index=None, is_signpost=False 32 | ) 33 | OS_SIGNPOST_EMIT_WITH_NAME_CALL_INFO = LogCallInfo( 34 | type_index=2, name_index=4, format_index=5, buf_index=6, size_index=7, is_signpost=True 35 | ) 36 | 37 | OS_LOG_NAMES: dict[str | re.Pattern, LogCallInfo] = { 38 | "__os_log_impl": OS_LOG_IMPL_CALL_INFO, 39 | "__os_log_error_impl": OS_LOG_IMPL_CALL_INFO, 40 | "__os_log_debug_impl": OS_LOG_IMPL_CALL_INFO, 41 | "__os_log_info_impl": OS_LOG_IMPL_CALL_INFO, 42 | re.compile(r"__os_log_impl_(\d+)"): OS_LOG_IMPL_CALL_INFO, 43 | re.compile(r"__os_log_error_impl_(\d+)"): OS_LOG_IMPL_CALL_INFO, 44 | re.compile(r"__os_log_debug_impl_(\d+)"): OS_LOG_IMPL_CALL_INFO, 45 | re.compile(r"__os_log_info_impl(\d+)"): OS_LOG_IMPL_CALL_INFO, 46 | "__os_signpost_emit_with_name_impl": OS_SIGNPOST_EMIT_WITH_NAME_CALL_INFO, 47 | re.compile(r"__os_signpost_emit_with_name_impl_(\d+)"): OS_SIGNPOST_EMIT_WITH_NAME_CALL_INFO, 48 | } 49 | 50 | LOG_TYPES: dict[int, str] = {0: "default", 1: "info", 2: "debug", 16: "error", 17: "fault"} 51 | SIGNPOST_TYPES: dict[int, str] = {0: "event", 1: "intervalBegin", 2: "intervalEnd"} 52 | 53 | 54 | def log_type_to_str(log_type: int, is_signpost: bool) -> str: 55 | if not is_signpost: 56 | return LOG_TYPES.get(log_type, f"log{log_type}") 57 | else: 58 | return SIGNPOST_TYPES.get(log_type, f"signpost{log_type}") 59 | 60 | 61 | def get_call_info_for_name(name: str) -> LogCallInfo | None: 62 | return match_dict(OS_LOG_NAMES, name) 63 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/func_renamers/external.py: -------------------------------------------------------------------------------- 1 | __all__ = ["rename_function_by_arg", "rename_function_by_callback"] 2 | 3 | from collections.abc import Callable 4 | 5 | import ida_funcs 6 | from idahelper import memory 7 | 8 | from .func_renamers import apply_specific_global_rename 9 | from .renamer import FuncHandler, Modifications 10 | from .visitor import Call, FuncXref, SourceXref, XrefsMatcher 11 | 12 | 13 | class LogFuncNameRenamer(FuncHandler): 14 | def __init__(self, func_name: str, get_name: Callable[[Call], str | None], force_name_change: bool = False): 15 | super().__init__(func_name) 16 | func_ea = memory.ea_from_name(func_name) 17 | if func_ea is None: 18 | raise ValueError(f"Function {func_name} not found") 19 | func = ida_funcs.get_func(func_ea) 20 | if func is None: 21 | raise ValueError(f"Function {func_name} at {func_ea:X} is not a valid function") 22 | 23 | self._func_ea: int = func_ea 24 | self._get_name: Callable[[Call], str | None] = get_name 25 | self._force_name_change = force_name_change 26 | 27 | def get_source_xref(self) -> SourceXref | None: 28 | return FuncXref(self._func_ea) 29 | 30 | def on_call(self, call: Call, modifications: Modifications): 31 | name = self._get_name(call) 32 | if name is not None: 33 | modifications.set_func_name(name, self._force_name_change) 34 | 35 | 36 | def rename_function_by_arg(func_name: str, arg_index: int, prefix: str = "", force_name_change: bool = False): 37 | def get_name(call: Call) -> str | None: 38 | if arg_index >= len(call.params): 39 | return None 40 | param = call.params[arg_index] 41 | if not isinstance(param, str): 42 | return None 43 | return f"{prefix}_{param}" 44 | 45 | rename_function_by_callback(func_name, get_name, force_name_change) 46 | 47 | 48 | def rename_function_by_callback( 49 | func_name: str, callback: Callable[[Call], str | None], force_name_change: bool = False 50 | ): 51 | renamer = LogFuncNameRenamer(func_name, callback, force_name_change) 52 | # noinspection PyTypeChecker 53 | apply_specific_global_rename(renamer, XrefsMatcher.build([(renamer.get_source_xref(), renamer.on_call)])) 54 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build distribution 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Setup Environment 19 | uses: ./.github/actions/setup-env 20 | - name: Build a binary wheel and a source tarball 21 | run: uv build 22 | - name: Store the distribution packages 23 | uses: actions/upload-artifact@v4 24 | with: 25 | name: python-package-distributions 26 | path: dist/ 27 | 28 | publish-to-pypi: 29 | name: Publish Python distribution to PyPI 30 | needs: 31 | - build 32 | runs-on: ubuntu-latest 33 | 34 | environment: 35 | name: pypi 36 | url: https://pypi.org/p/idahelper # Replace with your PyPI project name 37 | permissions: 38 | id-token: write # IMPORTANT: mandatory for trusted publishing 39 | 40 | steps: 41 | - name: Download all the dists 42 | uses: actions/download-artifact@v4 43 | with: 44 | name: python-package-distributions 45 | path: dist/ 46 | - name: Publish distribution to PyPI 47 | uses: pypa/gh-action-pypi-publish@release/v1 48 | 49 | github-release: 50 | name: Upload the Python distribution as GitHub Release 51 | needs: 52 | - publish-to-pypi 53 | runs-on: ubuntu-latest 54 | 55 | permissions: 56 | contents: write # IMPORTANT: mandatory for making GitHub Releases 57 | 58 | steps: 59 | - name: Download all the dists 60 | uses: actions/download-artifact@v4 61 | with: 62 | name: python-package-distributions 63 | path: dist/ 64 | - name: Create GitHub Release 65 | env: 66 | GITHUB_TOKEN: ${{ github.token }} 67 | run: >- 68 | gh release create 69 | '${{ github.ref_name }}' 70 | --repo '${{ github.repository }}' 71 | --notes "" 72 | - name: Upload artifacts to GitHub Release 73 | env: 74 | GITHUB_TOKEN: ${{ github.token }} 75 | # Upload to GitHub Release using the `gh` CLI. 76 | # `dist/` contains the built packages 77 | run: >- 78 | gh release upload 79 | '${{ github.ref_name }}' dist/** 80 | --repo '${{ github.repository }}' 81 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/func_renamers/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "apply_global_rename", 3 | "apply_pac", 4 | "apply_pac_component", 5 | "local_func_renamer_component", 6 | "mass_func_renamer_component", 7 | "rename_function_by_arg", 8 | "rename_function_by_callback", 9 | ] 10 | 11 | import ida_kernwin 12 | import idaapi 13 | from ida_kernwin import action_handler_t 14 | from idahelper import widgets 15 | 16 | from ioshelper.base.reloadable_plugin import HexraysHookComponent, UIAction, UIActionsComponent 17 | 18 | from .external import rename_function_by_arg, rename_function_by_callback 19 | from .func_renamers import apply_global_rename, hooks 20 | from .pac_applier import apply_pac 21 | 22 | ACTION_ID = "ioshelper:func_renamer" 23 | 24 | local_func_renamer_component = HexraysHookComponent.factory("Local rename based on function calls", [hooks]) 25 | 26 | mass_func_renamer_component = UIActionsComponent.factory( 27 | "Mass rename based on function calls", 28 | [ 29 | lambda core: UIAction( 30 | ACTION_ID, 31 | idaapi.action_desc_t( 32 | ACTION_ID, "Mass rename globals and fields based on specific function calls", FuncRenameGlobalAction() 33 | ), 34 | menu_location=UIAction.base_location(core), 35 | ) 36 | ], 37 | ) 38 | 39 | 40 | class FuncRenameGlobalAction(action_handler_t): 41 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 42 | apply_global_rename() 43 | return 0 44 | 45 | def update(self, ctx): 46 | return idaapi.AST_ENABLE_ALWAYS 47 | 48 | 49 | ACTION_ID_PAC = "ioshelper:apply_pac_on_function" 50 | 51 | 52 | apply_pac_component = UIActionsComponent.factory( 53 | "Apply PAC types on current function", 54 | [ 55 | lambda core: UIAction( 56 | ACTION_ID_PAC, 57 | idaapi.action_desc_t(ACTION_ID_PAC, "Apply PAC types on current function", ApplyPACAction(), "Ctrl+P"), 58 | menu_location=UIAction.base_location(core), 59 | ) 60 | ], 61 | ) 62 | 63 | 64 | class ApplyPACAction(action_handler_t): 65 | def activate(self, ctx: ida_kernwin.action_ctx_base_t) -> bool: 66 | if ctx.cur_func is None: 67 | print("[Error] Not inside a function") 68 | return False 69 | 70 | if apply_pac(ctx.cur_func) and ctx.widget is not None: 71 | widgets.refresh_widget(ctx.widget) 72 | return False 73 | 74 | def update(self, ctx): 75 | return idaapi.AST_ENABLE_ALWAYS 76 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/swift/swift_strings/swift_string.py: -------------------------------------------------------------------------------- 1 | __all__ = ["decode"] 2 | 3 | import ida_bytes 4 | 5 | MAX_READ = 1024 6 | OBJECT_OFFSET = 0x20 7 | 8 | 9 | def _read_cstring_from(addr: int, max_read: int = MAX_READ) -> tuple[str, bytes]: 10 | """Try to read a C-string from the given address. Returns (string, raw bytes).""" 11 | if not addr: 12 | return "", b"" 13 | raw = ida_bytes.get_bytes(addr, max_read) or b"" 14 | if not raw: 15 | return "", b"" 16 | z = raw.find(b"\x00") 17 | if z >= 0: 18 | raw = raw[:z] 19 | try: 20 | s = raw.decode("utf-8") 21 | except UnicodeDecodeError: 22 | s = raw.decode("latin-1", errors="replace") 23 | return s, raw 24 | 25 | 26 | def _decode_string_d(obj_addr: int) -> str: 27 | """ 28 | Pointer-backed Swift::String ('D' layout): 29 | actual base is masked with 0x7FFF..., string is at (base + OBJECT_OFFSET) 30 | """ 31 | base = obj_addr & 0x7FFFFFFFFFFFFFFF 32 | s, _ = _read_cstring_from(base + OBJECT_OFFSET) 33 | return s 34 | 35 | 36 | def _decode_string_e(bits_val: int, obj_addr: int) -> str: 37 | """ 38 | Immediate small string when top nibble of _object is 0xE. 39 | Length is (object >> 56) & 0xF. 40 | Bytes come from bits_val first (LE), then _object if needed. 41 | """ 42 | top_nib = (obj_addr >> 60) & 0xF 43 | if top_nib != 0xE: 44 | return "" 45 | length = (obj_addr >> 56) & 0xF 46 | if length == 0: 47 | return "" 48 | cf = bits_val.to_bytes(8, byteorder="little", signed=False) 49 | oa = obj_addr.to_bytes(8, byteorder="little", signed=False) 50 | data = cf[:length] 51 | if len(data) < length: 52 | data += oa[: length - len(data)] 53 | try: 54 | return data.decode("utf-8", errors="strict") 55 | except UnicodeDecodeError: 56 | return data.decode("latin-1", errors="replace") 57 | 58 | 59 | def decode(bits_val: int, obj_val: int, escape: bool = True) -> str | None: 60 | """Decode a Swift::String from the given countAndFlagsBits and _object values""" 61 | 62 | string_type_d = ((bits_val >> 60) & 0xF) == 0xD 63 | string_type_e = ((obj_val >> 60) & 0xF) == 0xE 64 | 65 | if string_type_d: 66 | s = _decode_string_d(obj_val) 67 | elif string_type_e: 68 | s = _decode_string_e(bits_val, obj_val) 69 | else: 70 | print(f"[swift-string] Got bad string: {bits_val:x} {obj_val:x}") 71 | return None 72 | 73 | if escape and s: 74 | s = s.encode("unicode_escape").decode("utf-8") 75 | 76 | return s 77 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/kalloc_type/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["apply_kalloc_type_component", "apply_kalloc_types", "create_type_from_kalloc_component"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from ida_kernwin import action_handler_t 6 | from idahelper import tif 7 | 8 | from ioshelper.base.reloadable_plugin import UIAction, UIActionsComponent 9 | 10 | from .kalloc_type import apply_kalloc_types, create_struct_from_kalloc_type 11 | 12 | ACTION_ID_APPLY_KALLOC_TYPE = "ioshelper:apply_kalloc_type" 13 | 14 | apply_kalloc_type_component = UIActionsComponent.factory( 15 | "Locate all the kalloc_type_view in the kernelcache and apply them on types", 16 | [ 17 | lambda core: UIAction( 18 | ACTION_ID_APPLY_KALLOC_TYPE, 19 | idaapi.action_desc_t( 20 | ACTION_ID_APPLY_KALLOC_TYPE, 21 | "Locate all the kalloc_type_view in the kernelcache and apply them on types", 22 | ApplyKallocTypesAction(), 23 | ), 24 | menu_location=UIAction.base_location(core), 25 | ) 26 | ], 27 | ) 28 | 29 | ACTION_ID_CREATE_TYPE_FROM_KALLOC = "ioshelper:create_type_from_kalloc" 30 | 31 | 32 | def dynamic_menu_add(widget, _popup) -> bool: 33 | if idaapi.get_widget_type(widget) != idaapi.BWN_DISASM: 34 | return False 35 | current_ea = idaapi.get_screen_ea() 36 | typ = tif.from_ea(current_ea) 37 | return typ is not None and typ.get_type_name() == "kalloc_type_view" 38 | 39 | 40 | create_type_from_kalloc_component = UIActionsComponent.factory( 41 | "Create a struct from the currently selected kalloc_type_view", 42 | [ 43 | lambda core: UIAction( 44 | ACTION_ID_CREATE_TYPE_FROM_KALLOC, 45 | idaapi.action_desc_t( 46 | ACTION_ID_CREATE_TYPE_FROM_KALLOC, 47 | "Create a struct from the currently selected kalloc_type_view", 48 | CreateTypeFromKalloc(), 49 | ), 50 | dynamic_menu_add=dynamic_menu_add, 51 | menu_location=UIAction.base_location(core), 52 | ) 53 | ], 54 | ) 55 | 56 | 57 | class ApplyKallocTypesAction(action_handler_t): 58 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 59 | apply_kalloc_types() 60 | return 0 61 | 62 | def update(self, ctx): 63 | return idaapi.AST_ENABLE_ALWAYS 64 | 65 | 66 | class CreateTypeFromKalloc(action_handler_t): 67 | def activate(self, ctx: ida_kernwin.action_ctx_base_t): 68 | create_struct_from_kalloc_type(ctx) 69 | return 0 70 | 71 | def update(self, ctx): 72 | return idaapi.AST_ENABLE_ALWAYS 73 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/oslog/error_case_optimizer.py: -------------------------------------------------------------------------------- 1 | __all__ = ["log_error_case_optimizer_t"] 2 | 3 | import re 4 | 5 | import ida_hexrays 6 | from ida_hexrays import mblock_t, minsn_t, minsn_visitor_t, mop_t, mop_visitor_t, optinsn_t 7 | from idahelper.microcode import mop 8 | 9 | from ioshelper.base.utils import CounterMixin, match_dict 10 | 11 | # Replace var with val 12 | VARIABLES_TO_OPTIMIZE_OUT: dict[str | re.Pattern, int] = { 13 | "_gNumLogObjects": 0x1000, 14 | re.compile(r"_gNumLogObjects_(\d+)"): 0x1000, 15 | "_gNumLogSignpostObjects": 0x1000, 16 | re.compile(r"_gNumLogSignpostObjects_(\d+)"): 0x1000, 17 | } 18 | 19 | # replace jz var, 0, #addr with goto/nop 20 | # bool is "isZero" 21 | JZ_TO_OPTIMIZE: dict[str | re.Pattern, bool] = { 22 | "_gLogObjects": False, 23 | re.compile(r"_gLogObjects_(\d+)"): False, 24 | "_gLogSignpostObjects": False, 25 | re.compile(r"_gLogSignpostObjects_(\d+)"): False, 26 | } 27 | 28 | 29 | class variable_optimizer_t(mop_visitor_t, CounterMixin): 30 | def visit_mop(self, op: mop_t, tp, is_target) -> int: 31 | # we want global mops 32 | if is_target or op.g is None: 33 | return 0 34 | 35 | # We want named global 36 | name = mop.get_name(op) 37 | if name is None: 38 | return 0 39 | 40 | # Convert number to the optimized value 41 | if (val := match_dict(VARIABLES_TO_OPTIMIZE_OUT, name)) is not None: 42 | op.make_number(val, op.size) 43 | self.count() 44 | 45 | return 0 46 | 47 | 48 | class jz_optimizer_t(minsn_visitor_t, CounterMixin): 49 | def visit_minsn(self) -> int: 50 | insn: minsn_t = self.curins 51 | if insn.opcode not in [ida_hexrays.m_jnz, ida_hexrays.m_jz]: 52 | return 0 53 | 54 | # We want conditions on global variables 55 | name = mop.get_name(insn.l) 56 | if name is None: 57 | return 0 58 | 59 | if (is_zero := match_dict(JZ_TO_OPTIMIZE, name)) is not None: 60 | # not zero, zero 61 | # jnz 1 0 62 | # jz 0 1 63 | should_jmp = is_zero == (insn.opcode == ida_hexrays.m_jz) 64 | 65 | # We don't optimize it directly to goto/nop, since it will require blocks in/out. 66 | # IDA can optimize it for us :) 67 | insn.l.make_number(0, 1) 68 | insn.r.make_number(0, 1) 69 | insn.opcode = ida_hexrays.m_jz if should_jmp else ida_hexrays.m_jnz 70 | self.count() 71 | 72 | return 0 73 | 74 | 75 | class log_error_case_optimizer_t(optinsn_t): 76 | def func(self, blk: mblock_t, insn: minsn_t, optflags: int) -> int: 77 | if blk.mba.maturity < ida_hexrays.MMAT_CALLS: 78 | return 0 79 | 80 | variable_optimizer = variable_optimizer_t() 81 | jz_optimizer = jz_optimizer_t() 82 | insn.for_all_ops(variable_optimizer) 83 | insn.for_all_insns(jz_optimizer) 84 | cnt = variable_optimizer.cnt + jz_optimizer.cnt 85 | 86 | if cnt: 87 | blk.mark_lists_dirty() 88 | return cnt 89 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/oslog/log_enabled_optimizer.py: -------------------------------------------------------------------------------- 1 | __all__ = ["os_log_enabled_optimizer_t"] 2 | 3 | import re 4 | 5 | import ida_hexrays 6 | from ida_hexrays import ( 7 | mblock_t, 8 | mcallinfo_t, 9 | minsn_t, 10 | mop_t, 11 | mop_visitor_t, 12 | ) 13 | from idahelper.microcode import minsn 14 | 15 | from ioshelper.base.utils import CounterMixin, match 16 | 17 | from . import os_log 18 | 19 | OSLOG_FUNCTIONS_TO_REPLACE_WITH_HELPER: list[str | re.Pattern] = [ 20 | "_os_log_type_enabled", 21 | re.compile(r"_os_log_type_enabled_(\d+)"), 22 | ] 23 | OSLOG_TYPE_INDEX = 1 24 | 25 | SIGNPOST_FUNCTIONS_TO_REPLACE_WITH_HELPER: list[str | re.Pattern] = [ 26 | "_os_signpost_enabled", 27 | re.compile(r"_os_signpost_enabled_(\d+)"), 28 | ] 29 | 30 | 31 | class mop_optimizer_t(mop_visitor_t, CounterMixin): 32 | def visit_mop(self, op: mop_t, tp, is_target: bool) -> int: 33 | # No assignment dest, we want a call instruction 34 | if not is_target and op.d is not None: 35 | self.visit_instruction_mop(op) 36 | return 0 37 | 38 | def visit_instruction_mop(self, op: mop_t) -> None: 39 | # We only want calls 40 | insn: minsn_t = op.d 41 | if insn.opcode != ida_hexrays.m_call: 42 | return 43 | 44 | # Calls with names 45 | name = minsn.get_func_name_of_call(insn) 46 | if name is None: 47 | return 48 | 49 | # If it should be optimized to first arg, optimize 50 | if match(OSLOG_FUNCTIONS_TO_REPLACE_WITH_HELPER, name): 51 | fi: mcallinfo_t = insn.d.f 52 | if fi.args.empty(): 53 | # No arguments, probably IDA have not optimized it yet 54 | return 55 | 56 | # Log type 57 | log_type_arg = fi.args[OSLOG_TYPE_INDEX] 58 | if log_type_arg.t != ida_hexrays.mop_n: 59 | return 60 | log_type = log_type_arg.unsigned_value() 61 | 62 | # Change name 63 | insn.l.make_helper(f"oslog_{os_log.log_type_to_str(log_type, is_signpost=False)}_enabled") 64 | self.count() 65 | # Remove arguments 66 | fi.args.clear() 67 | fi.solid_args = 0 68 | self.count() 69 | elif match(SIGNPOST_FUNCTIONS_TO_REPLACE_WITH_HELPER, name): 70 | fi: mcallinfo_t = insn.d.f 71 | 72 | # Change name 73 | insn.l.make_helper("ossignpost_enabled") 74 | self.count() 75 | # Remove arguments 76 | fi.args.clear() 77 | fi.solid_args = 0 78 | self.count() 79 | 80 | 81 | class os_log_enabled_optimizer_t(ida_hexrays.optinsn_t): 82 | def func(self, blk: mblock_t, ins: minsn_t, optflags: int) -> int: 83 | # Let IDA reconstruct the calls before 84 | if blk.mba.maturity < ida_hexrays.MMAT_CALLS: 85 | return 0 86 | 87 | mop_optimizer = mop_optimizer_t(blk.mba, blk) 88 | ins.for_all_ops(mop_optimizer) 89 | changes = mop_optimizer.cnt 90 | if changes: 91 | blk.mark_lists_dirty() 92 | return changes 93 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/run_callback.py: -------------------------------------------------------------------------------- 1 | __all__ = ["run_callback"] 2 | 3 | from collections.abc import Callable 4 | 5 | import idaapi 6 | from ida_funcs import func_t 7 | from idahelper import file_format 8 | 9 | from ioshelper.base.reloadable_plugin import PluginCore 10 | 11 | from ..kernelcache.func_renamers import apply_global_rename, apply_pac 12 | from ..kernelcache.kalloc_type import apply_kalloc_types 13 | from .clang_blocks import run_objc_plugin_on_func, try_add_block_arg_byref_to_func 14 | from .outline import mark_all_outline_functions 15 | 16 | RUN_GLOBAL_ANALYSIS = 1 17 | RUN_LOCAL_ANALYSIS = 2 18 | NETNODE_NAME = "$ idaioshelper" 19 | 20 | 21 | def run_callback(_core: PluginCore) -> Callable[[int], None]: 22 | def run(value: int): 23 | # Here you can implement the logic that uses the core and the value 24 | print(f"[Debug] iOS helper run({value})") 25 | if value == RUN_GLOBAL_ANALYSIS: 26 | run_global_analysis() 27 | elif value == RUN_LOCAL_ANALYSIS: 28 | ea = read_ea_arg() 29 | if ea is None: 30 | print("[Error] No function address provided for local analysis.") 31 | return 32 | func = idaapi.get_func(ea) 33 | if func is None: 34 | print(f"[Error] No function found at address {ea:X}.") 35 | return 36 | 37 | run_local_analysis(func) 38 | 39 | return run 40 | 41 | 42 | def run_global_analysis(): 43 | print("[Info] Running global analysis...") 44 | 45 | print("[Info] Running outline detection...") 46 | mark_all_outline_functions() 47 | print("[Info] Outline detection completed.") 48 | 49 | if file_format.is_kernelcache(): 50 | print("[Info] Applying kalloc types...") 51 | apply_kalloc_types() 52 | print("[Info] Apply kalloc types completed.") 53 | 54 | print("[Info] Running global renaming...") 55 | apply_global_rename() 56 | print("[Info] Global renaming completed.") 57 | 58 | print("[Info] Global analysis completed.") 59 | 60 | 61 | def run_local_analysis(func: func_t): 62 | print("[Info] Running local analysis...") 63 | # Implement local analysis logic here 64 | print("[Info] Use builtin Obj-C plugin to restore blocks") 65 | run_objc_plugin_on_func(func.start_ea) 66 | print("[Info] Use builtin Obj-C plugin to restore blocks completed.") 67 | 68 | print("[Info] Try restore byref arguments in blocks") 69 | try_add_block_arg_byref_to_func(func.start_ea) 70 | print("[Info] Try restore byref arguments in blocks completed.") 71 | 72 | print("[Info] Try use PAC to apply types to local variables and fields") 73 | apply_pac(func) 74 | print("[Info] Try use PAC to apply types to local variables and fields completed.") 75 | 76 | print("[Info] Local analysis completed.") 77 | 78 | 79 | def write_ea_arg(ea: int): 80 | n = idaapi.netnode() 81 | n.create(NETNODE_NAME) 82 | n.altset(1, ea, "R") 83 | 84 | 85 | def read_ea_arg() -> int | None: 86 | n = idaapi.netnode(NETNODE_NAME) 87 | val = n.altval(1, "R") 88 | n.kill() 89 | return val if val != 0 else None 90 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/func_renamers/func_renamers.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import cache 3 | 4 | import ida_hexrays 5 | from ida_hexrays import Hexrays_Hooks, cfunc_t 6 | from idahelper import xrefs 7 | from idahelper.microcode import mba 8 | 9 | from .handlers import GLOBAL_HANDLERS, LOCAL_HANDLERS 10 | from .renamer import ( 11 | FuncHandler, 12 | Modifications, 13 | ) 14 | from .visitor import FuncXref, XrefsMatcher, process_function_calls 15 | 16 | ALL_HANDLERS = [*LOCAL_HANDLERS, *GLOBAL_HANDLERS] 17 | 18 | 19 | @cache 20 | def get_global_xref_matcher() -> XrefsMatcher: 21 | """Get a matcher for global xrefs.""" 22 | callbacks = [] 23 | for handler in GLOBAL_HANDLERS: 24 | source_xref = handler.get_source_xref() 25 | if source_xref is None or not isinstance(source_xref, FuncXref): 26 | continue 27 | callbacks.append((source_xref, handler.on_call)) 28 | # noinspection PyTypeChecker 29 | return XrefsMatcher.build(callbacks) 30 | 31 | 32 | @cache 33 | def get_all_xref_matcher() -> XrefsMatcher: 34 | """Get a matcher for global xrefs.""" 35 | callbacks = [] 36 | for handler in ALL_HANDLERS: 37 | source_xref = handler.get_source_xref() 38 | if source_xref is None: 39 | continue 40 | callbacks.append((source_xref, handler.on_call)) 41 | # noinspection PyTypeChecker 42 | return XrefsMatcher.build(callbacks) 43 | 44 | 45 | def apply_global_rename(): 46 | before = time.time() 47 | for i, handler in enumerate(GLOBAL_HANDLERS): 48 | print(f"Applying global rename {i + 1}/{len(GLOBAL_HANDLERS)}: {handler.name}") 49 | apply_specific_global_rename(handler, get_global_xref_matcher()) 50 | after = time.time() 51 | print(f"Completed! Took {int(after - before)} seconds") 52 | 53 | 54 | def apply_specific_global_rename(handler: FuncHandler, xrefs_matcher: XrefsMatcher): 55 | source_xref = handler.get_source_xref() 56 | if source_xref is None or not isinstance(source_xref, FuncXref): 57 | print(f"Function {handler.name} has no global source xref, skipping") 58 | return 59 | func_ea = source_xref.ea 60 | 61 | xrefs_in_funcs = xrefs.func_xrefs_to(func_ea) 62 | if not xrefs_in_funcs: 63 | print(f"Function {handler.name} not called") 64 | return 65 | 66 | print(f"Found {len(xrefs_in_funcs)} functions that call {handler.name}:") 67 | for j, xref_func_ea in enumerate(xrefs_in_funcs): 68 | with Modifications(xref_func_ea, func_lvars=None) as modifications: 69 | print(f" {j + 1}/{len(xrefs_in_funcs)}: {xref_func_ea:#x}") 70 | process_function_calls(mba.from_func(xref_func_ea), xrefs_matcher, modifications) 71 | 72 | 73 | class LocalRenameHooks(Hexrays_Hooks): 74 | def maturity(self, cfunc: cfunc_t, new_maturity: int) -> int: 75 | # For some reason, this maturity level is required for typing to be applied for local variables. 76 | if new_maturity != ida_hexrays.CMAT_CASTED: 77 | return 0 78 | 79 | with Modifications(cfunc.entry_ea, func_lvars=cfunc.get_lvars()) as modifications: 80 | process_function_calls(cfunc.mba, get_all_xref_matcher(), modifications) 81 | 82 | return 0 83 | 84 | 85 | def hooks(): 86 | # Load cache 87 | get_all_xref_matcher() 88 | return LocalRenameHooks() 89 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/clang_blocks/utils.py: -------------------------------------------------------------------------------- 1 | __all__ = ["StructFieldAssignment", "get_struct_fields_assignments", "run_objc_plugin_on_func"] 2 | 3 | from dataclasses import dataclass 4 | 5 | import ida_hexrays 6 | import idaapi 7 | from ida_hexrays import cexpr_t, cfuncptr_t, cinsn_t, lvar_t 8 | from ida_typeinf import tinfo_t, udm_t 9 | from idahelper import tif 10 | 11 | 12 | @dataclass 13 | class StructFieldAssignment: 14 | type: tinfo_t 15 | member: udm_t 16 | expr: cexpr_t 17 | is_cast_assign: bool 18 | insn: cinsn_t 19 | 20 | def __repr__(self): 21 | return f"StructAssignment(member={self.member.name}, is_cast_assign={self.is_cast_assign}, expr={self.expr.dstr()}, insn={self.insn.dstr()})" 22 | 23 | 24 | class LvarFieldsAssignmentsCollector(ida_hexrays.ctree_visitor_t): 25 | """ 26 | Visitor for collecting assignments to a list of local variables. 27 | The local variables should be of struct type. 28 | The assignments we are searching for are for a member of the struct. 29 | 30 | For example: `a.b = c;` where `a` is a local variable of struct type and `b` is a member of that struct. 31 | """ 32 | 33 | def __init__(self, target_lvars: list[lvar_t]): 34 | super().__init__(ida_hexrays.CV_PARENTS) 35 | self._target_lvars_names: dict[str, lvar_t] = {lvar.name: lvar for lvar in target_lvars} 36 | self.assignments: dict[str, list[StructFieldAssignment]] = {} 37 | 38 | def visit_expr(self, exp: cexpr_t) -> int: 39 | # We search for "a.b = c;" or "*(_XWORD *)&a.b = c;" 40 | # Check if the expression is an assignment 41 | if exp.op != ida_hexrays.cot_asg: 42 | return 0 43 | 44 | # Check if the left side is a member reference 45 | target = remove_ref_cast_dref(exp.x) 46 | is_cast_assign = target != exp.x 47 | if target.op != ida_hexrays.cot_memref: 48 | return 0 49 | 50 | # Check that it is a member reference to a local variable 51 | target_obj = target.x 52 | if target_obj.op != ida_hexrays.cot_var: 53 | return 0 54 | 55 | # Check if the local variable is what we are looking for 56 | target_obj_lvar: lvar_t = target_obj.v.getv() 57 | if target_obj_lvar.name not in self._target_lvars_names: 58 | return 0 59 | 60 | lvar_type = target_obj_lvar.type() 61 | member = tif.get_member(lvar_type, target.m) 62 | if member is None: 63 | return 0 64 | 65 | # Save the assignment 66 | self.assignments.setdefault(target_obj_lvar.name, []).append( 67 | StructFieldAssignment( 68 | type=lvar_type, member=member, expr=exp.y, insn=self.parent_insn(), is_cast_assign=is_cast_assign 69 | ) 70 | ) 71 | return 0 72 | 73 | 74 | def remove_ref_cast_dref(expr: cexpr_t) -> cexpr_t: 75 | """Remove reference, cast and dereference from the expression""" 76 | while expr.op in (ida_hexrays.cot_ref, ida_hexrays.cot_cast, ida_hexrays.cot_ptr): 77 | expr = expr.x 78 | return expr 79 | 80 | 81 | def get_struct_fields_assignments(cfunc: cfuncptr_t, lvars: list[lvar_t]) -> dict[str, list[StructFieldAssignment]]: 82 | """Get all assignments of the form "a.b = c" to the given local variables""" 83 | collector = LvarFieldsAssignmentsCollector(lvars) 84 | collector.apply_to(cfunc.body, None) # pyright: ignore[reportArgumentType] 85 | return collector.assignments 86 | 87 | 88 | def run_objc_plugin_on_func(ea: int) -> None: 89 | """Run IDA's Objective-C>Analyze stack-allocated blocks on the function at ea.""" 90 | n = idaapi.netnode() 91 | n.create("$ objc") 92 | n.altset(1, ea, "R") # the address can be any address within the function 93 | idaapi.load_and_run_plugin("objc", 5) 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | 176 | # debug file 177 | src/DEBUG 178 | 179 | # Disabled plugins file 180 | src/DISABLED_PLUGINS 181 | 182 | # Mac cache file 183 | .DS_Store -------------------------------------------------------------------------------- /src/ioshelper/core.py: -------------------------------------------------------------------------------- 1 | __all__ = ["plugin_core"] 2 | 3 | import ida_kernwin 4 | import idaapi 5 | from idahelper import file_format, widgets 6 | 7 | from .base.reloadable_plugin import ComponentFactory, PluginCore, UIAction, UIActionsComponent 8 | from .plugins.common.clang_blocks import clang_block_args_analyzer_component, clang_block_optimizer_component 9 | from .plugins.common.globals import globals_component 10 | from .plugins.common.jump_to_string import jump_to_string_component 11 | from .plugins.common.outline import mark_outline_functions_component 12 | from .plugins.common.range_condition import range_condition_optimizer_component 13 | from .plugins.common.run_callback import run_callback 14 | from .plugins.common.segment_xrefs import show_segment_xrefs_component 15 | from .plugins.kernelcache.cpp_vtbl import jump_to_vtable_component 16 | from .plugins.kernelcache.func_renamers import ( 17 | apply_pac_component, 18 | local_func_renamer_component, 19 | mass_func_renamer_component, 20 | ) 21 | from .plugins.kernelcache.generic_calls_fix import generic_calls_fix_component 22 | from .plugins.kernelcache.kalloc_type import apply_kalloc_type_component, create_type_from_kalloc_component 23 | from .plugins.kernelcache.obj_this import this_arg_fixer_component 24 | from .plugins.objc.objc_ref import objc_xrefs_component 25 | from .plugins.objc.objc_refcnt import component as objc_refcount_component 26 | from .plugins.objc.objc_sugar import objc_sugar_component 27 | from .plugins.objc.oslog import component as oslog_component 28 | from .plugins.swift.swift_strings import swift_strings_component 29 | from .plugins.swift.swift_types import swift_types_component 30 | 31 | TOGGLE_ACTION_ID = "ioshelper:toggle" 32 | 33 | toggle_ios_helper_mount_component = UIActionsComponent.factory( 34 | "toggle plugin mounting", 35 | [ 36 | lambda core: UIAction( 37 | TOGGLE_ACTION_ID, 38 | idaapi.action_desc_t( 39 | TOGGLE_ACTION_ID, 40 | "Toggle iOS helper optimizations", 41 | IOSHelperToggleActionHandler(core), 42 | ), 43 | menu_location=UIAction.base_location(core), 44 | ) 45 | ], 46 | ) 47 | 48 | 49 | class IOSHelperToggleActionHandler(ida_kernwin.action_handler_t): 50 | def __init__(self, core: PluginCore): 51 | super().__init__() 52 | self.core = core 53 | 54 | def activate(self, ctx): 55 | if self.core.mounted: 56 | self.core.unmount() 57 | else: 58 | self.core.mount() 59 | 60 | widgets.refresh_pseudocode_widgets() 61 | 62 | print("Obj-C optimization are now:", "enabled" if self.core.mounted else "disabled") 63 | print("Note: You might need to perform decompile again for this change to take effect.") 64 | return 1 65 | 66 | def update(self, ctx) -> int: 67 | return idaapi.AST_ENABLE_ALWAYS 68 | 69 | 70 | def get_modules_for_file() -> list[ComponentFactory]: 71 | return [ 72 | *shared_modules(), 73 | *(objc_plugins() if file_format.is_objc() else []), 74 | *(kernel_cache_plugins() if file_format.is_kernelcache() else []), 75 | ] 76 | 77 | 78 | def shared_modules() -> list[ComponentFactory]: 79 | return [ 80 | toggle_ios_helper_mount_component, 81 | clang_block_args_analyzer_component, 82 | clang_block_optimizer_component, 83 | jump_to_string_component, 84 | objc_refcount_component, 85 | range_condition_optimizer_component, 86 | mark_outline_functions_component, 87 | show_segment_xrefs_component, 88 | globals_component, 89 | ] 90 | 91 | 92 | def objc_plugins() -> list[ComponentFactory]: 93 | return [ 94 | oslog_component, 95 | objc_xrefs_component, 96 | objc_sugar_component, 97 | swift_types_component, 98 | swift_strings_component, 99 | ] 100 | 101 | 102 | def kernel_cache_plugins() -> list[ComponentFactory]: 103 | return [ 104 | this_arg_fixer_component, 105 | jump_to_vtable_component, 106 | generic_calls_fix_component, 107 | local_func_renamer_component, 108 | mass_func_renamer_component, 109 | apply_kalloc_type_component, 110 | apply_pac_component, 111 | create_type_from_kalloc_component, 112 | ] 113 | 114 | 115 | plugin_core = PluginCore.factory("iOSHelper", get_modules_for_file(), run_callback) 116 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/clang_blocks/block.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "FLAG_BLOCK_HAS_COPY_DISPOSE", 3 | "BlockBaseFieldsAssignments", 4 | "block_member_is_arg_field", 5 | "get_block_type", 6 | "get_ida_block_lvars", 7 | "is_block_type", 8 | ] 9 | 10 | from dataclasses import dataclass 11 | 12 | import ida_hexrays 13 | from ida_hexrays import ( 14 | cexpr_t, 15 | cfuncptr_t, 16 | cinsn_t, 17 | lvar_t, 18 | ) 19 | from ida_typeinf import tinfo_t, udm_t 20 | from idahelper.ast import cexpr 21 | 22 | from .utils import StructFieldAssignment 23 | 24 | IDA_BLOCK_TYPE_NAME_PREFIX = "Block_layout_" 25 | IDA_BLOCK_TYPE_BASE_FIELD_NAMES = { 26 | "isa", 27 | "flags", 28 | "reserved", 29 | "invoke", 30 | "descriptor", 31 | } 32 | 33 | FLAG_BLOCK_HAS_COPY_DISPOSE = 1 << 25 34 | 35 | 36 | def get_ida_block_lvars(func: cfuncptr_t) -> list[lvar_t]: 37 | """Get all Obj-C block variables in the function""" 38 | return [lvar for lvar in func.get_lvars() if is_block_type(lvar.type())] 39 | 40 | 41 | def is_block_type(tinfo: tinfo_t) -> bool: 42 | """Check if the type is an Obj-C block type""" 43 | if not tinfo.is_struct(): 44 | return False 45 | # noinspection PyTypeChecker 46 | name: str | None = tinfo.get_type_name() 47 | return name is not None and name.startswith(IDA_BLOCK_TYPE_NAME_PREFIX) 48 | 49 | 50 | def block_member_is_arg_field(udm: udm_t) -> bool: 51 | """Check if the member is an argument field of an Obj-C block""" 52 | return udm.name not in IDA_BLOCK_TYPE_BASE_FIELD_NAMES 53 | 54 | 55 | BLOCK_TYPES: dict[str, str] = {} 56 | for typ in ["stack", "global", "malloc", "auto", "finalizing", "weak"]: 57 | typ_cap = typ.capitalize() 58 | BLOCK_TYPES[f"_NSConcrete{typ_cap}Block"] = typ 59 | BLOCK_TYPES[f"__NSConcrete{typ_cap}Block"] = typ 60 | BLOCK_TYPES[f"__NSConcrete{typ_cap}Block_ptr"] = typ 61 | BLOCK_TYPES[f"_OBJC_CLASS_$___NS{typ_cap}Block__"] = typ 62 | 63 | 64 | def get_block_type(isa: str) -> str: 65 | """Get the block type from the isa symbol""" 66 | return BLOCK_TYPES.get(isa, "unknown") 67 | 68 | 69 | @dataclass 70 | class BlockBaseFieldsAssignments: 71 | assignments: list[cinsn_t] 72 | ea: int | None 73 | type: tinfo_t | None 74 | isa: cexpr_t | None 75 | flags: cexpr_t | None 76 | reserved: cexpr_t | None 77 | invoke: cexpr_t | None 78 | descriptor: cexpr_t | None 79 | 80 | def __str__(self) -> str: 81 | return ( 82 | f"isa: {self.isa.dstr() if self.isa else 'None'}, " 83 | f"flags: {self.flags.dstr() if self.flags else 'None'}, " 84 | f"reserved: {self.reserved.dstr() if self.reserved else 'None'}, " 85 | f"invoke: {self.invoke.dstr() if self.invoke else 'None'}, " 86 | f"descriptor: {self.descriptor.dstr() if self.descriptor else 'None'}" 87 | ) 88 | 89 | @staticmethod 90 | def initial() -> "BlockBaseFieldsAssignments": 91 | return BlockBaseFieldsAssignments( 92 | assignments=[], isa=None, flags=None, reserved=None, invoke=None, descriptor=None, type=None, ea=None 93 | ) 94 | 95 | def is_completed(self) -> bool: 96 | """Check if all base fields have been assigned""" 97 | return ( 98 | self.isa is not None 99 | and self.flags is not None 100 | and self.reserved is not None 101 | and self.invoke is not None 102 | and self.descriptor is not None 103 | ) 104 | 105 | def add_assignment(self, assignment: StructFieldAssignment) -> bool: 106 | """Add an assignment to the list of assignments""" 107 | if self.type is None: 108 | self.type = assignment.type 109 | 110 | field_name = assignment.member.name 111 | if field_name == "isa": 112 | self.isa = assignment.expr 113 | self.ea = assignment.insn.ea 114 | elif field_name == "flags": 115 | if assignment.is_cast_assign: 116 | # We need to split it to flags and reserved 117 | expr = assignment.expr 118 | if expr.op != ida_hexrays.cot_num: 119 | print(f"[Error] invalid flags assignment. Expected const, got: {expr.dstr()}") 120 | return False 121 | 122 | num_val = expr.numval() 123 | self.flags = cexpr.from_const_value(num_val & 0xFF_FF_FF_FF, is_hex=True) 124 | self.reserved = cexpr.from_const_value(num_val >> 32, is_hex=True) 125 | else: 126 | self.flags = assignment.expr 127 | elif field_name == "reserved": 128 | self.reserved = assignment.expr 129 | elif field_name == "invoke": 130 | self.invoke = assignment.expr 131 | elif field_name == "descriptor": 132 | self.descriptor = assignment.expr 133 | else: 134 | return False 135 | 136 | self.assignments.append(assignment.insn) 137 | return True 138 | -------------------------------------------------------------------------------- /src/ioshelper/base/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import re 3 | from collections.abc import Callable, Hashable, Iterable, Iterator 4 | from functools import wraps 5 | from typing import Generic, TypeVar, overload 6 | 7 | 8 | class CounterMixin: 9 | cnt: int = 0 10 | 11 | def count(self, amount: int = 1): 12 | self.cnt += amount 13 | 14 | 15 | def match(arr: list[str | re.Pattern], item: str) -> bool: 16 | """Match a string against a list of strings or regex patterns.""" 17 | for pat in arr: 18 | if isinstance(pat, str): 19 | if item == pat: 20 | return True 21 | else: 22 | if pat.match(item): 23 | return True 24 | return False 25 | 26 | 27 | T = TypeVar("T") 28 | 29 | 30 | def match_dict(patterns: dict[str | re.Pattern, T], item: str) -> T | None: 31 | """match a string against a dictionary of strings or regex patterns, Returns the value if matched.""" 32 | for pat, val in patterns.items(): 33 | if isinstance(pat, str): 34 | if item == pat: 35 | return val 36 | else: 37 | if pat.match(item): 38 | return val 39 | return None 40 | 41 | 42 | K = TypeVar("K") 43 | V = TypeVar("V") 44 | 45 | 46 | class CustomDict(Generic[K, V]): 47 | def __init__(self, hasher: Callable[[K], Hashable]): 48 | self._hasher = hasher 49 | self._storage: dict[Hashable, tuple[K, V]] = {} 50 | 51 | def __setitem__(self, key: K, value: V): 52 | self._storage[self._hasher(key)] = (key, value) 53 | 54 | def __getitem__(self, key: K) -> V: 55 | return self._storage[self._hasher(key)][1] 56 | 57 | def __delitem__(self, key: K): 58 | del self._storage[self._hasher(key)] 59 | 60 | def __contains__(self, key: K) -> bool: 61 | return self._hasher(key) in self._storage 62 | 63 | def __str__(self) -> str: 64 | return f"{{ {', '.join(f'{key}: {value}' for key, value in self._storage.values())} }}" 65 | 66 | @overload 67 | def get(self, key: K) -> V | None: ... 68 | 69 | @overload 70 | def get(self, key: K, default: V) -> V: ... 71 | 72 | @overload 73 | def get(self, key: K, default: V | None) -> V | None: ... 74 | 75 | def get(self, key: K, default: V | None = None): 76 | if key in self: 77 | return self[key] 78 | return default 79 | 80 | def setdefault(self, key: K, default: V) -> V: 81 | cur = self.get(key) 82 | if cur is not None: 83 | return cur 84 | 85 | self[key] = default 86 | return default 87 | 88 | def __len__(self) -> int: 89 | return len(self._storage) 90 | 91 | def keys(self) -> Iterator[K]: 92 | return (k for k, _ in self._storage.values()) 93 | 94 | def values(self) -> Iterator[V]: 95 | return (v for _, v in self._storage.values()) 96 | 97 | def items(self) -> Iterator[tuple[K, V]]: 98 | return iter(self._storage.values()) 99 | 100 | def __iter__(self) -> Iterator[K]: 101 | return self.keys() 102 | 103 | def __bool__(self) -> bool: 104 | return bool(self._storage) 105 | 106 | 107 | class CustomSet(Generic[V]): 108 | def __init__(self, hasher: Callable[[V], Hashable]): 109 | self._hasher = hasher 110 | self._storage: dict[Hashable, V] = {} 111 | 112 | def add(self, value: V): 113 | self._storage[self._hasher(value)] = value 114 | 115 | def add_all(self, items: Iterator[V] | Iterable[V]): 116 | for item in items: 117 | self.add(item) 118 | 119 | def remove(self, value: V): 120 | del self._storage[self._hasher(value)] 121 | 122 | def discard(self, value: V): 123 | with contextlib.suppress(KeyError): 124 | self.remove(value) 125 | 126 | def __contains__(self, value: V) -> bool: 127 | return self._hasher(value) in self._storage 128 | 129 | def __len__(self) -> int: 130 | return len(self._storage) 131 | 132 | def __iter__(self) -> Iterator[V]: 133 | return iter(self._storage.values()) 134 | 135 | def __bool__(self) -> bool: 136 | return bool(self._storage) 137 | 138 | def update(self, other: "CustomSet[V]") -> None: 139 | self._storage.update(other._storage) 140 | 141 | def __or__(self, other: "CustomSet[V]") -> "CustomSet[V]": 142 | new_set = CustomSet(self._hasher) 143 | new_set |= self 144 | new_set |= other 145 | return new_set 146 | 147 | def __ior__(self, other: "CustomSet[V]") -> "CustomSet[V]": 148 | self.update(other) 149 | return self 150 | 151 | def intersection_update(self, other: "CustomSet[V]") -> None: 152 | if not other: 153 | self._storage.clear() 154 | return 155 | 156 | self._storage = {k: self._storage[k] for k in (self._storage.keys() & other._storage.keys())} 157 | 158 | def __iand__(self, other: "CustomSet[V]") -> "CustomSet[V]": 159 | self.intersection_update(other) 160 | return self 161 | 162 | 163 | def cache_fast(func: Callable[[], T]) -> Callable[[], T]: 164 | """Decorator to cache the result of a function for faster access.""" 165 | cached_value: T | None = None 166 | 167 | @wraps(func) 168 | def wrapper(): 169 | nonlocal cached_value 170 | if cached_value is None: 171 | cached_value = func() 172 | return cached_value 173 | 174 | return wrapper 175 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/clang_blocks/optimize_blocks_init.py: -------------------------------------------------------------------------------- 1 | import ida_hexrays 2 | from ida_hexrays import Hexrays_Hooks, cexpr_t, cfuncptr_t, cinsn_t 3 | from idahelper import memory 4 | from idahelper.ast import cexpr, cinsn 5 | 6 | from .block import BlockBaseFieldsAssignments, get_block_type, get_ida_block_lvars 7 | from .block_arg_byref import BlockByRefArgBaseFieldsAssignments, get_block_byref_args_lvars 8 | from .utils import StructFieldAssignment, get_struct_fields_assignments 9 | 10 | 11 | class objc_blocks_optimizer_hooks_t(Hexrays_Hooks): 12 | def maturity(self, func: cfuncptr_t, new_maturity: int) -> int: 13 | if new_maturity < ida_hexrays.CMAT_CPA: 14 | return 0 15 | 16 | optimize_blocks(func) 17 | optimize_block_byref_args(func) 18 | return 0 19 | 20 | 21 | # region byref args 22 | def optimize_block_byref_args(func: cfuncptr_t) -> bool: 23 | # Check if the function has blocks 24 | byref_lvars = get_block_byref_args_lvars(func) 25 | if not byref_lvars: 26 | return False 27 | 28 | assignments = get_struct_fields_assignments(func, byref_lvars) 29 | has_optimized = False 30 | for lvar, lvar_assignments in assignments.items(): 31 | has_optimized |= optimize_block_byref_arg(lvar, func, lvar_assignments) 32 | 33 | return has_optimized 34 | 35 | 36 | def optimize_block_byref_arg(lvar: str, func: cfuncptr_t, assignments: list[StructFieldAssignment]) -> bool: 37 | byref_fields = BlockByRefArgBaseFieldsAssignments.initial() 38 | for assignment in assignments: 39 | byref_fields.add_assignment(assignment) 40 | 41 | if not byref_fields.is_completed(): 42 | return False 43 | 44 | new_insn = create_byref_init_insn(lvar, func, byref_fields) 45 | first_assignment = byref_fields.assignments[0] 46 | for assignment in byref_fields.assignments[1:]: 47 | assignment.cleanup() 48 | first_assignment.swap(new_insn) 49 | return True 50 | 51 | 52 | def create_byref_init_insn(lvar: str, func: cfuncptr_t, byref_fields: BlockByRefArgBaseFieldsAssignments) -> cinsn_t: 53 | if byref_fields.byref_dispose is not None: 54 | call = cexpr.call_helper_from_sig( 55 | "_byref_block_arg_ex_init", 56 | byref_fields.type, 57 | [ 58 | cexpr_t(byref_fields.flags), 59 | cexpr_t(byref_fields.byref_keep), 60 | cexpr_t(byref_fields.byref_dispose), 61 | ], 62 | ) 63 | else: 64 | call = cexpr.call_helper_from_sig( 65 | "_byref_block_arg_init", 66 | byref_fields.type, 67 | [ 68 | cexpr_t(byref_fields.flags), 69 | ], 70 | ) 71 | 72 | lvar_exp = cexpr.from_var_name(lvar, func) 73 | 74 | return cinsn.from_expr(cexpr.from_assignment(lvar_exp, call), ea=byref_fields.ea) 75 | 76 | 77 | # endregion 78 | 79 | 80 | # region blocks 81 | def optimize_blocks(func: cfuncptr_t) -> bool: 82 | # Check if the function has blocks 83 | block_lvars = get_ida_block_lvars(func) 84 | if not block_lvars: 85 | return False 86 | 87 | assignments = get_struct_fields_assignments(func, block_lvars) 88 | has_optimized = False 89 | for lvar, lvar_assignments in assignments.items(): 90 | has_optimized |= optimize_block(lvar, func, lvar_assignments) 91 | 92 | return has_optimized 93 | 94 | 95 | def optimize_block(lvar: str, func: cfuncptr_t, assignments: list[StructFieldAssignment]) -> bool: 96 | block_fields = BlockBaseFieldsAssignments.initial() 97 | for assignment in assignments: 98 | block_fields.add_assignment(assignment) 99 | 100 | if not block_fields.is_completed(): 101 | return False 102 | 103 | new_insn = create_block_init_insn(lvar, func, block_fields) 104 | first_assignment = block_fields.assignments[0] 105 | for assignment in block_fields.assignments[1:]: 106 | assignment.cleanup() 107 | first_assignment.swap(new_insn) 108 | return True 109 | 110 | 111 | def create_block_init_insn(lvar: str, func: cfuncptr_t, block_fields: BlockBaseFieldsAssignments) -> cinsn_t: 112 | if (isa := get_isa(block_fields.isa)) is not None: 113 | call = cexpr.call_helper_from_sig( 114 | f"_{get_block_type(isa)}_block_init", 115 | block_fields.type, 116 | [ 117 | cexpr_t(block_fields.flags), 118 | cexpr_t(block_fields.descriptor), 119 | cexpr_t(block_fields.invoke), 120 | ], 121 | ) 122 | else: 123 | call = cexpr.call_helper_from_sig( 124 | "_block_init", 125 | block_fields.type, 126 | [ 127 | cexpr_t(block_fields.isa), 128 | cexpr_t(block_fields.flags), 129 | cexpr_t(block_fields.descriptor), 130 | cexpr_t(block_fields.invoke), 131 | ], 132 | ) 133 | 134 | lvar_exp = cexpr.from_var_name(lvar, func) 135 | 136 | return cinsn.from_expr(cexpr.from_assignment(lvar_exp, call), ea=block_fields.ea) 137 | 138 | 139 | def get_isa(isa: cexpr_t) -> str | None: 140 | """Get the isa name from the isa expression""" 141 | if isa.op == ida_hexrays.cot_ref: 142 | inner = isa.x 143 | if inner.op == ida_hexrays.cot_obj: 144 | return memory.name_from_ea(inner.obj_ea) 145 | elif isa.op == ida_hexrays.cot_helper: 146 | return isa.helper 147 | return None 148 | 149 | 150 | # endregion 151 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/generic_calls_fix/generic_calls_fix.py: -------------------------------------------------------------------------------- 1 | __all__ = ["CAST_FUNCTIONS", "generic_calls_fix_optimizer_t"] 2 | 3 | import re 4 | 5 | import ida_hexrays 6 | from ida_hexrays import mblock_t, mcallarg_t, mcallargs_t, mcallinfo_t, minsn_t, minsn_visitor_t 7 | from ida_typeinf import tinfo_t 8 | from idahelper import cpp, tif 9 | from idahelper.microcode import mcallarg, minsn, mop 10 | 11 | from ioshelper.base.utils import CounterMixin, match_dict 12 | 13 | CAST_FUNCTIONS: dict[str | re.Pattern, str] = { 14 | "OSMetaClassBase::safeMetaCast": "OSDynamicCast", 15 | "__ZN15OSMetaClassBase12safeMetaCastEPKS_PK11OSMetaClass": "OSDynamicCast", 16 | "OSMetaClassBase::requiredMetaCast": "OSRequiredCast", 17 | "__ZN15OSMetaClassBase16requiredMetaCastEPKS_PK11OSMetaClass": "OSDynamicCast", 18 | } 19 | 20 | ALLOC_FUNCTION: dict[str | re.Pattern, str] = { 21 | "OSObject_typed_operator_new": "OSObjectTypeAlloc", 22 | "_OSObject_typed_operator_new": "OSObjectTypeAlloc", 23 | } 24 | 25 | OS_OBJECT_TYPE: tinfo_t = tif.from_c_type("OSObject*") # type: ignore # noqa: PGH003 26 | SIZE_T_TYPE: tinfo_t = tif.from_c_type("size_t") # type: ignore # noqa: PGH003 27 | 28 | 29 | class insn_optimizer_t(minsn_visitor_t, CounterMixin): 30 | def visit_minsn(self) -> int: 31 | # We only want calls 32 | insn: minsn_t = self.curins 33 | if insn.opcode == ida_hexrays.m_call: 34 | self.visit_call_insn(insn, self.blk) 35 | return 0 36 | 37 | def visit_call_insn(self, insn: minsn_t, blk: mblock_t): 38 | res = self.try_convert_cast(insn) 39 | if not res: 40 | self.try_convert_alloc(insn) 41 | 42 | def try_convert_alloc(self, insn: minsn_t) -> bool: 43 | name = minsn.get_func_name_of_call(insn) 44 | if name is None or (new_name := match_dict(ALLOC_FUNCTION, name)) is None: 45 | return False 46 | 47 | # Verify call info 48 | call_info = self.get_call_info(insn, 2) 49 | if call_info is None: 50 | return True 51 | 52 | # Get the kty 53 | kty_name = self.get_arg_name(call_info, 0) 54 | if kty_name is None or not kty_name.endswith("_kty"): 55 | # No name, cannot optimize 56 | return True 57 | kty_name = kty_name[:-4] 58 | 59 | # Get the class 60 | cls_type = tif.from_struct_name(kty_name) 61 | if cls_type is None: 62 | print(f"[Error] Failed to get type for class: {kty_name}") 63 | return True 64 | 65 | self.modify_call(cls_type, new_name, insn, call_info, 0, SIZE_T_TYPE) 66 | return True 67 | 68 | def try_convert_cast(self, insn: minsn_t) -> bool: 69 | name = minsn.get_func_name_of_call(insn) 70 | if name is None or (new_name := match_dict(CAST_FUNCTIONS, name)) is None: 71 | return False 72 | 73 | # Verify call info 74 | call_info = self.get_call_info(insn, 2) 75 | if call_info is None: 76 | return True 77 | 78 | # Convert name to type 79 | cls_name_mangled = self.get_arg_name(call_info, 1) 80 | if cls_name_mangled is None: 81 | # No name, cannot optimize 82 | return True 83 | 84 | cls_name = cpp.demangle_class_only(cls_name_mangled) 85 | if cls_name is None: 86 | print(f"[Error] Failed to extract class name: {cls_name_mangled}") 87 | return True 88 | 89 | # Get the class 90 | cls_type = tif.from_struct_name(cls_name) 91 | if cls_type is None: 92 | print(f"[Error] Failed to get type for class: {cls_name}") 93 | return True 94 | 95 | self.modify_call(cls_type, new_name, insn, call_info, 1, OS_OBJECT_TYPE) 96 | return True 97 | 98 | def get_call_info(self, call_insn: minsn_t, required_args_count: int) -> mcallinfo_t | None: 99 | """Get call info of the given call instruction, verifying it has the required args count""" 100 | call_info: mcallinfo_t | None = call_insn.d.f 101 | if call_info is None or len(call_info.args) != required_args_count: 102 | return None 103 | return call_info 104 | 105 | def get_arg_name(self, call_info: mcallinfo_t, arg_index: int) -> str | None: 106 | """Get the name of the {arg_index} argument, assuming it is a global one""" 107 | arg: mcallarg_t = call_info.args[arg_index] 108 | if arg.t != ida_hexrays.mop_a: 109 | # not const 110 | return None 111 | 112 | return mop.get_name(arg.a) 113 | 114 | def modify_call( 115 | self, 116 | cls_type: tinfo_t, 117 | new_name: str, 118 | insn: minsn_t, 119 | call_info: mcallinfo_t, 120 | arg_to_remove: int, 121 | single_arg_type: tinfo_t, 122 | ) -> None: 123 | # Assumes size is 2 124 | 125 | cls_type_pointer = tif.pointer_of(cls_type) 126 | 127 | # Check if already handled 128 | if call_info.return_type == cls_type_pointer: 129 | return 130 | 131 | # Apply name and type changes 132 | insn.l.make_helper(f"{new_name}<{cls_type.get_type_name()}>") 133 | call_info.return_type = cls_type_pointer 134 | 135 | # Remove metaclass arg 136 | args: mcallargs_t = call_info.args 137 | if arg_to_remove == 0: 138 | args[0].swap(args[1]) 139 | 140 | args.pop_back() 141 | call_info.solid_args -= 1 142 | 143 | # Remove the name associated with the first parameter, so there will be no inlay hint 144 | new_arg = mcallarg.from_mop(call_info.args[0], single_arg_type) 145 | call_info.args.pop_back() 146 | call_info.args.push_back(new_arg) 147 | 148 | self.count() 149 | 150 | 151 | class generic_calls_fix_optimizer_t(ida_hexrays.optinsn_t): 152 | def func(self, blk: mblock_t, ins: minsn_t, optflags: int): 153 | # Let IDA reconstruct the calls before 154 | if blk.mba.maturity < ida_hexrays.MMAT_CALLS: 155 | return 0 156 | 157 | insn_optimizer = insn_optimizer_t(blk.mba, blk) 158 | ins.for_all_insns(insn_optimizer) 159 | return insn_optimizer.cnt 160 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/range_condition/range_condition.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | import ida_hexrays 4 | from ida_hexrays import Hexrays_Hooks, cexpr_t, cfunc_t 5 | from idahelper import tif 6 | from idahelper.ast import cexpr 7 | 8 | 9 | def is_unsigned_comparison_expr(e: cexpr_t) -> bool: 10 | """Check if the expression is an unsigned comparison.""" 11 | return e.op in (ida_hexrays.cot_uge, ida_hexrays.cot_ule, ida_hexrays.cot_ugt, ida_hexrays.cot_ult) 12 | 13 | 14 | class RangeConditionTreeVisitor(ida_hexrays.ctree_visitor_t): 15 | def __init__(self, func: cfunc_t): 16 | super().__init__(ida_hexrays.CV_FAST) 17 | self.func = func 18 | 19 | def visit_expr(self, e: cexpr_t) -> int: 20 | # search for pattern of x ± const_1 < const_2 21 | if not is_unsigned_comparison_expr(e): 22 | return 0 23 | 24 | lhs, rhs = e.x, e.y 25 | if e.x.op == ida_hexrays.cot_cast: 26 | is_cast = True 27 | lhs = e.x.x 28 | else: 29 | is_cast = False 30 | 31 | if ( 32 | rhs.op != ida_hexrays.cot_num 33 | or lhs.op not in (ida_hexrays.cot_add, ida_hexrays.cot_sub) 34 | or lhs.y.op != ida_hexrays.cot_num 35 | or lhs.x.has_side_effects() 36 | ): 37 | return 0 38 | 39 | # Get modulus for the expression size - use e.x instead of lhs to handle casts correctly 40 | expr_size_in_bytes = e.x.type.get_size() 41 | if expr_size_in_bytes not in (1, 2, 4, 8): 42 | print(f"[Warning] Unsupported expression size {expr_size_in_bytes} for {e.dstr()}") 43 | return 0 44 | mod = 1 << (expr_size_in_bytes << 3) 45 | 46 | # Get consts 47 | lhs_const = lhs.y.numval() % mod 48 | rhs_const = e.y.numval() % mod 49 | 50 | # Complement lhs const if the operation is addition 51 | if lhs.op == ida_hexrays.cot_add: 52 | lhs_const = mod - lhs_const 53 | 54 | x = cexpr.from_cast(lhs.x, e.x.type) if is_cast else lhs.x 55 | 56 | replacement_expr = ( 57 | create_range_condition_greater_than 58 | if e.op in (ida_hexrays.cot_ugt, ida_hexrays.cot_uge) 59 | else create_range_condition_less_than 60 | )(e, lhs_const, rhs_const, mod, x, lhs.y.ea, self.func) 61 | e.swap(replacement_expr) 62 | 63 | self.prune_now() 64 | return 0 65 | 66 | 67 | def create_range_condition_less_than( 68 | e: cexpr_t, lhs: int, rhs: int, mod: int, x: cexpr_t, lhs_ea: int, func: cfunc_t 69 | ) -> cexpr_t: 70 | """Create a range condition for the expression `x - lhs < rhs`.""" 71 | 72 | lhs_plus_rhs = lhs + rhs 73 | lhs_plus_rhs_mod_n = lhs_plus_rhs % mod 74 | # if lhs + rhs < mod, we can use a single range condition 75 | # x - lhs < rhs => x ∈ [lhs, lhs + rhs) ==> lhs <= x && x < lhs + rhs 76 | # x - lhs <= rhs => x ∈ [lhs, lhs + rhs] ==> lhs <= x && x <= lhs + rhs 77 | # if lhs + rhs >= mod, we need to use two range conditions 78 | # x - lhs < rhs => x ∈ [lhs, mod) U x ∈ [0, (lhs+rhs) mod n) ==> lhs <= x || x < (lhs + rhs) mod n 79 | # x - lhs <= rhs => x ∈ [lhs, mod) U x ∈ [0, (lhs+rhs) mod n] ==> lhs <= x || x <= (lhs + rhs) mod n 80 | # Notice that the conditions are the same, but it is either && or || depending on whether lhs + rhs < mod or not. 81 | 82 | op: Literal["&&", "||"] = "||" if lhs_plus_rhs >= mod else "&&" 83 | lhs_plus_rhs_expr = cexpr.from_const_value(lhs_plus_rhs_mod_n, func, lhs_ea) 84 | lhs_expr = cexpr.from_const_value(lhs, func, e.y.ea) 85 | return _bin_op( 86 | _bin_op(lhs_expr, "<=", cexpr_t(x), e.ea), 87 | op, 88 | _bin_op(cexpr_t(x), IDA_OP_TO_MATH_OP[e.op], lhs_plus_rhs_expr, e.ea), 89 | e.ea, 90 | ) 91 | 92 | 93 | def create_range_condition_greater_than( 94 | e: cexpr_t, lhs: int, rhs: int, mod: int, x: cexpr_t, lhs_ea: int, func: cfunc_t 95 | ) -> cexpr_t: 96 | """Create a range condition for the expression `x - lhs > rhs`.""" 97 | 98 | lhs_plus_rhs = lhs + rhs 99 | lhs_plus_rhs_mod_n = lhs_plus_rhs % mod 100 | 101 | lhs_plus_rhs_expr = cexpr.from_const_value(lhs_plus_rhs_mod_n, func, lhs_ea) 102 | lhs_expr = cexpr.from_const_value(lhs, func, e.y.ea) 103 | op: Literal["<", "<="] = "<" if e.op == ida_hexrays.cot_ugt else "<=" 104 | 105 | # if lhs + rhs < mod: 106 | # x - lhs > rhs => x ∈ (lhs + rhs, mod) U x ∈ [0, lhs) ==> lhs + rhs < x || x < lhs 107 | # x - lhs >= rhs => x ∈ [lhs + rhs, mod) U x ∈ [0, lhs] ==> lhs + rhs <= x || x <= lhs 108 | if lhs_plus_rhs < mod: 109 | return _bin_op( 110 | _bin_op(lhs_plus_rhs_expr, op, cexpr_t(x), e.ea), 111 | "||", 112 | _bin_op(cexpr_t(x), op, lhs_expr, e.ea), 113 | e.ea, 114 | ) 115 | else: 116 | # if lhs + rhs >= mod: 117 | # x - lhs > rhs => x ∈ (lhs + rhs (mod n), lhs) ==> lhs + rhs (mod n) < x && x < lhs 118 | # x - lhs >= rhs => x ∈ [lhs + rhs (mod n), lhs) ==> lhs + rhs (mod n) <= x && x < lhs 119 | return _bin_op( 120 | _bin_op(lhs_plus_rhs_expr, op, cexpr_t(x), e.ea), 121 | "&&", 122 | _bin_op(cexpr_t(x), "<", lhs_expr, e.ea), 123 | e.ea, 124 | ) 125 | 126 | 127 | def _bin_op(left: cexpr_t, op: Literal["<", "<=", ">", ">=", "&&", "||"], right: cexpr_t, ea: int) -> cexpr_t: 128 | """Create a boolean binary operation expression.""" 129 | if op == "&&": 130 | ida_op = ida_hexrays.cot_land 131 | elif op == "||": 132 | ida_op = ida_hexrays.cot_lor 133 | else: 134 | ida_op = MATH_OP_TO_IDA_OP[op] 135 | return cexpr.from_binary_op(left, right, ida_op, tif.BOOL, ea) 136 | 137 | 138 | IDA_OP_TO_MATH_OP: dict[int, Literal["<", "<=", ">", ">="]] = { 139 | ida_hexrays.cot_uge: ">=", 140 | ida_hexrays.cot_ule: "<=", 141 | ida_hexrays.cot_ugt: ">", 142 | ida_hexrays.cot_ult: "<", 143 | } 144 | MATH_OP_TO_IDA_OP = {v: k for k, v in IDA_OP_TO_MATH_OP.items()} 145 | 146 | 147 | class range_condition_optimizer(Hexrays_Hooks): 148 | def maturity(self, cfunc: cfunc_t, new_maturity: int) -> int: 149 | if new_maturity != ida_hexrays.CMAT_NICE: 150 | return 0 151 | 152 | RangeConditionTreeVisitor(cfunc).apply_to(cfunc.body, None) # pyright: ignore [reportArgumentType] 153 | return 0 154 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/cpp_vtbl/cpp_vtbl.py: -------------------------------------------------------------------------------- 1 | import ida_hexrays 2 | from ida_hexrays import cexpr_t 3 | from ida_typeinf import tinfo_t 4 | from idahelper import cpp, memory, tif, widgets 5 | from idahelper.widgets import EAChoose 6 | 7 | 8 | def get_vtable_call(verbose: bool = False) -> tuple[tinfo_t, str, int] | None: 9 | """If the mouse is on a virtual call, return the vtable type, method name and offset.""" 10 | citem = widgets.get_current_citem() 11 | if citem is None: 12 | if verbose: 13 | print("[Error] No citem found. Do you have your cursor on a virtual call?") 14 | return None 15 | if not citem.is_expr(): 16 | if verbose: 17 | print( 18 | f"[Error] Current citem is not an expression: {citem.dstr()}. Do you have your cursor on the virtual call?" 19 | ) 20 | return None 21 | 22 | return get_vtable_call_from_expr(citem.cexpr, verbose) 23 | 24 | 25 | def get_vtable_call_from_expr(expr: cexpr_t, verbose: bool = False) -> tuple[tinfo_t, str, int] | None: 26 | if expr.op not in [ida_hexrays.cot_memptr, ida_hexrays.cot_memref]: 27 | if verbose: 28 | print( 29 | f"[Error] Current citem is not a member pointer: {expr.dstr()} but a {ida_hexrays.get_ctype_name(expr.op)}. Do you have your cursor on the virtual call?" 30 | ) 31 | return None 32 | 33 | tp: tinfo_t = expr.type 34 | if not tp.is_funcptr() and not tp.is_func(): 35 | if verbose: 36 | print( 37 | f"[Error] Current member is not a function pointer: {expr.dstr()}. Do you have your cursor on a virtual call?" 38 | ) 39 | return None 40 | offset = expr.m 41 | vtable_type = expr.x.type 42 | 43 | # A bit hack but should work. We could implement a better way to get the name in the future... 44 | call_name = expr.dstr().split(".")[-1].split("->")[-1] 45 | return vtable_type, call_name, offset 46 | 47 | 48 | def show_vtable_xrefs(): 49 | vtable_call = get_vtable_call(verbose=True) 50 | if vtable_call is None: 51 | return 52 | 53 | vtable_type, call_name, offset = vtable_call 54 | actual_type = get_actual_class_from_vtable(vtable_type) 55 | if actual_type is None: 56 | print(f"[Error] failed to find actual type for {vtable_type.get_type_name()}") 57 | return 58 | 59 | matches = get_vtable_xrefs(vtable_type, offset) 60 | 61 | method_name = f"{actual_type.get_type_name()}->{call_name}" 62 | if not matches: 63 | print(f"[Error] No implementations found for {method_name}") 64 | if len(matches) == 1: 65 | # Just jump to the function 66 | widgets.jump_to(next(iter(matches.keys()))) 67 | elif matches: 68 | # Show the results in a chooser 69 | print(f"Implementations for {method_name}:") 70 | for ea, cls in matches.items(): 71 | print(f"{hex(ea)}: {memory.name_from_ea(ea)} by {cls}") 72 | 73 | xrefs_choose = EAChoose( 74 | f"Implementations for {method_name}", 75 | list(matches.items()), 76 | col_names=("EA", "Implementing class"), 77 | modal=True, 78 | ) 79 | xrefs_choose.show() 80 | 81 | 82 | def get_vtable_xrefs(vtable_type: tinfo_t, offset: int) -> dict[int, str]: 83 | """Given a vtable type and offset, return the address of the function at that offset.""" 84 | actual_type = get_actual_class_from_vtable(vtable_type) 85 | if actual_type is None: 86 | return {} 87 | 88 | children_classes = tif.get_children_classes(actual_type) or [] 89 | pure_virtual_ea = ( 90 | memory.ea_from_name("___cxa_pure_virtual") 91 | or memory.ea_from_name("__cxa_pure_virtual") 92 | or memory.ea_from_name("_cxa_pure_virtual") 93 | ) 94 | assert pure_virtual_ea is not None 95 | matches: dict[int, str] = {} # addr -> class_name 96 | 97 | # Get the base implementation, either from this class or its parent if it is inherited 98 | parent_impl = get_impl_from_parent(actual_type, offset, pure_virtual_ea) 99 | if parent_impl is not None: 100 | matches[parent_impl[0]] = parent_impl[1] 101 | 102 | for cls in children_classes: 103 | vtable_func_ea = get_vtable_entry(cls, offset, pure_virtual_ea) 104 | if vtable_func_ea is None: 105 | continue 106 | 107 | # Add it to the dict if not already present. 108 | # get_children_classes returns the classes in order of inheritance 109 | if vtable_func_ea not in matches: 110 | # noinspection PyTypeChecker 111 | matches[vtable_func_ea] = cls.get_type_name() # pyright: ignore[reportArgumentType] 112 | return matches 113 | 114 | 115 | def get_impl_from_parent(cls: tinfo_t, offset: int, pure_virtual_ea: int) -> tuple[int, str] | None: 116 | """ 117 | Given a class and an offset to vtable entry, Iterate over its parents to find what will be the implementation 118 | for the given offset. If no implementation is found, return None. 119 | """ 120 | impl, impl_cls = get_vtable_entry(cls, offset, pure_virtual_ea), cls.get_type_name() 121 | if impl is None: 122 | # If not implemented in this class, will not be implemented in its parents. 123 | return None 124 | for parent_cls in tif.get_parent_classes(cls): 125 | if offset >= cpp.vtable_methods_count(parent_cls, False) * 8: 126 | # If offset is greater than the size of the vtable, the method was defined in child class 127 | break 128 | this_impl = get_vtable_entry(parent_cls, offset, pure_virtual_ea) 129 | if this_impl is None or impl != this_impl: 130 | break 131 | else: 132 | impl_cls = parent_cls.get_type_name() 133 | 134 | return impl, f"{impl_cls} (Slot at {cls.get_type_name()})" 135 | 136 | 137 | def get_vtable_entry(cls: tinfo_t, offset: int, pure_virtual_ea: int) -> int | None: 138 | """Given a class and an offset to vtable entry, return the ea of the function at the given offset 139 | if it is not pure virtual.""" 140 | vtable_func_ea = cpp.vtable_func_at(cls, offset) 141 | return vtable_func_ea if vtable_func_ea and pure_virtual_ea != vtable_func_ea else None 142 | 143 | 144 | def get_actual_class_from_vtable(vtable_type: tinfo_t) -> tinfo_t | None: 145 | # It is usually a pointer to a pointer to a vtable 146 | if vtable_type.is_ptr(): 147 | vtable_type = vtable_type.get_pointed_object() 148 | 149 | return tif.type_from_vtable_type(vtable_type) 150 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_refcnt/optimizer.py: -------------------------------------------------------------------------------- 1 | __all__ = ["objc_calls_optimizer_t"] 2 | 3 | import re 4 | 5 | import ida_hexrays 6 | from ida_hexrays import ( 7 | mblock_t, 8 | mcallinfo_t, 9 | minsn_t, 10 | minsn_visitor_t, 11 | mop_t, 12 | mop_visitor_t, 13 | ) 14 | from idahelper.microcode import minsn, mreg 15 | 16 | from ioshelper.base.utils import CounterMixin, match 17 | 18 | # Replace f(x) with x 19 | ID_FUNCTIONS_TO_REPLACE_WITH_ARG: list[str | re.Pattern] = [ 20 | "objc_retain", 21 | "objc_retainAutorelease", 22 | "objc_autoreleaseReturnValue", 23 | "objc_autorelease", 24 | "_objc_claimAutoreleasedReturnValue", 25 | re.compile(r"_objc_claimAutoreleasedReturnValue_(\d+)"), 26 | "_objc_retainBlock", 27 | "objc_unsafeClaimAutoreleasedReturnValue", 28 | "objc_retainAutoreleasedReturnValue", 29 | "_swift_bridgeObjectRetain", 30 | ] 31 | 32 | # Remove f(x) calls 33 | VOID_FUNCTIONS_TO_REMOVE_WITH_SINGLE_ARG: list[str | re.Pattern] = [ 34 | # Objective-C 35 | "objc_release", 36 | # intrinsics 37 | "__break", 38 | # CFoundation 39 | "_CFRelease", 40 | re.compile(r"_CFRelease_(\d+)"), 41 | # swift 42 | "_swift_bridgeObjectRelease", 43 | ] 44 | 45 | VOID_FUNCTION_TO_REMOVE_WITH_MULTIPLE_ARGS: list[str | re.Pattern] = [ 46 | # Blocks 47 | "__Block_object_dispose", 48 | "_Block_object_dispose", 49 | re.compile(r"__Block_object_dispose_(\d+)"), 50 | ] 51 | 52 | # Replace assign(&x, y) with x = y; 53 | ASSIGN_FUNCTIONS: list[str | re.Pattern] = [ 54 | "_objc_storeStrong", 55 | "j__objc_storeStrong", 56 | re.compile(r"j__objc_storeStrong_(\d+)"), 57 | ] 58 | 59 | 60 | class mop_optimizer_t(mop_visitor_t, CounterMixin): 61 | def visit_mop(self, op: mop_t, tp, is_target: bool) -> int: 62 | # No assignment dest, we want a call instruction 63 | if not is_target and op.d is not None: 64 | self.visit_instruction_mop(op) 65 | return 0 66 | 67 | def visit_instruction_mop(self, op: mop_t): 68 | # We only want calls 69 | insn: minsn_t = op.d 70 | if insn.opcode != ida_hexrays.m_call: 71 | return 72 | 73 | # Calls with names 74 | name = minsn.get_func_name_of_call(insn) 75 | if name is None: 76 | return 77 | 78 | # If it should be optimized to first arg, optimize 79 | if match(ID_FUNCTIONS_TO_REPLACE_WITH_ARG, name): 80 | fi: mcallinfo_t = insn.d.f 81 | if fi.args.empty(): 82 | # No arguments, probably IDA have not optimized it yet 83 | return 84 | 85 | # Swap mop containing call with arg0 86 | op.swap(fi.args[0]) 87 | self.count() 88 | 89 | 90 | class insn_optimizer_t(minsn_visitor_t, CounterMixin): 91 | def visit_minsn(self) -> int: 92 | # We only want calls 93 | insn: minsn_t = self.curins 94 | if insn.opcode == ida_hexrays.m_call: 95 | self.visit_call_insn(insn, self.blk) 96 | return 0 97 | 98 | def visit_call_insn(self, insn: minsn_t, blk: mblock_t): 99 | # Calls with names 100 | name = minsn.get_func_name_of_call(insn) 101 | if name is None: 102 | return 103 | 104 | for optimization in [ 105 | self.void_function_to_remove, 106 | self.id_function_to_replace_with_their_arg, 107 | self.assign_functions, 108 | ]: 109 | # noinspection PyArgumentList 110 | if optimization(name, insn, blk): 111 | return 112 | 113 | def void_function_to_remove(self, name: str, insn: minsn_t, blk: mblock_t) -> bool: 114 | if match(VOID_FUNCTIONS_TO_REMOVE_WITH_SINGLE_ARG, name): 115 | single_arg = True 116 | elif match(VOID_FUNCTION_TO_REMOVE_WITH_MULTIPLE_ARGS, name): 117 | single_arg = False 118 | else: 119 | return False 120 | 121 | fi: mcallinfo_t = insn.d.f 122 | if fi.args.empty() or (single_arg and len(fi.args) != 1): 123 | # No arguments, probably not optimized yet 124 | # Or not matching the number of arguments 125 | return False 126 | 127 | if any(arg.has_side_effects() for arg in fi.args): 128 | print("[Error] arguments with side effects are not supported yet!") 129 | return False 130 | 131 | if not fi.return_type or not fi.return_type.is_void(): 132 | # embedded instruction, the result can be assigned to something. 133 | print( 134 | f"[Error] Cannot remove {name} as this is an embedded instruction. Is the return type correct? it should be void." 135 | ) 136 | return False 137 | 138 | blk.make_nop(insn) 139 | self.count() 140 | return True 141 | 142 | def id_function_to_replace_with_their_arg(self, name: str, insn: minsn_t, _blk: mblock_t) -> bool: 143 | if not match(ID_FUNCTIONS_TO_REPLACE_WITH_ARG, name): 144 | return False 145 | 146 | # Might be a call with destination (for example, if it is the last statement in the function) 147 | fi: mcallinfo_t = insn.d.f 148 | if fi.args.empty() or fi.retregs.empty(): 149 | # No arguments (probably not optimized yet) or no return reg 150 | return False 151 | 152 | # Make instruction mov instead of call 153 | insn.opcode = ida_hexrays.m_mov 154 | insn.l.swap(fi.args[0]) 155 | insn.d.swap(fi.retregs[0]) 156 | self.count() 157 | return True 158 | 159 | def assign_functions(self, name: str, insn: minsn_t, _blk: mblock_t) -> bool: 160 | if not match(ASSIGN_FUNCTIONS, name): 161 | return False 162 | 163 | fi: mcallinfo_t = insn.d.f 164 | if fi.args.size() != 2: 165 | # Not enough argument, probably not optimized yet 166 | return False 167 | insn.opcode = ida_hexrays.m_stx 168 | # src 169 | insn.l.swap(fi.args[1]) 170 | # dest 171 | insn.d.swap(fi.args[0]) 172 | # seg - need to be CS/DS according to the docs. 173 | insn.r.make_reg(mreg.cs_reg(), 2) 174 | self.count() 175 | return True 176 | 177 | 178 | class objc_calls_optimizer_t(ida_hexrays.optinsn_t): 179 | def func(self, blk: mblock_t, ins: minsn_t, optflags: int): 180 | # Let IDA reconstruct the calls before 181 | if blk.mba.maturity < ida_hexrays.MMAT_CALLS: 182 | return 0 183 | 184 | mop_optimizer = mop_optimizer_t(blk.mba, blk) 185 | insn_optimizer = insn_optimizer_t(blk.mba, blk) 186 | ins.for_all_ops(mop_optimizer) 187 | ins.for_all_insns(insn_optimizer) 188 | changes = mop_optimizer.cnt + insn_optimizer.cnt 189 | if changes: 190 | blk.mark_lists_dirty() 191 | return changes 192 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/clang_blocks/block_arg_byref.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "BlockArgByRefField", 3 | "BlockByRefArgBaseFieldsAssignments", 4 | "create_block_arg_byref_type", 5 | "get_block_byref_args_lvars", 6 | "is_block_arg_byref_type", 7 | ] 8 | 9 | from dataclasses import dataclass 10 | from enum import Enum 11 | 12 | import ida_hexrays 13 | from ida_hexrays import cexpr_t, cfunc_t, cinsn_t, lvar_t 14 | from ida_typeinf import tinfo_t 15 | from idahelper import tif 16 | from idahelper.ast import cexpr 17 | 18 | from .utils import StructFieldAssignment 19 | 20 | PTR_SIZE = 8 21 | INT_SIZE = 4 22 | 23 | OFFSET_ISA = 0 24 | OFFSET_FORWARDING = OFFSET_ISA + PTR_SIZE 25 | OFFSET_FLAGS = OFFSET_FORWARDING + PTR_SIZE 26 | OFFSET_SIZE = OFFSET_FLAGS + INT_SIZE 27 | OFFSET_BYREF_KEEP = OFFSET_SIZE + INT_SIZE 28 | OFFSET_BYREF_DISPOSE = OFFSET_BYREF_KEEP + PTR_SIZE 29 | OFFSETS = [OFFSET_ISA, OFFSET_FORWARDING, OFFSET_FLAGS, OFFSET_SIZE, OFFSET_BYREF_KEEP, OFFSET_BYREF_DISPOSE] 30 | 31 | 32 | class BlockArgByRefField(Enum): 33 | """ 34 | struct _block_byref_x { 35 | void *isa; 36 | struct _block_byref_x *forwarding; 37 | int flags; 38 | int size; 39 | /* optional */ void (*byref_keep)(void *dst, void *src); 40 | /* optional */ void (*byref_dispose)(void *); 41 | typeof(marked_variable) marked_variable; 42 | }; 43 | """ 44 | 45 | ISA = 0 46 | FORWARDING = 1 47 | FLAGS = 2 48 | SIZE = 3 49 | HELPER_KEEP = 4 50 | HELPER_DISPOSE = 5 51 | VARIABLE = 6 52 | 53 | def get_offset(self, has_helpers: bool | None = None) -> int: 54 | if self == BlockArgByRefField.VARIABLE and has_helpers is None: 55 | raise ValueError("has_helpers must be specified for VARIABLE") 56 | 57 | if self == BlockArgByRefField.VARIABLE: 58 | return OFFSET_BYREF_DISPOSE + PTR_SIZE if has_helpers else OFFSET_BYREF_KEEP 59 | 60 | return OFFSETS[self.value] 61 | 62 | 63 | TYPE_BLOCK_ARG_BYREF_PREFIX = "Block_byref_layout_" 64 | TYPE_DECL_BLOCK_ARG_BYREF_WITH_HELPERS = """ 65 | #pragma pack(push, 1) 66 | struct {class_name} {{ 67 | void *isa; 68 | struct {class_name} *forwarding; 69 | int flags; 70 | int size; 71 | void (*byref_keep)(void *dst, void *src); 72 | void (*byref_dispose)(void *); 73 | {var_type} value; 74 | }}; 75 | #pragma pack(pop) 76 | """ 77 | TYPE_DECL_BLOCK_ARG_BYREF_WITHOUT_HELPERS = """ 78 | #pragma pack(push, 1) 79 | struct {class_name} {{ 80 | void *isa; 81 | struct {class_name} *forwarding; 82 | int flags; 83 | int size; 84 | {var_type} value; 85 | }}; 86 | #pragma pack(pop) 87 | """ 88 | 89 | 90 | def get_type_name_for_addr(ea: int) -> str: 91 | return f"{TYPE_BLOCK_ARG_BYREF_PREFIX}{ea:08X}" 92 | 93 | 94 | def is_block_arg_byref_type(tinfo: tinfo_t) -> bool: 95 | """Check if the given `tif` is a block arg byref type""" 96 | if not tinfo.is_struct(): 97 | return False 98 | # noinspection PyTypeChecker 99 | name: str | None = tinfo.get_type_name() 100 | return name is not None and name.startswith(TYPE_BLOCK_ARG_BYREF_PREFIX) 101 | 102 | 103 | def create_block_arg_byref_type(ea: int, var_size: int, has_helpers: bool) -> tinfo_t: 104 | """Create a tinfo_t for the block byref type""" 105 | class_name = get_type_name_for_addr(ea) 106 | 107 | if (existing_type := tif.from_struct_name(class_name)) is not None: 108 | return existing_type 109 | 110 | var_type = tif.from_size(var_size).dstr() 111 | if has_helpers: 112 | type_decl = TYPE_DECL_BLOCK_ARG_BYREF_WITH_HELPERS.format(class_name=class_name, var_type=var_type) 113 | else: 114 | type_decl = TYPE_DECL_BLOCK_ARG_BYREF_WITHOUT_HELPERS.format(class_name=class_name, var_type=var_type) 115 | 116 | tif.create_from_c_decl(type_decl) 117 | return tif.from_struct_name(class_name) 118 | 119 | 120 | def get_block_byref_args_lvars(func: cfunc_t) -> list[lvar_t]: 121 | """Get all Obj-C block by ref args variables in the function""" 122 | return [lvar for lvar in func.get_lvars() if is_block_arg_byref_type(lvar.type())] 123 | 124 | 125 | @dataclass 126 | class BlockByRefArgBaseFieldsAssignments: 127 | assignments: list[cinsn_t] 128 | ea: int | None 129 | type: tinfo_t | None 130 | isa: cexpr_t | None 131 | forwarding: cexpr_t | None 132 | flags: cexpr_t | None 133 | size: cexpr_t | None 134 | byref_keep: cexpr_t | None 135 | byref_dispose: cexpr_t | None 136 | 137 | def __str__(self) -> str: 138 | return ( 139 | f"isa: {self.isa.dstr() if self.isa else 'None'}, " 140 | f"forwarding: {self.forwarding.dstr() if self.forwarding else 'None'}, " 141 | f"flags: {self.flags.dstr() if self.flags else 'None'}, " 142 | f"size: {self.size.dstr() if self.size else 'None'}, " 143 | f"byref_keep: {self.byref_keep.dstr() if self.byref_keep else 'None'}, " 144 | f"byref_dispose: {self.byref_dispose.dstr() if self.byref_dispose else 'None'}, " 145 | ) 146 | 147 | @staticmethod 148 | def initial() -> "BlockByRefArgBaseFieldsAssignments": 149 | return BlockByRefArgBaseFieldsAssignments( 150 | assignments=[], 151 | ea=None, 152 | type=None, 153 | isa=None, 154 | forwarding=None, 155 | flags=None, 156 | size=None, 157 | byref_keep=None, 158 | byref_dispose=None, 159 | ) 160 | 161 | def is_completed(self) -> bool: 162 | """Check if all base fields have been assigned""" 163 | return self.isa is not None and self.forwarding is not None and self.flags is not None and self.size is not None 164 | 165 | def add_assignment(self, assignment: StructFieldAssignment) -> bool: 166 | """Add an assignment to the list of assignments""" 167 | if self.type is None: 168 | self.type = assignment.type 169 | 170 | field_name = assignment.member.name 171 | if field_name == "isa": 172 | self.isa = assignment.expr 173 | self.ea = assignment.insn.ea 174 | elif field_name == "forwarding": 175 | self.forwarding = assignment.expr 176 | elif field_name == "flags": 177 | if assignment.is_cast_assign: 178 | # We need to split it to flags and reserved 179 | expr = assignment.expr 180 | if expr.op != ida_hexrays.cot_num: 181 | print(f"[Error] invalid flags assignment. Expected const, got: {expr.dstr()}") 182 | return False 183 | 184 | num_val = expr.numval() 185 | self.flags = cexpr.from_const_value(num_val & 0xFF_FF_FF_FF, is_hex=True) 186 | self.size = cexpr.from_const_value(num_val >> 32, is_hex=True) 187 | else: 188 | self.flags = assignment.expr 189 | elif field_name == "size": 190 | self.size = assignment.expr 191 | elif field_name == "byref_keep": 192 | self.byref_keep = assignment.expr 193 | elif field_name == "byref_dispose": 194 | self.byref_dispose = assignment.expr 195 | else: 196 | return False 197 | 198 | self.assignments.append(assignment.insn) 199 | return True 200 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "ida-ios-helper" 7 | source = { editable = "." } 8 | dependencies = [ 9 | { name = "idahelper" }, 10 | ] 11 | 12 | [package.dev-dependencies] 13 | dev = [ 14 | { name = "ruff" }, 15 | { name = "vermin" }, 16 | ] 17 | 18 | [package.metadata] 19 | requires-dist = [{ name = "idahelper", specifier = "==1.0.17" }] 20 | 21 | [package.metadata.requires-dev] 22 | dev = [ 23 | { name = "ruff", specifier = ">=0.12.2" }, 24 | { name = "vermin", specifier = ">=1.6.0" }, 25 | ] 26 | 27 | [[package]] 28 | name = "idahelper" 29 | version = "1.0.17" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/92/57/aac017234454c76bb712ed5cf4e6f33d4d1f2a8be9d18ce500cabcde66d0/idahelper-1.0.17.tar.gz", hash = "sha256:9b36f3455419ebeed2a3720765d0822e159f9deb2a009aec8fee3c87581d104f", size = 25612, upload-time = "2025-10-24T19:22:35.069Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/c1/f6/ddc32314179571bd8140f4dd99d513005c842f48070cd59ea890682054ee/idahelper-1.0.17-py3-none-any.whl", hash = "sha256:74df1c4eef0ac82474ab1b7b73183a235534643e151d636a3a63034a8984121d", size = 31556, upload-time = "2025-10-24T19:22:34.09Z" }, 34 | ] 35 | 36 | [[package]] 37 | name = "ruff" 38 | version = "0.12.2" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" }, 43 | { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" }, 44 | { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" }, 45 | { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" }, 46 | { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" }, 47 | { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" }, 48 | { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" }, 49 | { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" }, 50 | { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" }, 51 | { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" }, 52 | { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" }, 53 | { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" }, 54 | { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" }, 55 | { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" }, 56 | { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" }, 57 | { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" }, 58 | { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, 59 | ] 60 | 61 | [[package]] 62 | name = "vermin" 63 | version = "1.6.0" 64 | source = { registry = "https://pypi.org/simple" } 65 | sdist = { url = "https://files.pythonhosted.org/packages/3d/26/7b871396c33006c445c25ef7da605ecbd6cef830d577b496d2b73a554f9d/vermin-1.6.0.tar.gz", hash = "sha256:6266ca02f55d1c2aa189a610017c132eb2d1934f09e72a955b1eb3820ee6d4ef", size = 93181, upload-time = "2023-11-25T20:58:25.406Z" } 66 | wheels = [ 67 | { url = "https://files.pythonhosted.org/packages/2e/98/1a2ca43e6d646421eea16ec19977e2e6d1ea9079bd9d873bfae513d43f1c/vermin-1.6.0-py2.py3-none-any.whl", hash = "sha256:f1fa9ee40f59983dc40e0477eb2b1fa8061a3df4c3b2bcf349add462a5610efb", size = 90845, upload-time = "2023-11-25T20:58:22.69Z" }, 68 | ] 69 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/segment_xrefs/segment_xrefs.py: -------------------------------------------------------------------------------- 1 | __all__ = ["can_show_segment_xrefs", "get_current_expr", "show_segment_xrefs"] 2 | 3 | from dataclasses import dataclass 4 | 5 | import ida_hexrays 6 | import idaapi 7 | from ida_hexrays import cexpr_t, cfuncptr_t, cinsn_t, citem_t, get_widget_vdui 8 | from ida_kernwin import Choose 9 | from ida_typeinf import tinfo_t 10 | from idahelper import cpp, functions, memory, segments, tif, widgets, xrefs 11 | 12 | 13 | def show_segment_xrefs(expr: cexpr_t, func_ea: int): 14 | if not _can_show_segment_xrefs(expr): 15 | print(f"[Error] Cannot show segment xrefs for expression: {expr.dstr()}") 16 | return 17 | 18 | if expr.op == ida_hexrays.cot_obj: 19 | segment_xrefs = _ea_to_xrefs(expr.obj_ea) 20 | title = f"Segment Xrefs for {memory.name_from_ea(expr.obj_ea) or f'{expr.obj_ea:#X}'}" 21 | else: 22 | segment_xrefs = _expr_to_xrefs(expr, func_ea) 23 | title = f"Segment Xrefs for {expr.dstr()}" 24 | 25 | if not segment_xrefs: 26 | print("No segment xrefs found.") 27 | return 28 | 29 | res = XrefsChoose(title, segment_xrefs).show() 30 | if not res: 31 | print("[Error] failed to show segment xrefs.") 32 | return 33 | 34 | 35 | @dataclass 36 | class SegmentXref: 37 | address: int 38 | function: int 39 | line: str 40 | 41 | 42 | def _ea_to_xrefs(ea: int) -> list[SegmentXref]: 43 | """Get all xrefs to the given EA""" 44 | segment_xrefs: list[SegmentXref] = [] 45 | for xref in xrefs.code_xrefs_to(ea): 46 | decompiled = idaapi.decompile(ea, flags=ida_hexrays.DECOMP_GXREFS_FORCE) 47 | if decompiled is None: 48 | print(f"[Warning] Could not decompile function at {xref:X}") 49 | continue 50 | decompiled_line = _get_decompiled_line(decompiled, xref) or "" 51 | segment_xrefs.append(SegmentXref(xref, decompiled.entry_ea, decompiled_line)) 52 | 53 | return segment_xrefs 54 | 55 | 56 | def _expr_to_xrefs(expr: cexpr_t, func_ea: int) -> list[SegmentXref]: 57 | assert expr.op in (ida_hexrays.cot_memptr, ida_hexrays.cot_memref) 58 | expr_obj_type: tinfo_t = expr.x.type 59 | if expr_obj_type.is_ptr_or_array(): 60 | expr_obj_type.remove_ptr_or_array() 61 | expr_obj_offset: int = expr.m 62 | recursive_member = tif.get_member_recursive(expr_obj_type, expr_obj_offset) 63 | if recursive_member is None: 64 | print(f"[Error] Could not find member at offset {expr_obj_offset} in type {expr_obj_type.dstr()}") 65 | return [] 66 | relevant_type: tinfo_t = recursive_member[0] 67 | possible_types: set[str] = {str(t) for t in tif.get_children_classes(relevant_type) or []} 68 | possible_types.add(str(relevant_type)) 69 | 70 | current_segment = segments.get_segment_by_ea(func_ea) 71 | if current_segment is None: 72 | print(f"[Error] Could not find segment for function at {func_ea:X}") 73 | return [] 74 | 75 | segment_xrefs: list[SegmentXref] = [] 76 | for func in functions.iterate_functions(current_segment.start_ea, current_segment.end_ea): 77 | decompiled = idaapi.decompile(func.start_ea, flags=ida_hexrays.DECOMP_GXREFS_FORCE) 78 | if decompiled is None: 79 | print(f"[Warning] Could not decompile function at {func.start_ea:X}") 80 | continue 81 | 82 | segment_xrefs.extend(_find_xrefs_to_field(possible_types, expr_obj_offset, decompiled)) 83 | 84 | return segment_xrefs 85 | 86 | 87 | def _find_xrefs_to_field(possible_types: set[str], offset: int, func: cfuncptr_t) -> list[SegmentXref]: 88 | """Find all xrefs to the given field in the context of the function.""" 89 | segment_xrefs: list[SegmentXref] = [] 90 | for item in func.treeitems: 91 | actual_item: cexpr_t | cinsn_t = item.to_specific_type 92 | # Check if it is field access 93 | if not isinstance(actual_item, cexpr_t) or actual_item.op not in [ 94 | ida_hexrays.cot_memptr, 95 | ida_hexrays.cot_memref, 96 | ]: 97 | continue 98 | # Check if the type and offset match 99 | item_type: tinfo_t = actual_item.x.type 100 | if item_type.is_ptr_or_array(): 101 | item_type.remove_ptr_or_array() 102 | if str(item_type) not in possible_types or actual_item.m != offset: 103 | continue 104 | 105 | container_insn = _find_first_container_instruction(actual_item, func) 106 | if container_insn is None: 107 | print(f"[Warning] Could not find container instruction for item: {actual_item.dstr()}") 108 | continue 109 | 110 | item_ea = container_insn.ea 111 | segment_xrefs.append(SegmentXref(item_ea, func.entry_ea, container_insn.dstr())) 112 | return segment_xrefs 113 | 114 | 115 | def _find_first_container_instruction(item: citem_t | None, func: cfuncptr_t) -> cinsn_t | None: 116 | """Find the EA of the given item in the context of the function.""" 117 | while item is not None: 118 | if isinstance(item, cinsn_t): 119 | return item 120 | item = func.body.find_parent_of(item) 121 | if item is not None: 122 | item = item.to_specific_type 123 | 124 | return None 125 | 126 | 127 | def _get_decompiled_line(func: cfuncptr_t, ea: int) -> str | None: 128 | """Get the decompiled line for the given EA in the context of the function.""" 129 | ea_map = func.get_eamap() 130 | if ea not in ea_map: 131 | print(f"[Warning] {ea:X} is not in {func.entry_ea:X} ea map.") 132 | return None 133 | 134 | return "\n".join(stmt.dstr() for stmt in ea_map[ea]) 135 | 136 | 137 | def _can_show_segment_xrefs(expr: cexpr_t) -> bool: 138 | """Check if we can show segment xrefs for the given expression.""" 139 | return expr.op in (ida_hexrays.cot_obj, ida_hexrays.cot_memref, ida_hexrays.cot_memptr) 140 | 141 | 142 | def can_show_segment_xrefs(widget) -> bool: 143 | """Check if we can show segment xrefs in the current context.""" 144 | expr = get_current_expr(widget) 145 | return expr is not None and _can_show_segment_xrefs(expr) 146 | 147 | 148 | def get_current_expr(widget) -> cexpr_t | None: 149 | """Get the current expression in the context.""" 150 | if idaapi.get_widget_type(widget) != idaapi.BWN_PSEUDOCODE: 151 | return None 152 | vu = get_widget_vdui(widget) 153 | if not vu or not vu.item or vu.item.citype != ida_hexrays.VDI_EXPR: 154 | return None 155 | return vu.item.it.to_specific_type 156 | 157 | 158 | class XrefsChoose(Choose): 159 | def __init__(self, title: str, items: list[SegmentXref]): 160 | Choose.__init__( 161 | self, 162 | title, 163 | [ 164 | ["Address", 20 | Choose.CHCOL_EA], 165 | ["Function", 40 | Choose.CHCOL_FNAME], 166 | ["Line", 40 | Choose.CHCOL_PLAIN], 167 | ], 168 | flags=Choose.CH_RESTORE, 169 | embedded=False, 170 | ) 171 | self.items = items 172 | self.modal = False 173 | 174 | def OnInit(self) -> bool: 175 | return True 176 | 177 | def OnGetSize(self) -> int: 178 | return len(self.items) 179 | 180 | def OnGetLine(self, n): 181 | item = self.items[n] 182 | return ( 183 | f"{item.address:X}", 184 | cpp.demangle_name_only(memory.name_from_ea(item.function) or f"SUB_{item.function:X}"), 185 | item.line, 186 | ) 187 | 188 | def OnGetEA(self, n) -> int: 189 | return self.items[n].address 190 | 191 | def OnSelectLine(self, n): 192 | ea = self.items[n].address 193 | widgets.jump_to(ea) 194 | return (Choose.NOTHING_CHANGED,) 195 | 196 | def show(self) -> bool: 197 | ok = self.Show(self.modal) >= 0 198 | return ok 199 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/common/outline/outline.py: -------------------------------------------------------------------------------- 1 | __all__ = ["mark_all_outline_functions"] 2 | 3 | 4 | import re 5 | import zlib 6 | from collections import Counter 7 | from itertools import islice 8 | 9 | import ida_ua 10 | from ida_funcs import func_t 11 | from ida_ua import insn_t 12 | from idahelper import functions, instructions, memory, xrefs 13 | 14 | MAX_INSN_COUNT = 15 15 | OUTLINE_COMMON_REGISTERS = ["X19", "X20", "X21", "X22", "X23", "X24", "X25"] 16 | HASHES = { 17 | 2486657537, 18 | 4051340011, 19 | 835854959, 20 | 4053891209, 21 | 2284322540, 22 | 3734446324, 23 | 3486938057, 24 | 3100884732, 25 | 1520871260, 26 | 3035036398, 27 | 3354748321, 28 | 1286661669, 29 | 3963943914, 30 | 1065801208, 31 | 3035825621, 32 | 3374831749, 33 | 3277914534, 34 | 2587032924, 35 | 2868248941, 36 | 3315843181, 37 | 2622615498, 38 | 3720341170, 39 | 3337984774, 40 | 1893466989, 41 | 518987256, 42 | 2803156496, 43 | 671639713, 44 | 218684989, 45 | 1791333242, 46 | 3457579667, 47 | 3574749379, 48 | 3434086350, 49 | 4165857220, 50 | 2131640558, 51 | 3495698196, 52 | 1340405478, 53 | 229647317, 54 | 4006608232, 55 | 786104779, 56 | 1126219089, 57 | 2831582611, 58 | 289234555, 59 | 1661182837, 60 | 3423629888, 61 | 2309108340, 62 | 3075689215, 63 | 2236221560, 64 | 1839174898, 65 | 481463652, 66 | 766791884, 67 | 218698196, 68 | 752795631, 69 | 3344164986, 70 | 1827242795, 71 | 792695013, 72 | 230959901, 73 | 2997841865, 74 | 568754444, 75 | 1670421527, 76 | 2184318906, 77 | 3024877629, 78 | 2387594438, 79 | 557192575, 80 | 2458769492, 81 | 3922284957, 82 | 4107181330, 83 | 1578820194, 84 | 1546391466, 85 | 1163279958, 86 | 1540647309, 87 | 1049262605, 88 | 2130556960, 89 | 2163533787, 90 | 2092533000, 91 | 2426425641, 92 | 4207618, 93 | 2736070239, 94 | 1277505515, 95 | 1788212028, 96 | 1368299300, 97 | 3786044294, 98 | 563839748, 99 | 3199076566, 100 | 876720633, 101 | 2633791921, 102 | 1428282898, 103 | 146181378, 104 | 472218750, 105 | 304360983, 106 | 1808920834, 107 | 4174859059, 108 | 85578290, 109 | 631075697, 110 | 1189335917, 111 | 288603166, 112 | 4177653000, 113 | 3698829753, 114 | 1630600770, 115 | 1498027780, 116 | 1149213890, 117 | } 118 | 119 | 120 | def calculate_outline_hash(top_hashes_count: int): 121 | # Calculate hashes 122 | hashes = Counter() 123 | for func in functions.iterate_functions(): 124 | func_name = memory.name_from_ea(func.start_ea) 125 | assert func_name is not None 126 | 127 | if functions.has_flags(func, functions.FLAG_OUTLINE): 128 | hashes[function_hash(func)] += 1 129 | continue 130 | 131 | top_hashes = [key for key, _ in hashes.most_common(top_hashes_count)] 132 | top_hashes_match = sum(value for _, value in hashes.most_common(top_hashes_count)) 133 | print(f"Top {top_hashes_count} hashes, should match {top_hashes_match} / {hashes.total()}") 134 | print(top_hashes) 135 | 136 | # Check for false positives 137 | top_hashes = set(top_hashes) 138 | for func in functions.iterate_functions(): 139 | func_name = memory.name_from_ea(func.start_ea) 140 | assert func_name is not None 141 | 142 | if not functions.has_flags(func, functions.FLAG_OUTLINE): 143 | func_hash = function_hash(func) 144 | if func_hash in top_hashes: 145 | print(f"{func.start_ea:X} matches an hash for outlined func.") 146 | 147 | 148 | def mark_all_outline_functions(): 149 | count = 0 150 | for func in functions.iterate_functions(): 151 | func_name = memory.name_from_ea(func.start_ea) 152 | assert func_name is not None 153 | 154 | # if functions.has_flags(func, functions.FLAG_OUTLINE): 155 | # continue 156 | if func_name.startswith("_OUTLINED") or ( 157 | not heuristic_not_outline(func, func_name) and (function_hash(func) in HASHES or heuristic_outline(func)) 158 | ): 159 | print(f"Applied outline flag on {func.start_ea:X}") 160 | functions.apply_flag_to_function(func, functions.FLAG_OUTLINE) 161 | 162 | # Update name as well 163 | if "OUTLINED" not in func_name: 164 | memory.set_name(func.start_ea, f"__OUTLINED_{func_name}") 165 | count += 1 166 | 167 | print(f"Applied outlined to {count} functions") 168 | 169 | 170 | def heuristic_not_outline(func: func_t, name: str) -> bool: 171 | # Assuming outlined functions are small - less than 10 instructions. 172 | if func.size() > MAX_INSN_COUNT * 4: 173 | return True 174 | 175 | if not name.startswith("sub_"): 176 | return True 177 | 178 | # Outlined functions will have no data xrefs 179 | if xrefs.get_xrefs_to(func.start_ea, is_data=True): 180 | return True 181 | 182 | # We only care for functions with xrefs. 183 | # One might think outline functions has to have more than one xref, but apparently it is incorrect... 184 | if not xrefs.get_xrefs_to(func.start_ea): 185 | return True 186 | 187 | first_instruction = instructions.decode_instruction(func.start_ea) 188 | return bool(first_instruction is None or first_instruction.get_canon_mnem() in ["PAC", "BTI"]) 189 | 190 | 191 | def heuristic_outline(func: func_t) -> bool: 192 | # Empty function are not outlined I guess 193 | if func.size() == 0: 194 | return False 195 | 196 | first_insn = instructions.decode_instruction(func.start_ea) 197 | if first_insn is None: 198 | return False 199 | 200 | # We don't deal right now with instructions that may branch on first instruction 201 | if instructions.is_flow_instruction(first_insn): 202 | return False 203 | 204 | # Check if we use an outline register without definition in the first instruction 205 | read_0, write_0 = instructions.analyze_reg_dependencies(first_insn) 206 | if any(reg in read_0 for reg in OUTLINE_COMMON_REGISTERS): 207 | return True 208 | 209 | # Check if we use an outline register without definition in the second instruction 210 | second_insn = instructions.decode_next_instruction(first_insn, func) 211 | if second_insn is None: 212 | return False 213 | 214 | read_1, _ = instructions.analyze_reg_dependencies(second_insn) 215 | return any(reg in read_1 and reg not in write_0 for reg in OUTLINE_COMMON_REGISTERS) 216 | 217 | 218 | REG_GROUPS = { 219 | "a": range(0, 8), # x0-x7 220 | "t": range(8, 16), # x8-x15 221 | "i": range(16, 19), # x16-x18 222 | "s1": range(19, 24), # x19-x23 223 | "s2": range(24, 29), # x24-x28 224 | "fp": [29], 225 | "lr": [30], 226 | "z": ["xzr"], 227 | } 228 | 229 | 230 | def classify_reg(reg: int) -> str: 231 | reg_name = instructions.get_register_name(reg).lower().strip() 232 | if reg_name == "sp": 233 | return "sp" 234 | if reg_name == "xzr" or reg_name == "wzr": 235 | return "z" 236 | if reg_name.startswith(("x", "w")): 237 | regnum = int(re.findall(r"\d+", reg_name)[0]) 238 | for group, rset in REG_GROUPS.items(): 239 | if regnum in rset: 240 | return group 241 | return "r" # everything else 242 | return reg_name # leave non-registers as-is 243 | 244 | 245 | def get_normalized_pattern(insn: insn_t) -> str: 246 | mnem = insn.get_canon_mnem() 247 | ops = [] 248 | for op in insn.ops: 249 | if op.type == ida_ua.o_void: 250 | reg = "V" 251 | elif op.type == ida_ua.o_imm: 252 | reg = str(op.value) 253 | elif op.type == ida_ua.o_mem: 254 | reg = "M" 255 | elif op.type == ida_ua.o_reg: 256 | reg = classify_reg(op.reg) 257 | elif op.type == ida_ua.o_phrase or op.type == ida_ua.o_idpspec0: 258 | reg = f"{classify_reg(op.reg)}:{op.phrase}" 259 | elif op.type == ida_ua.o_displ: 260 | reg = f"{classify_reg(op.reg)}:{op.phrase}:{op.value}" 261 | else: 262 | reg = f"U{op.type}" 263 | 264 | ops.append(reg) 265 | return f"{mnem}_{'_'.join(ops)}" 266 | 267 | 268 | def get_function_pattern(func) -> str: 269 | patterns = [] 270 | for insn in islice(instructions.from_func(func), 2): 271 | patterns.append(get_normalized_pattern(insn)) 272 | return " ".join(patterns) 273 | 274 | 275 | def function_hash(func) -> int: 276 | pattern = get_function_pattern(func) 277 | return zlib.crc32(pattern.encode()) 278 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/func_renamers/handlers.py: -------------------------------------------------------------------------------- 1 | __all__ = ["GLOBAL_HANDLERS", "LOCAL_HANDLERS"] 2 | 3 | 4 | from idahelper import file_format, functions, instructions, memory, tif, xrefs 5 | 6 | from .renamer import ( 7 | FuncHandler, 8 | FuncHandlerByNameWithStringFinder, 9 | FuncHandlerVirtualGetter, 10 | FuncHandlerVirtualSetter, 11 | Modifications, 12 | ) 13 | from .visitor import Call, FuncXref, SourceXref 14 | 15 | 16 | class OSSymbolHandler(FuncHandlerByNameWithStringFinder): 17 | def __init__(self, name: str, search_string: str): 18 | super().__init__( 19 | name, 20 | tif.from_func_components("OSSymbol*", [tif.FuncParam("const char*", "cString")]), 21 | search_string, 22 | is_call=True, 23 | ) 24 | 25 | self._cached_symbol_type = tif.from_c_type("OSSymbol*") 26 | 27 | def on_call(self, call: Call, modifications: Modifications): 28 | self._rename_assignee_by_index(modifications, call, 0, modifier=lambda name: f"sym_{name}") 29 | self._retype_assignee(modifications, call, self._cached_symbol_type) 30 | 31 | 32 | OSSymbol_WithCStringNoCopy = OSSymbolHandler("OSSymbol::withCStringNoCopy", "IOMatchedPersonality") 33 | OSSymbol_WithCString = OSSymbolHandler("OSSymbol::withCString", "ACIPCInterfaceProtocol") 34 | 35 | 36 | class IORegistry_MakePlane(FuncHandlerByNameWithStringFinder): 37 | def __init__(self): 38 | super().__init__( 39 | "IORegistry::makePlane", 40 | tif.from_func_components("IORegistryPlane*", [tif.FuncParam("const char*", "name")]), 41 | "ChildLinks", 42 | is_call=False, 43 | ) 44 | self._cached_registry_plane_type = tif.from_c_type("IORegistryPlane*") 45 | 46 | def on_call(self, call: Call, modifications: Modifications): 47 | self._rename_assignee_by_index(modifications, call, 0, modifier=lambda name: f"plane_{name}") 48 | self._retype_assignee(modifications, call, self._cached_registry_plane_type) 49 | 50 | 51 | IOService_SetProperty = [ 52 | FuncHandlerVirtualSetter( 53 | "IORegistryEntry::setProperty", 54 | tif.from_c_type("IORegistryEntry"), 55 | offset, 56 | name_index=1, 57 | rename_index=2, 58 | rename_prefix="val_", 59 | ) 60 | for offset in range(0xB8, 0xF0, 8) 61 | ] 62 | IOService_GetProperty = FuncHandlerVirtualGetter( 63 | "IORegistryEntry::getProperty", tif.from_c_type("IORegistryEntry"), 0x118, name_index=1, rename_prefix="val_" 64 | ) 65 | IOService_CopyProperty = FuncHandlerVirtualGetter( 66 | "IORegistryEntry::copyProperty", tif.from_c_type("IORegistryEntry"), 0x148, name_index=1, rename_prefix="val_" 67 | ) 68 | 69 | 70 | class MetaClassConstructor(FuncHandlerByNameWithStringFinder): 71 | def __init__(self): 72 | super().__init__( 73 | "__ZN11OSMetaClassC2EPKcPKS_j", 74 | tif.from_func_components( 75 | "OSMetaClass*", 76 | [ 77 | tif.FuncParam("OSMetaClass*", "this"), 78 | tif.FuncParam("const char*", "className"), 79 | tif.FuncParam("const OSMetaClass*", "superClass"), 80 | tif.FuncParam("unsigned int", "classSize"), 81 | ], 82 | ), 83 | "OSMetaClass: preModLoad() wasn't called for class %s (runtime internal error).", 84 | is_call=False, 85 | ) 86 | 87 | self._cached_metaclass_type = tif.from_c_type("OSMetaClass*") 88 | 89 | def on_call(self, call: Call, modifications: Modifications): 90 | self._rename_parameter_by_index( 91 | modifications, 92 | call, 93 | name_index=1, 94 | rename_index=0, 95 | modifier=lambda name: f"__ZN{len(name)}{name}10gMetaclassE", 96 | ) 97 | self._retype_parameter_by_index(modifications, call, 0, self._cached_metaclass_type) 98 | 99 | 100 | class peParseBootArgn(FuncHandler): 101 | def __init__(self): 102 | super().__init__("PE_parse_boot_argn") 103 | 104 | def get_source_xref(self) -> SourceXref | None: 105 | existing = memory.ea_from_name("PE_parse_boot_argn") 106 | if existing is not None: 107 | return FuncXref(existing) 108 | 109 | def on_call(self, call: Call, modifications: Modifications): 110 | self._rename_parameter_by_index( 111 | modifications, call, 0, 1, modifier=lambda name: f"boot_{name.replace('-', '_')}" 112 | ) 113 | 114 | if isinstance(call.params[2], int): 115 | new_type = tif.from_size(call.params[2]) 116 | if new_type is not None: 117 | self._retype_parameter_by_index(modifications, call, 1, tif.pointer_of(new_type)) 118 | 119 | 120 | class StackCheckFail(FuncHandler): 121 | def __init__(self): 122 | # For some reason I cannot set the name of the function to the original name, as IDA hides call to the function 123 | # So we use a different name 124 | super().__init__("__xnu_stack_check_fail") 125 | 126 | def get_source_xref(self) -> SourceXref | None: 127 | existing = memory.ea_from_name(self.name) 128 | if existing is not None: 129 | return FuncXref(existing) 130 | searched = list(xrefs.string_xrefs_to("stack_protector.c")) 131 | if not searched: 132 | print("[Error] Could not find xrefs to 'stack_protector.c' for", self.name) 133 | return None 134 | ldr_addr = searched[0] 135 | 136 | func_start_ea = StackCheckFail.get_previous_pacibsp(ldr_addr) 137 | func_end_ea = StackCheckFail.get_after_next_bl(ldr_addr) 138 | if func_start_ea is None or func_end_ea is None: 139 | print("[Error] Could not find function boundaries:", self.name) 140 | return None 141 | 142 | if not functions.is_in_function(func_start_ea) and not functions.add_function(func_start_ea, func_end_ea): 143 | print(f"[Error] Could not add function {self.name} at {func_start_ea:#x}") 144 | return None 145 | 146 | if not tif.apply_tinfo_to_ea(tif.from_func_components("void", [tif.FuncParam("void")]), func_start_ea): 147 | print(f"[Error] Could not apply tinfo to function {self.name} at {func_start_ea:#x}") 148 | return None 149 | 150 | if not functions.apply_flag_to_function(func_start_ea, functions.FLAG_NO_RETURN): 151 | print(f"[Error] Could not apply no-return flag to function {self.name} at {func_start_ea:#x}") 152 | return None 153 | 154 | if not memory.set_name(func_start_ea, self.name, retry=True): 155 | print(f"[Error] Could not set name for function {self.name} at {func_start_ea:#x}") 156 | return None 157 | 158 | return FuncXref(func_start_ea) 159 | 160 | @staticmethod 161 | def get_previous_pacibsp(call_ea: int) -> int | None: 162 | """Given a call, search previous instructions to find a movk call""" 163 | insn = instructions.decode_instruction(call_ea) 164 | if not insn: 165 | return None 166 | 167 | for _ in range(10): 168 | insn = instructions.decode_previous_instruction(insn) 169 | # No more instructions in this execution flow 170 | if insn is None: 171 | break 172 | if insn.get_canon_mnem() == "PAC": 173 | return insn.ea 174 | return None 175 | 176 | @staticmethod 177 | def get_after_next_bl(call_ea: int) -> int | None: 178 | """Given a call, search previous instructions to find a movk call""" 179 | insn = instructions.decode_instruction(call_ea) 180 | if not insn: 181 | return None 182 | 183 | for _ in range(10): 184 | insn = instructions.decode_instruction(insn.ea + insn.size) 185 | # No more instructions in this execution flow 186 | if insn is None: 187 | break 188 | if insn.get_canon_mnem() == "BL": 189 | return insn.ea + insn.size 190 | return None 191 | 192 | def on_call(self, call: Call, modifications: Modifications): 193 | # Do nothing on call, we just want to rename the function 194 | pass 195 | 196 | 197 | if file_format.is_kernelcache(): 198 | GLOBAL_HANDLERS: list[FuncHandler] = [ 199 | OSSymbol_WithCStringNoCopy, 200 | OSSymbol_WithCString, 201 | IORegistry_MakePlane(), 202 | MetaClassConstructor(), 203 | ] 204 | LOCAL_HANDLERS: list[FuncHandler] = [ 205 | *IOService_SetProperty, 206 | IOService_GetProperty, 207 | IOService_CopyProperty, 208 | peParseBootArgn(), 209 | StackCheckFail(), 210 | ] 211 | else: 212 | GLOBAL_HANDLERS = [] 213 | LOCAL_HANDLERS = [] 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDA iOS Helper 2 | 3 | A plugin for IDA Pro 9.0+ to help with iOS code analysis. 4 | 5 | ## Supported features 6 | 7 | - KernelCache 8 | - Calls to `OSBaseClass::safeMetaCast` apply type info on the result. 9 | - Calls to `OSObject_typed_operator_new` apply type info on the result. 10 | - When the keyboard is on a virtual call (`cls->vcall()`), Shift+X will show a dialog with all the possible 11 | implementations of the virtual method. It requires vtable symbols to be present. 12 | - When in a C++ method named Class::func, Ctrl+T will change the first argument to `Class* this`. Also works for 13 | Obj-C instance methods. 14 | - Name globals from `OSSymbol::fromConst*` calls, locals from `get/setProperty` calls, ... 15 | - Rename and type all global kalloc_type_view. Use their signature to mark fields as pointers for the actual types. 16 | - Create a struct from a kalloc_type_view. 17 | - Objective-C 18 | - Hide memory management 19 | functions - `objc_retain`, `objc_release`, `objc_autorelease`, `objc_retainAutoreleasedReturnValue`. 20 | - Optimize `_objc_storeStrong` to an assignment. 21 | - collapse `__os_log_impl` calls. 22 | - Hide selectors and static classes from Objective-c calls. 23 | - When in Obj-C method, Ctrl+4 will show xrefs to the selector. 24 | - Swift 25 | - Add swift types declarations to the IDA type system. 26 | - Detect stack swift string and add a syntactic sugar for them. 27 | - Common 28 | - Remove `__break` calls. 29 | - collapse blocks initializers and detect `__block` variables (use Alt+Shift+S to trigger detection). 30 | - Use `Ctrl+S` to jump to function by a string constant found in the code 31 | - Transform ranged conditions to a more readable form. 32 | - Try to detect outline functions and mark them as such. 33 | - Use `Ctrl+Shift+X` to find xrefs to a field inside a segment. This will decompile the whole segment and then 34 | search for the field. 35 | 36 | ## Installation 37 | 38 | 1. Install this package using your IDA's python pip: `pip install ida-ios-helper` 39 | 2. copy `ida-plugin.json` and `ida_plugin_stub.py` to your IDA's plugins folder: `~/.idapro/plugins/ida-ios-helper`. 40 | 3. Restart IDA. 41 | 42 | ## Examples 43 | 44 | ### Solve condition constraints 45 | 46 | Before: 47 | 48 | ```c 49 | if ( valueLength - 21 <= 0xFFFFFFFFFFFFFFEFLL ) 50 | { 51 | ... 52 | } 53 | ``` 54 | 55 | After: 56 | 57 | ```c 58 | if ( 4 < valueLength || valueLength < 21 ) 59 | { 60 | ... 61 | } 62 | ``` 63 | 64 | ### Remove `__break` 65 | 66 | Before: 67 | 68 | ```c 69 | if ( ((v6 ^ (2 * v6)) & 0x4000000000000000LL) != 0 ) 70 | __break(0xC471u); 71 | ``` 72 | 73 | After: removed. 74 | 75 | ### Hide selectors of Obj-C calls 76 | 77 | Before: 78 | 79 | ```c 80 | -[NSFileManager removeItemAtPath:error:]( 81 | +[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager"), 82 | "removeItemAtPath:error:", 83 | +[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", *(_QWORD *)&buf[v5]), 84 | 0LL); 85 | ``` 86 | 87 | After: 88 | 89 | ```c 90 | -[NSFileManager removeItemAtPath:error:]( 91 | +[NSFileManager defaultManager](), 92 | +[NSString stringWithUTF8String:](*(_QWORD *)&buf[v5]), 93 | 0LL); 94 | ``` 95 | 96 | ### Block initializers 97 | 98 | Before: 99 | 100 | ```c 101 | v10 = 0LL; 102 | v15 = &v10; 103 | v16 = 0x2000000000LL; 104 | v17 = 0; 105 | if ( a1 ) 106 | { 107 | x0_8 = *(NSObject **)(a1 + 16); 108 | v13.isa = _NSConcreteStackBlock; 109 | *(_QWORD *)&v13.flags = 0x40000000LL; 110 | v13.invoke = func_name_block_invoke; 111 | v13.descriptor = &stru_100211F48; 112 | v13.lvar3 = a1; 113 | v13.lvar4 = a2; 114 | v13.lvar1 = a3; 115 | v13.lvar2 = &v10; 116 | dispatch_sync(queue: x0_8, block: &v13); 117 | v11 = *((_BYTE *)v15 + 24); 118 | } 119 | else 120 | { 121 | v11 = 0; 122 | } 123 | _Block_object_dispose(&v10, 8); 124 | return v11 & 1; 125 | ``` 126 | 127 | After: 128 | 129 | ```c 130 | v10 = _byref_block_arg_init(0); 131 | v10.value = 0; 132 | if ( a1 ) 133 | { 134 | v6 = *(NSObject **)(a1 + 16); 135 | v9 = _stack_block_init(0x40000000, &stru_100211F48, func_name_block_invoke); 136 | v9.lvar3 = a1; 137 | v9.lvar4 = a2; 138 | v9.lvar1 = a3; 139 | v9.lvar2 = &v10; 140 | dispatch_sync(queue: v6, block: &v9); 141 | value = v10.forwarding->value; 142 | } 143 | else 144 | { 145 | value = 0; 146 | } 147 | return value & 1; 148 | ``` 149 | 150 | ### Collapse `os_log` 151 | 152 | Before: 153 | 154 | ```c 155 | v9 = gLogObjects; 156 | v10 = gNumLogObjects; 157 | if ( gLogObjects && gNumLogObjects >= 46 ) 158 | { 159 | v11 = *(NSObject **)(gLogObjects + 360); 160 | } 161 | else 162 | { 163 | v11 = (NSObject *)&_os_log_default; 164 | if ( ((v6 ^ (2 * v6)) & 0x4000000000000000LL) != 0 ) 165 | __break(0xC471u); 166 | if ( os_log_type_enabled(oslog: (os_log_t)&_os_log_default, type: OS_LOG_TYPE_ERROR) ) 167 | { 168 | *(_DWORD *)buf = 134218240; 169 | *(_QWORD *)v54 = v9; 170 | *(_WORD *)&v54[8] = 1024; 171 | *(_DWORD *)&v54[10] = v10; 172 | if ( ((v6 ^ (2 * v6)) & 0x4000000000000000LL) != 0 ) 173 | __break(0xC471u); 174 | _os_log_error_impl( 175 | dso: (void *)&_mh_execute_header, 176 | log: (os_log_t)&_os_log_default, 177 | type: OS_LOG_TYPE_ERROR, 178 | format: "Make sure you have called init_logging()!\ngLogObjects: %p, gNumLogObjects: %d", 179 | buf: buf, 180 | size: 0x12u); 181 | } 182 | } 183 | if ( ((v6 ^ (2 * v6)) & 0x4000000000000000LL) != 0 ) 184 | __break(0xC471u); 185 | if ( os_log_type_enabled(oslog: v11, type: OS_LOG_TYPE_INFO) ) 186 | { 187 | if ( a1 ) 188 | v12 = *(_QWORD *)(a1 + 8); 189 | else 190 | v12 = 0LL; 191 | *(_DWORD *)buf = 138412290; 192 | *(_QWORD *)v54 = v12; 193 | if ( ((v6 ^ (2 * v6)) & 0x4000000000000000LL) != 0 ) 194 | __break(0xC471u); 195 | _os_log_impl( 196 | dso: (void *)&_mh_execute_header, 197 | log: v11, 198 | type: OS_LOG_TYPE_INFO, 199 | format: "Random log %@", 200 | buf: buf, 201 | size: 0xCu); 202 | } 203 | ``` 204 | 205 | after: 206 | 207 | ```c 208 | if ( oslog_info_enabled() ) 209 | { 210 | if ( a1 ) 211 | v4 = *(_QWORD *)(a1 + 8); 212 | else 213 | v4 = 0LL; 214 | oslog_info("Random log %@", v4); 215 | } 216 | ``` 217 | 218 | ## Automatic casts with `OSBaseClass::safeMetaCast` 219 | 220 | Before: 221 | 222 | ```c++ 223 | OSObject *v5; 224 | v5 = OSBaseClass::safeMetaCast(a2, &IOThunderboltController::metaClass); 225 | ``` 226 | 227 | After: 228 | 229 | ```c++ 230 | IOThunderboltController *v5; 231 | v5 = OSDynamicCast(a2); 232 | ``` 233 | 234 | ## Automatic typing for `OSObject_typed_operator_new` 235 | 236 | Run `Edit->Plugins->iOSHelper->Locate all kalloc_type_view` before. 237 | 238 | Before: 239 | 240 | ```c++ 241 | IOAccessoryPowerSourceItemUSB_TypeC_Current *sub_FFFFFFF009B2AA14() 242 | { 243 | OSObject *v0; // x19 244 | 245 | v0 = (OSObject *)OSObject_typed_operator_new(&UNK_FFFFFFF007DBC480, size: 0x38uLL); 246 | OSObject::OSObject(this: v0, &IOAccessoryPowerSourceItemUSB_TypeC_Current::gMetaclass)->__vftable = (OSObject_vtbl *)off_FFFFFFF007D941B0; 247 | OSMetaClass::instanceConstructed(this: &IOAccessoryPowerSourceItemUSB_TypeC_Current::gMetaclass); 248 | return (IOAccessoryPowerSourceItemUSB_TypeC_Current *)v0; 249 | } 250 | ``` 251 | 252 | After: 253 | 254 | ```c++ 255 | IOAccessoryPowerSourceItemUSB_TypeC_Current *sub_FFFFFFF009B2AA14() 256 | { 257 | IOAccessoryPowerSourceItemUSB_TypeC_Current *v0; // x19 258 | 259 | v0 = OSObjectTypeAlloc(0x38uLL); 260 | OSObject::OSObject(this: v0, &IOAccessoryPowerSourceItemUSB_TypeC_Current::gMetaclass)->__vftable = (OSObject_vtbl *)off_FFFFFFF007D941B0; 261 | OSMetaClass::instanceConstructed(this: &IOAccessoryPowerSourceItemUSB_TypeC_Current::gMetaclass); 262 | return v0; 263 | } 264 | ``` 265 | 266 | ## Jump to virtual call 267 | 268 | Use `Shift+X` on a virtual call to jump. 269 | 270 | ![Jump to virtual call](res/jump_to_virtual_call.png) 271 | 272 | ## Xrefs to selector 273 | 274 | Use `Ctrl+4` inside an Objective-C method to list xrefs to its selector. 275 | 276 | ![Jump to selector](res/jump_to_selector_xrefs.png) 277 | 278 | ## Rename function by argument of logging function 279 | 280 | Given that the code contains calls like: 281 | 282 | ```c 283 | log("func_name", ....); 284 | ``` 285 | 286 | You could use `rename_function_by_arg` to mass rename all functions that contain such calls. 287 | 288 | ```python 289 | rename_function_by_arg(func_name="log", arg_index=0, prefix="_", force_name_change=False) 290 | ``` 291 | 292 | This will run on all the functions that call the log function, and rename them to the first argument of the call. 293 | 294 | ## Call the plugin from python 295 | 296 | ```python 297 | import idaapi 298 | 299 | # Call global analysis 300 | idaapi.load_and_run_plugin("iOS Helper", 1) 301 | 302 | 303 | # Call local analysis 304 | def write_ea_arg(ea: int): 305 | n = idaapi.netnode() 306 | n.create("$ idaioshelper") 307 | n.altset(1, ea, "R") 308 | 309 | 310 | write_ea_arg(func_ea) 311 | idaapi.load_and_run_plugin("iOS Helper", 2) 312 | ``` 313 | 314 | ## Development 315 | 316 | In order to have autocomplete while developing, you need to add IDA's include folder ( `$IDA_INSTALLATION/python/3` ) to 317 | your IDE. 318 | 319 | - on Visual Studio code you can add the folder to the analyzer's extra paths in the `settings.json` file: 320 | 321 | ```json 322 | { 323 | "python.analysis.extraPaths": [ 324 | "$IDA_INSTALLATION\\python\\3" 325 | ] 326 | } 327 | ``` 328 | 329 | - on PyCharm you can add the folder to the interpreter's paths in the project settings. 330 | Alternatively, you can create `idapython.pth` in `$VENV_FOLDER/Lib/site-packages` and add the path to it. 331 | 332 | Inside IDA, you can use `ioshelper.reload()` to reload the plugin during development. 333 | If you create file name `DEBUG` inside `src/`, then you can use `F2` to reload the 334 | plugin. -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/oslog/log_macro_optimizer.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from enum import Enum 3 | 4 | import ida_hexrays 5 | from ida_hexrays import mblock_t, mcallinfo_t, minsn_t, mop_t, optblock_t 6 | from ida_typeinf import tinfo_t 7 | from idahelper import tif 8 | from idahelper.microcode import mblock, mcallarg, minsn, mop 9 | from idahelper.microcode.optimizers import optblock_counter_t, optblock_counter_wrapper_t 10 | 11 | from .os_log import LogCallInfo, LogCallParams, get_call_info_for_name, log_type_to_str 12 | 13 | 14 | def log_func_to_tif() -> tinfo_t | None: 15 | """Create tif for a log function: void f(char *fmt, ...)""" 16 | return tif.from_func_components("void", [tif.FuncParam("char*", "fmt"), tif.FuncParam("...")]) 17 | 18 | 19 | class ScanLogState(Enum): 20 | HEADER = 0 21 | ITEM_HEADER = 1 22 | ITEM_VALUE = 2 23 | 24 | 25 | @dataclasses.dataclass 26 | class CollectLogParamsResult: 27 | instructions: list[minsn_t] 28 | call_params: list[mop_t] 29 | 30 | 31 | # noinspection PyMethodMayBeStatic 32 | class log_macro_optimizer_t(optblock_counter_t): 33 | def func(self, blk: mblock_t) -> int: 34 | if blk.mba.maturity < ida_hexrays.MMAT_CALLS: 35 | return 0 36 | 37 | self.optimize_log_macro(blk) 38 | 39 | if self.cnt: 40 | blk.mark_lists_dirty() 41 | return self.cnt 42 | 43 | def optimize_log_macro(self, blk: mblock_t) -> None: 44 | # Find log call and extract params 45 | if (res := self.find_log_call(blk)) is None: 46 | return 47 | call_insn, params = res 48 | 49 | # Collect parameters to log 50 | log_params_res = self.collect_log_params(blk, params) 51 | if log_params_res is None: 52 | return 53 | 54 | # All instructions up to the call are part of the logging macro starting from `from_index`, 55 | # so they can be safely removed. 56 | # First, convert the call insn to the helper call 57 | prefix = "ossignpost_" if params.is_signpost else "os_log_" 58 | call_insn.l.make_helper(f"{prefix}{log_type_to_str(params.log_type, params.is_signpost)}") 59 | self.count() 60 | 61 | # Then modify callinfo 62 | # Remove all arguments. 63 | fi: mcallinfo_t = call_insn.d.f 64 | fi.args.clear() 65 | # Not necessary but IDA will crash on inconsistency, and we prefer to keep it alive if there is a bug. 66 | fi.solid_args = 0 67 | self.count() 68 | 69 | # Add optional name string argument 70 | if params.name_str_ea is not None: 71 | new_arg = mcallarg.from_mop(mop.from_global_ref(params.name_str_ea), tif.from_c_type("char*")) 72 | fi.args.push_back(new_arg) 73 | fi.solid_args += 1 74 | self.count() 75 | 76 | # Add format string argument 77 | new_arg = mcallarg.from_mop(mop.from_global_ref(params.format_str_ea), tif.from_c_type("char*")) 78 | fi.args.push_back(new_arg) 79 | fi.solid_args += 1 80 | self.count() 81 | 82 | # Add params 83 | for param in log_params_res.call_params: 84 | fi.args.push_back(mcallarg.from_mop(param, tif.from_size(param.size))) 85 | fi.solid_args += 1 86 | self.count() 87 | 88 | # Apply final type signature. For some reason `set_type` crashes IDA, but swap works great... 89 | fi.get_type().swap(log_func_to_tif()) # TODO: sometimes IDA inserts incorrect casts. Fix it. 90 | self.count() 91 | 92 | # Finally, convert other instructions (that are part of the log macro) to nop 93 | for insn in log_params_res.instructions: 94 | blk.make_nop(insn) 95 | self.count() 96 | 97 | def collect_log_params(self, blk: mblock_t, params: LogCallParams) -> CollectLogParamsResult | None: # noqa: C901 98 | """ 99 | Collect the parameters of a log macro starting from `params.call_ea` and going backwards. 100 | Returns the index of the first instruction that is a part of the macro and the list of parameters. 101 | """ 102 | base = params.stack_base_offset 103 | end = base + params.size 104 | call_params: list[mop_t] = [] 105 | buffer_instructions: list[minsn_t] = [] 106 | buffer_size = 0 107 | size_left_for_header: int | None = None 108 | state = ScanLogState.HEADER 109 | 110 | for insn in mblock.instructions(blk): 111 | # Stop at the call 112 | if insn.ea == params.call_ea: 113 | break 114 | 115 | if not self.check_insn_part_of_log_macro(insn, params.call_ea, base, end): 116 | continue 117 | 118 | buffer_instructions.append(insn) 119 | 120 | # Advance state 121 | if state == ScanLogState.HEADER: 122 | if insn.l.size == 2: 123 | # Only the header 124 | state = ScanLogState.ITEM_HEADER 125 | elif insn.l.size == 4: 126 | # Header + item header 127 | state = ScanLogState.ITEM_VALUE 128 | else: 129 | print(f"[Error] invalid log macro header size of {hex(params.call_ea)}: {insn.dstr()}") 130 | return None 131 | elif state == ScanLogState.ITEM_HEADER: 132 | if insn.d.size != 2: 133 | if insn.d.size > 2: 134 | print( 135 | f"[Error] Unsupported log macro because header size is bigger than 2 bytes: {insn.dstr()}" 136 | ) 137 | return None 138 | elif size_left_for_header is None: 139 | size_left_for_header = 2 - insn.d.size 140 | else: 141 | size_left_for_header -= insn.d.size 142 | if size_left_for_header == 0: 143 | state = ScanLogState.ITEM_VALUE 144 | size_left_for_header = None 145 | else: 146 | state = ScanLogState.ITEM_VALUE 147 | else: 148 | call_params.append(insn.l) 149 | state = ScanLogState.ITEM_HEADER 150 | 151 | buffer_size += insn.d.size 152 | 153 | if state == ScanLogState.HEADER: 154 | # Never found the beginning of the log macro 155 | return None 156 | elif state == ScanLogState.ITEM_VALUE: 157 | print(f"[Error] failed to parse log macro of {hex(params.call_ea)}") 158 | return None 159 | 160 | if buffer_size != params.size: 161 | print( 162 | f"[Error] log macro size mismatch of {hex(params.call_ea)}: " 163 | f"expected - {params.size}, found - {buffer_size}" 164 | ) 165 | return None 166 | 167 | return CollectLogParamsResult(buffer_instructions, call_params) 168 | 169 | def find_log_call(self, blk: mblock_t) -> tuple[minsn_t, LogCallParams] | None: 170 | """Find an `os_log` call in the given block and extract the parameters from it""" 171 | for insn in mblock.instructions(blk): 172 | if insn.opcode != ida_hexrays.m_call: 173 | continue 174 | 175 | # Search for log call 176 | call_name = minsn.get_func_name_of_call(insn) 177 | if (call_info := get_call_info_for_name(call_name)) is None: 178 | continue 179 | 180 | params = self.extract_params_from_log_call(insn, call_info) 181 | if params is None: 182 | continue 183 | 184 | return insn, params 185 | return None 186 | 187 | def check_insn_part_of_log_macro( 188 | self, insn: minsn_t, call_ea: int, base: int, end: int, print_error: bool = False 189 | ) -> bool: 190 | """Check that the given `insn` is indeed part of a log macro""" 191 | # It is an Assignment 192 | if insn.opcode not in [ 193 | ida_hexrays.m_mov, 194 | ida_hexrays.m_and, 195 | ida_hexrays.m_xds, 196 | ida_hexrays.m_xdu, 197 | ida_hexrays.m_low, 198 | ]: 199 | if print_error: 200 | print(f"[Error] unsupported instruction in log block of {hex(call_ea)}: {insn.dstr()}") 201 | return False 202 | 203 | # To stack 204 | if insn.d.t != ida_hexrays.mop_S: 205 | if print_error: 206 | print(f"[Error] unsupported dest in log block of {hex(call_ea)}: {insn.dstr()}") 207 | return False 208 | 209 | # in range 210 | addr, size = insn.d.s.off, insn.d.size 211 | if not (base <= addr and addr + size <= end): 212 | if print_error: 213 | print(f"[Error] assignment not in range in log block of {hex(call_ea)}: {insn.dstr()}") 214 | return False 215 | 216 | return True 217 | 218 | def extract_params_from_log_call(self, insn: minsn_t, log_call_info: LogCallInfo) -> LogCallParams | None: 219 | """Given the log call instruction and information about the indices to extract, extract them""" 220 | call_info: mcallinfo_t = insn.d.f 221 | if call_info.args.empty(): 222 | # Not calls args, probably too early in IDA's optimization 223 | return None 224 | 225 | # Get operands 226 | size_param = call_info.args[log_call_info.size_index] 227 | buf_param = call_info.args[log_call_info.buf_index] 228 | format_param = call_info.args[log_call_info.format_index] 229 | type_param = call_info.args[log_call_info.type_index] 230 | name_param = call_info.args[log_call_info.name_index] if log_call_info.name_index is not None else None 231 | # Verify types of operands 232 | if ( 233 | size_param.t != ida_hexrays.mop_n 234 | or type_param.t != ida_hexrays.mop_n 235 | or not format_param.is_glbaddr() 236 | or buf_param.t != ida_hexrays.mop_a 237 | or (name_param is not None and not name_param.is_glbaddr()) 238 | ): 239 | return None 240 | 241 | size = size_param.unsigned_value() 242 | log_type = type_param.unsigned_value() 243 | format_str_ea = format_param.a.g 244 | name_str_ea = name_param.a.g if name_param is not None else None 245 | # Check if this is a stack variable 246 | if buf_param.a.t != ida_hexrays.mop_S: 247 | return None 248 | stack_base_offset = buf_param.a.s.off 249 | return LogCallParams( 250 | log_type, size, stack_base_offset, format_str_ea, insn.ea, name_str_ea, log_call_info.is_signpost 251 | ) 252 | 253 | 254 | def optimizer() -> optblock_t: 255 | return optblock_counter_wrapper_t(log_macro_optimizer_t) 256 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/objc/objc_sugar/objc_sugar.py: -------------------------------------------------------------------------------- 1 | __all__ = ["objc_selector_hexrays_hooks_t"] 2 | 3 | import itertools 4 | import re 5 | 6 | import ida_hexrays 7 | from ida_hexrays import Hexrays_Hooks, carg_t, carglist_t, cexpr_t, cfunc_t, citem_t 8 | from ida_kernwin import simpleline_t 9 | from ida_lines import ( 10 | COLOR_OFF, 11 | COLOR_ON, 12 | COLSTR, 13 | SCOLOR_ADDR, 14 | SCOLOR_DEMNAME, 15 | SCOLOR_IMPNAME, 16 | SCOLOR_LOCNAME, 17 | SCOLOR_SYMBOL, 18 | tag_remove, 19 | ) 20 | from ida_pro import strvec_t 21 | from idahelper import memory, objc 22 | from idahelper.ast import cexpr 23 | 24 | SELECTOR_MARKER = "!@#$sel$#@!" 25 | COMMA_COLORED = COLSTR(",", SCOLOR_SYMBOL) 26 | INSIGNIFICANT_LENGTH_FOR_LINE = 5 27 | MAX_LINE_SIZE = 120 28 | 29 | 30 | # COLOR_ON = 0x1, SCOLOR_ADDR = 0x28, SCOLOR_LOCNAME = 0x19, SCOLOR_SYMBOL = 0x9 31 | 32 | SEL_TOKEN_REGEX = re.compile( 33 | "(?P" 34 | + re.escape(COMMA_COLORED + " ") 35 | + r")?" 36 | + re.escape(COLOR_ON + SCOLOR_ADDR) 37 | + r"(?P[0-9A-Fa-f]{16})" 38 | + re.escape(COLOR_ON + SCOLOR_ADDR) 39 | + r"(?P=index)" 40 | + re.escape(COLSTR('"', SCOLOR_SYMBOL)) 41 | + re.escape(COLOR_ON + SCOLOR_LOCNAME) 42 | + r"(?P[A-Za-z0-9_:]+)" 43 | + '"' 44 | + re.escape(COLOR_OFF + SCOLOR_LOCNAME) 45 | ) 46 | 47 | """ 48 | a regex for a possible selector in IDA's pseudocode, with support for the prefix ", " 49 | Its groups are: prefix, index, and selector. 50 | """ 51 | 52 | SEL_TOKEN_REGEX_2 = re.compile( 53 | "(?P" 54 | + re.escape(COMMA_COLORED + " ") 55 | + r")?" 56 | + re.escape(COLOR_ON + SCOLOR_ADDR) 57 | + r"(?P[0-9A-Fa-f]{16})" 58 | + re.escape(COLOR_ON + SCOLOR_LOCNAME) 59 | + r'"(?P[A-Za-z0-9_:]+)"' 60 | + re.escape(COLOR_OFF + SCOLOR_LOCNAME) 61 | ) 62 | """ 63 | another regex for a possible selector in IDA's pseudocode that I found in IDA 9.2, with support for the prefix ", " 64 | Its groups are: prefix, index, and selector. 65 | """ 66 | 67 | SEL_TOKEN_REGEXES = [SEL_TOKEN_REGEX, SEL_TOKEN_REGEX_2] 68 | 69 | # noinspection RegExpDuplicateCharacterInClass 70 | CLASS_TOKEN_REGEX = re.compile( 71 | re.escape(COLOR_ON + SCOLOR_ADDR) 72 | + r"(?P[0-9A-Fa-f]{16})" 73 | + re.escape(COLSTR("&", SCOLOR_SYMBOL)) 74 | + re.escape(COLOR_ON + SCOLOR_ADDR) 75 | + r"(?P[0-9A-Fa-f]{16})" 76 | + re.escape(COLOR_ON) 77 | + ("[" + re.escape(SCOLOR_IMPNAME) + "|" + re.escape(SCOLOR_DEMNAME) + "]") 78 | + "OBJC_CLASS___" 79 | + "(?P[A-Za-z0-9_]+)" 80 | + re.escape(COLOR_OFF) 81 | + ("[" + re.escape(SCOLOR_IMPNAME) + "|" + re.escape(SCOLOR_DEMNAME) + "]") 82 | + "(?P" 83 | + re.escape(COMMA_COLORED) 84 | + r" ?)?" 85 | ) 86 | """ 87 | a regex for possible obj-c class in IDA's pseudocode, with support for the postfix "," 88 | Its groups are: index, index2, class and postfix. 89 | """ 90 | 91 | 92 | class objc_selector_hexrays_hooks_t(Hexrays_Hooks): 93 | def func_printed(self, cfunc: cfunc_t) -> int: # noqa: C901 94 | selectors_to_remove: dict[int, str] = {} # obj_id -> selector 95 | classes_to_remove: set[int] = set() # obj_id 96 | index_to_sel: dict[int, str] = {} # index, selector 97 | class_indices_to_remove: set[int] = set() # index 98 | 99 | for i, call_item in enumerate(cfunc.treeitems): 100 | call_item: citem_t 101 | # Get the index of the selector/class AST element 102 | if call_item.obj_id in selectors_to_remove: 103 | index_to_sel[i] = selectors_to_remove.pop(call_item.obj_id) 104 | elif call_item.obj_id in classes_to_remove: 105 | class_indices_to_remove.add(i) 106 | classes_to_remove.remove(call_item.obj_id) 107 | 108 | elif call_item.op == ida_hexrays.cot_call: 109 | call_expr: cexpr_t = call_item.cexpr 110 | 111 | # 1. Check if the function name looks like an Obj-C method 112 | call_func_name = cexpr.get_call_name(call_expr) 113 | if call_func_name is None or not objc.is_objc_method(call_func_name): 114 | continue 115 | 116 | # 2. Collect selector from arglist 117 | arglist: carglist_t = call_expr.a 118 | if len(arglist) < 2: 119 | print("[Error]: Obj-C method call with less than 2 arguments:", call_expr.dstr()) 120 | continue 121 | sel_arg: carg_t = arglist[1] 122 | if sel_arg.op == ida_hexrays.cot_str: 123 | selectors_to_remove[sel_arg.obj_id] = sel_arg.string 124 | elif sel_arg.op == ida_hexrays.cot_obj and (sel := memory.str_from_ea(sel_arg.obj_ea)) is not None: 125 | selectors_to_remove[sel_arg.obj_id] = sel 126 | else: 127 | print("[Error]: Obj-C method call with non-string selector:", call_expr.dstr()) 128 | continue 129 | 130 | # 3. Check if the function is a class method 131 | if objc.is_objc_static_method(call_func_name): 132 | # 4. Check if the class name is a ref to obj 133 | class_arg: carg_t = arglist[0] 134 | if class_arg.op != ida_hexrays.cot_ref or class_arg.x.op != ida_hexrays.cot_obj: 135 | print("[Error]: Obj-C class method with unsupported class", call_expr.dstr()) 136 | continue 137 | # 5. Collect the class name 138 | classes_to_remove.add(class_arg.obj_id) 139 | 140 | if selectors_to_remove or classes_to_remove: 141 | print("[Error]: unmatched Obj-C selectors in the function: ", hex(cfunc.entry_ea)) 142 | elif index_to_sel: 143 | modify_text(cfunc, index_to_sel, class_indices_to_remove) 144 | return 0 145 | 146 | 147 | def modify_text(cfunc: cfunc_t, index_to_sel: dict[int, str], class_indices_to_remove: set[int]): 148 | # Early return if no tokens to replace 149 | if not index_to_sel: 150 | return 151 | 152 | ps: strvec_t = cfunc.get_pseudocode() 153 | lines_marked_for_removal: list[simpleline_t] = [] 154 | for i, line in enumerate(ps): 155 | line: simpleline_t 156 | prev_line = ps[i - 1].line if i != 0 else "" 157 | should_merge = modify_selectors(index_to_sel, line, prev_line) 158 | should_merge |= modify_class(class_indices_to_remove, line, prev_line) 159 | 160 | if should_merge and i != 0: 161 | lines_marked_for_removal.append(line) 162 | merge(line.line, ps[i - 1]) 163 | 164 | # Remove lines that are marked for removal 165 | for line_to_remove in reversed(lines_marked_for_removal): 166 | ps.erase(line_to_remove) 167 | 168 | 169 | def modify_selectors(index_to_sel: dict[int, str], line: simpleline_t, prev_line: str): 170 | """Try to remove selectors from a line. Returns whether we should merge the line with the previous line""" 171 | should_merge = False 172 | # Find all obj-c calls in the line 173 | results_unsorted = itertools.chain(*[re.finditer(pattern, line.line) for pattern in SEL_TOKEN_REGEXES]) 174 | # Reverse the results so indices will not change 175 | results = sorted(results_unsorted, key=lambda m: m.start(), reverse=True) 176 | for result in results: 177 | result: re.Match 178 | index = int(result.group("index"), 16) 179 | if index in index_to_sel: 180 | # We found a selector token, remove it from the list 181 | sel = index_to_sel.pop(index) 182 | if sel != result.group("selector"): 183 | print("[Error]: selector mismatch. Expected:", sel, "Actual:", result.group("selector")) 184 | continue 185 | 186 | # Remove the selector, check if we need to merge lines 187 | left, right = result.span() 188 | before_selector, after_selector = line.line[:left], line.line[right:] 189 | should_merge = should_merge or should_merge_line(before_selector, after_selector, prev_line) 190 | line.line = before_selector + after_selector 191 | return should_merge 192 | 193 | 194 | def modify_class(class_indices_to_remove: set[int], line: simpleline_t, prev_line: str): 195 | """Try to remove class from a line. Returns whether we should merge the line with the previous line""" 196 | should_merge = False 197 | 198 | # Reverse the results so indices will not change 199 | for result in reversed(list(re.finditer(CLASS_TOKEN_REGEX, line.line))): 200 | result: re.Match 201 | index = int(result.group("index"), 16) 202 | index2 = int(result.group("index2"), 16) 203 | if index in class_indices_to_remove: 204 | # We found a class token, remove it from the list 205 | class_indices_to_remove.remove(index) 206 | 207 | if index2 != index + 1: 208 | print("[Error]: class indices mismatch for second object. Expected:", index + 1, "Actual:", index2) 209 | continue 210 | 211 | # Remove the class, check if we need to merge lines 212 | left, right = result.span() 213 | before_class, after_class = line.line[:left], line.line[right:] 214 | should_merge = should_merge or should_merge_line(before_class, after_class, prev_line) 215 | line.line = before_class + after_class 216 | return should_merge 217 | 218 | 219 | def should_merge_line(before_selector: str, after_selector: str, prev_line: str) -> bool: 220 | """ 221 | Given a `line` with a selector marker and the previous line, should we merge the two lines. 222 | """ 223 | before_without_tags = tag_remove(before_selector) 224 | 225 | # We only remove lines that starts with the selector 226 | if before_without_tags and not before_without_tags.isspace(): 227 | return False 228 | 229 | after_without_tags = tag_remove(after_selector) 230 | # If the line is short, allow merging 231 | if len(after_without_tags) < INSIGNIFICANT_LENGTH_FOR_LINE: 232 | return True 233 | 234 | # Merge if it will not lead to a long line 235 | prev_line_without_tags = tag_remove(prev_line) 236 | return len(after_without_tags) + len(prev_line_without_tags) < MAX_LINE_SIZE 237 | 238 | 239 | def merge(text: str, line: simpleline_t) -> None: 240 | """Merge `text` to the end of `line`. If `line` ends with a comma, the comma will be removed.""" 241 | line_without_tags = tag_remove(line.line) 242 | if line_without_tags.endswith(","): 243 | last_comma_index = line.line.rfind(COMMA_COLORED) 244 | line.line = line.line[:last_comma_index] + line.line[last_comma_index + len(COMMA_COLORED) :] 245 | line.line += text.strip() 246 | 247 | 248 | def to_hex(n: int, *, length: int) -> str: 249 | """Convert an integer to a hex string with leading zeros""" 250 | return f"{n:0{length}X}" 251 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/kalloc_type/kalloc_type.py: -------------------------------------------------------------------------------- 1 | __all__ = ["apply_kalloc_types", "create_struct_from_kalloc_type"] 2 | 3 | import ida_kernwin 4 | from ida_typeinf import tinfo_t 5 | from idahelper import memory, segments, tif, widgets 6 | 7 | KALLOC_TYPE_DEFINITIONS = """ 8 | struct zone_view { 9 | void* zv_zone; 10 | void* zv_stats; 11 | const char *zv_name; 12 | void* zv_next; 13 | }; 14 | 15 | enum kalloc_type_flags_t : uint32_t { 16 | KT_DEFAULT = 0x0001, 17 | KT_PRIV_ACCT = 0x0002, 18 | KT_SHARED_ACCT = 0x0004, 19 | KT_DATA_ONLY = 0x0008, 20 | KT_VM = 0x0010, 21 | KT_CHANGED = 0x0020, 22 | KT_CHANGED2 = 0x0040, 23 | KT_PTR_ARRAY = 0x0080, 24 | KT_NOSHARED = 0x2000, 25 | KT_SLID = 0x4000, 26 | KT_PROCESSED = 0x8000, 27 | KT_HASH = 0xffff0000, 28 | }; 29 | 30 | struct kalloc_type_view { 31 | struct zone_view kt_zv; 32 | const char *kt_signature; 33 | kalloc_type_flags_t kt_flags; 34 | uint32_t kt_size; 35 | void *unused1; 36 | void *unused2; 37 | }; 38 | 39 | struct kalloc_type_var_view { 40 | uint16_t kt_version; 41 | uint16_t kt_size_hdr; 42 | uint32_t kt_size_type; 43 | void *kt_stats; 44 | const char *kt_name; 45 | void *kt_next; 46 | uint16_t kt_heap_start; 47 | uint8_t kt_zones[22]; 48 | const char *kt_sig_hdr; 49 | const char *kt_sig_type; 50 | kalloc_type_flags_t kt_flags; 51 | } 52 | """ 53 | KALLOC_TYPE_VIEW_OFFSET_NAME = 16 # void *zv_zone + void *zv_stats 54 | KALLOC_TYPE_VIEW_OFFSET_SIGNATURE = 32 # zone_view 55 | 56 | KALLOC_TYPE_VAR_VIEW_OFFSET_NAME = ( 57 | 16 # uint16_t kt_version + uint16_t kt_size_hdr + uint32_t kt_size_type + void* kt_stats 58 | ) 59 | 60 | VOID_PTR_TYPE: tinfo_t = tif.from_c_type("void*") # type: ignore # noqa: PGH003 61 | 62 | 63 | def apply_kalloc_types(): 64 | kalloc_type_view_tif = tif.from_struct_name("kalloc_type_view") 65 | kalloc_type_var_view_tif = tif.from_struct_name("kalloc_type_var_view") 66 | if kalloc_type_view_tif is None or kalloc_type_var_view_tif is None: 67 | if not tif.create_from_c_decl(KALLOC_TYPE_DEFINITIONS): 68 | print("[Error] failed to created kalloc_type_view type") 69 | 70 | kalloc_type_view_tif = tif.from_struct_name("kalloc_type_view") 71 | kalloc_type_var_view_tif = tif.from_struct_name("kalloc_type_var_view") 72 | if kalloc_type_view_tif is None or kalloc_type_var_view_tif is None: 73 | print("[Error] could not find kalloc type view") 74 | return 75 | 76 | classes_handled: set[str] = set() 77 | for segment in segments.get_segments(): 78 | if segment.name.endswith("__kalloc_type"): 79 | set_kalloc_type_for_segment(segment, kalloc_type_view_tif, classes_handled) 80 | if segment.name.endswith("__kalloc_var"): 81 | set_kalloc_var_for_segment(segment, kalloc_type_var_view_tif) 82 | 83 | 84 | def set_kalloc_type_for_segment(segment: segments.Segment, kalloc_type_view_tif: tinfo_t, classes_handled: set[str]): 85 | kalloc_type_view_size = kalloc_type_view_tif.get_size() 86 | 87 | if segment.size % kalloc_type_view_size != 0: 88 | print( 89 | f"[Warning] {segment.name} at {segment.start_ea:X} is not a multiple of kalloc_type_view size. is: {segment.size}, not multiple of: {kalloc_type_view_size}" 90 | ) 91 | return 92 | 93 | for kty_ea in range(segment.start_ea, segment.end_ea, kalloc_type_view_size): 94 | if not tif.apply_tinfo_to_ea(kalloc_type_view_tif, kty_ea): 95 | print(f"[Error] failed to apply kalloc_type_view on {kty_ea:X}") 96 | 97 | site_name_ea = memory.qword_from_ea(kty_ea + KALLOC_TYPE_VIEW_OFFSET_NAME) 98 | site_name = memory.str_from_ea(site_name_ea) 99 | if site_name is None: 100 | print(f"[Error] failed to read name for kalloc_type_view on {kty_ea:X}") 101 | continue 102 | 103 | if not site_name.startswith("site."): 104 | print(f"[Error] invalid site name on {kty_ea:X}, is: {site_name!r}") 105 | continue 106 | 107 | class_name = site_name[5:] 108 | if class_name.startswith("struct "): 109 | class_name = class_name[7:] 110 | if class_name.startswith("typeof(") or class_name == "T": 111 | # Clang generates them using macro, so it might lead to some eccentric specific ones... 112 | continue 113 | 114 | new_name = f"{escape_name(class_name)}_kty" 115 | if not memory.set_name(kty_ea, new_name, retry=True, retry_count=50): 116 | print(f"[Error] failed to rename kalloc_type_view on {kty_ea:X} to {new_name!r}") 117 | continue 118 | 119 | signature_ea = memory.qword_from_ea(kty_ea + KALLOC_TYPE_VIEW_OFFSET_SIGNATURE) 120 | signature = memory.str_from_ea(signature_ea) 121 | if signature is None: 122 | print(f"[Error] failed to read signature for {new_name} on {kty_ea:X}") 123 | continue 124 | 125 | try_enrich_type(class_name, signature, classes_handled) 126 | 127 | 128 | def try_enrich_type(class_name: str, signature: str, classes_handled: set[str]): 129 | # Don't try to enrich the same type multiple times 130 | if class_name in classes_handled: 131 | return 132 | classes_handled.add(class_name) 133 | 134 | class_tif = tif.from_struct_name(class_name) 135 | if class_tif is None: 136 | return 137 | 138 | class_base_offset = tif.get_base_offset_for_class(class_tif) 139 | if class_base_offset is None: 140 | return 141 | 142 | # Align it to multiplication of 8 143 | class_base_offset = int((class_base_offset + 7) / 8) * 8 144 | 145 | # The signature is on 8 bytes each time 146 | for i in range(class_base_offset // 8, len(signature)): 147 | c = signature[i] 148 | if c != "1": 149 | continue 150 | 151 | member = tif.get_member(class_tif, i * 8) 152 | if member is None: 153 | print(f"[Error] {class_name} has no member for at offset {i * 8:#X}") 154 | return 155 | 156 | # I check for if this is a pointer, because if it was union of data + pointer, the compiler should have yelled 157 | # So if you changed the type to non pointer, you were mistaken... 158 | if not member.type.is_ptr() and not tif.set_udm_type(class_tif, member, VOID_PTR_TYPE): 159 | print(f"[Error] failed to set type for {class_name} member at offset {member.offset}") 160 | 161 | 162 | def set_kalloc_var_for_segment(segment: segments.Segment, kalloc_type_var_view_tif: tinfo_t): 163 | kalloc_type_view_size = kalloc_type_var_view_tif.get_size() 164 | 165 | if segment.size % kalloc_type_view_size != 0: 166 | print( 167 | f"[Warning] {segment.name} at {segment.start_ea:X} is not a multiple of kalloc_type_view size. is: {segment.size}, not multiple of: {kalloc_type_view_size}" 168 | ) 169 | return 170 | 171 | for kty_ea in range(segment.start_ea, segment.end_ea, kalloc_type_view_size): 172 | if not tif.apply_tinfo_to_ea(kalloc_type_var_view_tif, kty_ea): 173 | print(f"[Error] failed to apply kalloc_type_view on {kty_ea:X}") 174 | 175 | site_name_ea = memory.qword_from_ea(kty_ea + KALLOC_TYPE_VAR_VIEW_OFFSET_NAME) 176 | site_name = memory.str_from_ea(site_name_ea) 177 | if site_name is None: 178 | print(f"[Error] failed to read name for kalloc_type_view on {kty_ea:X}") 179 | continue 180 | 181 | if not site_name.startswith("site."): 182 | print(f"[Error] invalid site name on {kty_ea:X}, is: {site_name!r}") 183 | continue 184 | 185 | class_name = site_name[5:] 186 | if class_name.startswith("struct "): 187 | class_name = class_name[7:] 188 | if class_name.startswith("typeof(") or class_name == "T": 189 | # Clang generates them using macro, so it might lead to some eccentric specific ones... 190 | continue 191 | 192 | new_name = f"{escape_name(class_name)}_kty" 193 | if not memory.set_name(kty_ea, new_name, retry=True, retry_count=50): 194 | print(f"[Error] failed to rename kalloc_type_view on {kty_ea:X} to {new_name!r}") 195 | continue 196 | 197 | 198 | def create_struct_from_kalloc_type(ctx: ida_kernwin.action_ctx_base_t): 199 | kty_ea: int = ctx.cur_ea 200 | cur_type = tif.from_ea(kty_ea) 201 | if cur_type is None or cur_type.get_type_name() != "kalloc_type_view": 202 | print(f"[Error] You must be on a kalloc_type_view to create the struct. Current addr: {kty_ea:X}") 203 | return 204 | 205 | signature_ea = memory.qword_from_ea(kty_ea + KALLOC_TYPE_VIEW_OFFSET_SIGNATURE) 206 | signature = memory.str_from_ea(signature_ea) 207 | if signature is None: 208 | print(f"[Error] failed to read signature for {kty_ea:X}") 209 | return 210 | 211 | site_name_ea = memory.qword_from_ea(kty_ea + KALLOC_TYPE_VIEW_OFFSET_NAME) 212 | site_name = memory.str_from_ea(site_name_ea) 213 | class_name = None 214 | if site_name is None: 215 | print(f"[Error] failed to read name for kalloc_type_view on {kty_ea:X}") 216 | elif not site_name.startswith("site."): 217 | print(f"[Error] invalid site name on {kty_ea:X}, is: {site_name!r}") 218 | else: 219 | class_name = site_name[5:] 220 | if class_name.startswith("struct "): 221 | class_name = class_name[7:] 222 | if class_name.startswith("typeof(") or class_name == "T": 223 | # Clang generates them using macro, so it might lead to some eccentric specific ones... 224 | class_name = None 225 | 226 | chosen_name = widgets.show_string_input("Choose class name", class_name or site_name) 227 | if chosen_name is None: 228 | return 229 | chosen_name = chosen_name.strip() 230 | if not chosen_name: 231 | return 232 | 233 | create_struct_from_name_signature(class_name, signature) 234 | 235 | 236 | def create_struct_from_name_signature(class_name: str, signature: str) -> bool: 237 | existing_type = tif.from_struct_name(class_name) 238 | if existing_type is not None: 239 | print(f"[Error] struct for {class_name} already exists") 240 | return False 241 | 242 | struct_definition = f"struct {class_name} {{\n" 243 | for i, t in enumerate(signature): 244 | t = int(t) 245 | field_name = f"field_{i * 8:#04x}" 246 | type_def = "void*" if t & 1 != 0 else "__int64" 247 | struct_definition += f" {type_def} {field_name};\n" 248 | struct_definition += "};" 249 | 250 | if not tif.create_from_c_decl(struct_definition): 251 | print(f"[Error] failed to create struct for {class_name}") 252 | return False 253 | print(f"[Info] Created struct for {class_name}") 254 | 255 | return True 256 | 257 | 258 | def escape_name(site_name: str) -> str: 259 | return site_name.replace("*", "_ptr_").replace(" ", "_").replace("<", "_").replace(">", "_") 260 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/swift/swift_strings/swift_string_fixup.py: -------------------------------------------------------------------------------- 1 | __all__ = ["SwiftStringsHook"] 2 | 3 | from dataclasses import dataclass 4 | from typing import Literal 5 | 6 | import ida_hexrays 7 | from ida_hexrays import ( 8 | cexpr_t, 9 | cfuncptr_t, 10 | cinsn_t, 11 | citem_t, 12 | ctree_parentee_t, 13 | ) 14 | from ida_typeinf import tinfo_t 15 | from idahelper import tif 16 | from idahelper.ast import cexpr 17 | 18 | from .swift_string import decode as decode_swift_string 19 | 20 | 21 | @dataclass 22 | class MemrefConstInfo: 23 | """Holds the parts of a `. op ` expression where op is either `=` or `==`.""" 24 | 25 | var: cexpr_t 26 | mem_off: int 27 | value: int 28 | # noinspection PyTypeHints 29 | op: "Literal[ida_hexrays.cot_asg] | Literal[ida_hexrays.cot_eq]" 30 | 31 | 32 | def _unpack_memref_const(e: cexpr_t) -> MemrefConstInfo | None: 33 | """If `e` is `. op `, return the parts. Otherwise, return None.""" 34 | # Check assign 35 | if e.op not in (ida_hexrays.cot_asg, ida_hexrays.cot_eq): 36 | return None 37 | 38 | lhs, rhs = e.x, e.y 39 | # Check LHS is a memref 40 | if lhs.op != ida_hexrays.cot_memref: 41 | return None 42 | 43 | # Support a cast around the number 44 | if rhs.op == ida_hexrays.cot_cast and rhs.x.op == ida_hexrays.cot_num: 45 | rhs = rhs.x 46 | if rhs.op != ida_hexrays.cot_num: 47 | return None 48 | 49 | return MemrefConstInfo(var=lhs.x, mem_off=lhs.m, value=rhs.numval(), op=e.op) 50 | 51 | 52 | def _is_memref_const_specific(e: cexpr_t, var_x: cexpr_t, wanted_off: int, wanted_op: int) -> bool: 53 | """Check if 'e' is '. op '.""" 54 | if (info := _unpack_memref_const(e)) is None: 55 | return False 56 | return _is_info_specific(info, var_x, wanted_off, wanted_op) 57 | 58 | 59 | def _is_info_specific(info: MemrefConstInfo, var_x: cexpr_t, wanted_off: int, wanted_op: int) -> bool: 60 | """Check if 'e' is '. op '.""" 61 | return info.var == var_x and info.mem_off == wanted_off and info.op == wanted_op 62 | 63 | 64 | @dataclass 65 | class CommaContext: 66 | """Context for neutralizing a prior complementary assignment. For `x, y` expression when we are `x`""" 67 | 68 | parent: cexpr_t 69 | 70 | 71 | @dataclass 72 | class BlockContext: 73 | """Context for neutralizing a prior complementary assignment. For a block when we are statement at index `idx`""" 74 | 75 | parent: cexpr_t 76 | idx: int 77 | 78 | 79 | def _find_prior_complementary_assignment( # noqa: C901 80 | parents: list[citem_t], current: cexpr_t, var_x: cexpr_t, wanted_off: int 81 | ) -> tuple[cexpr_t, CommaContext | BlockContext] | tuple[None, None]: 82 | """ 83 | Walk up the parent chain. If inside a comma-expr (we are 'y'), scan the left spine for a match. 84 | If inside a block, scan earlier statements in the block for a match. 85 | Also, if we enter a cit_expr that wraps our current cexpr_t, promote `current` to that cinsn_t 86 | so that the next cit_block step can locate the statement. 87 | If not found, return (None, None). 88 | """ 89 | # We'll mutate this as we climb so that when we hit cit_block we point at the cinsn_t 90 | cur: cexpr_t | cinsn_t = current 91 | 92 | for _parent in reversed(parents): 93 | if _parent is None: 94 | continue 95 | parent: cexpr_t | cinsn_t = _parent.to_specific_type 96 | 97 | # If we're entering a statement wrapper for our expression, promote it to cinsn_t 98 | if parent.op == ida_hexrays.cit_expr: 99 | cur = parent 100 | continue 101 | # Comma-expression: (... , cur). We're the right side iff cur is exactly parent.y 102 | elif parent.op == ida_hexrays.cot_comma: 103 | if cur == parent.y: 104 | # Iterate the left spine for assignments to var_x at wanted_off 105 | comma_expr: cexpr_t = parent 106 | while True: 107 | left_expr = comma_expr.x 108 | if _is_memref_const_specific(left_expr, var_x, wanted_off, ida_hexrays.cot_asg): 109 | return left_expr, CommaContext(parent) 110 | elif left_expr.op == ida_hexrays.cot_comma: 111 | comma_expr = left_expr 112 | else: 113 | break 114 | 115 | # If we are here we are either a left side, or the comma expression did not contain an assignment 116 | # Either way, move up 117 | cur = parent 118 | continue 119 | # Block: scan earlier statements 120 | elif parent.op == ida_hexrays.cit_block: 121 | block = parent.cblock 122 | # Find our index as a statement (cur must be a cinsn_t by now in normal cases) 123 | for i, insn_i in enumerate(block): 124 | if insn_i == cur: 125 | # Look backwards for a candidate 126 | for j in range(i - 1, -1, -1): 127 | insn_j = block[j] 128 | if insn_j.op == ida_hexrays.cit_expr: 129 | insn_j_expr = insn_j.cexpr 130 | if _is_memref_const_specific(insn_j_expr, var_x, wanted_off, ida_hexrays.cot_asg): 131 | return insn_j_expr, BlockContext(parent, j) 132 | break 133 | cur = parent 134 | continue 135 | 136 | # Default: keep walking up 137 | cur = parent 138 | return None, None 139 | 140 | 141 | def _remove_prior_with_ctx(ctx: CommaContext | BlockContext, current: cexpr_t): 142 | """ 143 | Neutralize the earlier complementary assignment. 144 | - In comma-exprs: replace '(left, current)' with '(1, current)'. 145 | - In blocks: turn the victim instruction into an empty statement (';'). 146 | """ 147 | if isinstance(ctx, CommaContext): 148 | # Swaping the parent can lead to deleting nodes we iterate over, so replace LHS with constant 1 149 | # TODO: Is there a cleaner way to do this? 150 | ctx.parent.x.swap(cexpr.from_const_value(1)) 151 | elif isinstance(ctx, BlockContext): 152 | victim = ctx.parent.cblock[ctx.idx] 153 | victim.cleanup() 154 | 155 | 156 | class SwiftStringsHook(ida_hexrays.Hexrays_Hooks): 157 | def maturity(self, func: cfuncptr_t, new_maturity: int) -> int: 158 | # Run once the function has a reasonably stable AST 159 | if new_maturity < ida_hexrays.CMAT_CPA: 160 | return 0 161 | 162 | swift_str_type = tif.from_c_type("Swift::String") 163 | if swift_str_type is None: 164 | return 0 165 | 166 | # noinspection PyTypeChecker,PyPropertyAccess 167 | SwiftStringVisitor(swift_str_type).apply_to(func.body, None) # pyright: ignore[reportArgumentType] 168 | return 0 169 | 170 | 171 | class SwiftStringVisitor(ctree_parentee_t): 172 | """ 173 | Finds pairs of assignments to Swift::String.{_countAndFlagsBits (off 0), _object (off 8)} 174 | in either order, possibly separated by other statements, decodes the string, 175 | and rewrites the second assignment to construct the Swift::String directly. 176 | """ 177 | 178 | def __init__(self, swift_str_type: tinfo_t): 179 | super().__init__() 180 | self.swift_str_type = swift_str_type 181 | 182 | def visit_expr(self, expr: cexpr_t) -> int: 183 | # Only process assignments 184 | if expr.op == ida_hexrays.cot_asg: 185 | self.visit_asg_expr(expr) 186 | elif expr.op == ida_hexrays.cot_land: 187 | self.visit_land_expr(expr) 188 | return 0 189 | 190 | def visit_asg_expr(self, expr: cexpr_t): 191 | if (asg_info := _unpack_memref_const(expr)) is None: 192 | return 193 | var_x, cur_off, value = asg_info.var, asg_info.mem_off, asg_info.value 194 | 195 | # Only offsets 0 (countAndFlagsBits) & 8 (_object) 196 | if cur_off not in (0, 8): 197 | return 198 | 199 | # Find the complementary assignment earlier in the same block/comma 200 | need_off = 0 if cur_off == 8 else 8 201 | prior_expr, ctx = _find_prior_complementary_assignment(self.parents, expr, var_x, need_off) 202 | if prior_expr is None: 203 | return 204 | 205 | # Extract values (bits @ off 0, object @ off 8) 206 | if cur_off == 8: 207 | bits_val = _unpack_memref_const(prior_expr).value 208 | obj_val = value 209 | else: # cur_off == 0 210 | bits_val = value 211 | obj_val = _unpack_memref_const(prior_expr).value 212 | 213 | # Decode the string 214 | s = decode_swift_string(bits_val, obj_val) 215 | if s is None: 216 | return 217 | 218 | # Build a helper-call that returns Swift::String from a C string 219 | call = cexpr.call_helper_from_sig( 220 | "__SwiftStr", 221 | self.swift_str_type, 222 | [cexpr.from_string(s)], 223 | ) 224 | 225 | # Replace RHS with the call 226 | expr.y.swap(call) 227 | 228 | # Assign directly to the struct/object (remove '._object'/'._countAndFlagsBits') 229 | lhs_parent = cexpr_t(expr.x.x) 230 | expr.x.swap(lhs_parent) 231 | 232 | # # Neutralize the older complementary assignment 233 | _remove_prior_with_ctx(ctx, expr) 234 | 235 | def visit_land_expr(self, expr: cexpr_t): 236 | # Support equality comparisons, for cases like `if (str._countAndFlagsBits == 0 && str._object == 0)` 237 | lhs, rhs = expr.x, expr.y 238 | 239 | # Check both sides are equality comparisons 240 | if lhs.op != ida_hexrays.cot_eq or rhs.op != ida_hexrays.cot_eq: 241 | return 242 | 243 | if (lhs_eq_info := _unpack_memref_const(lhs)) is None or (rhs_eq_info := _unpack_memref_const(rhs)) is None: 244 | return 245 | var_x_lhs, cur_off_lhs, value_lhs = lhs_eq_info.var, lhs_eq_info.mem_off, lhs_eq_info.value 246 | 247 | # Only offsets 0 (countAndFlagsBits) & 8 (_object) 248 | if cur_off_lhs not in (0, 8): 249 | return 250 | 251 | # Check if both sides refer to the same variable 252 | need_off = 0 if cur_off_lhs == 8 else 8 253 | if not _is_info_specific(rhs_eq_info, var_x_lhs, need_off, ida_hexrays.cot_eq): 254 | return 255 | 256 | # Extract values (bits @ off 0, object @ off 8) 257 | if cur_off_lhs == 8: 258 | bits_val = rhs_eq_info.value 259 | obj_val = value_lhs 260 | else: # cur_off_lhs == 0 261 | bits_val = value_lhs 262 | obj_val = rhs_eq_info.value 263 | 264 | # Decode the string 265 | s = decode_swift_string(bits_val, obj_val) 266 | if not s: 267 | return 268 | 269 | # Build a helper-call that returns Swift::String from a C string 270 | call = cexpr.call_helper_from_sig( 271 | "__SwiftStr", 272 | self.swift_str_type, 273 | [cexpr.from_string(s)], 274 | ) 275 | 276 | new_comparison = cexpr.from_binary_op( 277 | cexpr_t(lhs.x.x), call, ida_hexrays.cot_eq, tif.from_c_type("bool"), expr.ea 278 | ) 279 | expr.swap(new_comparison) 280 | self.prune_now() 281 | -------------------------------------------------------------------------------- /src/ioshelper/plugins/kernelcache/func_renamers/pac_applier.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Iterable, Sequence 2 | 3 | import ida_hexrays 4 | import idaapi 5 | from ida_funcs import func_t 6 | from ida_hexrays import cexpr_t, ctree_parentee_t, lvar_t 7 | from ida_typeinf import tinfo_t 8 | from idahelper import tif 9 | from idahelper.ast import cfunc 10 | from idahelper.ast.lvars import VariableModification 11 | from idahelper.pac import client as pac 12 | 13 | from ioshelper.base.utils import CustomDict, CustomSet 14 | 15 | from .renamer import ( 16 | Modifications, 17 | ) 18 | from .visitor import Call, XrefsMatcher, process_function_calls 19 | 20 | 21 | def apply_pac(func: func_t) -> bool: 22 | print(f"Trying to apply pac signature on current function: {func.start_ea:X}") 23 | helper = MostSpecificAncestorHelper() 24 | xref_matcher = XrefsMatcher.build([], on_unknown_call_wrapper(helper)) # type: ignore # noqa: PGH003 25 | decompiled_func = cfunc.from_func(func) 26 | if decompiled_func is None: 27 | return False 28 | 29 | with Modifications(decompiled_func.entry_ea, decompiled_func.get_lvars()) as modifications: 30 | process_function_calls(decompiled_func.mba, xref_matcher, modifications) 31 | has_changed = False 32 | for lvar, typ, calls in helper.lvars(): 33 | if not lvar.has_user_type and should_modify_type(lvar.type(), typ): 34 | has_changed = True 35 | print(f"Modifying lvar {lvar.name} from {lvar.type()}* to {typ}") 36 | modifications.modify_local(lvar.name, VariableModification(type=tif.pointer_of(typ))) 37 | fix_calls(typ, calls) 38 | 39 | for (cls_type, offset), typ, calls in helper.fields(): 40 | typ_res = tif.get_member_recursive(cls_type, offset) 41 | if typ_res is None: 42 | print(f"[Warning] Could not find member at offset {offset:X} in {cls_type}") 43 | continue 44 | actual_typ, member = typ_res 45 | if member.size == 64 and should_modify_type(member.type, typ): 46 | has_changed = True 47 | print(f"Modifying field {cls_type}::{member.name} from {member.type}* to {typ}") 48 | # noinspection PyTypeChecker 49 | modifications.modify_type( 50 | actual_typ.get_type_name(), # pyright: ignore [reportArgumentType] 51 | offset, 52 | VariableModification(type=tif.pointer_of(typ)), 53 | ) 54 | fix_calls(typ, calls) 55 | 56 | if has_changed: 57 | fix_empty_calls(func) 58 | 59 | return has_changed 60 | 61 | 62 | def fix_empty_calls(func: func_t): 63 | """If after the modifications there are calls with no parameters, do force the call type as it is better then nothing""" 64 | decompiled_func = ida_hexrays.decompile(func.start_ea, flags=ida_hexrays.DECOMP_NO_CACHE) 65 | if decompiled_func is None: 66 | print(f"[Warning] Could not decompile function {func.start_ea:X} to fix empty calls") 67 | return 68 | 69 | # noinspection PyTypeChecker 70 | EmptyCallTreeVisitor().apply_to(decompiled_func.body, None) # pyright: ignore[reportIncompatibleMethodCall] 71 | 72 | 73 | class EmptyCallTreeVisitor(ctree_parentee_t): 74 | def visit_expr(self, expr: cexpr_t) -> int: # pyright: ignore[reportIncompatibleMethodOverride] 75 | # Filter dynamic calls with no parameters 76 | if ( 77 | expr.op != ida_hexrays.cot_call 78 | or not expr.x.type.is_funcptr() 79 | or expr.a.size() != 0 80 | or expr.ea == idaapi.BADADDR 81 | ): 82 | return 0 83 | 84 | # Make sure it is a call to a vtable member 85 | x = expr.x 86 | # Handle cast of the function type 87 | if x.op == ida_hexrays.cot_cast: 88 | x = x.x 89 | 90 | # Check it is a member 91 | if x.op != ida_hexrays.cot_memptr: 92 | return 0 93 | 94 | # Check it is a member of a vtable 95 | possible_vtable_type: tinfo_t = x.x.type 96 | if not possible_vtable_type.is_ptr() or not possible_vtable_type.get_pointed_object().is_vftable(): 97 | return 0 98 | 99 | # This is a vtable call with no parameters, apply the type to the call 100 | apply_vtable_type_to_call(expr.ea, possible_vtable_type.get_pointed_object(), expr.x.m, apply_if_no_args=True) 101 | return 0 102 | 103 | 104 | def fix_calls(class_type: tinfo_t, calls: list[Call]): 105 | """Apply the call type from vtable definition to each of the calls in the list.""" 106 | if not calls: 107 | return 108 | 109 | vtable_type = tif.vtable_type_from_type(class_type) 110 | if vtable_type is None: 111 | print(f"[Warning] Could not find vtable type for {class_type}") 112 | return 113 | 114 | for call in calls: 115 | assert call.indirect_info is not None 116 | offset = call.indirect_info.offset 117 | apply_vtable_type_to_call(call.ea, vtable_type, offset, apply_if_no_args=False) 118 | 119 | 120 | def apply_vtable_type_to_call(call_ea: int, vtable_type: tinfo_t, offset: int, apply_if_no_args: bool) -> bool: 121 | """Apply the vtable type to the call at the given ea.""" 122 | vtable_member = tif.get_member(vtable_type, offset) 123 | if vtable_member is None: 124 | print(f"[Warning] Could not find vtable member for {vtable_type} at {offset:X}") 125 | return False 126 | 127 | # There are a lot of false positive signatures that have only "this" argument. 128 | # We prefer not to force non-arguments calls rather than hide arguments. 129 | vtable_member_type: tinfo_t = tinfo_t(vtable_member.type) 130 | vtable_member_type.remove_ptr_or_array() 131 | if apply_if_no_args or vtable_member_type.get_nargs() != 1: 132 | tif.apply_tinfo_to_call(vtable_member.type, call_ea) 133 | print( 134 | f"Applying vtable type {vtable_member.type} to call at {call_ea:X} for {vtable_type}::{vtable_member.name} at offset {offset:X}" 135 | ) 136 | return True 137 | return False 138 | 139 | 140 | def should_modify_type(current_type: tinfo_t, new_type: tinfo_t) -> bool: 141 | if current_type == new_type: 142 | return False 143 | elif not current_type.is_ptr(): 144 | return True 145 | current_type.remove_ptr_or_array() 146 | 147 | if current_type.is_void() or not current_type.is_struct() or current_type.get_type_name() == "OSObject": 148 | return True 149 | 150 | return current_type in tif.get_parent_classes(new_type, True) 151 | 152 | 153 | class MostSpecificAncestorHelper: 154 | """Helper class to find the most specific ancestor of a PAC call.""" 155 | 156 | def __init__(self): 157 | self._lvars: CustomDict[lvar_t, CustomSet[tinfo_t]] = CustomDict(lambda v: v.name) 158 | self._fields: CustomDict[tuple[tinfo_t, int], CustomSet[tinfo_t]] = CustomDict(lambda t: (t[0].get_tid(), t[1])) 159 | 160 | self._lvar_to_calls: CustomDict[lvar_t, list[Call]] = CustomDict(lambda v: v.name) 161 | self._fields_to_calls: CustomDict[tuple[tinfo_t, int], list[Call]] = CustomDict( 162 | lambda t: (t[0].get_tid(), t[1]) 163 | ) 164 | 165 | self._children_classes_cache: CustomDict[tinfo_t, list[tinfo_t]] = CustomDict(lambda t: t.get_tid()) 166 | """Cache of mapping from a type to its children classes.""" 167 | 168 | def update_lvar(self, lvar: lvar_t, predicate: Sequence[tinfo_t], call: Call): 169 | minimized = self._minimize(predicate) 170 | if lvar in self._lvars: 171 | self._lvars[lvar] &= self._children_of_union(minimized) 172 | else: 173 | self._lvars[lvar] = self._children_of_union(minimized) 174 | 175 | self._lvar_to_calls.setdefault(lvar, []).append(call) 176 | 177 | def update_field(self, cls_type: tinfo_t, offset: int, predicate: Sequence[tinfo_t], call: Call): 178 | if cls_type.is_vftable(): 179 | return 180 | 181 | minimized = self._minimize(predicate) 182 | key = (cls_type, offset) 183 | if key in self._fields: 184 | self._fields[key] &= self._children_of_union(minimized) 185 | else: 186 | self._fields[key] = self._children_of_union(minimized) 187 | 188 | self._fields_to_calls.setdefault(key, []).append(call) 189 | 190 | def lvars(self) -> Iterable[tuple[lvar_t, tinfo_t, list[Call]]]: 191 | """Get all lvars with their most specific type.""" 192 | for lvar, state in self._lvars.items(): 193 | ancestor = tif.get_common_ancestor(list(state)) 194 | if ancestor is None: 195 | continue 196 | yield lvar, ancestor, self._lvar_to_calls[lvar] 197 | 198 | def fields(self) -> Iterable[tuple[tuple[tinfo_t, int], tinfo_t, list[Call]]]: 199 | """Get all fields with their most specific type.""" 200 | for field, state in self._fields.items(): 201 | ancestor = tif.get_common_ancestor(list(state)) 202 | if ancestor is None: 203 | continue 204 | yield field, ancestor, self._fields_to_calls[field] 205 | 206 | def _get_children_classes(self, typ: tinfo_t) -> list[tinfo_t]: 207 | """Get all children of a type, caching the result.""" 208 | if typ not in self._children_classes_cache: 209 | children = tif.get_children_classes(typ) or [] 210 | children.append(typ) 211 | self._children_classes_cache[typ] = children 212 | return self._children_classes_cache[typ] 213 | 214 | def _children_of_union(self, union: tuple[tinfo_t, ...]) -> CustomSet[tinfo_t]: 215 | """Get all children of a union type.""" 216 | children: CustomSet[tinfo_t] = CustomSet(lambda t: t.get_tid()) 217 | children.add_all(union) 218 | 219 | for typ in union: 220 | children.add_all(self._get_children_classes(typ)) 221 | return children 222 | 223 | @staticmethod 224 | def _minimize(union: Sequence[tinfo_t]) -> tuple[tinfo_t, ...]: 225 | """Minimize the union to its most specific types.""" 226 | if len(union) == 1: 227 | return (union[0],) 228 | 229 | minimized: list[tinfo_t] = [] 230 | 231 | for i, typ in enumerate(union): 232 | # Quick exit if the type is OSObject, as it is a base class for all objects 233 | if typ.get_type_name() == "OSObject": 234 | return (typ,) 235 | 236 | typ_parents = tif.get_parent_classes(typ, True) 237 | for j, other_typ in enumerate(union): 238 | if i == j: 239 | continue 240 | 241 | if other_typ in typ_parents: 242 | break 243 | else: 244 | minimized.append(typ) 245 | 246 | return tuple(minimized) 247 | 248 | 249 | def on_unknown_call_wrapper(helper: MostSpecificAncestorHelper) -> Callable[[Call, Modifications], None]: 250 | def on_unknown_call(call: Call, _modifications: Modifications): 251 | """Called when a call is found""" 252 | if call.indirect_info is None: 253 | return 254 | 255 | prev_movk = pac.get_previous_movk(call.ea) 256 | if prev_movk is None: 257 | return 258 | candidates = pac.pac_class_candidates_from_movk(prev_movk) 259 | if candidates: 260 | if call.indirect_info.var is not None: 261 | lvar = call.indirect_info.var 262 | helper.update_lvar(lvar, candidates, call) 263 | elif call.indirect_info.field is not None: 264 | cls_type, offset = call.indirect_info.field 265 | helper.update_field(cls_type, offset, candidates, call) 266 | 267 | return on_unknown_call 268 | --------------------------------------------------------------------------------