ida-plugin-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDeveloping IDA Pro plugins
开发IDA Pro插件
Use this skill when developing plugins for IDA Pro using Python.
IDA's UI and analysis passes can be almost completely replaced through plugins.
There's a lot of power (and a lot of complexity), so its important to follow known patterns.
This document lists tips and tricks for creating new plugins for modern versions of IDA.
Key concepts covered in this document:
- Use the IDA Domain API - prefer the high-level Pythonic interface
- Plugin Manager Integration - packaging and distribution
- Plugin Entry Point - version checking and conditional loading
- Hook Registration - pairwise register/unregister pattern
- Cross-Plugin Communication via IDC Functions - invoke plugin functionality from scripts/other plugins
- Save/Load state from netnodes - persist plugin data in IDB
- Respond to current address and selection change - UI location hooks
- Find widgets by prefix - managing multiple widget instances
- Context Menu Entries - "Send to Foo" patterns
- User Defined Prefix - add contextual markers in disassembly
- Viewer Hints - hover popups with context
- Overriding rendering - custom colors and mnemonics
- Custom Viewers - tagged lines with clickable addresses
当使用Python开发IDA Pro插件时,可参考本技能指南。
通过插件几乎可以完全替换IDA的UI界面和分析流程。插件功能强大,但也存在不少复杂度,因此遵循成熟的开发模式十分重要。本文档列出了为新版IDA开发插件的技巧和方法。
本文档涵盖的核心概念:
- 使用IDA Domain API - 优先使用高级Python风格接口
- 插件管理器集成 - 打包与分发
- 插件入口点 - 版本检查与条件加载
- 钩子注册 - 成对注册/注销模式
- 通过IDC函数实现跨插件通信 - 从脚本/其他插件调用插件功能
- 从netnodes保存/加载状态 - 在IDB中持久化插件数据
- 响应当前地址和选择变更 - UI位置钩子
- 按前缀查找部件 - 管理多部件实例
- 上下文菜单条目 - "发送至Foo"模式
- 用户自定义前缀 - 在反汇编中添加上下文标记
- 查看器提示 - 带上下文的悬停弹窗
- 覆盖渲染逻辑 - 自定义颜色与助记符
- 自定义查看器 - 带可点击地址的标记行
Use the IDA Domain API
使用IDA Domain API
Always prefer the IDA Domain API over the legacy low-level IDA Python SDK. The Domain API provides a clean, Pythonic interface that is easier to use and understand.
However, there will be some things that the Domain API doesn't cover, especially around plugin registration and GUI handling.
Right now: read this intro guide: https://ida-domain.docs.hex-rays.com/getting_started/index.md
Always refer to the documentation rather than doing introspection, because the documentation explains concepts, not just symbol names.
To fetch specific API documentation, use URLs like:
- - Function analysis API
https://ida-domain.docs.hex-rays.com/ref/functions/index.md - - Cross-reference API
https://ida-domain.docs.hex-rays.com/ref/xrefs/index.md - - String analysis API
https://ida-domain.docs.hex-rays.com/ref/strings/index.md
Available API modules: , , , , , , , , , , , , , , ,
URL pattern: https://ida-domain.docs.hex-rays.com/ref/{module}/index.md
bytescommentsdatabaseentriesflowchartfunctionsheadshooksinstructionsnamesoperandssegmentssignature_filesstringstypesxrefsYou can always ask a subagent to answer a question by exploring the documentation and summarizing its findings.
始终优先使用IDA Domain API,而非旧版底层IDA Python SDK。Domain API提供了简洁、符合Python风格的接口,更易于使用和理解。不过,Domain API并非涵盖所有场景,尤其是插件注册和GUI处理方面。
始终参考官方文档而非自行内省,因为文档不仅会说明符号名称,还会解释相关概念。如需获取特定API文档,可使用如下格式的URL:
- - 函数分析API
https://ida-domain.docs.hex-rays.com/ref/functions/index.md - - 交叉引用API
https://ida-domain.docs.hex-rays.com/ref/xrefs/index.md - - 字符串分析API
https://ida-domain.docs.hex-rays.com/ref/strings/index.md
可用API模块:, , , , , , , , , , , , , , ,
URL格式:https://ida-domain.docs.hex-rays.com/ref/{module}/index.md
bytescommentsdatabaseentriesflowchartfunctionsheadshooksinstructionsnamesoperandssegmentssignature_filesstringstypesxrefs你可以让子代理通过查阅文档并总结结果来回答相关问题。
Key Database Properties
核心数据库属性
python
with Database.open(path, ida_options) as db:
db.minimum_ea # Start address
db.maximum_ea # End address
db.metadata # Database metadata
db.architecture # Target architecture
db.functions # All functions (iterable)
db.strings # All strings (iterable)
db.segments # Memory segments
db.names # Symbols and labels
db.entries # Entry points
db.types # Type definitions
db.comments # All comments
db.xrefs # Cross-reference utilities
db.bytes # Byte manipulation
db.instructions # Instruction accesspython
with Database.open(path, ida_options) as db:
db.minimum_ea # 起始地址
db.maximum_ea # 结束地址
db.metadata # 数据库元数据
db.architecture # 目标架构
db.functions # 所有函数(可迭代)
db.strings # 所有字符串(可迭代)
db.segments # 内存段
db.names # 符号与标签
db.entries # 入口点
db.types # 类型定义
db.comments # 所有注释
db.xrefs # 交叉引用工具
db.bytes # 字节操作
db.instructions # 指令访问Common Analysis Tasks
常见分析任务
List Functions
列出函数
python
func: func_t
for func in db.functions:
name = db.functions.get_name(func)
print(f"{hex(func.start_ea)}: {name} ({func.size} bytes)")Interesting properties:
func_tpython
class func_t:
name: str
flags: int
start_ea: int
end_ea: int
size: int
does_return: bool
referers: list[int] # function start addresses
addresses: list[int]
frame_object: tinfo_t
prototype: tinfo_tpython
func: func_t
for func in db.functions:
name = db.functions.get_name(func)
print(f"{hex(func.start_ea)}: {name} ({func.size} bytes)")func_tpython
class func_t:
name: str
flags: int
start_ea: int
end_ea: int
size: int
does_return: bool
referers: list[int] # 函数起始地址
addresses: list[int]
frame_object: tinfo_t
prototype: tinfo_tCross-references
交叉引用
python
for xref in db.xrefs.to_ea(target_addr):
print(f"Referenced from {hex(xref.from_ea)} (type: {xref.type.name})")
for xref in db.xrefs.from_ea(source_addr):
print(f"References {hex(xref.to_ea)}")
for xref in db.xrefs.calls_to_ea(func_addr):
print(f"Called from {hex(xref.from_ea)}")XrefInfopython
XrefInfo(
from_ea: int,
to_ea: int,
is_code: bool,
type: XrefType,
user: bool,
)python
for xref in db.xrefs.to_ea(target_addr):
print(f"引用自 {hex(xref.from_ea)} (类型: {xref.type.name})")
for xref in db.xrefs.from_ea(source_addr):
print(f"引用地址 {hex(xref.to_ea)}")
for xref in db.xrefs.calls_to_ea(func_addr):
print(f"调用自 {hex(xref.from_ea)}")XrefInfopython
XrefInfo(
from_ea: int,
to_ea: int,
is_code: bool,
type: XrefType,
user: bool,
)Read data
读取数据
python
db.bytes.get_byte_at(addr)
db.bytes.get_bytes_at(addr)
db.bytes.get_cstring_at(addr)
db.bytes.get_word_at(addr)
db.bytes.get_dword_at(addr)
db.bytes.get_qword_at(addr)
db.bytes.get_disassembly_at(addr)
db.bytes.get_flags_at(addr)python
db.bytes.get_byte_at(addr)
db.bytes.get_bytes_at(addr)
db.bytes.get_cstring_at(addr)
db.bytes.get_word_at(addr)
db.bytes.get_dword_at(addr)
db.bytes.get_qword_at(addr)
db.bytes.get_disassembly_at(addr)
db.bytes.get_flags_at(addr)Plugin Manager Integration
插件管理器集成
Plugins must be compatible with the Hex-Rays Plugin Manager.
Making your plugin available via Plugin Manager offers several benefits:
- simplified plugin installation
- improved plugin discoverability through the central index
- easy Python dependency management
The key points to make your IDA plugin available via Plugin Manager are:
- Add
ida-plugin.json - Package your plugin into a ZIP archive (via source archives or GitHub Actions)
- Publish releases on GitHub
A complete example:
ida-plugin.jsonjson
{
"IDAMetadataDescriptorVersion": 1,
"plugin": {
"name": "ida-terminal-plugin",
"entryPoint": "index.py",
"version": "1.0.0",
"idaVersions": ">=9.2",
"platforms": [
"windows-x86_64",
"linux-x86_64",
"macos-x86_64",
"macos-aarch64",
],
"description": "A lightweight terminal integration for IDA Pro that lets you open a fully functional terminal within the IDA GUI.\nQuickly access shell commands, scripts, or tooling without leaving your reversing environment.",
"license": "MIT",
"logoPath": "ida-plugin.png",
"categories": [
"ui-ux-and-visualization"
],
"keywords": [
"terminal",
"shell",
"cli",
],
"pythonDependencies": [
"pydantic>=2.12"
],
"urls": {
"repository": "https://github.com/williballenthin/idawilli"
},
"authors": [{
"name": "Willi Ballenthin",
"email": "wballenthin@hex-rays.com"
}],
"settings": [
{
"key": "theme",
"type": "string",
"required": true,
"default": "darcula",
"name": "color theme",
"documentation": "the color theme name, picked from https://windowsterminalthemes.dev/",
}
]
}
}Before completing your work, review the following resources for packaging hints:
- https://hcli.docs.hex-rays.com/reference/plugin-repository-architecture/
- https://hcli.docs.hex-rays.com/reference/plugin-packaging-and-format/
- https://hcli.docs.hex-rays.com/reference/packaging-your-existing-plugin/
Use the script to invoke HCLI in a consistent way and lint the current plugin.
./scripts/hcli-package.py插件必须兼容Hex-Rays插件管理器。
通过插件管理器发布插件有诸多优势:
- 简化插件安装流程
- 通过中央索引提升插件可发现性
- 便捷的Python依赖管理
让IDA插件兼容插件管理器的核心要点:
- 添加配置文件
ida-plugin.json - 将插件打包为ZIP归档(通过源码归档或GitHub Actions)
- 在GitHub上发布版本
完整的示例:
ida-plugin.jsonjson
{
"IDAMetadataDescriptorVersion": 1,
"plugin": {
"name": "ida-terminal-plugin",
"entryPoint": "index.py",
"version": "1.0.0",
"idaVersions": ">=9.2",
"platforms": [
"windows-x86_64",
"linux-x86_64",
"macos-x86_64",
"macos-aarch64",
],
"description": "A lightweight terminal integration for IDA Pro that lets you open a fully functional terminal within the IDA GUI.\nQuickly access shell commands, scripts, or tooling without leaving your reversing environment.",
"license": "MIT",
"logoPath": "ida-plugin.png",
"categories": [
"ui-ux-and-visualization"
],
"keywords": [
"terminal",
"shell",
"cli",
],
"pythonDependencies": [
"pydantic>=2.12"
],
"urls": {
"repository": "https://github.com/williballenthin/idawilli"
},
"authors": [{
"name": "Willi Ballenthin",
"email": "wballenthin@hex-rays.com"
}],
"settings": [
{
"key": "theme",
"type": "string",
"required": true,
"default": "darcula",
"name": "color theme",
"documentation": "the color theme name, picked from https://windowsterminalthemes.dev/",
}
]
}
}完成开发前,可参考以下资源获取打包提示:
- https://hcli.docs.hex-rays.com/reference/plugin-repository-architecture/
- https://hcli.docs.hex-rays.com/reference/plugin-packaging-and-format/
- https://hcli.docs.hex-rays.com/reference/packaging-your-existing-plugin/
使用脚本以一致方式调用HCLI并检查当前插件。
./scripts/hcli-package.pyUse ida-settings for configuration values
使用ida-settings管理配置值
ida-settings is a Python library used by IDA Pro plugins to fetch configuration values from the shared settings infrastructure.
During plugin installation, the plugin manager prompts users for the configuration values and stores them in .
Subsequently, users can invoke HCLI (or later, the IDA Pro GUI) to update their configuration.
ida-settings is the library that plugins use to fetch the configuration values.
ida-config.jsonFor example:
python
import ida_settings
api_key = ida_settings.get_current_plugin_setting("openai_key")Note that this must be called from within the plugin ( or ), not a callback or hook;
capture an instance of the plugin settings and pass it around as necessary:
plugin_tplugmod_tpython
class Hooks(idaapi.IDP_Hooks):
def __init__(self, settings):
super().__init__()
self.settings = settings
def ev_get_bg_color(self, color, ea):
mnem = ida_ua.print_insn_mnem(ea)
if mnem == "call" or mnem == "CALL":
bgcolor = ctypes.cast(int(color), ctypes.POINTER(ctypes.c_int))
bgcolor[0] = int(settings.get_setting("bg_color"))
return 1
else:
return 0
class FooPluginMod(ida_idaapi.plugmod_t):
def run(self, arg):
settings = ida_settings.get_current_plugin_settings()
self.hooks = Hooks(settings)
self.hooks.hook()
Available APIs are:
del(_current)_plugin_settingget(_current)_plugin_settinghas(_current)_plugin_settingset(_current)_plugin_settinglist(_current)_plugin_settings
ida-settings是IDA Pro插件用于从共享设置基础设施获取配置值的Python库。
插件安装时,插件管理器会提示用户输入配置值并存储到中。后续用户可调用HCLI(或未来的IDA Pro GUI)更新配置。插件通过ida-settings库获取配置值。
ida-config.json示例:
python
import ida_settings
api_key = ida_settings.get_current_plugin_setting("openai_key")注意此调用必须在插件内部(或)执行,而非回调或钩子中;需捕获插件设置实例并按需传递:
plugin_tplugmod_tpython
class Hooks(idaapi.IDP_Hooks):
def __init__(self, settings):
super().__init__()
self.settings = settings
def ev_get_bg_color(self, color, ea):
mnem = ida_ua.print_insn_mnem(ea)
if mnem == "call" or mnem == "CALL":
bgcolor = ctypes.cast(int(color), ctypes.POINTER(ctypes.c_int))
bgcolor[0] = int(settings.get_setting("bg_color"))
return 1
else:
return 0
class FooPluginMod(ida_idaapi.plugmod_t):
def run(self, arg):
settings = ida_settings.get_current_plugin_settings()
self.hooks = Hooks(settings)
self.hooks.hook()
可用API:
del(_current)_plugin_settingget(_current)_plugin_settinghas(_current)_plugin_settingset(_current)_plugin_settinglist(_current)_plugin_settings
Use standard logging module
使用标准logging模块
Don't use for status messages - use routines.
Do not configure logging from within a plugin - its up to the user to
configure which levels and sources they want to see in their output window.
printlogging.*不要使用输出状态信息,应使用方法。不要在插件内部配置日志,日志的级别和输出源应由用户自行配置。
printlogging.*Plugin Entry Point
插件入口点
The entrypoint of the plugin should be
which imports from only if the environment is correct.
foo_entry.pyfoo.pyIf the plugin runs in all IDA environments (assuming dependencies are present, which is reasonable),
then you don't need a special wrapper like this.
For example, if the plugin requires Qt and/or IDA to be running graphically, you could do something like:
foo_entry.pypython
import logging
import os
import ida_kernwin
logger = logging.getLogger(__name__)
def should_load():
"""Returns True if IDA 9.2+ is running interactively."""
if not ida_kernwin.is_idaq():
# https://community.hex-rays.com/t/how-to-check-if-idapythonrc-py-is-running-in-ida-pro-or-idalib/297/4
return False
if os.environ.get("IDA_IS_INTERACTIVE") != "1":
# https://community.hex-rays.com/t/how-to-check-if-idapythonrc-py-is-running-in-ida-pro-or-idalib/297/2
return False
kernel_version: tuple[int, ...] = tuple(
int(part) for part in ida_kernwin.get_kernel_version().split(".") if part.isdigit()
) or (0,)
if kernel_version < (9, 2): # type: ignore
logger.warning("IDA too old (must be 9.2+): %s", ida_kernwin.get_kernel_version())
return False
return True
if should_load():
# only attempt to import the plugin once we know the required dependencies are present.
# otherwise we'll hit ImportError and other problems
from foo import foo_plugin_t
def PLUGIN_ENTRY():
return foo_plugin_t()
else:
try:
import ida_idaapi
except ImportError:
import idaapi as ida_idaapi
class foo_nop_plugin_t(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_HIDE | ida_idaapi.PLUGIN_UNL
wanted_name = "foo disabled"
comment = "foo is disabled for this IDA version"
help = ""
wanted_hotkey = ""
def init(self):
return ida_idaapi.PLUGIN_SKIP
# we have to define this symbol, or IDA logs a message
def PLUGIN_ENTRY():
# we have to return something here, or IDA logs a message
return foo_nop_plugin_t()foo.pypython
class foo_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
# IDA doesn't invoke this for plugmod_t, only plugin_t
self.init()
def init(self):
# do things here that will always run,
# and don't require the menu entry (edit > plugins > ...) being selected.
#
# note: IDA doesn't call init, we do in __init__
if not ida_auto.auto_is_ok():
# don't capture events before auto-analysis is done, or we get all the system events.
#
# note:
# - when we first load a program, this plugin will be run before auto-analysis is complete
# (actually, before auto-analysis even starts).
# so auto_is_ok() returns False
# - when we load an existing IDB, auto_is_ok() return True.
# so we can safely use this to wait until auto-analysis is complete for the first time.
logger.debug("waiting for auto-analysis to complete before subscribing to events")
ida_auto.auto_wait()
logger.debug("auto-analysis complete, now subscribing to events")
...
def run(self, arg):
# do things here that users invoke via the menu entry (edit > plugins > ...)
...
def term(self):
# cleanup resources, unhook handlers, etc.
...
class foo_plugin_t(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_MULTI
help = "Do some foo"
comment = ""
wanted_name = "Foo"
wanted_hotkey = ""
def init(self):
return foo_plugmod_t()插件入口点应为,仅当环境符合要求时才从导入代码。
foo_entry.pyfoo.py如果插件可在所有IDA环境中运行(假设依赖已安装,这是合理的),则无需此类特殊包装。
例如,如果插件需要Qt和/或IDA以图形化方式运行,可按如下方式实现:
foo_entry.pypython
import logging
import os
import ida_kernwin
logger = logging.getLogger(__name__)
def should_load():
"""如果IDA 9.2+以交互模式运行,则返回True。"""
if not ida_kernwin.is_idaq():
# https://community.hex-rays.com/t/how-to-check-if-idapythonrc-py-is-running-in-ida-pro-or-idalib/297/4
return False
if os.environ.get("IDA_IS_INTERACTIVE") != "1":
# https://community.hex-rays.com/t/how-to-check-if-idapythonrc-py-is-running-in-ida-pro-or-idalib/297/2
return False
kernel_version: tuple[int, ...] = tuple(
int(part) for part in ida_kernwin.get_kernel_version().split(".") if part.isdigit()
) or (0,)
if kernel_version < (9, 2): # type: ignore
logger.warning("IDA版本过旧(需9.2+): %s", ida_kernwin.get_kernel_version())
return False
return True
if should_load():
# 仅在确认依赖满足后才尝试导入插件
# 否则会触发ImportError等问题
from foo import foo_plugin_t
def PLUGIN_ENTRY():
return foo_plugin_t()
else:
try:
import ida_idaapi
except ImportError:
import idaapi as ida_idaapi
class foo_nop_plugin_t(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_HIDE | ida_idaapi.PLUGIN_UNL
wanted_name = "foo disabled"
comment = "foo在当前IDA版本中不可用"
help = ""
wanted_hotkey = ""
def init(self):
return ida_idaapi.PLUGIN_SKIP
# 必须定义此符号,否则IDA会记录错误信息
def PLUGIN_ENTRY():
# 必须返回内容,否则IDA会记录错误信息
return foo_nop_plugin_t()foo.pypython
class foo_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
# IDA不会为plugmod_t调用此方法,仅为plugin_t调用
self.init()
def init(self):
# 在此执行始终需要运行的逻辑,
# 无需依赖菜单入口(编辑>插件>...)被选中。
#
# 注意:IDA不会调用init,我们在__init__中自行调用
if not ida_auto.auto_is_ok():
# 自动分析完成前不要捕获事件,否则会接收所有系统事件。
#
# 注意:
# - 首次加载程序时,插件会在自动分析完成前运行
# (实际上,自动分析甚至还未开始)。
# 因此auto_is_ok()返回False
# - 加载已有的IDB时,auto_is_ok()返回True。
# 因此可通过此方法安全等待首次自动分析完成。
logger.debug("等待自动分析完成后再订阅事件")
ida_auto.auto_wait()
logger.debug("自动分析完成,开始订阅事件")
...
def run(self, arg):
# 在此执行用户通过菜单入口(编辑>插件>...)触发的逻辑
...
def term(self):
# 清理资源、注销钩子等
...
class foo_plugin_t(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_MULTI
help = "执行Foo操作"
comment = ""
wanted_name = "Foo"
wanted_hotkey = ""
def init(self):
return foo_plugmod_t()Hook Registration
钩子注册
Create pairwise helper functions for registering/unregistering hooks,
and call these from /
inittermpython
class oplog_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
self.idb_hooks: IDBChangedHook | None = None
self.location_hooks: UILocationHook | None = None
...
def register_idb_hooks(self):
assert self.events is not None
self.idb_hooks = IDBChangedHook(self.events)
self.idb_hooks.hook()
def unregister_idb_hooks(self):
if self.idb_hooks:
self.idb_hooks.unhook()
def register_location_hooks(self):
assert self.events is not None
self.location_hooks = UILocationHook(self.events)
self.location_hooks.hook()
def unregister_location_hooks(self):
if self.location_hooks:
self.location_hooks.unhook()
def init(self):
...
self.register_idb_hooks()
self.register_location_hooks()
def run(self, arg):
...
def term(self):
# cleanup in reverse order
self.unregister_location_hooks()
self.unregister_idb_hooks()
...创建用于注册/注销钩子的成对辅助函数,并在/中调用这些函数
inittermpython
class oplog_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
self.idb_hooks: IDBChangedHook | None = None
self.location_hooks: UILocationHook | None = None
...
def register_idb_hooks(self):
assert self.events is not None
self.idb_hooks = IDBChangedHook(self.events)
self.idb_hooks.hook()
def unregister_idb_hooks(self):
if self.idb_hooks:
self.idb_hooks.unhook()
def register_location_hooks(self):
assert self.events is not None
self.location_hooks = UILocationHook(self.events)
self.location_hooks.hook()
def unregister_location_hooks(self):
if self.location_hooks:
self.location_hooks.unhook()
def init(self):
...
self.register_idb_hooks()
self.register_location_hooks()
def run(self, arg):
...
def term(self):
# 按相反顺序清理
self.unregister_location_hooks()
self.unregister_idb_hooks()
...Cross-Plugin Communication via IDC Functions
通过IDC函数实现跨插件通信
Python plugins can import shared libraries, and two plugins may even have the same dependencies. One plugin can import code from another plugin's module. However, to invoke functionality on a specific instance of a running plugin (accessing its state, calling methods that depend on instance data), you need a different mechanism.
Use to register a callable with a well-known name, and to invoke it from scripts or other plugins.
ida_expr.add_idc_funcidc.eval_idcKey constraints:
- The function name must be globally unique - only one plugin should register a given name
- There's only a single provider for that name (no multiple instances registering the same name)
- The registering plugin must unregister the function during
term()
python
import ida_expr
class foo_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
self.data: list[str] = []
self.init()
def register_idc_func(self):
data = self.data
def foo_get_data(index: int) -> str:
if 0 <= index < len(data):
return data[index]
return ""
def foo_add_data(value: str) -> int:
data.append(value)
return len(data)
if ida_expr.add_idc_func("foo_get_data", foo_get_data, (ida_expr.VT_LONG,)):
logger.debug("registered foo_get_data IDC function")
else:
logger.warning("failed to register foo_get_data IDC function")
if ida_expr.add_idc_func("foo_add_data", foo_add_data, (ida_expr.VT_STR,)):
logger.debug("registered foo_add_data IDC function")
else:
logger.warning("failed to register foo_add_data IDC function")
def unregister_idc_func(self):
ida_expr.del_idc_func("foo_get_data")
ida_expr.del_idc_func("foo_add_data")
def init(self):
self.register_idc_func()
def term(self):
self.unregister_idc_func()Callers invoke the function via :
idc.eval_idcpython
import idc
idc.eval_idc('foo_add_data("hello")')
result = idc.eval_idc('foo_get_data(0)')Parameter types for :
add_idc_func- - string parameter
ida_expr.VT_STR - - integer parameter
ida_expr.VT_LONG - - floating point parameter
ida_expr.VT_FLOAT
This pattern is useful for:
- Exporting plugin data to external scripts (headless testing, automation)
- Allowing one plugin to trigger actions in another
- Providing a stable API for plugin functionality that doesn't depend on Python imports
Python插件可以导入共享库,两个插件甚至可能有相同的依赖。一个插件可以从另一个插件的模块导入代码。但是,要调用正在运行的插件特定实例的功能(访问其状态、调用依赖实例数据的方法),则需要不同的机制。
使用注册一个具有知名名称的可调用对象,使用从脚本或其他插件中调用它。
ida_expr.add_idc_funcidc.eval_idc关键约束:
- 函数名称必须全局唯一 - 仅一个插件应注册给定名称
- 每个名称只能有一个提供者(不允许多个实例注册同一名称)
- 注册插件必须在期间注销该函数
term()
python
import ida_expr
class foo_plugmod_t(ida_idaapi.plugmod_t):
def __init__(self):
self.data: list[str] = []
self.init()
def register_idc_func(self):
data = self.data
def foo_get_data(index: int) -> str:
if 0 <= index < len(data):
return data[index]
return ""
def foo_add_data(value: str) -> int:
data.append(value)
return len(data)
if ida_expr.add_idc_func("foo_get_data", foo_get_data, (ida_expr.VT_LONG,)):
logger.debug("已注册foo_get_data IDC函数")
else:
logger.warning("注册foo_get_data IDC函数失败")
if ida_expr.add_idc_func("foo_add_data", foo_add_data, (ida_expr.VT_STR,)):
logger.debug("已注册foo_add_data IDC函数")
else:
logger.warning("注册foo_add_data IDC函数失败")
def unregister_idc_func(self):
ida_expr.del_idc_func("foo_get_data")
ida_expr.del_idc_func("foo_add_data")
def init(self):
self.register_idc_func()
def term(self):
self.unregister_idc_func()调用方通过调用函数:
idc.eval_idcpython
import idc
idc.eval_idc('foo_add_data("hello")')
result = idc.eval_idc('foo_get_data(0)')add_idc_func- - 字符串参数
ida_expr.VT_STR - - 整数参数
ida_expr.VT_LONG - - 浮点参数
ida_expr.VT_FLOAT
此模式适用于:
- 将插件数据导出到外部脚本(无头测试、自动化)
- 允许一个插件触发另一个插件的操作
- 提供不依赖Python导入的稳定插件功能API
Save/Load state from netnodes
从netnodes保存/加载状态
Use netnodes to store data within the IDB.
Serialize the current plugin state during shutdown, saving it to a netnode.
Reload the state upon startup.
python
import pydantic
OUR_NETNODE = "$ com.williballenthin.idawilli.foo"
class State(pydantic.BaseModel):
...
def to_json(self):
return self.model_dump_json()
@classmethod
def from_json(cls, json_str: str):
return cls(State.model_validate_json(json_str))
def save_state(state: State):
buf = zlib.compress(state.to_json().encode("utf-8"))
node = ida_netnode.netnode(OUR_NETNODE)
node.setblob(buf, 0, "I")
logger.info("saved state")
def load_state() -> State:
node = ida_netnode.netnode(OUR_NETNODE)
if not node:
logger.info("no existing state")
return State()
buf = node.getblob(0, "I")
if not buf:
logger.info("no existing state (no data)")
return State()
state = State.from_json(zlib.decompress(buf).decode("utf-8"))
logger.info("loaded state")
return state
class UI_Closing_Hooks(ida_kernwin.UI_Hooks):
"""Respond to UI events and save the events into the database."""
# we could also use IDB_Hooks, but I found it less reliable:
# - closebase: "the database will be closed now", however, I couldn't figure out when its actually triggered.
# - savebase: notified during File -> Save, but not File -> Close.
# easier to keep all the hooks in one place.
def __init__(self, events: Events, *args, **kwargs):
super().__init__(*args, **kwargs)
self.events = events
def preprocess_action(self, action: str):
if action == "CloseBase":
# File -> Close
save_events(self.events)
return 0
elif action == "QuitIDA":
# File -> Quit
save_events(self.events)
return 0
elif action == "SaveBase":
# File -> Save
save_events(self.events)
return 0
else:
return 0使用netnodes在IDB中存储数据。在关闭时序列化当前插件状态并保存到netnodes中,启动时重新加载状态。
python
import pydantic
OUR_NETNODE = "$ com.williballenthin.idawilli.foo"
class State(pydantic.BaseModel):
...
def to_json(self):
return self.model_dump_json()
@classmethod
def from_json(cls, json_str: str):
return cls(State.model_validate_json(json_str))
def save_state(state: State):
buf = zlib.compress(state.to_json().encode("utf-8"))
node = ida_netnode.netnode(OUR_NETNODE)
node.setblob(buf, 0, "I")
logger.info("已保存状态")
def load_state() -> State:
node = ida_netnode.netnode(OUR_NETNODE)
if not node:
logger.info("无现有状态")
return State()
buf = node.getblob(0, "I")
if not buf:
logger.info("无现有状态(无数据)")
return State()
state = State.from_json(zlib.decompress(buf).decode("utf-8"))
logger.info("已加载状态")
return state
class UI_Closing_Hooks(ida_kernwin.UI_Hooks):
"""响应UI事件并将事件保存到数据库中。"""
# 我们也可以使用IDB_Hooks,但我发现它不太可靠:
# - closebase: "数据库即将关闭",但我无法确定它实际触发的时机。
# - savebase: 在文件>保存时触发,但文件>关闭时不触发。
# 因此将所有钩子放在一处更简单。
def __init__(self, events: Events, *args, **kwargs):
super().__init__(*args, **kwargs)
self.events = events
def preprocess_action(self, action: str):
if action == "CloseBase":
# 文件>关闭
save_events(self.events)
return 0
elif action == "QuitIDA":
# 文件>退出
save_events(self.events)
return 0
elif action == "SaveBase":
# 文件>保存
save_events(self.events)
return 0
else:
return 0Respond to current address and selection change
响应当前地址和选择变更
python
class UILocationHook(ida_kernwin.UI_Hooks):
def handle_current_address_change(self, ea: int):
...
def handle_current_selection_change(self, start: int, end: int):
...
def screen_ea_changed(self, ea: ida_idaapi.ea_t, prev_ea: ida_idaapi.ea_t) -> None:
if ea == prev_ea:
return
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
# BWN_PSEUDOCODE
# BWN_CUSTVIEW
# BWN_OUTPUT the text area, in the output window
# BWN_CLI the command-line, in the output window
# BWN_STRINGS
# ...
):
return
if ida_kernwin.get_viewer_place_type(v) != ida_kernwin.TCCPT_IDAPLACE:
# other viewers might have other place types, when not address-oriented
return
has_range, start, end = ida_kernwin.read_range_selection(v)
if not has_range:
return self.handle_current_address_change(ea)
if ida_idaapi.BADADDR in (start, end):
return
return self.handle_current_selection_change(start, end)python
class UILocationHook(ida_kernwin.UI_Hooks):
def handle_current_address_change(self, ea: int):
...
def handle_current_selection_change(self, start: int, end: int):
...
def screen_ea_changed(self, ea: ida_idaapi.ea_t, prev_ea: ida_idaapi.ea_t) -> None:
if ea == prev_ea:
return
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
# BWN_PSEUDOCODE
# BWN_CUSTVIEW
# BWN_OUTPUT 输出窗口中的文本区域
# BWN_CLI 输出窗口中的命令行
# BWN_STRINGS
# ...
):
return
if ida_kernwin.get_viewer_place_type(v) != ida_kernwin.TCCPT_IDAPLACE:
# 其他查看器可能有不同的位置类型,非地址导向
return
has_range, start, end = ida_kernwin.read_range_selection(v)
if not has_range:
return self.handle_current_address_change(ea)
if ida_idaapi.BADADDR in (start, end):
return
return self.handle_current_selection_change(start, end)Find widgets by prefix
按前缀查找部件
python
def list_widgets(prefix: str) -> list[str]:
"""Probe A-Z for existing widgets, return found captions.
Args:
prefix: Caption prefix to search for
Returns: List of found widget captions (e.g., ["Foo-A", "Foo-C"])
"""
if not prefix.endswith("-"):
raise ValueError("prefix must end with dash")
found = []
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
caption = f"{prefix}{letter}"
if ida_kernwin.find_widget(caption) is not None:
found.append(caption)
return found
def find_next_available_caption(prefix: str) -> str:
"""Find first gap or next letter for widget caption.
Args:
prefix: Caption prefix to use
Returns: First available caption (e.g., "Foo-B")
Raises:
RuntimeError: If all 26 instances are in use
"""
if not prefix.endswith("-"):
raise ValueError("prefix must end with dash")
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
caption = f"{prefix}{letter}"
if ida_kernwin.find_widget(caption) is None:
return caption
raise RuntimeError("All 26 instances in use")python
def list_widgets(prefix: str) -> list[str]:
"""遍历A-Z查找现有部件,返回找到的标题。
参数:
prefix: 要搜索的标题前缀
返回: 找到的部件标题列表(例如: ["Foo-A", "Foo-C"])
"""
if not prefix.endswith("-"):
raise ValueError("前缀必须以连字符结尾")
found = []
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
caption = f"{prefix}{letter}"
if ida_kernwin.find_widget(caption) is not None:
found.append(caption)
return found
def find_next_available_caption(prefix: str) -> str:
"""查找部件标题的首个可用间隙或下一个字母。
参数:
prefix: 要使用的标题前缀
返回: 首个可用标题(例如: "Foo-B")
异常:
RuntimeError: 所有26个实例均已被使用
"""
if not prefix.endswith("-"):
raise ValueError("前缀必须以连字符结尾")
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
caption = f"{prefix}{letter}"
if ida_kernwin.find_widget(caption) is None:
return caption
raise RuntimeError("所有26个实例均已被使用")Context Menu Entries, "Send to Foo" and "Send to Foo-A"
上下文菜单条目:"发送至Foo"和"发送至Foo-A"
When creating custom views, especially when there might be more than one,
name them like "Foo-A", "Foo-B", etc.
And, as appropriate, add context menu items for "sending" addresses/selections
to the new views.
The new view is an instance of and may have arbitrary Qt widgets.
The plugin instance maintains a registry of created views, and registers the action
handlers for opening new views, as well as notifying the views of events from a central place.
Action handlers encapsulate the code that's invoked during an event.
ida_kernwin.PluginFormpython
class FooForm(ida_kernwin.PluginForm):
def __init__(
self,
caption: str = "Foo-A",
form_registry: dict[str, "FooForm"] | None = None,
) -> None:
super().__init__()
self.TITLE = caption
self.form_registry = form_registry
def OnCreate(self, form):
self.parent = self.FormToPyQtWidget(form)
self.w = FooWidget(parent=self.parent, show_ida_buttons=True)
... # other Qt stuff here
if self.form_registry is not None:
self.form_registry[self.TITLE] = self
def OnClose(self, form):
if self.form_registry is not None:
self.form_registry.pop(self.TITLE, None)
class create_foo_widget_action_handler_t(ida_kernwin.action_handler_t):
def __init__(self, plugmod: "foo_plugmod_t", *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.plugmod = plugmod
def activate(self, ctx):
self.plugmod.create_viewer()
def update(self, ctx):
return ida_kernwin.AST_ENABLE_ALWAYS
class send_to_foo_action_handler_t(ida_kernwin.action_handler_t):
"""Action handler for 'Send to Foo' context menu item."""
def __init__(self, plugmod: "foo_plugmod_t", *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.plugmod = plugmod
def activate(self, ctx):
"""Handle 'Send to Foo' action - always creates new instance."""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# for example: only allow sending from hexview or disassembly view
return 0
form = self.plugmod.create_viewer()
if form and form.w:
... # do initialization
return 1
def update(self, ctx):
"""Enable action when there's a valid selection."""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# for example: only allow sending from hexview or disassembly view
return ida_kernwin.AST_DISABLE
return ida_kernwin.AST_ENABLE
class send_to_specific_widget_action_handler_t(ida_kernwin.action_handler_t):
"""Action handler for sending to a specific Foo instance."""
def __init__(
self,
form_registry: dict[str, FooForm],
caption: str,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.form_registry = form_registry
self.caption = caption
def activate(self, ctx):
"""Send selection to specific Foo instance."""
v = ida_kernwin.get_current_viewer()
widget = ida_kernwin.find_widget(self.caption)
if widget is None:
logger.warning(f"Widget {self.caption} not found")
return 0
ida_kernwin.activate_widget(widget, True)
form = self.form_registry.get(self.caption)
if form and hasattr(form, "w"):
# access some specific model methods on the form
...
else:
logger.warning(f"Cannot populate {self.caption} - unable to access form")
return 1
def update(self, ctx):
"""Enable action when there's a valid selection."""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# for example: only allow sending from hexview or disassembly view
return ida_kernwin.AST_DISABLE
return ida_kernwin.AST_ENABLE
class foo_plugmod_t(ida_idaapi.plugmod_t):
ACTION_NAME = "foo:create"
SEND_ACTION_NAME = "foo:send_selection"
MENU_PATH = "View/Open subviews/Foo"
def __init__(self):
super().__init__()
self.form_registry: dict[str, FooForm] = {}
...
def register_instance_actions(self):
"""Register actions for all existing widget instances."""
existing = list_widgets("Foo-")
for caption in existing:
action_name = f"foo:send_to_{caption.replace('-', '_').lower()}"
if ida_kernwin.unregister_action(action_name):
pass
ida_kernwin.register_action(
ida_kernwin.action_desc_t(
action_name,
f"Send to {caption}",
send_to_specific_widget_action_handler_t(
self.form_registry, caption
),
None,
f"Send selected bytes to {caption}",
-1,
)
)
def create_viewer(self, caption: str | None = None) -> FooForm:
if caption is None:
caption = find_next_available_caption()
form = FooForm(caption, self.form_registry)
form.Show(form.TITLE)
return form
def register_open_action(self):
ida_kernwin.register_action(
ida_kernwin.action_desc_t(
self.ACTION_NAME,
"Foo",
create_foo_widget_action_handler_t(self),
)
)
# TODO: add icon
ida_kernwin.attach_action_to_menu(
self.MENU_PATH, self.ACTION_NAME, ida_kernwin.SETMENU_APP
)
def unregister_open_action(self):
ida_kernwin.unregister_action(self.ACTION_NAME)
ida_kernwin.detach_action_from_menu(self.MENU_PATH, self.ACTION_NAME)
def init(self):
self.register_open_action()
...
def run(self, arg):
self.create_viewer()
def term(self):
...
self.unregister_open_action()创建自定义查看器时,尤其是可能存在多个实例的情况下,应将其命名为"Foo-A"、"Foo-B"等格式。并酌情添加上下文菜单项,用于将地址/选择内容"发送"到新的查看器中。
新查看器是的实例,可包含任意Qt部件。插件实例维护已创建查看器的注册表,并注册用于打开新查看器的动作处理器,同时从中心位置向查看器通知事件。动作处理器封装了事件触发时执行的代码。
ida_kernwin.PluginFormpython
class FooForm(ida_kernwin.PluginForm):
def __init__(
self,
caption: str = "Foo-A",
form_registry: dict[str, "FooForm"] | None = None,
) -> None:
super().__init__()
self.TITLE = caption
self.form_registry = form_registry
def OnCreate(self, form):
self.parent = self.FormToPyQtWidget(form)
self.w = FooWidget(parent=self.parent, show_ida_buttons=True)
... # 其他Qt相关代码
if self.form_registry is not None:
self.form_registry[self.TITLE] = self
def OnClose(self, form):
if self.form_registry is not None:
self.form_registry.pop(self.TITLE, None)
class create_foo_widget_action_handler_t(ida_kernwin.action_handler_t):
def __init__(self, plugmod: "foo_plugmod_t", *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.plugmod = plugmod
def activate(self, ctx):
self.plugmod.create_viewer()
def update(self, ctx):
return ida_kernwin.AST_ENABLE_ALWAYS
class send_to_foo_action_handler_t(ida_kernwin.action_handler_t):
"""'发送至Foo'上下文菜单项的动作处理器。"""
def __init__(self, plugmod: "foo_plugmod_t", *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.plugmod = plugmod
def activate(self, ctx):
"""处理'发送至Foo'动作 - 始终创建新实例。"""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# 例如:仅允许从十六进制查看器或反汇编查看器发送
return 0
form = self.plugmod.create_viewer()
if form and form.w:
... # 执行初始化操作
return 1
def update(self, ctx):
"""当存在有效选择时启用动作。"""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# 例如:仅允许从十六进制查看器或反汇编查看器发送
return ida_kernwin.AST_DISABLE
return ida_kernwin.AST_ENABLE
class send_to_specific_widget_action_handler_t(ida_kernwin.action_handler_t):
"""发送至特定Foo实例的动作处理器。"""
def __init__(
self,
form_registry: dict[str, FooForm],
caption: str,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.form_registry = form_registry
self.caption = caption
def activate(self, ctx):
"""将选择内容发送至特定Foo实例。"""
v = ida_kernwin.get_current_viewer()
widget = ida_kernwin.find_widget(self.caption)
if widget is None:
logger.warning(f"未找到部件 {self.caption}")
return 0
ida_kernwin.activate_widget(widget, True)
form = self.form_registry.get(self.caption)
if form and hasattr(form, "w"):
# 访问表单上的特定模型方法
...
else:
logger.warning(f"无法填充 {self.caption} - 无法访问表单")
return 1
def update(self, ctx):
"""当存在有效选择时启用动作。"""
v = ida_kernwin.get_current_viewer()
if ida_kernwin.get_widget_type(v) not in (
ida_kernwin.BWN_HEXVIEW,
ida_kernwin.BWN_DISASM,
):
# 例如:仅允许从十六进制查看器或反汇编查看器发送
return ida_kernwin.AST_DISABLE
return ida_kernwin.AST_ENABLE
class foo_plugmod_t(ida_idaapi.plugmod_t):
ACTION_NAME = "foo:create"
SEND_ACTION_NAME = "foo:send_selection"
MENU_PATH = "View/Open subviews/Foo"
def __init__(self):
super().__init__()
self.form_registry: dict[str, FooForm] = {}
...
def register_instance_actions(self):
"""为所有现有部件实例注册动作。"""
existing = list_widgets("Foo-")
for caption in existing:
action_name = f"foo:send_to_{caption.replace('-', '_').lower()}"
if ida_kernwin.unregister_action(action_name):
pass
ida_kernwin.register_action(
ida_kernwin.action_desc_t(
action_name,
f"发送至 {caption}",
send_to_specific_widget_action_handler_t(
self.form_registry, caption
),
None,
f"将选中字节发送至 {caption}",
-1,
)
)
def create_viewer(self, caption: str | None = None) -> FooForm:
if caption is None:
caption = find_next_available_caption()
form = FooForm(caption, self.form_registry)
form.Show(form.TITLE)
return form
def register_open_action(self):
ida_kernwin.register_action(
ida_kernwin.action_desc_t(
self.ACTION_NAME,
"Foo",
create_foo_widget_action_handler_t(self),
)
)
# TODO: 添加图标
ida_kernwin.attach_action_to_menu(
self.MENU_PATH, self.ACTION_NAME, ida_kernwin.SETMENU_APP
)
def unregister_open_action(self):
ida_kernwin.unregister_action(self.ACTION_NAME)
ida_kernwin.detach_action_from_menu(self.MENU_PATH, self.ACTION_NAME)
def init(self):
self.register_open_action()
...
def run(self, arg):
self.create_viewer()
def term(self):
...
self.unregister_open_action()User Defined Prefix
用户自定义前缀
A user defined prefix is a great way to add some contextual data before each disassembly line.
Put symbols or numbers here to indicate there's more context available somewhere.
python
def refresh_disassembly():
ida_kernwin.request_refresh(ida_kernwin.IWID_DISASM)
class FooPrefix(ida_lines.user_defined_prefix_t):
ICON = " β "
def __init__(self, marks: set[int]):
super().__init__(len(self.ICON))
self.marks = marks
def get_user_defined_prefix(self, ea, insn, lnnum, indent, line):
if ea in self.marks:
# wrap the icon in color tags so its easy to identify.
# otherwise, the icon may merge with other spans, which
# makes checking for equality more difficult.
return ida_lines.COLSTR(self.ICON, ida_lines.SCOLOR_SYMBOL)
return " " * len(self.ICON)
class FooPrefixPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.marks: set[int] = {1, 2, 3}
self.prefixer: FooPrefix | None = None
def run(self, arg):
# self.prefixer is installed simply by constructing it
self.prefixer = FooPrefix(self.marks)
# since we're updating the disassembly listing by adding the line prefix,
# we need to re-render all the lines.
refresh_disassembly()
def term(self):
# gc will clean up prefixer and uninstall it (during plugin termination)
self.prefixer = None
# refresh and remove the prefix entries
refresh_disassembly()用户自定义前缀是在每条反汇编行前添加上下文信息的绝佳方式。可在此处添加符号或数字,以表明存在更多上下文信息。
python
def refresh_disassembly():
ida_kernwin.request_refresh(ida_kernwin.IWID_DISASM)
class FooPrefix(ida_lines.user_defined_prefix_t):
ICON = " β "
def __init__(self, marks: set[int]):
super().__init__(len(self.ICON))
self.marks = marks
def get_user_defined_prefix(self, ea, insn, lnnum, indent, line):
if ea in self.marks:
# 将图标用颜色标签包裹,便于识别。
# 否则图标可能与其他内容合并,导致难以检查是否相等。
return ida_lines.COLSTR(self.ICON, ida_lines.SCOLOR_SYMBOL)
return " " * len(self.ICON)
class FooPrefixPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.marks: set[int] = {1, 2, 3}
self.prefixer: FooPrefix | None = None
def run(self, arg):
# 只需构造prefixer即可完成安装
self.prefixer = FooPrefix(self.marks)
# 由于我们通过添加行前缀更新反汇编列表,
# 需要重新渲染所有行。
refresh_disassembly()
def term(self):
# 垃圾回收会清理prefixer并完成卸载(在插件终止期间)
self.prefixer = None
# 刷新并移除前缀条目
refresh_disassembly()Viewer Hints
查看器提示
A view hint is a really good way to display complex information in a popup hover pane
that displays when mousing over particular regions of an IDA view.
Use this to show context about a symbol or address, for example: MSDN documentation for API functions.
Use this in combination with User Defined Prefixes that indicate context is available and
show the context in the viewer hint (possibly when hovering over the prefix).
python
class FooHints(ida_kernwin.UI_Hooks):
def __init__(self, notes: dict[int, str], *args, **kwargs):
super().__init__(*args, **kwargs)
self.notes = notes
def get_custom_viewer_hint(self, viewer, place):
if not place:
return
ea = place.toea()
if not ea:
return
if ea not in self.notes:
return
curline = ida_kernwin.get_custom_viewer_curline(viewer, True)
curline = ida_lines.tag_remove(curline)
_, x, _ = ida_kernwin.get_custom_viewer_place(viewer, True)
# example: show on first column
# more advanced: inspect the symbol, and if it matches a query, then show some data
if x == 1:
note = self.notes.get(ea)
if not note:
return
return (f"note: {note}", 1)
class FooHintsPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.notes: dict[int, str] = {}
self.hinter: FooHints | None = None
def run(self, arg):
self.hinter = FooHints(self.notes)
self.hinter.hook()
def term(self):
if self.hinter is not None:
self.hinter.unhook()
self.hinter = None查看器提示是在IDA视图特定区域鼠标悬停时显示复杂信息的弹窗的绝佳方式。可用于显示符号或地址的上下文信息,例如API函数的MSDN文档。
可结合用户自定义前缀使用:前缀表明存在上下文信息,查看器提示则显示具体上下文(可能在悬停前缀时触发)。
python
class FooHints(ida_kernwin.UI_Hooks):
def __init__(self, notes: dict[int, str], *args, **kwargs):
super().__init__(*args, **kwargs)
self.notes = notes
def get_custom_viewer_hint(self, viewer, place):
if not place:
return
ea = place.toea()
if not ea:
return
if ea not in self.notes:
return
curline = ida_kernwin.get_custom_viewer_curline(viewer, True)
curline = ida_lines.tag_remove(curline)
_, x, _ = ida_kernwin.get_custom_viewer_place(viewer, True)
# 示例:在第一列显示
# 更高级的用法:检查符号,如果匹配查询则显示相关数据
if x == 1:
note = self.notes.get(ea)
if not note:
return
return (f"备注: {note}", 1)
class FooHintsPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.notes: dict[int, str] = {}
self.hinter: FooHints | None = None
def run(self, arg):
self.hinter = FooHints(self.notes)
self.hinter.hook()
def term(self):
if self.hinter is not None:
self.hinter.unhook()
self.hinter = NoneOverriding rendering
覆盖渲染逻辑
python
class ColorHooks(idaapi.IDP_Hooks):
def ev_get_bg_color(self, color, ea):
"""
Get item background color.
Plugins can hook this callback to color disassembly lines dynamically
// background color in RGB
typedef uint32 bgcolor_t;
ref: https://hex-rays.com/products/ida/support/sdkdoc/pro_8h.html#a3df5040891132e50157aee66affdf1de
args:
color: (bgcolor_t *), out
ea: (::ea_t)
returns:
retval 0: not implemented
retval 1: color set
"""
mnem = ida_ua.print_insn_mnem(ea)
if mnem == "call" or mnem == "CALL":
bgcolor = ctypes.cast(int(color), ctypes.POINTER(ctypes.c_int))
bgcolor[0] = 0xDDDDDD
return 1
else:
return 0
def ev_out_mnem(self, ctx) -> int:
"""
Generate instruction mnemonics.
This callback should append the colored mnemonics to ctx.outbuf
Optional notification, if absent, out_mnem will be called.
args:
ctx: (outctx_t *)
returns:
retval 1: if appended the mnemonics
retval 0: not implemented
"""
mnem = ctx.insn.get_canon_mnem()
if mnem == "call":
# you can manipulate this, but note that it affects `ida_ua.print_insn_mnem` which is inconvenient for formatting.
# also, you only have access to theme colors, like COLOR_PREFIX, not arbitrary control.
ctx.out_custom_mnem("CALL")
return 1
else:
return 0
class ColoringPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.hooks: ColorHooks | None = None
def run(self, arg):
self.hooks = ColorHooks()
self.hooks.hook()
def term(self):
if self.hooks is not None:
self.hooks.unhook()
self.hooks = Nonepython
class ColorHooks(idaapi.IDP_Hooks):
def ev_get_bg_color(self, color, ea):
"""
获取项背景颜色。
插件可挂钩此回调来动态为反汇编行着色
// 背景颜色为RGB格式
typedef uint32 bgcolor_t;
参考: https://hex-rays.com/products/ida/support/sdkdoc/pro_8h.html#a3df5040891132e50157aee66affdf1de
参数:
color: (bgcolor_t *), 输出参数
ea: (::ea_t)
返回:
返回值0: 未实现
返回值1: 已设置颜色
"""
mnem = ida_ua.print_insn_mnem(ea)
if mnem == "call" or mnem == "CALL":
bgcolor = ctypes.cast(int(color), ctypes.POINTER(ctypes.c_int))
bgcolor[0] = 0xDDDDDD
return 1
else:
return 0
def ev_out_mnem(self, ctx) -> int:
"""
生成指令助记符。
此回调应将带颜色的助记符追加到ctx.outbuf中
可选通知,如果未实现,则会调用out_mnem。
参数:
ctx: (outctx_t *)
返回:
返回值1: 已追加助记符
返回值0: 未实现
"""
mnem = ctx.insn.get_canon_mnem()
if mnem == "call":
# 可对此进行修改,但注意这会影响`ida_ua.print_insn_mnem`,不利于格式化。
# 此外,你只能访问主题颜色,如COLOR_PREFIX,无法随意控制。
ctx.out_custom_mnem("CALL")
return 1
else:
return 0
class ColoringPluginMod(ida_idaapi.plugmod_t):
def __init__(self):
self.hooks: ColorHooks | None = None
def run(self, arg):
self.hooks = ColorHooks()
self.hooks.hook()
def term(self):
if self.hooks is not None:
self.hooks.unhook()
self.hooks = NoneCustom Viewers
自定义查看器
Use a custom viewer to show text data, optionally with tags, and respond to basic events (clicks).
Use the tagged line concepts to embed and parse metadata about the symbols in a line,
such as which address it refers to.
python
def addr_from_tag(raw: bytes) -> int:
assert raw[0] == 0x01 # ida_lines.COLOR_ON
assert raw[1] == ida_lines.COLOR_ADDR
addr_hex = raw[2 : 2 + ida_lines.COLOR_ADDR_SIZE].decode("ascii")
try:
# Parse as hex address (IDA uses qsscanf with "%a" format)
return int(addr_hex, 16)
except ValueError:
raise
def get_tagged_line_section_byte_offsets(section: ida_kernwin.tagged_line_section_t) -> tuple[int, int]:
# tagged_line_section_t.byte_offsets is not exposed by swig
# so we parse directly from the string representation (puke)
s = str(section)
text_start_index = s.index("text_start=")
text_end_index = s.index("text_end=")
text_start_s = s[text_start_index + len("text_start=") :].partition(",")[0]
text_end_s = s[text_end_index + len("text_end=") :].partition("}")[0]
return int(text_start_s), int(text_end_s)
@dataclass
class TaggedLineSection:
tag: int
string: str
# valid when the found tag section starts with an embedded address
address: int | None
def get_current_tag(line: str, x: int) -> TaggedLineSection:
ret = TaggedLineSection(ida_lines.COLOR_DEFAULT, line, None)
tls = ida_kernwin.tagged_line_sections_t()
if not ida_kernwin.parse_tagged_line_sections(tls, line):
return ret
# find any section at the X coordinate
current_section = tls.nearest_at(x, 0) # 0 = any tag
if not current_section:
# TODO: we only want the section that isn't tagged
# while there might be a section totally before or totally after x.
return ret
ret.tag = current_section.tag
boring_line = ida_lines.tag_remove(line)
ret.string = boring_line[current_section.start : current_section.start + current_section.length]
# try to find an embedded address at the start of the current segment
current_section_start, _ = get_tagged_line_section_byte_offsets(current_section)
addr_section = tls.nearest_before(current_section, x, ida_lines.COLOR_ADDR)
if addr_section:
addr_section_start, _ = get_tagged_line_section_byte_offsets(addr_section)
# addr_section_start initially points just after the address data (ON ADDR 001122...FF)
# so rewind to the start of the tag (16 bytes of hex integer, 2 bytes of tags "ON ADDR")
addr_tag_start = addr_section_start - (ida_lines.COLOR_ADDR_SIZE + 2)
assert addr_tag_start >= 0
# and this should match current_section_start, since that points just after the tag "ON SYMBOL"
# if it doesn't, we're dealing with an edge case we didn't prepare for
# maybe like multiple ADDR tags or something.
# skip those and stick to things we know.
if current_section_start == addr_tag_start:
raw = line.encode("utf-8")
addr = addr_from_tag(raw[addr_tag_start : addr_tag_start + ida_lines.COLOR_ADDR_SIZE + 2])
ret.address = addr
return ret
class foo_viewer_t(ida_kernwin.simplecustviewer_t):
TITLE = "foo"
def __init__(self):
super().__init__()
self.timer: QtCore.QTimer = QtCore.QTimer()
self.timer.timeout.connect(self.on_timer_timeout)
def Create(self):
if not super().Create(self.TITLE):
return False
self.render()
return True
def Show(self, *args):
if not super().Show(*args):
return False
ida_kernwin.attach_action_to_popup(self.GetWidget(), None, some_action_handler_t.ACTION_NAME)
return True
def on_timer_timeout(self):
self.render()
def OnClose(self):
self.timer.stop()
def render(self):
self.ClearLines()
self.AddLine(datetime.datetime.now.isoformat())
self.AddLine(ida_lines.COLSTR(ida_lines.tag_addr(0x401000) + "sub_401000", ida_lines.SCOLOR_CNAME))
def OnDblClick(self, shift):
line = self.GetCurrentLine()
if not line:
return False
_linen, x, _y = self.GetPos()
section = get_current_tag(line, x)
if section.address is not None:
ida_kernwin.jumpto(section.address)
item_address = ida_name.get_name_ea(0, section.string)
if item_address != ida_idaapi.BADADDR:
logger.debug(f"found address for '{section.string}': {item_address:x}")
ida_kernwin.jumpto(item_address)
return True # handled使用自定义查看器显示文本数据,可选择添加标记,并响应基本事件(点击)。使用标记行概念嵌入和解析行中符号的元数据,例如其指向的地址。
python
def addr_from_tag(raw: bytes) -> int:
assert raw[0] == 0x01 # ida_lines.COLOR_ON
assert raw[1] == ida_lines.COLOR_ADDR
addr_hex = raw[2 : 2 + ida_lines.COLOR_ADDR_SIZE].decode("ascii")
try:
# 解析为十六进制地址(IDA使用qsscanf和"%a"格式)
return int(addr_hex, 16)
except ValueError:
raise
def get_tagged_line_section_byte_offsets(section: ida_kernwin.tagged_line_section_t) -> tuple[int, int]:
# tagged_line_section_t.byte_offsets未被swig暴露
# 因此直接从字符串表示中解析(无奈之举)
s = str(section)
text_start_index = s.index("text_start=")
text_end_index = s.index("text_end=")
text_start_s = s[text_start_index + len("text_start=") :].partition(",")[0]
text_end_s = s[text_end_index + len("text_end=") :].partition("}")[0]
return int(text_start_s), int(text_end_s)
@dataclass
class TaggedLineSection:
tag: int
string: str
# 当找到的标记段以嵌入地址开头时有效
address: int | None
def get_current_tag(line: str, x: int) -> TaggedLineSection:
ret = TaggedLineSection(ida_lines.COLOR_DEFAULT, line, None)
tls = ida_kernwin.tagged_line_sections_t()
if not ida_kernwin.parse_tagged_line_sections(tls, line):
return ret
# 查找X坐标处的段
current_section = tls.nearest_at(x, 0) # 0 = 任意标记
if not current_section:
# TODO: 我们只需要未标记的段
# 可能存在完全在x之前或之后的段。
return ret
ret.tag = current_section.tag
boring_line = ida_lines.tag_remove(line)
ret.string = boring_line[current_section.start : current_section.start + current_section.length]
# 尝试在当前段开头查找嵌入的地址
current_section_start, _ = get_tagged_line_section_byte_offsets(current_section)
addr_section = tls.nearest_before(current_section, x, ida_lines.COLOR_ADDR)
if addr_section:
addr_section_start, _ = get_tagged_line_section_byte_offsets(addr_section)
# addr_section_start最初指向地址数据之后(ON ADDR 001122...FF)
# 因此回退到标记开头(16字节十六进制整数,2字节标记"ON ADDR")
addr_tag_start = addr_section_start - (ida_lines.COLOR_ADDR_SIZE + 2)
assert addr_tag_start >= 0
# 这应与current_section_start匹配,因为current_section_start指向标记"ON SYMBOL"之后
# 如果不匹配,则说明遇到了我们未处理的边缘情况
# 例如多个ADDR标记等。
# 跳过这些情况,只处理我们已知的场景。
if current_section_start == addr_tag_start:
raw = line.encode("utf-8")
addr = addr_from_tag(raw[addr_tag_start : addr_tag_start + ida_lines.COLOR_ADDR_SIZE + 2])
ret.address = addr
return ret
class foo_viewer_t(ida_kernwin.simplecustviewer_t):
TITLE = "foo"
def __init__(self):
super().__init__()
self.timer: QtCore.QTimer = QtCore.QTimer()
self.timer.timeout.connect(self.on_timer_timeout)
def Create(self):
if not super().Create(self.TITLE):
return False
self.render()
return True
def Show(self, *args):
if not super().Show(*args):
return False
ida_kernwin.attach_action_to_popup(self.GetWidget(), None, some_action_handler_t.ACTION_NAME)
return True
def on_timer_timeout(self):
self.render()
def OnClose(self):
self.timer.stop()
def render(self):
self.ClearLines()
self.AddLine(datetime.datetime.now.isoformat())
self.AddLine(ida_lines.COLSTR(ida_lines.tag_addr(0x401000) + "sub_401000", ida_lines.SCOLOR_CNAME))
def OnDblClick(self, shift):
line = self.GetCurrentLine()
if not line:
return False
_linen, x, _y = self.GetPos()
section = get_current_tag(line, x)
if section.address is not None:
ida_kernwin.jumpto(section.address)
item_address = ida_name.get_name_ea(0, section.string)
if item_address != ida_idaapi.BADADDR:
logger.debug(f"找到'{section.string}'的地址: {item_address:x}")
ida_kernwin.jumpto(item_address)
return True # 已处理