Skip to content

Instantly share code, notes, and snippets.

@mahmoudimus
Last active August 2, 2024 18:05
Show Gist options
  • Save mahmoudimus/043ab0665f2aa92ac875a20ef33ea503 to your computer and use it in GitHub Desktop.
Save mahmoudimus/043ab0665f2aa92ac875a20ef33ea503 to your computer and use it in GitHub Desktop.
Exploiting IDA Pro's Qt 5.5 Keybindings For Emacs-like Mnemonic Keybindings
"""
Further updates located at https://github.com/mahmoudimus/ida-keybinder
Mahmoud Abdelkader
"""
import re
import sys
from PyQt5 import QtWidgets
import ida_kernwin
# this plugin requires Python 3
SUPPORTED_PYTHON = sys.version_info[0] == 3
# this plugin requires IDA 7.6 or newer
try:
import ida_pro
import ida_idaapi
IDA_GLOBAL_SCOPE = sys.modules["__main__"]
SUPPORTED_IDA = ida_pro.IDA_SDK_VERSION >= 760
except:
SUPPORTED_IDA = False
# is this deemed to be a compatible environment for the plugin to load?
SUPPORTED_ENVIRONMENT = bool(SUPPORTED_IDA and SUPPORTED_PYTHON)
# ------------------------------------------------------------------------------
# IDA Plugin Stub
# ------------------------------------------------------------------------------
class KeyHooker(ida_kernwin.UI_Hooks):
_TILDE_PATTERN_rgx = re.compile(r"~(.*?)~")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.LABEL_TO_ACTION = {}
self.ACTIONS = {}
def enumerate_actions(self):
# Get the instance of the QApplication
main_window = QtWidgets.QApplication.instance()
widgets = [main_window]
while widgets:
widget = widgets.pop(0)
widgets.extend(list(widget.findChildren(QtWidgets.QAction)))
if not isinstance(widget, QtWidgets.QAction):
continue
if widget.shortcut().isEmpty():
continue
self.LABEL_TO_ACTION[widget.text()] = widget
@staticmethod
def replace_tilde_with_ampersand(text):
# Define a pattern that matches text between ~ and ~
# The lambda function prefixes the matched text (group 1) with &
replaced_text = KeyHooker._TILDE_PATTERN_rgx.sub(lambda match: f"&{match.group(1)}", text)
return replaced_text
def run(self):
# ida_kernwin.update_action_shortcut("JumpOpXref", "Meta+g, x")
self.enumerate_actions()
for action_name in ida_kernwin.get_registered_actions():
if not ida_kernwin.get_action_shortcut(action_name):
continue
# labels have ~ between the mnemonic shortkey identifier
label = ida_kernwin.get_action_label(action_name)
label = self.replace_tilde_with_ampersand(label)
action = self.LABEL_TO_ACTION.get(label)
if not action:
print(
"WARNING: action ",
action_name,
"(",
label,
") not found in action widgets.",
)
else:
self.ACTIONS[action_name] = action
print(f"[+] {KeybinderPlugin.wanted_name} by mahmoudimus. Setting shortcuts.")
# Now proceed to set shortcuts
self.ACTIONS["ChooserEdit"].setShortcut("Meta+e, c")
self.ACTIONS["Edit/Plugins/IDA Patcher"].setShortcut("Meta+P, P")
self.ACTIONS["Edit/Plugins/Signature Maker"].setShortcut("Meta+P, S")
self.ACTIONS["EditEnum"].setShortcut("Meta+E, N")
self.ACTIONS["EditSegment"].setShortcut("Meta+E, S")
self.ACTIONS["ExpandStruct"].setShortcut("Meta+E, E")
self.ACTIONS["JumpEntryPoint"].setShortcut("Meta+G, E")
self.ACTIONS["JumpFunction"].setShortcut("Meta+G, F")
self.ACTIONS["JumpOpXref"].setShortcut("Meta+g, x") # Go to Xref
self.ACTIONS["JumpText"].setShortcut("Meta+G, T")
self.ACTIONS["SetSegmentRegister"].setShortcut("Meta+E, R")
self.ACTIONS["TracingMainTracebufChangeDesc"].setShortcut("Meta+E, T")
self.ACTIONS["watch:Edit"].setShortcut("Meta+E, W")
def ready_to_run(self):
self.run()
def get_lines_rendering_info(self, out, widget, rin):
pass
def populating_widget_popup(self, widget, popup, ctx):
pass
class KeybinderPlugin(ida_idaapi.plugin_t):
"""
The Keybinder plugin stub.
"""
#
# Plugin flags:
# - PLUGIN_PROC: Load / unload this plugin when an IDB opens / closes
# - PLUGIN_HIDE: Hide this plugin from the IDA plugin menu
# - PLUGIN_UNL: Unload the plugin after calling run()
#
flags = ida_idaapi.PLUGIN_PROC | ida_idaapi.PLUGIN_HIDE | ida_idaapi.PLUGIN_UNL
comment = "A plugin to enable Emacs-like key sequences and key chord sequences in IDA"
help = ""
wanted_name = "Keybinder"
wanted_hotkey = ""
def __init__(self):
self.__updated = getattr(IDA_GLOBAL_SCOPE, "RESTART_REQUIRED", False)
# --------------------------------------------------------------------------
# IDA Plugin Overloads
# --------------------------------------------------------------------------
def init(self):
"""
This is called by IDA when it is loading the plugin.
"""
if not SUPPORTED_ENVIRONMENT or self.__updated:
return ida_idaapi.PLUGIN_SKIP
# defer loading via hook
self._ui_hook = KeyHooker()
self._ui_hook.hook()
# mark the plugin as loaded
return ida_idaapi.PLUGIN_KEEP
def run(self, arg):
"""
This is called by IDA when this file is loaded as a script.
"""
pass
def term(self):
"""
This is called by IDA when it is unloading the plugin.
"""
pass
def PLUGIN_ENTRY():
"""
Required plugin entry point for IDAPython plugins.
"""
if not SUPPORTED_ENVIRONMENT:
print("plugin", KeybinderPlugin.wanted_name, "is not compatible with this IDA/Python version")
return KeybinderPlugin()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment