├── 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 |
4 |
5 |
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 |
4 |
5 |
13 |
14 |
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 | 
271 |
272 | ## Xrefs to selector
273 |
274 | Use `Ctrl+4` inside an Objective-C method to list xrefs to its selector.
275 |
276 | 
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 |
--------------------------------------------------------------------------------