From 8c87fb50a651a5ff4aac84432e2aee55c944d444 Mon Sep 17 00:00:00 2001 From: hzqst <113660872@qq.com> Date: Thu, 23 Apr 2026 15:56:23 +0800 Subject: [PATCH 1/4] Add autostart feature with per-IDB preference Auto-patch PLT section when IDA opens an ELF64 database, with a configurable toggle under Edit -> Plugins -> PLT Patcher Configuration. Preference is stored per-IDB via netnode and defaults to enabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- plt_patcher.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/plt_patcher.py b/plt_patcher.py index 7e22d33..4f65131 100644 --- a/plt_patcher.py +++ b/plt_patcher.py @@ -7,6 +7,63 @@ import idaapi import idautils import ida_segment +import ida_netnode +import ida_kernwin + +NETNODE_AUTOSTART = "$ plt_patcher.autostart" + +def _get_autostart(): + """Read the autostart preference from the IDB. Defaults to True.""" + node = ida_netnode.netnode(NETNODE_AUTOSTART) + val = node.altval(0) # 0 = not set (defaults True), 1 = off, 2 = on + return val != 1 + +def _set_autostart(enabled): + """Persist the autostart preference into the IDB.""" + node = ida_netnode.netnode(NETNODE_AUTOSTART, 0, True) + node.altset(0, 1 if not enabled else 2) + +CONFIG_ACTION_ID = "plt_patcher:configure" +CONFIG_ACTION_LABEL = "PLT Patcher Configuration" + +class PltPatcherConfigHandler(idaapi.action_handler_t): + def __init__(self, plugin): + idaapi.action_handler_t.__init__(self) + self.plugin = plugin + + def activate(self, ctx): + current = self.plugin.autostart + result = ida_kernwin.ask_yn( + 1 if current else 0, + "Auto-patch PLT section when IDA opens this database?" + ) + if result == -1: # cancelled + return 0 + new_autostart = result == 1 + if new_autostart != current: + self.plugin.autostart = new_autostart + _set_autostart(new_autostart) + print(f"[PltPatcher] Autostart {'enabled' if new_autostart else 'disabled'}") + return 1 + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + +class PltPatcherUIHooks(ida_kernwin.UI_Hooks): + """Defers menu attachment and autostart until the UI is fully ready.""" + + def __init__(self, plugin): + super().__init__() + self.plugin = plugin + + def ready_to_run(self): + ida_kernwin.attach_action_to_menu( + "Edit/Plugins/", CONFIG_ACTION_ID, idaapi.SETMENU_APP + ) + if self.plugin.autostart: + print("[PltPatcher] Auto-patching PLT section...") + self.plugin.run(0) + self.unhook() def get_dynamic_struct(): if get_dynamic_struct.dyn_data is None: @@ -112,6 +169,10 @@ def patch_plt(): idc.set_name(got_plt_offs, f'{func_name}_ptr', idaapi.SN_FORCE) if target_ea is not None: + # Skip if .got.plt entry already points to the target + if idaapi.get_qword(got_plt_offs) == target_ea: + continue + # Patch .got.plt entry to point to extern function idaapi.put_qword(got_plt_offs, target_ea) idaapi.add_dref(got_plt_offs, target_ea, idaapi.dr_O) @@ -132,7 +193,7 @@ def patch_plt(): print(f'!!! Failed to find/create {got_plt_offs:x} [{func_name}] function in exports') class PltPatcher(idaapi.plugin_t): - flags = idaapi.PLUGIN_UNL + flags = idaapi.PLUGIN_KEEP comment = 'Plt Patcher' help = 'Patches plt sections when IDA fails' wanted_name = 'Patch Plt Section' @@ -141,6 +202,24 @@ def init(self): if 'ELF64' not in idaapi.get_file_type_name(): return idaapi.PLUGIN_SKIP + self.autostart = _get_autostart() + + ida_kernwin.register_action( + ida_kernwin.action_desc_t( + CONFIG_ACTION_ID, + CONFIG_ACTION_LABEL, + PltPatcherConfigHandler(self), + ) + ) + + self._ui_hooks = PltPatcherUIHooks(self) + self._ui_hooks.hook() + + if self.autostart: + print('[PltPatcher] Plugin loaded, PLT will be auto-patched') + else: + print('[PltPatcher] Plugin loaded, use Edit -> Plugins -> Patch Plt Section to run manually') + return idaapi.PLUGIN_KEEP def run(self, arg): @@ -151,7 +230,9 @@ def run(self, arg): print('Plt patcher finished.') def term(self): - pass + if hasattr(self, '_ui_hooks'): + self._ui_hooks.unhook() + ida_kernwin.unregister_action(CONFIG_ACTION_ID) def PLUGIN_ENTRY(): From e0cca1bf9fbeb4b6a9ea37709221ed6e8e0a0dc6 Mon Sep 17 00:00:00 2001 From: hzqst <113660872@qq.com> Date: Thu, 23 Apr 2026 19:06:23 +0800 Subject: [PATCH 2/4] Update plt_patcher.py, PLUGIN_KEEP should be PLUGIN_FIX --- plt_patcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plt_patcher.py b/plt_patcher.py index 4f65131..610b70f 100644 --- a/plt_patcher.py +++ b/plt_patcher.py @@ -193,7 +193,7 @@ def patch_plt(): print(f'!!! Failed to find/create {got_plt_offs:x} [{func_name}] function in exports') class PltPatcher(idaapi.plugin_t): - flags = idaapi.PLUGIN_KEEP + flags = idaapi.PLUGIN_FIX comment = 'Plt Patcher' help = 'Patches plt sections when IDA fails' wanted_name = 'Patch Plt Section' @@ -236,4 +236,4 @@ def term(self): def PLUGIN_ENTRY(): - return PltPatcher() \ No newline at end of file + return PltPatcher() From 74980d1449ef30b2c5134b4a040e993e17fee0c9 Mon Sep 17 00:00:00 2001 From: hzqst <113660872@qq.com> Date: Thu, 23 Apr 2026 20:49:36 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E5=B0=86=20autostart=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=94=A8=E6=88=B7=E5=85=A8=E5=B1=80=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plt_patcher.py | 80 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/plt_patcher.py b/plt_patcher.py index 610b70f..039cb83 100644 --- a/plt_patcher.py +++ b/plt_patcher.py @@ -3,25 +3,81 @@ # Copyright (c) 2025 GAMMACASE # https://github.com/GAMMACASE/PltPatcher +import json +import os + import idc import idaapi import idautils import ida_segment -import ida_netnode import ida_kernwin -NETNODE_AUTOSTART = "$ plt_patcher.autostart" +try: + import ida_diskio +except ImportError: + ida_diskio = None + +CONFIG_FILENAME = "plt_patcher.json" +DEFAULT_AUTOSTART = True + +def _get_user_idadir(): + for module in (idaapi, globals().get("ida_diskio")): + if module is None: + continue + getter = getattr(module, "get_user_idadir", None) + if getter is None: + continue + try: + user_dir = getter() + except Exception as exc: + print(f"[PltPatcher] Failed to query IDA user directory: {exc}") + continue + if user_dir: + return user_dir + + user_dir = os.environ.get("IDAUSR") + if user_dir: + return user_dir + + return os.path.join(os.path.expanduser("~"), ".idapro") + +def _get_config_path(): + return os.path.join(_get_user_idadir(), CONFIG_FILENAME) def _get_autostart(): - """Read the autostart preference from the IDB. Defaults to True.""" - node = ida_netnode.netnode(NETNODE_AUTOSTART) - val = node.altval(0) # 0 = not set (defaults True), 1 = off, 2 = on - return val != 1 + """Read the autostart preference from the current-user config.""" + path = _get_config_path() + try: + with open(path, "r", encoding="utf-8") as config_file: + config = json.load(config_file) + except FileNotFoundError: + return DEFAULT_AUTOSTART + except Exception as exc: + print(f"[PltPatcher] Failed to read config '{path}': {exc}") + return DEFAULT_AUTOSTART + + autostart = config.get("autostart") + if isinstance(autostart, bool): + return autostart + + print(f"[PltPatcher] Invalid autostart config in '{path}', using default") + return DEFAULT_AUTOSTART def _set_autostart(enabled): - """Persist the autostart preference into the IDB.""" - node = ida_netnode.netnode(NETNODE_AUTOSTART, 0, True) - node.altset(0, 1 if not enabled else 2) + """Persist the autostart preference into the current-user config.""" + path = _get_config_path() + try: + config_dir = os.path.dirname(path) + if config_dir: + os.makedirs(config_dir, exist_ok=True) + with open(path, "w", encoding="utf-8") as config_file: + json.dump({"autostart": bool(enabled)}, config_file, indent=2) + config_file.write("\n") + except Exception as exc: + print(f"[PltPatcher] Failed to persist autostart config '{path}': {exc}") + return False + + return True CONFIG_ACTION_ID = "plt_patcher:configure" CONFIG_ACTION_LABEL = "PLT Patcher Configuration" @@ -35,15 +91,17 @@ def activate(self, ctx): current = self.plugin.autostart result = ida_kernwin.ask_yn( 1 if current else 0, - "Auto-patch PLT section when IDA opens this database?" + "Auto-patch PLT section when this user opens ELF64 databases?" ) if result == -1: # cancelled return 0 new_autostart = result == 1 if new_autostart != current: self.plugin.autostart = new_autostart - _set_autostart(new_autostart) + saved = _set_autostart(new_autostart) print(f"[PltPatcher] Autostart {'enabled' if new_autostart else 'disabled'}") + if not saved: + print("[PltPatcher] Autostart change applies to this session only") return 1 def update(self, ctx): From 2d6125123a67c6f8888b6937e5b3775080ba1387 Mon Sep 17 00:00:00 2001 From: hzqst <113660872@qq.com> Date: Thu, 23 Apr 2026 21:39:45 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E7=AD=89=E5=BE=85=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=AE=8C=E6=88=90=E5=90=8E=E5=86=8D=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=20PLT=20patch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plt_patcher.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plt_patcher.py b/plt_patcher.py index 039cb83..7ccf48b 100644 --- a/plt_patcher.py +++ b/plt_patcher.py @@ -9,6 +9,7 @@ import idc import idaapi import idautils +import ida_auto import ida_segment import ida_kernwin @@ -123,6 +124,13 @@ def ready_to_run(self): self.plugin.run(0) self.unhook() +def _wait_for_auto_analysis(): + if ida_auto.auto_is_ok(): + return + + print("[PltPatcher] Waiting for IDA auto-analysis to finish...") + ida_auto.auto_wait() + def get_dynamic_struct(): if get_dynamic_struct.dyn_data is None: phoff = idaapi.get_qword(idaapi.inf_get_min_ea() + 0x20) + idaapi.inf_get_min_ea() @@ -283,6 +291,7 @@ def init(self): def run(self, arg): print('Starting patching plt section...') + _wait_for_auto_analysis() patch_plt() print('Plt patcher finished.')