├── requirements.txt ├── src └── ida_pro_mcp │ ├── mcp_config.json │ ├── __init__.py │ ├── idalib_server.py │ ├── script_utils.py │ ├── server_generated.py │ ├── server.py │ └── mcp-plugin.py ├── .gitignore ├── pyproject.toml ├── CONTRIBUTING.md └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.6.0 2 | pydantic>=2.0.0 3 | setuptools>=65.0.0 4 | typing-extensions>=4.0.0 -------------------------------------------------------------------------------- /src/ida_pro_mcp/mcp_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "port": 13338, 4 | "script_utils_path": "script_utils.py", 5 | "plugin": { 6 | "port": 13338, 7 | "auto_reuse": true, 8 | "auto_select_port": true 9 | }, 10 | "simple_server": { 11 | "port": 13338 12 | }, 13 | "idalib_server": { 14 | "port": 8746 15 | } 16 | } -------------------------------------------------------------------------------- /src/ida_pro_mcp/__init__.py: -------------------------------------------------------------------------------- 1 | # IDA Pro MCP 智能逆向分析平台(增强版) 2 | # 基于 mrexodia/ida-pro-mcp 二次开发增强 3 | 4 | __version__ = "1.4.0" 5 | __author__ = "namename333" 6 | __description__ = "IDA Pro MCP 智能逆向分析平台,支持多客户端和自动化分析" 7 | 8 | # 导入主要模块,方便直接使用 9 | from .server import main as server_main 10 | try: 11 | from .idalib_server import main as idalib_main 12 | except ImportError: 13 | idalib_main = None 14 | 15 | # 导出主要功能 16 | __all__ = [ 17 | "__version__", 18 | "__author__", 19 | "__description__", 20 | "server_main", 21 | "idalib_main", 22 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | .venv 8 | .venv_test 9 | 10 | # Distribution / packaging 11 | build/ 12 | dist/ 13 | *.egg-info/ 14 | *.egg/ 15 | .eggs/ 16 | 17 | # IDEs and editors 18 | .vscode/ 19 | .idea/ 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # OS generated files 27 | Thumbs.db 28 | .DS_Store 29 | 30 | # Environment variables 31 | .env 32 | .env.local 33 | .env.production.local 34 | .env.development.local 35 | .env.test.local 36 | 37 | # Logs 38 | *.log 39 | log/ 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | *.pid.lock 46 | 47 | # Coverage directory 48 | coverage/ 49 | *.cover 50 | 51 | # Temporary files 52 | tmp/ 53 | temp/ 54 | *.tmp 55 | *.temp 56 | 57 | # IDA Pro related files 58 | *.idb 59 | *.i64 60 | *.til 61 | *.id0 62 | *.id1 63 | *.id2 64 | *.nam 65 | *.til 66 | 67 | # Windows specific 68 | *.dll 69 | *.exe 70 | *.lib 71 | *.exp 72 | 73 | # macOS specific 74 | .DS_Store 75 | .AppleDouble 76 | .LSOverride 77 | 78 | # Linux specific 79 | *~ 80 | .fuse_hidden* 81 | .directory 82 | .Trash-* 83 | .nfs* 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ida-pro-mcp" 3 | version = "1.4.0" 4 | description = "Vibe reversing with IDA Pro" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "namename333" }] 8 | keywords = ["ida", "mcp", "llm", "plugin"] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Operating System :: MacOS", 17 | "Operating System :: Microsoft :: Windows", 18 | "Operating System :: POSIX :: Linux", 19 | ] 20 | dependencies = [ 21 | "mcp>=1.16.0", 22 | "pydantic>=2.0.0", 23 | "setuptools>=65.0.0", 24 | "typing-extensions>=4.0.0", 25 | ] 26 | 27 | [project.urls] 28 | Repository = "https://github.com/namename333/idapromcp_333" 29 | Issues = "https://github.com/namename333/idapromcp_333/issues" 30 | 31 | [build-system] 32 | requires = ["setuptools"] 33 | build-backend = "setuptools.build_meta" 34 | 35 | [dependency-groups] 36 | dev = [ 37 | "mcp[cli]>=1.6.0", 38 | "pytest>=7.0.0", 39 | "black>=23.0.0", 40 | "isort>=5.0.0", 41 | ] 42 | 43 | [project.scripts] 44 | ida-pro-mcp = "ida_pro_mcp.server:main" 45 | idalib-mcp = "ida_pro_mcp.idalib_server:main" 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 感谢您考虑为 IDA Pro MCP 插件项目做出贡献!以下是一些帮助您开始贡献的指南。 4 | 5 | ## 现有贡献者 6 | 7 | 我们特别感谢以下开发者对项目的贡献: 8 | - **namename333** - 主要开发者 9 | - **grand师傅** - 功能开发与测试 10 | - **Britney师傅** - 功能开发与测试 11 | 12 | ## 行为准则 13 | 14 | 参与本项目时,请保持尊重和专业的态度。我们欢迎各种形式的贡献,包括 bug 报告、功能请求、代码贡献和文档改进。 15 | 16 | ## 如何贡献 17 | 18 | ### 1. 报告 Bug 19 | 20 | 如果您发现了 bug,请通过 GitHub Issues 提交报告。在提交前,请先查看是否已有类似的 issue。 21 | 22 | 提交 bug 报告时,请包含: 23 | - 详细的问题描述 24 | - 复现步骤 25 | - 预期行为和实际行为 26 | - 环境信息(IDA Pro 版本、操作系统、Python 版本等) 27 | - 如果可能,提供截图或错误日志 28 | 29 | ### 2. 提交功能请求 30 | 31 | 如果您有新功能的想法,可以通过 GitHub Issues 提交功能请求。请描述: 32 | - 您想要的功能 33 | - 这个功能解决的问题 34 | - 可能的实现方式(如果您有想法) 35 | 36 | ### 3. 代码贡献 37 | 38 | #### 准备工作 39 | 40 | 1. Fork 本仓库 41 | 2. 克隆您的 fork 到本地: 42 | ```bash 43 | git clone https://github.com/YOUR_USERNAME/idapromcp_333.git 44 | cd idapromcp_333 45 | ``` 46 | 3. 创建一个新分支: 47 | ```bash 48 | git checkout -b feature-or-bugfix-branch 49 | ``` 50 | 51 | #### 开发流程 52 | 53 | 1. 安装开发依赖: 54 | ```bash 55 | pip install -e . 56 | ``` 57 | 58 | 2. 进行代码修改 59 | 60 | 3. 测试您的修改 61 | 62 | 4. 提交您的更改: 63 | ```bash 64 | git add . 65 | git commit -m "描述您的更改" 66 | ``` 67 | 68 | 5. 推送到您的 fork: 69 | ```bash 70 | git push origin feature-or-bugfix-branch 71 | ``` 72 | 73 | 6. 创建 Pull Request 74 | 75 | ### 4. 文档贡献 76 | 77 | 文档改进也是非常宝贵的贡献。您可以通过修改 README.md 或添加更详细的使用示例来改进文档。 78 | 79 | ## 代码风格 80 | 81 | 请遵循 Python 的 PEP 8 风格指南。确保您的代码有适当的注释和文档字符串。 82 | 83 | ## 提交信息规范 84 | 85 | 请使用清晰、描述性的提交信息。好的提交信息应该清楚地说明您做了什么以及为什么这样做。 86 | 87 | ## 许可证 88 | 89 | 通过向此项目贡献代码,您同意您的贡献将在 MIT 许可证下发布。 90 | 91 | ## 联系方式 92 | 93 | 如有任何问题或建议,请通过 GitHub Issues 或直接联系项目维护者。 94 | 95 | 感谢您的贡献! 96 | -------------------------------------------------------------------------------- /src/ida_pro_mcp/idalib_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | import logging 4 | import argparse 5 | import importlib 6 | import os 7 | import json 8 | from pathlib import Path 9 | import typing_inspection.introspection as intro 10 | 11 | from mcp.server.fastmcp import FastMCP 12 | 13 | # idapro must go first to initialize idalib 14 | import idapro 15 | 16 | import ida_auto 17 | import ida_hexrays 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | mcp = FastMCP("github.com/namename333/idapromcp_333#idalib") 22 | 23 | def get_config_file_path(): 24 | """ 25 | 获取配置文件路径 26 | 支持多种配置文件位置 27 | """ 28 | # 优先检查用户目录 29 | user_dir = os.path.expanduser("~") 30 | config_paths = [ 31 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp_config.json"), 32 | os.path.join(user_dir, ".mcp_config.json"), 33 | os.path.join(user_dir, "mcp_config.json") 34 | ] 35 | 36 | # 检查是否存在IDA插件目录下的配置文件 37 | try: 38 | import ida_idaapi 39 | plugin_dir = ida_idaapi.idadir("plugins") 40 | config_paths.append(os.path.join(plugin_dir, "mcp_config.json")) 41 | except ImportError: 42 | pass # 如果不在IDA环境中运行,忽略 43 | 44 | # 返回第一个存在的配置文件 45 | for path in config_paths: 46 | if os.path.exists(path): 47 | return path 48 | 49 | # 如果没有找到配置文件,返回默认路径 50 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp_config.json") 51 | 52 | def load_config(): 53 | """ 54 | 加载配置文件 55 | 返回配置字典,如果配置文件不存在则返回默认配置 56 | """ 57 | default_config = { 58 | "host": "127.0.0.1", 59 | "port": 8745 # idalib服务器默认端口为8745 60 | } 61 | 62 | config_path = get_config_file_path() 63 | if os.path.exists(config_path): 64 | try: 65 | with open(config_path, 'r', encoding='utf-8') as f: 66 | user_config = json.load(f) 67 | # 合并默认配置和用户配置 68 | default_config.update(user_config) 69 | except Exception as e: 70 | print(f"警告: 加载配置文件失败: {e}") 71 | 72 | # 从环境变量覆盖配置(使用IDALIB_MCP_PORT区分) 73 | if "MCP_HOST" in os.environ: 74 | default_config["host"] = os.environ["MCP_HOST"] 75 | 76 | if "IDALIB_MCP_PORT" in os.environ: 77 | try: 78 | default_config["port"] = int(os.environ["IDALIB_MCP_PORT"]) 79 | except ValueError: 80 | print("警告: 环境变量IDALIB_MCP_PORT不是有效的端口号") 81 | elif "MCP_PORT" in os.environ: 82 | # 如果没有指定IDALIB_MCP_PORT,也可以使用通用的MCP_PORT 83 | try: 84 | default_config["port"] = int(os.environ["MCP_PORT"]) 85 | except ValueError: 86 | print("警告: 环境变量MCP_PORT不是有效的端口号") 87 | 88 | return default_config 89 | 90 | def fixup_tool_argument_descriptions(mcp: FastMCP): 91 | # 在`mcp-plugin.py`的工具定义中,我们在函数参数上使用`typing.Annotated` 92 | # 来附加文档。例如: 93 | # 94 | # def get_function_by_name( 95 | # name: Annotated[str, "Name of the function to get"] 96 | # ) -> Function: 97 | # """Get a function by its name""" 98 | # ... 99 | # 100 | # 然而,Annotated的解释权由静态分析工具和其他工具决定。 101 | # FastMCP对这些注释没有特殊处理,因此我们需要手动将它们添加到工具元数据中。 102 | # 103 | # 示例:修改前 104 | # 105 | # tool.parameter={ 106 | # properties: { 107 | # name: { 108 | # title: "Name", 109 | # type: "string" 110 | # } 111 | # }, 112 | # required: ["name"], 113 | # title: "get_function_by_nameArguments", 114 | # type: "object" 115 | # } 116 | # 117 | # 示例:修改后 118 | # 119 | # tool.parameter={ 120 | # properties: { 121 | # name: { 122 | # title: "Name", 123 | # type: "string" 124 | # description: "Name of the function to get" 125 | # } 126 | # }, 127 | # required: ["name"], 128 | # title: "get_function_by_nameArguments", 129 | # type: "object" 130 | # } 131 | # 132 | # 参考资料: 133 | # - https://docs.python.org/3/library/typing.html#typing.Annotated 134 | # - https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations 135 | 136 | # 遗憾的是,FastMCP.list_tools()是异步的,因此我们违背最佳实践直接访问`._tool_manager` 137 | # 而不是为了获取(非异步的)工具列表而启动asyncio运行时。 138 | for tool in mcp._tool_manager.list_tools(): 139 | sig = inspect.signature(tool.fn) 140 | for name, parameter in sig.parameters.items(): 141 | # this instance is a raw `typing._AnnotatedAlias` that we can't do anything with directly. 142 | # it renders like: 143 | # 144 | # typing.Annotated[str, 'Name of the function to get'] 145 | if not parameter.annotation: 146 | continue 147 | 148 | # this instance will look something like: 149 | # 150 | # InspectedAnnotation(type=, qualifiers=set(), metadata=['Name of the function to get']) 151 | # 152 | annotation = intro.inspect_annotation( 153 | parameter.annotation, 154 | annotation_source=intro.AnnotationSource.ANY 155 | ) 156 | 157 | # for our use case, where we attach a single string annotation that is meant as documentation, 158 | # we extract that string and assign it to "description" in the tool metadata. 159 | 160 | if annotation.type is not str: 161 | continue 162 | 163 | if len(annotation.metadata) != 1: 164 | continue 165 | 166 | description = annotation.metadata[0] 167 | if not isinstance(description, str): 168 | continue 169 | 170 | logger.debug("adding parameter documentation %s(%s='%s')", tool.name, name, description) 171 | tool.parameters["properties"][name]["description"] = description 172 | 173 | def main(): 174 | # 先加载配置文件,作为命令行参数的默认值 175 | config = load_config() 176 | 177 | parser = argparse.ArgumentParser(description="MCP server for IDA Pro via idalib") 178 | parser.add_argument("--verbose", "-v", action="store_true", help="Show debug messages") 179 | parser.add_argument("--host", type=str, default=config["host"], help=f"Host to listen on, default: {config['host']}") 180 | parser.add_argument("--port", type=int, default=config["port"], help=f"Port to listen on, default: {config['port']}") 181 | parser.add_argument("input_path", type=Path, help="Path to the input file to analyze.") 182 | args = parser.parse_args() 183 | 184 | if args.verbose: 185 | log_level = logging.DEBUG 186 | idapro.enable_console_messages(True) 187 | else: 188 | log_level = logging.INFO 189 | idapro.enable_console_messages(False) 190 | 191 | mcp.settings.log_level = logging.getLevelName(log_level) 192 | mcp.settings.host = args.host 193 | mcp.settings.port = args.port 194 | logging.basicConfig(level=log_level) 195 | 196 | # 重置可能在idapythonrc.py中初始化的日志级别 197 | # 该文件在导入idalib时会被执行 198 | logging.getLogger().setLevel(log_level) 199 | 200 | if not args.input_path.exists(): 201 | raise FileNotFoundError(f"输入文件不存在: {args.input_path}") 202 | 203 | # TODO: add a tool for specifying the idb/input file (sandboxed) 204 | logger.info("正在打开数据库: %s", args.input_path) 205 | if idapro.open_database(str(args.input_path), run_auto_analysis=True): 206 | raise RuntimeError("分析输入文件失败") 207 | 208 | logger.debug("idalib: waiting for analysis...") 209 | ida_auto.auto_wait() 210 | 211 | if not ida_hexrays.init_hexrays_plugin(): 212 | raise RuntimeError("Hex-Rays反编译器初始化失败") 213 | 214 | plugin = importlib.import_module("ida_pro_mcp.mcp-plugin") 215 | logger.debug("正在添加工具...") 216 | # 无条件添加所有功能(包括unsafe) 217 | for name, callable in plugin.rpc_registry.methods.items(): 218 | logger.debug("添加工具: %s: %s", name, callable) 219 | mcp.add_tool(callable, name) 220 | 221 | # NOTE: https://github.com/modelcontextprotocol/python-sdk/issues/466 222 | fixup_tool_argument_descriptions(mcp) 223 | 224 | # NOTE: npx @modelcontextprotocol/inspector for debugging 225 | logger.info("MCP服务器在: http://%s:%d/sse 可用", mcp.settings.host, mcp.settings.port) 226 | try: 227 | mcp.run(transport="sse") 228 | except KeyboardInterrupt: 229 | pass 230 | 231 | if __name__ == "__main__": 232 | main() 233 | -------------------------------------------------------------------------------- /src/ida_pro_mcp/script_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | 脚本生成工具模块 3 | 提供各种辅助函数,用于生成不同类型的脚本代码 4 | """ 5 | 6 | def _is_address_string(target: str) -> bool: 7 | """ 8 | 检查目标字符串是否表示一个有效的十六进制地址 9 | 10 | 参数: 11 | target: 要检查的字符串 12 | 返回: 13 | bool: 如果是有效的十六进制地址则返回True 14 | """ 15 | try: 16 | if target.startswith('0x'): 17 | int(target, 16) 18 | return True 19 | except ValueError: 20 | pass 21 | return False 22 | 23 | def _get_target_expression(target: str, is_address: bool) -> str: 24 | """ 25 | 根据目标类型生成Frida表达式 26 | 27 | 参数: 28 | target: 目标函数名或地址 29 | is_address: 是否为地址 30 | 返回: 31 | str: 适合在Frida脚本中使用的表达式 32 | """ 33 | if is_address: 34 | return f"ptr('{target}')" 35 | return f"'{target}'" 36 | 37 | def _get_app_environment(app_type: str) -> tuple[str, str]: 38 | """ 39 | 根据应用类型获取环境前缀和后缀 40 | 41 | 参数: 42 | app_type: 应用类型 ('native' 或 'java') 43 | 返回: 44 | tuple: (script_prefix, script_suffix) 45 | """ 46 | if app_type == 'java': 47 | return "Java.perform(function() {\n", "\n});" 48 | return "", "" 49 | 50 | def _generate_hook_script(target: str, is_address: bool, options: dict) -> str: 51 | """ 52 | 生成函数Hook脚本 53 | 54 | 参数: 55 | target: 目标函数名或地址 56 | is_address: 是否为地址 57 | options: 配置选项 58 | 返回: 59 | str: Hook脚本内容 60 | """ 61 | # 根据是否为地址生成目标表达式 62 | target_expr = _get_target_expression(target, is_address) 63 | module_name = options.get('module', 'target') 64 | arg_count = options.get('arg_count', 4) 65 | 66 | return f""" 67 | console.log("开始Hook目标函数: {target}"); 68 | 69 | // 尝试不同的模块和导出方式 70 | const moduleName = "{module_name}"; 71 | const module = Process.getModuleByName(moduleName); 72 | 73 | if (!module) {{ 74 | console.log(`未找到模块: ${{moduleName}}`); 75 | return; 76 | }} 77 | 78 | let targetFunc = null; 79 | try {{ 80 | if ({is_address}) {{ 81 | // 如果是地址,直接使用ptr 82 | targetFunc = {target_expr}; 83 | }} else {{ 84 | // 如果是函数名,尝试在模块中查找导出函数 85 | targetFunc = Module.findExportByName(moduleName, {target_expr}); 86 | if (!targetFunc) {{ 87 | console.log(`在模块${{moduleName}}中未找到导出函数: {target}`); 88 | // 尝试使用模糊搜索查找函数 89 | console.log("尝试模糊搜索函数..."); 90 | const matches = Memory.scanSync(module.base, module.size, `[${' ' * 20}]{target}${' ' * 20}`); 91 | if (matches.length > 0) {{ 92 | console.log(`找到 ${{matches.length}} 个可能的匹配`); 93 | targetFunc = matches[0].address; 94 | console.log(`使用第一个匹配: ${{targetFunc}}`); 95 | }} 96 | }} 97 | }} 98 | 99 | if (!targetFunc) {{ 100 | console.log(`未找到目标函数: {target}`); 101 | return; 102 | }} 103 | 104 | Interceptor.attach(targetFunc, {{ 105 | onEnter: function(args) {{ 106 | console.log(`\n[+] 调用 {target}:`); 107 | // 打印参数 - 可以根据函数签名调整 108 | for (let i = 0; i < {arg_count}; i++) {{ 109 | console.log(` 参数 ${{i}}:`, args[i]); 110 | // 尝试将参数解析为字符串 111 | try {{ 112 | const str = Memory.readUtf8String(args[i]); 113 | if (str) console.log(` 字符串值: ${{str}}`); 114 | }} catch (e) {{ 115 | // 静默失败,不影响主流程 116 | }} 117 | }} 118 | // 保存参数供onLeave使用 119 | this.args = args; 120 | }}, 121 | onLeave: function(retval) {{ 122 | console.log(`[+] {target} 返回:`); 123 | console.log(` 返回值:`, retval); 124 | // 尝试解析返回值为字符串 125 | try {{ 126 | const str = Memory.readUtf8String(retval); 127 | if (str) console.log(` 字符串值: ${{str}}`); 128 | }} catch (e) {{ 129 | // 静默失败,不影响主流程 130 | }} 131 | console.log("----------------------------------------"); 132 | }} 133 | }}); 134 | }} catch (e) {{ 135 | console.log(`Hook失败: ${{e}}`); 136 | }} 137 | """ 138 | 139 | def _generate_memory_dump_script(target: str, options: dict) -> str: 140 | """ 141 | 生成内存监控脚本 142 | 143 | 参数: 144 | target: 目标地址 145 | options: 配置选项 146 | 返回: 147 | str: 内存监控脚本内容 148 | """ 149 | mem_size = options.get('size', 1024) 150 | 151 | # 判断是否为地址字符串 152 | is_address = _is_address_string(target) 153 | target_expr = _get_target_expression(target, is_address) 154 | 155 | return f""" 156 | console.log("开始内存监控..."); 157 | 158 | const targetAddr = {target_expr}; 159 | const memSize = {mem_size}; // 要监控的内存大小 160 | 161 | console.log(`监控地址范围: ${{targetAddr}} - ${{ptr(targetAddr).add(memSize)}}`); 162 | 163 | try {{ 164 | // 监控内存读写 165 | Memory.protect(ptr(targetAddr), memSize, 'rwx'); 166 | 167 | // Windows下可能需要特殊处理 168 | if (Process.platform === 'windows') {{ 169 | console.log("Windows平台: 使用内存断点方式监控"); 170 | // 为内存区域设置读写断点 171 | Memory.watchpoint(ptr(targetAddr), memSize, {{ 172 | read: true, 173 | write: true, 174 | onAccess: function(details) {{ 175 | console.log(`\n[+] 内存访问:`); 176 | console.log(` 地址: ${{details.address}}`); 177 | console.log(` 类型: ${{details.type}}`); // 'read' 或 'write' 178 | console.log(` 大小: ${{details.size}} 字节`); 179 | 180 | // 打印调用栈 181 | try {{ 182 | const stackTrace = Thread.backtrace(this.context, Backtracer.ACCURATE) 183 | .map(DebugSymbol.fromAddress).join('\n'); 184 | console.log(` 调用栈:\n${{stackTrace}}`); 185 | }} catch (e) {{ 186 | console.log(` 调用栈获取失败: ${{e}}`); 187 | }} 188 | 189 | // 对于写操作,尝试打印写入的值 190 | if (details.type === 'write') {{ 191 | try {{ 192 | console.log(` 写入值:`, Memory.readByteArray(details.address, details.size)); 193 | }} catch (e) {{ 194 | // 静默失败,不影响主流程 195 | }} 196 | }} 197 | console.log("----------------------------------------"); 198 | }} 199 | }}); 200 | }} else {{ 201 | // 其他平台使用accessMonitor 202 | Memory.accessMonitor.enable(); 203 | 204 | // 监听内存访问事件 205 | Memory.accessMonitor.on('access', function(event) {{ 206 | if (event.address.compare(ptr(targetAddr)) >= 0 && 207 | event.address.compare(ptr(targetAddr).add(memSize)) < 0) {{ 208 | console.log(`\n[+] 内存访问:`); 209 | console.log(` 地址: ${{event.address}}`); 210 | console.log(` 类型: ${{event.type}}`); // 'read' 或 'write' 211 | console.log(` 大小: ${{event.size}} 字节`); 212 | 213 | // 打印调用栈 214 | try {{ 215 | const stackTrace = Thread.backtrace(event.thread, Backtracer.ACCURATE) 216 | .map(DebugSymbol.fromAddress).join('\n'); 217 | console.log(` 调用栈:\n${{stackTrace}}`); 218 | }} catch (e) {{ 219 | console.log(` 调用栈获取失败: ${{e}}`); 220 | }} 221 | 222 | // 对于写操作,尝试打印写入的值 223 | if (event.type === 'write') {{ 224 | try {{ 225 | console.log(` 写入值:`, Memory.readByteArray(event.address, event.size)); 226 | }} catch (e) {{ 227 | // 静默失败,不影响主流程 228 | }} 229 | }} 230 | console.log("----------------------------------------"); 231 | }} 232 | }}); 233 | }} 234 | 235 | console.log("内存监控已启动。按Ctrl+C停止。"); 236 | }} catch (e) {{ 237 | console.log(`内存监控设置失败: ${{e}}`); 238 | }} 239 | """ 240 | 241 | def _generate_string_hook_script(target: str, is_address: bool, options: dict = None) -> str: 242 | """ 243 | 生成字符串监控脚本 244 | 245 | 参数: 246 | target: 目标函数名或地址 247 | is_address: 是否为地址 248 | options: 配置选项 (可选) 249 | 返回: 250 | str: 字符串监控脚本内容 251 | """ 252 | # 根据是否为地址生成目标表达式 253 | target_expr = _get_target_expression(target, is_address) 254 | # 如果options为None,初始化为空字典 255 | if options is None: 256 | options = {} 257 | memory_scan_code = """ 258 | // Hook目标函数附近的字符串操作 259 | if ({is_address}) {{ 260 | // 如果指定了地址,也监控该地址附近的内存读取 261 | try {{ 262 | const targetAddr = {target_expr}; 263 | console.log(`监控地址附近的字符串: ${{targetAddr}}`); 264 | Memory.scan(ptr(targetAddr).sub(0x1000), 0x2000, "[41-7a]{{4,}}", {{ 265 | onMatch: function(address, size) {{ 266 | try {{ 267 | const str = Memory.readUtf8String(address); 268 | if (str.length >= 4 && !collectedStrings.has(str)) {{ 269 | collectedStrings.add(str); 270 | console.log(`\n[+] 发现字符串:`); 271 | console.log(` 地址: ${{address}}`); 272 | console.log(` 内容: "${{str}}"`); 273 | 274 | // 获取调用栈 275 | try {{ 276 | const stackTrace = Thread.backtrace(Thread.currentThread(), Backtracer.ACCURATE) 277 | .map(DebugSymbol.fromAddress).join('\n'); 278 | console.log(` 访问栈:\n${{stackTrace}}`); 279 | }} catch (e) {{ 280 | console.log(` 调用栈获取失败: ${{e}}`); 281 | }} 282 | }} 283 | }} catch (e) {{ 284 | // 静默失败,继续扫描 285 | }} 286 | }}, 287 | onComplete: function() {{}} 288 | }}); 289 | }} catch (e) {{ 290 | console.log(`字符串扫描失败: ${{e}}`); 291 | }} 292 | }} 293 | """ 294 | if not is_address: 295 | memory_scan_code = "" 296 | 297 | return f""" 298 | console.log("开始字符串监控..."); 299 | 300 | // 存储已收集的字符串 301 | const collectedStrings = new Set(); 302 | 303 | // Hook常见的字符串处理函数 304 | const stringFuncs = {{ 305 | 'strcmp': Module.findExportByName(null, 'strcmp'), 306 | 'strcpy': Module.findExportByName(null, 'strcpy'), 307 | 'strlen': Module.findExportByName(null, 'strlen'), 308 | 'memcpy': Module.findExportByName(null, 'memcpy'), 309 | 'strcat': Module.findExportByName(null, 'strcat') 310 | }}; 311 | 312 | {memory_scan_code} 313 | 314 | // Hook字符串函数 315 | for (const [name, func] of Object.entries(stringFuncs)) {{ 316 | if (func) {{ 317 | Interceptor.attach(func, {{ 318 | onEnter: function(args) {{ 319 | try {{ 320 | // 尝试读取第一个参数作为字符串 321 | const str = Memory.readUtf8String(args[0]); 322 | if (str && str.length >= 3 && !collectedStrings.has(str)) {{ 323 | collectedStrings.add(str); 324 | console.log(`\n[+] 函数 {{name}} 使用字符串:`); 325 | console.log(` 字符串: "${{str}}"`); 326 | 327 | // 获取调用栈 328 | try {{ 329 | const stackTrace = Thread.backtrace(this.context, Backtracer.ACCURATE) 330 | .map(DebugSymbol.fromAddress).join('\n'); 331 | console.log(` 调用栈:\n${{stackTrace}}`); 332 | }} catch (e) {{ 333 | console.log(` 调用栈获取失败: ${{e}}`); 334 | }} 335 | }} 336 | }} catch (e) {{ 337 | // 静默失败,继续处理 338 | }} 339 | }} 340 | }}); 341 | }} 342 | }} 343 | 344 | console.log("字符串监控已启动。按Ctrl+C停止。"); 345 | console.log("已收集的字符串将自动去重并显示。"); 346 | """ 347 | 348 | def _get_usage_notes() -> str: 349 | """ 350 | 获取使用说明注释 351 | 352 | 返回: 353 | str: 使用说明文本 354 | """ 355 | return """// 使用说明: 356 | // 1. 确保已安装frida-tools: pip install frida-tools 357 | // 2. 对于Windows原生程序,使用以下命令运行: 358 | // frida -p <进程ID> -l <脚本文件> --no-pause 359 | // 或附加到已运行的程序 360 | // 3. 对于Java程序,使用: 361 | // frida -U -f <包名> -l <脚本文件> --no-pause""" 362 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDA Pro MCP 插件 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Python Version](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/downloads/) 5 | [![IDA Pro Version](https://img.shields.io/badge/IDA%20Pro-7.5%2B-green.svg)](https://www.hex-rays.com/products/ida/) 6 | 7 | ## 1. 项目概述 8 | 9 | IDA Pro MCP (Modding and Code Analysis Plugin) 是一个功能强大的 IDA Pro 插件,旨在提供丰富的代码分析、自动化和修改功能,帮助逆向工程师和安全研究人员更高效地进行二进制分析。该插件实现了标准 MCP 协议,支持与各种 MCP 客户端进行交互。 10 | 本项目基于 mrexodia/ida-pro-mcp( https://github.com/mrexodia/ida-pro-mcp ) 11 | 二次开发增强,保留原核心功能并自行diy扩展了一些功能。 12 | 十分建议大家去下载原项目,我本意是自己根据原项目自己加一点trace功能什么的和优化一些操作方便我自己使用,但是原项目更新太快太厉害了,我如果要更新要么功能远不如,要么就基本上照抄。。 13 | 14 | ### 1.1 核心价值 15 | 16 | - **自动化分析流程**:减少重复工作,提高分析效率 17 | - **增强的函数和变量分析**:提供更深入的代码洞察 18 | - **批量处理功能**:支持大规模代码修改和分析 19 | - **动态分析辅助**:生成 Frida 和 Angr 脚本,辅助动态分析 20 | - **混淆检测**:识别常见的代码混淆技术 21 | - **算法识别**:自动识别常见加密算法和哈希函数 22 | - **标准 MCP 协议兼容**:支持与各种 MCP 客户端交互 23 | 24 | ### 1.2 技术栈 25 | 26 | | 技术/依赖 | 版本要求 | 用途 | 27 | | ----------- | -------- | ---------------- | 28 | | Python | >= 3.11 | 插件开发语言 | 29 | | IDA Pro | >= 7.5 | 宿主环境 | 30 | | mcp | >= 1.6.0 | MCP 协议支持 | 31 | | pydantic | >= 2.0.0 | 数据验证和序列化 | 32 | | fastmcp | 最新版 | MCP 服务器实现 | 33 | | frida-tools | 可选 | 动态分析支持 | 34 | | angr | 可选 | 符号执行支持 | 35 | 36 | ## 2. 目录结构 37 | 38 | ``` 39 | ida-pro-mcp/ 40 | ├── src/ 41 | │ └── ida_pro_mcp/ 42 | │ ├── __init__.py # 插件入口 43 | │ ├── idalib_server.py # IDA 库服务器 44 | │ ├── mcp-plugin.py # 主插件文件 45 | │ ├── mcp_config.json # 插件配置 46 | │ ├── script_utils.py # 脚本生成辅助模块 47 | │ ├── server.py # MCP 服务器 48 | │ └── server_generated.py # 自动生成的服务器代码 49 | ├── .gitignore # Git 忽略配置 50 | ├── CONTRIBUTING.md # 贡献指南 51 | ├── README.md # 项目说明文档 52 | ├── ida-plugin.json # IDA 插件配置 53 | ├── pyproject.toml # 项目配置文件 54 | ├── requirements.txt # 依赖列表 55 | ├── test_script_utils.py # 测试脚本 56 | └── uv.lock # 依赖锁定文件 57 | ``` 58 | 59 | ## 3. 核心功能 60 | 61 | ### 3.1 批量处理功能 62 | 63 | - **批量重命名函数**:根据命名规则或模式批量重命名函数 64 | - **批量添加注释**:自动为函数和变量添加描述性注释 65 | - **批量设置变量类型**:根据分析结果自动设置本地变量类型 66 | - **批量设置函数原型**:统一设置函数的参数和返回类型 67 | 68 | ### 3.2 混淆检测功能 69 | 70 | - **控制流平坦化检测**:识别控制流混淆 71 | - **字符串加密检测**:检测字符串加密和动态解密模式 72 | - **反调试检测**:识别常见的反调试技术 73 | - **死代码检测**:发现无用的代码块 74 | 75 | ### 3.3 算法识别功能 76 | 77 | 自动识别常见加密算法、哈希函数和编码方式: 78 | 79 | - **加密算法**:AES、DES、RC4、TEA、XOR 等 80 | - **哈希函数**:MD5、SHA1、SHA256、CRC32 等 81 | - **编码方式**:Base64、Base32、Base58、Base85、Rot13 等 82 | - **压缩算法**:zlib、LZMA 等 83 | - **随机数生成**:Mersenne Twister 等 84 | 85 | ### 3.4 动态分析辅助功能 86 | 87 | - **生成 Angr 符号执行脚本**:辅助进行符号执行和爆破 88 | - **生成 Frida 动态分析脚本**:支持函数 Hook、内存监控和字符串监控 89 | - **脚本保存和运行**:便捷地保存和执行生成的分析脚本 90 | - **Windows 平台兼容性优化**:特别优化了在 Windows 环境下的使用体验 91 | 92 | ### 3.5 调用图分析 93 | 94 | 生成函数调用关系图,支持可视化展示: 95 | 96 | - 生成邻接表格式调用图 97 | - 生成 Mermaid 格式调用图,支持可视化 98 | - 可指定递归深度 99 | 100 | ### 3.6 script_utils 模块 101 | 102 | 提供脚本生成辅助函数库,包含: 103 | 104 | - **地址检测**:验证十六进制地址格式 105 | - **目标表达式生成**:根据目标类型生成 Frida 表达式 106 | - **脚本内容构建**:生成标准化的脚本模板 107 | - **环境管理**:根据应用类型生成脚本环境 108 | 109 | ## 4. 安装步骤 110 | 111 | ### 4.1 系统要求 112 | 113 | - IDA Pro 7.5+(支持 Python 3.8+) 114 | - Python 3.11 或更高版本 115 | - 支持 Windows、macOS 和 Linux 操作系统 116 | 117 | ### 4.2 安装方法 118 | 119 | #### 手动安装 120 | 121 | 1. 克隆或下载项目代码: 122 | 123 | ```bash 124 | git clone https://github.com/namename333/idapromcp_333.git 125 | cd idapromcp_333 126 | ``` 127 | 128 | 2. 安装依赖: 129 | 130 | ```bash 131 | pip install -e . 132 | # 安装额外的动态分析工具(推荐) 133 | pip install angr frida-tools 134 | ``` 135 | 136 | 3. 安装 IDA 插件: 137 | 138 | ```bash 139 | # Windows 示例 140 | ida-pro-mcp --install 141 | ``` 142 | 143 | ### 4.3 验证安装 144 | 145 | 1. 启动 IDA Pro 146 | 2. 通过以下方式启动 MCP 服务器: 147 | - 菜单:`Edit -> Plugins -> MCP` 148 | - 快捷键:`Ctrl-Alt-M` 149 | 3. 服务器启动后,将在端口 13337 上监听 JSON-RPC 请求 150 | 4. 使用以下命令测试连接: 151 | 152 | ```python 153 | import requests 154 | import json 155 | 156 | url = "http://localhost:13337/mcp" 157 | headers = {"Content-Type": "application/json"} 158 | 159 | payload = { 160 | "jsonrpc": "2.0", 161 | "method": "check_connection", 162 | "params": [], 163 | "id": 1 164 | } 165 | 166 | response = requests.post(url, data=json.dumps(payload), headers=headers) 167 | print(response.json()) 168 | ``` 169 | 170 | ## 5. 使用指南 171 | 172 | ### 5.1 基本用法 173 | 174 | MCP 插件通过 JSON-RPC 协议提供 API,您可以使用任何支持 HTTP 请求的工具或编程语言与之交互。以下是一个基本的请求示例: 175 | 176 | ```python 177 | import requests 178 | import json 179 | 180 | url = "http://localhost:13337/jsonrpc" 181 | headers = {"Content-Type": "application/json"} 182 | 183 | payload = { 184 | "jsonrpc": "2.0", 185 | "method": "get_function", 186 | "params": ["0x401000"], 187 | "id": 1 188 | } 189 | 190 | response = requests.post(url, data=json.dumps(payload), headers=headers) 191 | result = response.json() 192 | print(result) 193 | ``` 194 | 195 | ### 5.2 脚本生成示例 196 | 197 | #### 5.2.1 生成 Frida Hook 脚本 198 | 199 | ```python 200 | # 生成函数 Hook 脚本 201 | hook_script = ida_pro_mcp.generate_frida_script( 202 | target="main", # 目标函数名 203 | script_type="hook", # 脚本类型为 hook 204 | options={ 205 | "app_type": "native", # 应用类型为原生二进制 206 | "log_args": True, # 记录函数参数 207 | "log_return": True, # 记录返回值 208 | "detailed_log": False # 简洁日志输出 209 | } 210 | ) 211 | ``` 212 | 213 | #### 5.2.2 生成内存监控脚本 214 | 215 | ```python 216 | # 生成内存监控脚本 217 | memory_script = ida_pro_mcp.generate_frida_script( 218 | target="0x12345678", # 要监控的内存地址 219 | script_type="memory_dump", # 脚本类型为内存监控 220 | options={ 221 | "size": 1024, # 监控内存大小(字节) 222 | "interval": 100, # 监控间隔(毫秒) 223 | "compare_before_after": True # 比较内存前后变化 224 | } 225 | ) 226 | ``` 227 | 228 | #### 5.2.3 生成字符串监控脚本 229 | 230 | ```python 231 | # 生成字符串监控脚本 232 | string_script = ida_pro_mcp.generate_frida_script( 233 | target="", # 不需要特定目标函数 234 | script_type="string_hook", # 脚本类型为字符串监控 235 | options={ 236 | "string_functions": True, # Hook 常见字符串函数 237 | "address_monitor": "0x10000000", # 监控特定内存区域 238 | "capture_callstack": True # 捕获调用栈 239 | } 240 | ) 241 | ``` 242 | 243 | ## 6. API 文档 244 | 245 | ### 6.1 元数据和基本信息获取 246 | 247 | #### `get_metadata()` 248 | 249 | **功能**:获取当前 IDB 文件的元数据信息 250 | 251 | **返回**:包含元数据的字典 252 | 253 | ```python 254 | { 255 | "file_path": "", # 文件路径 256 | "file_size": 0, # 文件大小(字节) 257 | "arch": "", # 架构信息 258 | "bits": 0, # 位数(32/64) 259 | "compiler": "" # 编译器信息(如果可识别) 260 | } 261 | ``` 262 | 263 | ### 6.2 函数和变量分析 264 | 265 | #### `get_function(function_address)` 266 | 267 | **功能**:获取函数的详细信息 268 | 269 | **参数**: 270 | 271 | - `function_address`: 函数地址(字符串格式) 272 | 273 | **返回**:包含函数信息的字典 274 | 275 | ```python 276 | { 277 | "name": "", # 函数名称 278 | "address": "", # 函数地址 279 | "size": 0, # 函数大小(字节) 280 | "prototype": "", # 函数原型 281 | "is_imported": False, # 是否为导入函数 282 | "is_thunk": False, # 是否为 thunk 函数 283 | "callers": [], # 调用此函数的函数列表 284 | "callees": [] # 此函数调用的函数列表 285 | } 286 | ``` 287 | 288 | #### `list_functions()` 289 | 290 | **功能**:列出数据库中的所有函数 291 | 292 | **返回**:函数信息列表(每个元素与 `get_function` 返回格式相同) 293 | 294 | ### 6.3 反汇编和反编译 295 | 296 | #### `decompile_function(function_address)` 297 | 298 | **功能**:反编译函数并获取伪代码 299 | 300 | **参数**: 301 | 302 | - `function_address`: 函数地址 303 | 304 | **返回**:反编译得到的伪代码字符串 305 | 306 | #### `disassemble_function(function_address)` 307 | 308 | **功能**:获取函数的汇编代码信息 309 | 310 | **参数**: 311 | 312 | - `function_address`: 函数地址 313 | 314 | **返回**:包含汇编代码信息的字典 315 | 316 | ```python 317 | { 318 | "address": "", # 函数地址 319 | "name": "", # 函数名称 320 | "instructions": [ # 指令列表 321 | { 322 | "address": "", # 指令地址 323 | "mnem": "", # 指令助记符 324 | "op_str": "", # 操作数字符串 325 | "bytes": "" # 指令字节码 326 | } 327 | # ...更多指令 328 | ] 329 | } 330 | ``` 331 | 332 | ### 6.4 调用图分析 333 | 334 | #### `get_function_call_graph(start_address, depth=3, mermaid=False)` 335 | 336 | **功能**:生成函数调用图 337 | 338 | **参数**: 339 | 340 | - `start_address`: 起始函数地址 341 | - `depth`: 递归深度(默认 3) 342 | - `mermaid`: 是否返回 mermaid 格式的图表定义(默认 False) 343 | 344 | **返回**:包含调用图信息的字典 345 | 346 | ```python 347 | { 348 | "graph": {}, # 邻接表或 mermaid 字符串 349 | "nodes": [], # 节点列表(函数地址) 350 | "edges": [] # 边列表(调用关系) 351 | } 352 | ``` 353 | 354 | **实现原理**: 355 | 356 | 调用图分析功能通过深度优先搜索 (DFS) 遍历函数调用关系,生成函数间的调用关系图。 357 | 358 | **工作流程**: 359 | 360 | 1. **解析起始地址**:将输入的地址字符串转换为整数 361 | 2. **初始化数据结构**:创建访问集合、节点集合和边集合 362 | 3. **深度优先遍历**: 363 | - 从起始函数开始,获取所有被调用的函数 364 | - 记录调用关系(边)和函数(节点) 365 | - 递归处理被调用的函数,直到达到指定深度或所有函数都被访问 366 | 4. **生成结果**:根据参数返回邻接表或 mermaid 格式的调用图 367 | 368 | **核心实现代码**: 369 | 370 | ```python 371 | visited = set() 372 | edges = set() 373 | nodes = set() 374 | 375 | def dfs(addr, d): 376 | if d < 0 or addr in visited: 377 | return 378 | visited.add(addr) 379 | func = idaapi.get_func(parse_address(addr)) 380 | if not func: 381 | return 382 | nodes.add(addr) 383 | for ref in idautils.CodeRefsFrom(func.start_ea, 1): 384 | callee_func = idaapi.get_func(ref) 385 | if callee_func: 386 | callee_addr = hex(callee_func.start_ea) 387 | edges.add((addr, callee_addr)) 388 | dfs(callee_addr, d-1) 389 | 390 | dfs(start_address, depth) 391 | ``` 392 | 393 | ### 6.5 混淆检测 394 | 395 | #### `detect_obfuscation(function_address)` 396 | 397 | **功能**:检测常见混淆模式 398 | 399 | **参数**: 400 | 401 | - `function_address`: 要检测混淆的函数地址 402 | 403 | **返回**:包含混淆检测结果的字典 404 | 405 | ```python 406 | { 407 | "flattening": False, # 是否存在控制流平坦化 408 | "string_encryption": False, # 是否存在字符串加密 409 | "anti_debug": False, # 是否存在反调试技术 410 | "dead_code": False, # 是否存在死代码 411 | "details": "" # 详细信息 412 | } 413 | ``` 414 | 415 | **实现原理**: 416 | 417 | 混淆检测功能通过分析函数的控制流、代码结构和特征模式来识别常见的混淆技术。 418 | 419 | **检测方法**: 420 | 421 | - **控制流平坦化检测**:分析函数的控制流图,检测异常的分支结构 422 | - **字符串加密检测**:识别动态解密字符串的模式和特征 423 | - **反调试检测**:查找常见的反调试指令和API调用 424 | - **死代码检测**:识别无法执行的代码块 425 | 426 | **工作流程**: 427 | 428 | 1. **获取函数信息**:获取目标函数的基本信息和反编译代码 429 | 2. **控制流分析**:生成并分析函数的控制流图 430 | 3. **特征匹配**:匹配已知的混淆模式和特征 431 | 4. **统计分析**:分析代码复杂度、分支数量、指令分布等统计特征 432 | 5. **结果生成**:返回检测结果和详细信息 433 | 434 | **核心实现逻辑**: 435 | 436 | ```python 437 | # 分析控制流图 438 | flowchart = ida_gdl.FlowChart(func) 439 | branch_count = sum(len(list(block.succs())) for block in flowchart) 440 | 441 | # 分析反编译代码 442 | code = decompile_function(function_address) 443 | 444 | # 检测控制流平坦化 445 | flattening = branch_count > 100 # 示例阈值检测 446 | 447 | # 检测字符串加密 448 | string_encryption = "memcpy" in code and "xor" in code # 示例特征匹配 449 | 450 | # 检测反调试 451 | anti_debug = any(kw in code for kw in ["IsDebuggerPresent", "CheckRemoteDebuggerPresent"]) 452 | ``` 453 | 454 | ### 6.6 算法识别 455 | 456 | #### `get_algorithm_signature(function_address)` 457 | 458 | **功能**:自动识别常见算法 459 | 460 | **参数**: 461 | 462 | - `function_address`: 要识别算法的函数地址 463 | 464 | **返回**:包含算法识别结果的字典 465 | 466 | ```python 467 | { 468 | "algorithm": "unknown", # 识别出的算法名称(多个用逗号分隔) 469 | "confidence": 0.0 # 置信度(0.0-1.0) 470 | } 471 | ``` 472 | 473 | **实现原理**: 474 | 475 | 算法识别功能通过分析函数的指令序列、常量特征和模式匹配来识别常见的加密算法、哈希函数和编码方式。 476 | 477 | **识别方法**: 478 | 479 | - **特征匹配**:匹配已知算法的指令序列和常量特征 480 | - **算法签名**:使用预定义的算法签名进行匹配 481 | - **统计分析**:分析函数的指令分布、操作数特征等 482 | 483 | **工作流程**: 484 | 485 | 1. **获取函数信息**:获取目标函数的反编译代码和指令序列 486 | 2. **特征提取**:提取函数的指令特征、常量、操作数等 487 | 3. **模式匹配**:与预定义的算法模式进行匹配 488 | 4. **置信度计算**:计算识别结果的置信度 489 | 5. **结果生成**:返回识别结果和置信度 490 | 491 | **核心实现逻辑**: 492 | 493 | ```python 494 | # 预定义的算法特征 495 | algorithm_patterns = { 496 | "MD5": ["0x67452301", "0xefcdab89", "0x98badcfe", "0x10325476"], 497 | "SHA1": ["0x67452301", "0xefcdab89", "0x98badcfe", "0x10325476", "0xc3d2e1f0"], 498 | "AES": ["sbox", "0x63", "0x7c", "0x77", "0x7b"], 499 | "RC4": ["swap", "xor", "key"], 500 | "Base64": ["A-Za-z0-9+/", "padding", "encode", "decode"] 501 | } 502 | 503 | # 匹配算法特征 504 | max_confidence = 0.0 505 | best_algorithm = "unknown" 506 | for algo, patterns in algorithm_patterns.items(): 507 | matched = 0 508 | total = len(patterns) 509 | for pattern in patterns: 510 | if pattern in code: 511 | matched += 1 512 | confidence = matched / total 513 | if confidence > max_confidence: 514 | max_confidence = confidence 515 | best_algorithm = algo 516 | ``` 517 | 518 | ### 6.7 动态分析辅助 519 | 520 | #### `generate_frida_script(target, script_type, options=None)` 521 | 522 | **功能**:生成 Frida 动态分析脚本 523 | 524 | **参数**: 525 | 526 | - `target`: 目标函数名或地址 527 | - `script_type`: 脚本类型('hook', 'memory_dump', 'string_hook') 528 | - `options`: 可选配置参数 529 | 530 | **返回**:生成的 JavaScript 脚本代码 531 | 532 | #### `save_generated_script(script_content, script_type, file_name=None)` 533 | 534 | **功能**:保存生成的脚本到文件 535 | 536 | **参数**: 537 | 538 | - `script_content`: 脚本内容 539 | - `script_type`: 脚本类型('angr' 或 'frida') 540 | - `file_name`: 保存的文件名(可选,默认生成带时间戳的文件名) 541 | 542 | **返回**:保存结果信息,包含文件路径和运行命令提示 543 | 544 | ## 7. 典型工作流 545 | 546 | ### 7.1 基础分析流程 547 | 548 | 1. 加载二进制文件到 IDA Pro 549 | 2. 启动 MCP 插件服务器(Ctrl-Alt-M) 550 | 3. 使用 `get_metadata()` 获取基本信息 551 | 4. 使用 `list_functions()` 浏览所有函数 552 | 5. 使用 `get_function()` 深入分析特定函数 553 | 6. 使用 `decompile_function()` 获取伪代码 554 | 7. 使用 `detect_obfuscation()` 检测混淆 555 | 8. 使用 `get_algorithm_signature()` 识别算法 556 | 557 | ### 7.2 动态分析流程 558 | 559 | 1. 确定分析目标函数 560 | 2. 使用 `generate_frida_script()` 生成 Hook 脚本 561 | 3. 使用 `save_generated_script()` 保存脚本 562 | 4. 使用 `run_external_script()` 运行脚本并观察结果 563 | 5. 分析输出结果并调整脚本参数 564 | 565 | ### 7.3 批量处理流程 566 | 567 | 1. 选择需要处理的函数集 568 | 2. 使用 `batch_rename_functions()` 统一命名规范 569 | 3. 使用 `batch_set_local_variable_types()` 设置变量类型 570 | 4. 使用 `batch_set_function_prototypes()` 规范函数原型 571 | 5. 使用 `batch_add_comments()` 添加描述性注释 572 | 573 | ## 8. 高级用法 574 | 575 | ### 8.1 使用调用图进行分析 576 | 577 | 1. 使用 `get_function_call_graph()` 生成调用图 578 | 2. 分析关键函数的调用关系 579 | 3. 识别程序的核心组件和数据流 580 | 4. 使用 Mermaid 渲染调用图,可视化分析结果 581 | 582 | ### 8.2 调试器控制 583 | 584 | MCP 插件提供了基本的调试器控制功能,可用于自动化调试过程: 585 | 586 | - `dbg_continue_process()`: 继续执行进程 587 | - `dbg_run_to(address)`: 运行到指定地址 588 | - `dbg_set_breakpoint(address)`: 设置断点 589 | - `dbg_remove_breakpoint(address)`: 移除断点 590 | 591 | ### 8.3 自定义脚本生成 592 | 593 | 通过扩展 `script_utils.py` 模块,您可以自定义脚本生成逻辑,满足特定的分析需求: 594 | 595 | ```python 596 | # 示例:扩展 script_utils.py,添加自定义脚本生成函数 597 | def _generate_custom_script(target, options): 598 | # 自定义脚本生成逻辑 599 | pass 600 | ``` 601 | 602 | ## 9. 故障排除 603 | 604 | ### 9.1 常见问题 605 | 606 | | 问题 | 可能原因 | 解决方案 | 607 | | --------------------- | ----------------------------------- | --------------------------------------------- | 608 | | 插件加载失败 | 插件目录结构不正确 | 检查插件文件是否正确放置在 IDA Pro 插件目录中 | 609 | | 服务器无法启动 | 端口 13337 已被占用 | 修改 `mcp-plugin.py` 中的端口设置 | 610 | | Python 版本不匹配 | IDA Pro 使用的 Python 版本低于 3.11 | 升级 IDA Pro 或使用兼容的 Python 版本 | 611 | | 依赖错误 | 缺少必要的依赖包 | 重新运行 `pip install -r requirements.txt` | 612 | | script_utils 模块错误 | 配置文件中 script_utils_path 不正确 | 检查 config.json 中的 script_utils_path 配置 | 613 | | Frida 脚本生成失败 | 目标函数名或地址无效 | 确保提供的目标是有效的 | 614 | 615 | ### 9.2 日志和调试 616 | 617 | - MCP 插件会在 IDA 控制台输出 [MCP] 前缀的日志 618 | - 检查这些日志可以帮助排查问题 619 | - 可以通过修改 `server.py` 中的 log_level 来调整日志级别 620 | 621 | ## 10. 贡献指南 622 | 623 | 我们欢迎社区贡献!如果您想为 IDA Pro MCP 插件做出贡献,请参考以下指南: 624 | 625 | ### 10.1 开发环境设置 626 | 627 | 1. 克隆项目仓库 628 | 2. 安装开发依赖:`pip install -r requirements.txt` 629 | 3. 安装开发工具:`pip install pytest black isort` 630 | 4. 运行测试:`python test_script_utils.py` 631 | 632 | ### 10.2 代码规范 633 | 634 | - 遵循 PEP 8 代码风格 635 | - 使用 Black 进行代码格式化 636 | - 使用 isort 进行导入排序 637 | - 为所有函数添加文档字符串 638 | - 编写单元测试 639 | 640 | ### 10.3 提交流程 641 | 642 | 1. Fork 项目仓库 643 | 2. 创建一个新的分支 644 | 3. 提交您的更改 645 | 4. 运行测试确保所有测试通过 646 | 5. 创建 Pull Request 647 | 648 | ### 10.4 报告问题 649 | 650 | 如果您发现任何问题或有建议,请在 GitHub Issues 中报告: 651 | 652 | https://github.com/namename333/idapromcp_333/issues 653 | 654 | ## 11. 版本历史 655 | 656 | ### v2.0 (最新) 657 | 658 | - **新增 script_utils 模块** - 提供脚本生成辅助函数库 659 | - **优化 Frida 脚本生成** - 重构 generate_frida_script 函数 660 | - **增强错误处理** - 添加完善的异常处理和回退机制 661 | - **改进安装脚本** - 支持更多平台和 IDA 版本 662 | - **添加日志记录** - 优化日志系统,便于调试和问题排查 663 | - **扩展功能支持** - 增强对 Java 应用、Windows 平台和不同架构的支持 664 | 665 | ### v1.x 666 | 667 | - 初始版本 668 | - 基本 MCP 协议支持 669 | - 核心功能实现 670 | 671 | ## 12. 许可证 672 | 673 | 本项目采用 MIT 许可证,详细信息请查看 [LICENSE](LICENSE) 文件。 674 | 675 | ## 13. 联系方式 676 | 677 | ### 开发者 678 | 679 | - **name** - 开发者(QQ:1559820232) 680 | - **grand** - 开发者 (QQ: 3527424707) 681 | - **Britney** - 开发者 (QQ: 2855057900) 682 | 683 | ### 项目链接 684 | 685 | - GitHub 仓库:https://github.com/namename333/idapromcp_333 686 | - 原项目:https://github.com/mrexodia/ida-pro-mcp 687 | 688 | ## 14. 致谢 689 | 690 | - 感谢 mrexodia 提供的原始 IDA Pro MCP 插件 691 | - 感谢所有为该项目做出贡献的开发者和用户 692 | -------------------------------------------------------------------------------- /src/ida_pro_mcp/server_generated.py: -------------------------------------------------------------------------------- 1 | # NOTE: This file has been automatically generated, do not modify! 2 | # Architecture based on https://github.com/namename333/idapromcp_333 (MIT License) 3 | import sys 4 | if sys.version_info >= (3, 12): 5 | from typing import Annotated, Optional, TypedDict, Generic, TypeVar, NotRequired, Union 6 | else: 7 | from typing_extensions import Annotated, Optional, TypedDict, Generic, TypeVar, NotRequired 8 | from typing import Union 9 | from pydantic import Field 10 | 11 | T = TypeVar("T") 12 | 13 | class Metadata(TypedDict): 14 | path: str 15 | module: str 16 | base: str 17 | size: str 18 | md5: str 19 | sha256: str 20 | crc32: str 21 | filesize: str 22 | 23 | class Function(TypedDict): 24 | address: str 25 | name: str 26 | size: str 27 | 28 | class ConvertedNumber(TypedDict): 29 | decimal: str 30 | hexadecimal: str 31 | bytes: str 32 | ascii: Optional[str] 33 | binary: str 34 | 35 | class Page(TypedDict, Generic[T]): 36 | data: list[T] 37 | next_offset: Optional[int] 38 | 39 | class Global(TypedDict): 40 | address: str 41 | name: str 42 | 43 | class Import(TypedDict): 44 | address: str 45 | imported_name: str 46 | module: str 47 | 48 | class String(TypedDict): 49 | address: str 50 | length: int 51 | string: str 52 | 53 | class DisassemblyLine(TypedDict): 54 | segment: NotRequired[str] 55 | address: str 56 | label: NotRequired[str] 57 | instruction: str 58 | comments: NotRequired[list[str]] 59 | 60 | class Argument(TypedDict): 61 | name: str 62 | type: str 63 | 64 | class DisassemblyFunction(TypedDict): 65 | name: str 66 | start_ea: str 67 | return_type: NotRequired[str] 68 | arguments: NotRequired[list[Argument]] 69 | stack_frame: list[dict] 70 | lines: list[DisassemblyLine] 71 | 72 | class Xref(TypedDict): 73 | address: str 74 | type: str 75 | function: Optional[Function] 76 | 77 | class StackFrameVariable(TypedDict): 78 | name: str 79 | offset: str 80 | size: str 81 | type: str 82 | 83 | class StructureMember(TypedDict): 84 | name: str 85 | offset: str 86 | size: str 87 | type: str 88 | 89 | class StructureDefinition(TypedDict): 90 | name: str 91 | size: str 92 | members: list[StructureMember] 93 | 94 | @mcp.tool() 95 | def check_connection() -> dict: 96 | """ 97 | 标准MCP协议接口:检查与服务器的连接 98 | 用于客户端验证服务是否正常运行 99 | """ 100 | return make_jsonrpc_request('check_connection') 101 | 102 | @mcp.tool() 103 | def get_methods() -> list[dict]: 104 | """ 105 | 获取所有可用的JSON-RPC方法列表及其元数据 106 | 支持MCP协议的自描述功能 107 | """ 108 | return make_jsonrpc_request('get_methods') 109 | 110 | @mcp.tool() 111 | def get_metadata() -> Metadata: 112 | """获取当前 IDB 的元数据""" 113 | return make_jsonrpc_request('get_metadata') 114 | 115 | @mcp.tool() 116 | def get_function_by_name(name: Annotated[str, Field(description='要获取的函数名称')]) -> Function: 117 | """根据函数名称获取函数""" 118 | return make_jsonrpc_request('get_function_by_name', name) 119 | 120 | @mcp.tool() 121 | def get_function_by_address(address: Annotated[str, Field(description='要获取的函数地址')]) -> Function: 122 | """根据函数地址获取函数""" 123 | return make_jsonrpc_request('get_function_by_address', address) 124 | 125 | @mcp.tool() 126 | def get_current_address() -> str: 127 | """获取用户当前选中的地址""" 128 | return make_jsonrpc_request('get_current_address') 129 | 130 | @mcp.tool() 131 | def get_current_function() -> Optional[Function]: 132 | """获取用户当前选中的函数""" 133 | return make_jsonrpc_request('get_current_function') 134 | 135 | @mcp.tool() 136 | def convert_number(text: Annotated[str, Field(description='要转换的数字的文本表示')], size: Annotated[Optional[int], Field(description='变量的大小(字节)')]) -> ConvertedNumber: 137 | """将数字(十进制、十六进制)转换为不同表示""" 138 | return make_jsonrpc_request('convert_number', text, size) 139 | 140 | @mcp.tool() 141 | def list_functions(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的函数数量 (100 是默认值,0 表示剩余)')]) -> Page[Function]: 142 | """列出数据库中的所有函数(分页) 143 | 144 | 警告: 此API已弃用,请使用更通用的query_database API 145 | """ 146 | return make_jsonrpc_request('list_functions', offset, count) 147 | 148 | @mcp.tool() 149 | def query_database(entity_type: Annotated[str, Field(description="实体类型: 'functions', 'globals', 'strings', 'imports'")], offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的实体数量 (100 是默认值,0 表示剩余)')], filter: Annotated[str, Field(description='可选的过滤模式,空字符串表示无过滤')]) -> Page[dict]: 150 | """统一的数据库查询API,可查询各种实体类型 151 | 152 | 用于替代特定的list_xxx和list_xxx_filter函数 153 | """ 154 | return make_jsonrpc_request('query_database', entity_type, offset, count, filter) 155 | 156 | @mcp.tool() 157 | def list_globals_filter(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的全局变量数量 (100 是默认值,0 表示剩余)')], filter: Annotated[str, Field(description='要应用的过滤器 (必需参数,空字符串表示无过滤). 大小写不敏感包含或 /regex/ 语法')]) -> Page[Global]: 158 | """列出数据库中的匹配全局变量(分页,过滤)""" 159 | return make_jsonrpc_request('list_globals_filter', offset, count, filter) 160 | 161 | @mcp.tool() 162 | def list_globals(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的全局变量数量 (100 是默认值,0 表示剩余)')]) -> Page[Global]: 163 | """列出数据库中的所有全局变量(分页) 164 | 165 | 警告: 此API已弃用,请使用更通用的query_database API 166 | """ 167 | return make_jsonrpc_request('list_globals', offset, count) 168 | 169 | @mcp.tool() 170 | def list_imports(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的导入符号数量 (100 是默认值,0 表示剩余)')]) -> Page[Import]: 171 | """ 列出所有导入符号及其名称和模块(分页) """ 172 | return make_jsonrpc_request('list_imports', offset, count) 173 | 174 | @mcp.tool() 175 | def list_strings_filter(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的字符串数量 (100 是默认值,0 表示剩余)')], filter: Annotated[str, Field(description='要应用的过滤器 (必需参数,空字符串表示无过滤). 大小写不敏感包含或 /regex/ 语法')]) -> Page[String]: 176 | """列出数据库中的匹配字符串(分页,过滤)""" 177 | return make_jsonrpc_request('list_strings_filter', offset, count, filter) 178 | 179 | @mcp.tool() 180 | def list_strings(offset: Annotated[int, Field(description='从 (0) 开始列出偏移量')], count: Annotated[int, Field(description='要列出的字符串数量 (100 是默认值,0 表示剩余)')]) -> Page[String]: 181 | """列出数据库中的所有字符串(分页) 182 | 183 | 警告: 此API已弃用,请使用更通用的query_database API 184 | """ 185 | return make_jsonrpc_request('list_strings', offset, count) 186 | 187 | @mcp.tool() 188 | def list_local_types(): 189 | """列出数据库中的所有本地类型""" 190 | return make_jsonrpc_request('list_local_types') 191 | 192 | @mcp.tool() 193 | def decompile_function(address: Annotated[str, Field(description='要反编译的函数地址')]) -> str: 194 | """反编译给定地址的函数""" 195 | return make_jsonrpc_request('decompile_function', address) 196 | 197 | @mcp.tool() 198 | def disassemble_function(start_address: Annotated[str, Field(description='要反汇编的函数地址')]) -> DisassemblyFunction: 199 | """获取函数汇编代码""" 200 | return make_jsonrpc_request('disassemble_function', start_address) 201 | 202 | @mcp.tool() 203 | def get_xrefs_to(address: Annotated[str, Field(description='要获取交叉引用的地址')]) -> list[Xref]: 204 | """获取给定地址的所有交叉引用""" 205 | return make_jsonrpc_request('get_xrefs_to', address) 206 | 207 | @mcp.tool() 208 | def get_xrefs_to_field(struct_name: Annotated[str, Field(description='结构体名称 (类型)')], field_name: Annotated[str, Field(description='要获取交叉引用的字段名称 (成员)')]) -> list[Xref]: 209 | """获取命名结构体字段 (成员) 的所有交叉引用""" 210 | return make_jsonrpc_request('get_xrefs_to_field', struct_name, field_name) 211 | 212 | @mcp.tool() 213 | def get_entry_points() -> list[Function]: 214 | """获取数据库中的所有入口点""" 215 | return make_jsonrpc_request('get_entry_points') 216 | 217 | @mcp.tool() 218 | def set_comment(address: Annotated[str, Field(description='要设置注释的函数地址')], comment: Annotated[str, Field(description='注释文本')]): 219 | """设置给定函数反汇编和伪代码中的注释""" 220 | return make_jsonrpc_request('set_comment', address, comment) 221 | 222 | @mcp.tool() 223 | def rename_local_variable(function_address: Annotated[str, Field(description='包含变量的函数地址')], old_name: Annotated[str, Field(description='变量的当前名称')], new_name: Annotated[str, Field(description='变量的新名称 (空表示默认名称)')]): 224 | """重命名函数中的本地变量""" 225 | return make_jsonrpc_request('rename_local_variable', function_address, old_name, new_name) 226 | 227 | @mcp.tool() 228 | def rename_global_variable(old_name: Annotated[str, Field(description='全局变量的当前名称')], new_name: Annotated[str, Field(description='全局变量的新名称 (空表示默认名称)')]): 229 | """重命名全局变量""" 230 | return make_jsonrpc_request('rename_global_variable', old_name, new_name) 231 | 232 | @mcp.tool() 233 | def set_global_variable_type(variable_name: Annotated[str, Field(description='全局变量的名称')], new_type: Annotated[str, Field(description='变量的新类型')]): 234 | """设置全局变量的类型""" 235 | return make_jsonrpc_request('set_global_variable_type', variable_name, new_type) 236 | 237 | @mcp.tool() 238 | def get_global_variable_value_by_name(variable_name: Annotated[str, Field(description='全局变量的名称')]) -> str: 239 | """ 240 | 读取全局变量的值(如果编译时已知) 241 | 242 | 优先使用此函数,而不是 `data_read_*` 函数。 243 | """ 244 | return make_jsonrpc_request('get_global_variable_value_by_name', variable_name) 245 | 246 | @mcp.tool() 247 | def get_global_variable_value_at_address(ea: Annotated[str, Field(description='全局变量的地址')]) -> str: 248 | """ 249 | 通过地址读取全局变量的值(如果编译时已知) 250 | 251 | 优先使用此函数,而不是 `data_read_*` 函数。 252 | """ 253 | return make_jsonrpc_request('get_global_variable_value_at_address', ea) 254 | 255 | @mcp.tool() 256 | def rename_function(function_address: Annotated[str, Field(description='要重命名的函数地址')], new_name: Annotated[str, Field(description='函数的新名称 (空表示默认名称)')]): 257 | """重命名函数""" 258 | return make_jsonrpc_request('rename_function', function_address, new_name) 259 | 260 | @mcp.tool() 261 | def set_function_prototype(function_address: Annotated[str, Field(description='函数地址')], prototype: Annotated[str, Field(description='新的函数原型')]): 262 | """设置函数原型""" 263 | return make_jsonrpc_request('set_function_prototype', function_address, prototype) 264 | 265 | @mcp.tool() 266 | def declare_c_type(c_declaration: Annotated[str, Field(description='类型C声明。示例包括:typedef int foo_t; struct bar { int a; bool b; };')]): 267 | """从C声明创建或更新本地类型""" 268 | return make_jsonrpc_request('declare_c_type', c_declaration) 269 | 270 | @mcp.tool() 271 | def set_local_variable_type(function_address: Annotated[str, Field(description='要反编译的函数地址')], variable_name: Annotated[str, Field(description='变量名称')], new_type: Annotated[str, Field(description='变量的新类型')]): 272 | """设置本地变量的类型""" 273 | return make_jsonrpc_request('set_local_variable_type', function_address, variable_name, new_type) 274 | 275 | @mcp.tool() 276 | def get_stack_frame_variables(function_address: Annotated[str, Field(description='要获取栈帧变量的反汇编函数地址')]) -> list[StackFrameVariable]: 277 | """获取给定函数的栈帧变量""" 278 | return make_jsonrpc_request('get_stack_frame_variables', function_address) 279 | 280 | @mcp.tool() 281 | def get_defined_structures() -> list[StructureDefinition]: 282 | """返回所有定义的结构体列表""" 283 | return make_jsonrpc_request('get_defined_structures') 284 | 285 | @mcp.tool() 286 | def rename_stack_frame_variable(function_address: Annotated[str, Field(description='要设置栈帧变量的反汇编函数地址')], old_name: Annotated[str, Field(description='变量的当前名称')], new_name: Annotated[str, Field(description='变量的新名称 (空表示默认名称)')]): 287 | """更改IDA函数中栈帧变量的名称""" 288 | return make_jsonrpc_request('rename_stack_frame_variable', function_address, old_name, new_name) 289 | 290 | @mcp.tool() 291 | def create_stack_frame_variable(function_address: Annotated[str, Field(description='要设置栈帧变量的反汇编函数地址')], offset: Annotated[str, Field(description='栈帧变量的偏移量')], variable_name: Annotated[str, Field(description='栈变量名称')], type_name: Annotated[str, Field(description='栈变量类型')]): 292 | """对于给定的函数,在指定偏移量处创建一个栈变量并设置特定类型""" 293 | return make_jsonrpc_request('create_stack_frame_variable', function_address, offset, variable_name, type_name) 294 | 295 | @mcp.tool() 296 | def set_stack_frame_variable_type(function_address: Annotated[str, Field(description='要设置栈帧变量的反汇编函数地址')], variable_name: Annotated[str, Field(description='栈变量名称')], type_name: Annotated[str, Field(description='栈变量类型')]): 297 | """对于给定的反汇编函数,设置栈变量的类型""" 298 | return make_jsonrpc_request('set_stack_frame_variable_type', function_address, variable_name, type_name) 299 | 300 | @mcp.tool() 301 | def delete_stack_frame_variable(function_address: Annotated[str, Field(description='要设置栈帧变量的函数地址')], variable_name: Annotated[str, Field(description='栈变量名称')]): 302 | """删除给定函数的命名栈变量""" 303 | return make_jsonrpc_request('delete_stack_frame_variable', function_address, variable_name) 304 | 305 | @mcp.tool() 306 | def read_memory_bytes(memory_address: Annotated[str, Field(description='要读取的字节地址')], size: Annotated[int, Field(description='要读取的内存大小')]) -> str: 307 | """ 308 | 读取指定地址的字节。 309 | 310 | 仅当 `get_global_variable_at` 和 `get_global_variable_by_name` 311 | 都失败时才使用此函数。 312 | """ 313 | return make_jsonrpc_request('read_memory_bytes', memory_address, size) 314 | 315 | @mcp.tool() 316 | def dbg_get_registers() -> list[dict[str, str]]: 317 | """获取所有寄存器及其值。此函数仅在调试时可用。""" 318 | return make_jsonrpc_request('dbg_get_registers') 319 | 320 | @mcp.tool() 321 | def dbg_get_call_stack() -> list[dict[str, str]]: 322 | """获取当前调用堆栈。""" 323 | return make_jsonrpc_request('dbg_get_call_stack') 324 | 325 | @mcp.tool() 326 | def dbg_control_process(action: Annotated[str, Field(description="调试操作类型: 'start', 'exit', 'continue', 'run_to'")], address: Annotated[Optional[str], Field(description='目标地址,仅run_to操作需要')]=None) -> str: 327 | """统一的调试器控制接口 328 | 329 | Args: 330 | action: 调试操作类型,支持'start', 'exit', 'continue', 'run_to' 331 | address: 目标地址,仅在action为'run_to'时需要 332 | 333 | Returns: 334 | 操作结果消息 335 | """ 336 | return make_jsonrpc_request('dbg_control_process', action, address) 337 | 338 | @mcp.tool() 339 | def dbg_start_process() -> str: 340 | """启动调试器 (已弃用,请使用dbg_control_process)""" 341 | return make_jsonrpc_request('dbg_start_process') 342 | 343 | @mcp.tool() 344 | def dbg_exit_process() -> str: 345 | """退出调试器 (已弃用,请使用dbg_control_process)""" 346 | return make_jsonrpc_request('dbg_exit_process') 347 | 348 | @mcp.tool() 349 | def dbg_continue_process() -> str: 350 | """继续调试器 (已弃用,请使用dbg_control_process)""" 351 | return make_jsonrpc_request('dbg_continue_process') 352 | 353 | @mcp.tool() 354 | def dbg_run_to(address: Annotated[str, Field(description='运行调试器到指定地址')]) -> str: 355 | """运行调试器到指定地址 (已弃用,请使用dbg_control_process)""" 356 | return make_jsonrpc_request('dbg_run_to', address) 357 | 358 | @mcp.tool() 359 | def dbg_manage_breakpoint(action: Annotated[str, Field(description="断点操作类型: 'list', 'set', 'delete', 'enable'")], address: Annotated[Optional[str], Field(description='断点地址,仅set/delete/enable操作需要')]=None, enable: Annotated[Optional[bool], Field(description='是否启用断点,仅enable操作需要')]=None) -> Union[str, list[dict[str, str]]]: 360 | """统一的断点管理接口 361 | 362 | Args: 363 | action: 断点操作类型,支持'list', 'set', 'delete', 'enable' 364 | address: 断点地址,仅在action为'set', 'delete', 'enable'时需要 365 | enable: 是否启用断点,仅在action为'enable'时需要 366 | 367 | Returns: 368 | 操作结果消息或断点列表 369 | """ 370 | return make_jsonrpc_request('dbg_manage_breakpoint', action, address, enable) 371 | 372 | @mcp.tool() 373 | def dbg_list_breakpoints(): 374 | """列出程序中的所有断点 (已弃用,请使用dbg_manage_breakpoint)""" 375 | return make_jsonrpc_request('dbg_list_breakpoints') 376 | 377 | @mcp.tool() 378 | def dbg_set_breakpoint(address: Annotated[str, Field(description='在指定地址设置断点')]) -> str: 379 | """在指定地址设置断点 (已弃用,请使用dbg_manage_breakpoint)""" 380 | return make_jsonrpc_request('dbg_set_breakpoint', address) 381 | 382 | @mcp.tool() 383 | def dbg_delete_breakpoint(address: Annotated[str, Field(description='del a breakpoint at the specified address')]) -> str: 384 | """del a breakpoint at the specified address (已弃用,请使用dbg_manage_breakpoint)""" 385 | return make_jsonrpc_request('dbg_delete_breakpoint', address) 386 | 387 | @mcp.tool() 388 | def dbg_enable_breakpoint(address: Annotated[str, Field(description='Enable or disable a breakpoint at the specified address')], enable: Annotated[bool, Field(description='Enable or disable a breakpoint')]) -> str: 389 | """Enable or disable a breakpoint at the specified address (已弃用,请使用dbg_manage_breakpoint)""" 390 | return make_jsonrpc_request('dbg_enable_breakpoint', address, enable) 391 | 392 | @mcp.tool() 393 | def generate_angr_script(function_address: Annotated[str, Field(description='要分析的函数地址')], script_type: Annotated[str, Field(description="脚本类型: 'symbolic_execution', 'brute_force', 'control_flow'")], options: Annotated[dict, Field(description='可选配置参数,例如输入约束、输出格式等')]=None) -> str: 394 | """ 395 | 生成angr符号执行或爆破脚本。 396 | 参数: 397 | function_address: 目标函数地址 398 | script_type: 脚本类型 399 | options: 可选配置 400 | 返回: 401 | 生成的Python脚本代码 402 | """ 403 | return make_jsonrpc_request('generate_angr_script', function_address, script_type, options) 404 | 405 | @mcp.tool() 406 | def generate_frida_script(target: Annotated[str, Field(description='目标函数名或地址')], script_type: Annotated[str, Field(description="脚本类型: 'hook', 'memory_dump', 'string_hook'")], options: Annotated[dict, Field(description='可选配置参数')]=None) -> str: 407 | """ 408 | 生成frida动态分析脚本。 409 | 参数: 410 | target: 目标函数名或地址 411 | script_type: 脚本类型 412 | options: 可选配置 413 | 返回: 414 | 生成的JavaScript脚本代码 415 | """ 416 | return make_jsonrpc_request('generate_frida_script', target, script_type, options) 417 | 418 | @mcp.tool() 419 | def save_generated_script(script_content: Annotated[str, Field(description='脚本内容')], script_type: Annotated[str, Field(description="脚本类型: 'angr' 或 'frida'")], file_name: Annotated[str, Field(description='保存的文件名(可选)')]=None) -> str: 420 | """ 421 | 保存生成的脚本到文件。 422 | 参数: 423 | script_content: 脚本内容 424 | script_type: 脚本类型 425 | file_name: 保存的文件名(可选) 426 | 返回: 427 | 保存的文件路径 428 | """ 429 | return make_jsonrpc_request('save_generated_script', script_content, script_type, file_name) 430 | 431 | @mcp.tool() 432 | def run_external_script(script_path: Annotated[str, Field(description='脚本文件路径')], script_type: Annotated[str, Field(description="脚本类型: 'angr' 或 'frida'")], target_binary: Annotated[str, Field(description='目标二进制文件路径')]=None, target_pid: Annotated[int, Field(description='目标进程ID(可选,用于frida)')]=None) -> str: 433 | """ 434 | 运行外部脚本(angr或frida)。 435 | 参数: 436 | script_path: 脚本文件路径 437 | script_type: 脚本类型 438 | target_binary: 目标二进制文件路径(可选) 439 | target_pid: 目标进程ID(可选,用于frida附加到运行中的进程) 440 | 返回: 441 | 脚本执行的输出结果 442 | """ 443 | return make_jsonrpc_request('run_external_script', script_path, script_type, target_binary, target_pid) 444 | 445 | @mcp.tool() 446 | def get_function_call_graph(start_address: Annotated[str, Field(description='起始函数地址')], depth: Annotated[int, Field(description='递归深度')]=3, mermaid: Annotated[bool, Field(description='是否返回 mermaid 格式')]=False) -> dict: 447 | """ 448 | 获取函数调用图。 449 | 参数: 450 | start_address: 起始函数地址(字符串) 451 | depth: 递归深度,默认3 452 | mermaid: 是否返回 mermaid 格式(默认False,返回邻接表) 453 | 返回: 454 | {"graph": 邻接表或mermaid字符串, "nodes": 节点列表, "edges": 边列表} 455 | """ 456 | return make_jsonrpc_request('get_function_call_graph', start_address, depth, mermaid) 457 | 458 | @mcp.tool() 459 | def get_analysis_report() -> dict: 460 | """ 461 | 自动生成结构化分析报告。 462 | """ 463 | return make_jsonrpc_request('get_analysis_report') 464 | 465 | @mcp.tool() 466 | def get_incremental_changes() -> list: 467 | """ 468 | 返回自上次分析以来的增量变更。 469 | """ 470 | return make_jsonrpc_request('get_incremental_changes') 471 | 472 | @mcp.tool() 473 | def get_dynamic_string_map() -> dict: 474 | """ 475 | 动态字符串解密映射(静态+动态分析结果)。 476 | """ 477 | return make_jsonrpc_request('get_dynamic_string_map') 478 | 479 | @mcp.tool() 480 | def generate_analysis_report_md() -> str: 481 | """ 482 | 一键生成结构化 markdown 报告,帮助用户快速理解程序核心逻辑。 483 | """ 484 | return make_jsonrpc_request('generate_analysis_report_md') 485 | 486 | -------------------------------------------------------------------------------- /src/ida_pro_mcp/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ast 4 | import json 5 | import shutil 6 | import argparse 7 | import http.client 8 | from urllib.parse import urlparse 9 | 10 | from mcp.server.fastmcp import FastMCP 11 | 12 | # log_level 对于 Cline 正常工作是必需的:https://github.com/jlowin/fastmcp/issues/81 13 | mcp = FastMCP( 14 | "github.com/namename333/idapromcp_333", 15 | log_level="ERROR" 16 | ) 17 | 18 | jsonrpc_request_id = 1 19 | 20 | def get_config_file_path(): 21 | """ 22 | 获取配置文件路径 23 | 支持多种配置文件位置 24 | """ 25 | # 优先检查用户目录 26 | user_dir = os.path.expanduser("~") 27 | config_paths = [ 28 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp_config.json"), 29 | os.path.join(user_dir, ".mcp_config.json"), 30 | os.path.join(user_dir, "mcp_config.json") 31 | ] 32 | 33 | # 检查是否存在IDA插件目录下的配置文件 34 | try: 35 | import ida_idaapi 36 | plugin_dir = ida_idaapi.idadir("plugins") 37 | config_paths.append(os.path.join(plugin_dir, "mcp_config.json")) 38 | except ImportError: 39 | pass # 如果不在IDA环境中运行,忽略 40 | 41 | # 返回第一个存在的配置文件 42 | for path in config_paths: 43 | if os.path.exists(path): 44 | return path 45 | 46 | # 如果没有找到配置文件,返回默认路径 47 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp_config.json") 48 | 49 | def load_config(): 50 | """ 51 | 加载配置文件 52 | 返回配置字典,如果配置文件不存在则返回默认配置 53 | """ 54 | default_config = { 55 | "host": "127.0.0.1", 56 | "port": 13337 57 | } 58 | 59 | config_path = get_config_file_path() 60 | if os.path.exists(config_path): 61 | try: 62 | with open(config_path, 'r', encoding='utf-8') as f: 63 | user_config = json.load(f) 64 | # 合并默认配置和用户配置 65 | default_config.update(user_config) 66 | except Exception as e: 67 | print(f"警告: 加载配置文件失败: {e}") 68 | 69 | # 从环境变量覆盖配置 70 | if "MCP_HOST" in os.environ: 71 | default_config["host"] = os.environ["MCP_HOST"] 72 | 73 | if "MCP_PORT" in os.environ: 74 | try: 75 | default_config["port"] = int(os.environ["MCP_PORT"]) 76 | except ValueError: 77 | print("警告: 环境变量MCP_PORT不是有效的端口号") 78 | 79 | return default_config 80 | 81 | # 加载配置 82 | config = load_config() 83 | 84 | # 服务器配置 85 | ida_host = config["host"] 86 | ida_port = config["port"] 87 | 88 | def make_jsonrpc_request(method: str, *params): 89 | """Make a JSON-RPC request to the IDA plugin""" 90 | global jsonrpc_request_id, ida_host, ida_port 91 | conn = http.client.HTTPConnection(ida_host, ida_port) 92 | request = { 93 | "jsonrpc": "2.0", 94 | "method": method, 95 | "params": list(params), 96 | "id": jsonrpc_request_id, 97 | } 98 | jsonrpc_request_id += 1 99 | 100 | try: 101 | conn.request("POST", "/mcp", json.dumps(request), { 102 | "Content-Type": "application/json" 103 | }) 104 | response = conn.getresponse() 105 | data = json.loads(response.read().decode()) 106 | 107 | if "error" in data: 108 | error = data["error"] 109 | code = error["code"] 110 | message = error["message"] 111 | pretty = f"JSON-RPC error {code}: {message}" 112 | if "data" in error: 113 | pretty += "\n" + error["data"] 114 | raise Exception(pretty) 115 | 116 | result = data["result"] 117 | # NOTE: LLMs do not respond well to empty responses 118 | if result is None: 119 | result = "success" 120 | return result 121 | except Exception: 122 | raise 123 | finally: 124 | conn.close() 125 | 126 | @mcp.tool() 127 | def check_connection() -> str: 128 | """检查 IDA 插件是否正在运行""" 129 | try: 130 | metadata = make_jsonrpc_request("get_metadata") 131 | return f"成功连接到 IDA Pro (打开文件: {metadata['module']})" 132 | except Exception as e: 133 | if sys.platform == "darwin": 134 | shortcut = "Ctrl+Option+M" 135 | else: 136 | shortcut = "Ctrl+Alt+M" 137 | return f"无法连接到 IDA Pro! 您是否运行了 Edit -> Plugins -> MCP ({shortcut}) 启动服务器?" 138 | 139 | # Code taken from https://github.com/namename333/idapromcp_333 (MIT License) 140 | class MCPVisitor(ast.NodeVisitor): 141 | def __init__(self): 142 | self.types: dict[str, ast.ClassDef] = {} 143 | self.functions: dict[str, ast.FunctionDef] = {} 144 | self.descriptions: dict[str, str] = {} 145 | self.unsafe: list[str] = [] 146 | 147 | def visit_FunctionDef(self, node): 148 | for decorator in node.decorator_list: 149 | if isinstance(decorator, ast.Name): 150 | if decorator.id == "jsonrpc": 151 | for i, arg in enumerate(node.args.args): 152 | arg_name = arg.arg 153 | arg_type = arg.annotation 154 | if arg_type is None: 155 | raise Exception(f"缺少参数类型 {node.name}.{arg_name}") 156 | if isinstance(arg_type, ast.Subscript): 157 | assert isinstance(arg_type.value, ast.Name) 158 | assert arg_type.value.id == "Annotated" 159 | assert isinstance(arg_type.slice, ast.Tuple) 160 | assert len(arg_type.slice.elts) == 2 161 | annot_type = arg_type.slice.elts[0] 162 | annot_description = arg_type.slice.elts[1] 163 | assert isinstance(annot_description, ast.Constant) 164 | node.args.args[i].annotation = ast.Subscript( 165 | value=ast.Name(id="Annotated", ctx=ast.Load()), 166 | slice=ast.Tuple( 167 | elts=[ 168 | annot_type, 169 | ast.Call( 170 | func=ast.Name(id="Field", ctx=ast.Load()), 171 | args=[], 172 | keywords=[ 173 | ast.keyword( 174 | arg="description", 175 | value=annot_description)])], 176 | ctx=ast.Load()), 177 | ctx=ast.Load()) 178 | elif isinstance(arg_type, ast.Name): 179 | pass 180 | else: 181 | raise Exception(f"意外的参数类型注解 {node.name}.{arg_name} -> {type(arg_type)}") 182 | 183 | body_comment = node.body[0] 184 | if isinstance(body_comment, ast.Expr) and isinstance(body_comment.value, ast.Constant): 185 | new_body = [body_comment] 186 | self.descriptions[node.name] = body_comment.value.value 187 | else: 188 | new_body = [] 189 | 190 | call_args = [ast.Constant(value=node.name)] 191 | for arg in node.args.args: 192 | call_args.append(ast.Name(id=arg.arg, ctx=ast.Load())) 193 | new_body.append(ast.Return( 194 | value=ast.Call( 195 | func=ast.Name(id="make_jsonrpc_request", ctx=ast.Load()), 196 | args=call_args, 197 | keywords=[]))) 198 | decorator_list = [ 199 | ast.Call( 200 | func=ast.Attribute( 201 | value=ast.Name(id="mcp", ctx=ast.Load()), 202 | attr="tool", 203 | ctx=ast.Load()), 204 | args=[], 205 | keywords=[] 206 | ) 207 | ] 208 | node_nobody = ast.FunctionDef(node.name, node.args, new_body, decorator_list, node.returns, node.type_comment, lineno=node.lineno, col_offset=node.col_offset) 209 | assert node.name not in self.functions, f"重复函数: {node.name}" 210 | self.functions[node.name] = node_nobody 211 | elif decorator.id == "unsafe": 212 | self.unsafe.append(node.name) 213 | 214 | def visit_ClassDef(self, node): 215 | for base in node.bases: 216 | if isinstance(base, ast.Name): 217 | if base.id == "TypedDict": 218 | self.types[node.name] = node 219 | 220 | 221 | SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 222 | IDA_PLUGIN_PY = os.path.join(SCRIPT_DIR, "mcp-plugin.py") 223 | GENERATED_PY = os.path.join(SCRIPT_DIR, "server_generated.py") 224 | 225 | # NOTE: This is in the global scope on purpose 226 | if not os.path.exists(IDA_PLUGIN_PY): 227 | raise RuntimeError(f"IDA 插件未找到 {IDA_PLUGIN_PY} (您是否移动了它?)") 228 | with open(IDA_PLUGIN_PY, "r", encoding="utf-8") as f: 229 | code = f.read() 230 | module = ast.parse(code, IDA_PLUGIN_PY) 231 | visitor = MCPVisitor() 232 | visitor.visit(module) 233 | code = """# NOTE: This file has been automatically generated, do not modify! 234 | # Architecture based on https://github.com/namename333/idapromcp_333 (MIT License) 235 | import sys 236 | if sys.version_info >= (3, 12): 237 | from typing import Annotated, Optional, TypedDict, Generic, TypeVar, NotRequired, Union 238 | else: 239 | from typing_extensions import Annotated, Optional, TypedDict, Generic, TypeVar, NotRequired 240 | from typing import Union 241 | from pydantic import Field 242 | 243 | T = TypeVar("T") 244 | 245 | """ 246 | for type in visitor.types.values(): 247 | code += ast.unparse(type) 248 | code += "\n\n" 249 | for function in visitor.functions.values(): 250 | code += ast.unparse(function) 251 | code += "\n\n" 252 | 253 | print("Code generation complete. Writing to file...") 254 | try: 255 | with open(GENERATED_PY, "w", encoding="utf-8") as f: 256 | f.write(code) 257 | print(f"Successfully wrote to {GENERATED_PY}") 258 | except Exception as e: 259 | print(f"Failed to write file: {e}") 260 | raise 261 | 262 | print("Compiling and executing...") 263 | with open(GENERATED_PY, "w", encoding="utf-8") as f: 264 | f.write(code) 265 | exec(compile(code, GENERATED_PY, "exec")) 266 | 267 | # 所有可用的 MCP 函数列表 - 确保包含标准MCP协议接口 268 | MCP_FUNCTIONS = ["check_connection", "get_methods"] + list(visitor.functions.keys()) 269 | UNSAFE_FUNCTIONS = visitor.unsafe 270 | 271 | def generate_readme(): 272 | print("README:") 273 | print(f"- `check_connection()`: 检查 IDA 插件是否正在运行。") 274 | def get_description(name: str): 275 | if name not in visitor.functions: 276 | return f"- `{name}()`: <函数未定义>" 277 | function = visitor.functions[name] 278 | signature = function.name + "(" 279 | for i, arg in enumerate(function.args.args): 280 | if i > 0: 281 | signature += ", " 282 | signature += arg.arg 283 | signature += ")" 284 | description = visitor.descriptions.get(function.name, "<无描述>").strip() 285 | if description and description[-1] != ".": 286 | description += "." 287 | return f"- `{signature}`: {description}" 288 | # 只处理有描述的函数 289 | for safe_function in MCP_FUNCTIONS: # Changed from SAFE_FUNCTIONS to MCP_FUNCTIONS 290 | if safe_function in visitor.functions: 291 | print(get_description(safe_function)) 292 | print("\n不安全函数 (`--unsafe` 标志需要)`:\n") 293 | for unsafe_function in UNSAFE_FUNCTIONS: 294 | if unsafe_function in visitor.functions: 295 | print(get_description(unsafe_function)) 296 | print("\nMCP 配置:") 297 | mcp_config = { 298 | "mcpServers": { 299 | "github.com/namename333/idapromcp_333": { 300 | "command": get_python_executable(), 301 | "args": [ 302 | __file__, 303 | ], 304 | "timeout": 1800, 305 | "disabled": False, 306 | } 307 | } 308 | } 309 | print(json.dumps(mcp_config, indent=2)) 310 | 311 | def get_python_executable(): 312 | """获取 Python 可执行文件的路径""" 313 | venv = os.environ.get("VIRTUAL_ENV") 314 | if venv: 315 | if sys.platform == "win32": 316 | python = os.path.join(venv, "Scripts", "python.exe") 317 | else: 318 | python = os.path.join(venv, "bin", "python3") 319 | if os.path.exists(python): 320 | return python 321 | 322 | for path in sys.path: 323 | if sys.platform == "win32": 324 | path = path.replace("/", "\\") 325 | 326 | split = path.split(os.sep) 327 | if split[-1].endswith(".zip"): 328 | path = os.path.dirname(path) 329 | if sys.platform == "win32": 330 | python_executable = os.path.join(path, "python.exe") 331 | else: 332 | python_executable = os.path.join(path, "..", "bin", "python3") 333 | python_executable = os.path.abspath(python_executable) 334 | 335 | if os.path.exists(python_executable): 336 | return python_executable 337 | return sys.executable 338 | 339 | def print_mcp_config(): 340 | print(json.dumps({ 341 | "mcpServers": { 342 | mcp.name: { 343 | "command": get_python_executable(), 344 | "args": [ 345 | __file__, 346 | ], 347 | "timeout": 1800, 348 | "disabled": False, 349 | "protocolVersion": mcp.protocol_version, 350 | "paths": ["/jsonrpc", "/mcp"] 351 | } 352 | } 353 | }, indent=2) 354 | ) 355 | 356 | def install_mcp_servers(*, uninstall=False, quiet=False, env={}): 357 | if sys.platform == "win32": 358 | configs = { 359 | "Cline": (os.path.join(os.getenv("APPDATA"), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings"), "cline_mcp_settings.json"), 360 | "Roo Code": (os.path.join(os.getenv("APPDATA"), "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings"), "mcp_settings.json"), 361 | "Claude": (os.path.join(os.getenv("APPDATA"), "Claude"), "claude_desktop_config.json"), 362 | "Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"), 363 | "Windsurf": (os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"), "mcp_config.json"), 364 | # Windows does not support Claude Code, yet. 365 | "LM Studio": (os.path.join(os.path.expanduser("~"), ".lmstudio"), "mcp.json"), 366 | } 367 | elif sys.platform == "darwin": 368 | configs = { 369 | "Cline": (os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings"), "cline_mcp_settings.json"), 370 | "Roo Code": (os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings"), "mcp_settings.json"), 371 | "Claude": (os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Claude"), "claude_desktop_config.json"), 372 | "Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"), 373 | "Windsurf": (os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"), "mcp_config.json"), 374 | "Claude Code": (os.path.join(os.path.expanduser("~")), ".claude.json"), 375 | "LM Studio": (os.path.join(os.path.expanduser("~"), ".lmstudio"), "mcp.json"), 376 | } 377 | elif sys.platform == "linux": 378 | configs = { 379 | "Cline": (os.path.join(os.path.expanduser("~"), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings"), "cline_mcp_settings.json"), 380 | "Roo Code": (os.path.join(os.path.expanduser("~"), ".config", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings"), "mcp_settings.json"), 381 | # Claude not supported on Linux 382 | "Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"), 383 | "Windsurf": (os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"), "mcp_config.json"), 384 | "Claude Code": (os.path.join(os.path.expanduser("~")), ".claude.json"), 385 | "LM Studio": (os.path.join(os.path.expanduser("~"), ".lmstudio"), "mcp.json"), 386 | } 387 | else: 388 | print(f"不支持的平台: {sys.platform}") 389 | return 390 | 391 | installed = 0 392 | for name, (config_dir, config_file) in configs.items(): 393 | config_path = os.path.join(config_dir, config_file) 394 | if not os.path.exists(config_dir): 395 | action = "卸载" if uninstall else "安装" 396 | if not quiet: 397 | print(f"跳过 {name} {action}\n 配置: {config_path} (未找到)") 398 | continue 399 | if not os.path.exists(config_path): 400 | config = {} 401 | else: 402 | with open(config_path, "r", encoding="utf-8") as f: 403 | data = f.read().strip() 404 | if len(data) == 0: 405 | config = {} 406 | else: 407 | try: 408 | config = json.loads(data) 409 | except json.decoder.JSONDecodeError: 410 | if not quiet: 411 | print(f"跳过 {name} 卸载\n 配置: {config_path} (无效 JSON)") 412 | continue 413 | if "mcpServers" not in config: 414 | config["mcpServers"] = {} 415 | mcp_servers = config["mcpServers"] 416 | if uninstall: 417 | if mcp.name not in mcp_servers: 418 | if not quiet: 419 | print(f"跳过 {name} 卸载\n 配置: {config_path} (未安装)") 420 | continue 421 | del mcp_servers[mcp.name] 422 | else: 423 | if mcp.name in mcp_servers: 424 | for key, value in mcp_servers[mcp.name].get("env", {}): 425 | env[key] = value 426 | mcp_servers[mcp.name] = { 427 | "command": get_python_executable(), 428 | "args": [ 429 | __file__, 430 | ], 431 | "timeout": 1800, 432 | "disabled": False, 433 | "autoApprove": MCP_FUNCTIONS, # Changed from SAFE_FUNCTIONS to MCP_FUNCTIONS 434 | "alwaysAllow": MCP_FUNCTIONS, # Changed from SAFE_FUNCTIONS to MCP_FUNCTIONS 435 | } 436 | if env: 437 | mcp_servers[mcp.name]["env"] = env 438 | with open(config_path, "w", encoding="utf-8") as f: 439 | json.dump(config, f, indent=2) 440 | if not quiet: 441 | action = "卸载" if uninstall else "安装" 442 | print(f"{action} {name} MCP 服务器 (需要重启)\n 配置: {config_path}") 443 | installed += 1 444 | if not uninstall and installed == 0: 445 | print("没有 MCP 服务器安装。对于不支持的 MCP 客户端,请使用以下配置:\n") 446 | print_mcp_config() 447 | 448 | def install_ida_plugin(*, uninstall: bool = False, quiet: bool = False): 449 | if sys.platform == "win32": 450 | ida_plugin_folder = os.path.join(os.getenv("APPDATA"), "Hex-Rays", "IDA Pro", "plugins") 451 | else: 452 | ida_plugin_folder = os.path.join(os.path.expanduser("~"), ".idapro", "plugins") 453 | plugin_destination = os.path.join(ida_plugin_folder, "mcp-plugin.py") 454 | if uninstall: 455 | if not os.path.exists(plugin_destination): 456 | print(f"跳过 IDA 插件卸载\n 路径: {plugin_destination} (未找到)") 457 | return 458 | os.remove(plugin_destination) 459 | if not quiet: 460 | print(f"卸载 IDA 插件\n 路径: {plugin_destination}") 461 | else: 462 | # Create IDA plugins folder 463 | if not os.path.exists(ida_plugin_folder): 464 | os.makedirs(ida_plugin_folder) 465 | 466 | # Skip if symlink already up to date 467 | realpath = os.path.realpath(plugin_destination) 468 | if realpath == IDA_PLUGIN_PY: 469 | if not quiet: 470 | print(f"跳过 IDA 插件安装 (符号链接已是最新)\n 插件: {realpath}") 471 | else: 472 | # Remove existing plugin 473 | if os.path.lexists(plugin_destination): 474 | os.remove(plugin_destination) 475 | 476 | # Symlink or copy the plugin 477 | try: 478 | os.symlink(IDA_PLUGIN_PY, plugin_destination) 479 | except OSError: 480 | shutil.copy(IDA_PLUGIN_PY, plugin_destination) 481 | 482 | if not quiet: 483 | print(f"安装 IDA Pro 插件 (需要重启 IDA)\n 插件: {plugin_destination}") 484 | 485 | def auto_run_ida_and_load_file(binary_path): 486 | """自动启动 IDA Pro 并加载指定的二进制文件 487 | 488 | Args: 489 | binary_path: 要分析的二进制文件路径 490 | """ 491 | import subprocess 492 | import time 493 | import os 494 | import platform 495 | 496 | # 确保文件存在 497 | if not os.path.exists(binary_path): 498 | print(f"错误: 无法找到文件 '{binary_path}'") 499 | return 500 | 501 | # 获取 IDA Pro 可执行文件路径 502 | ida_path = None 503 | if platform.system() == "Windows": 504 | # 尝试从常见位置查找 IDA Pro 505 | possible_paths = [ 506 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 7.7", "ida64.exe"), 507 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 7.8", "ida64.exe"), 508 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 7.9", "ida64.exe"), 509 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 8.0", "ida64.exe"), 510 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 8.1", "ida64.exe"), 511 | os.path.join(os.getenv("ProgramFiles"), "IDA Pro 9.1", "ida64.exe"), 512 | ] 513 | 514 | for path in possible_paths: 515 | if os.path.exists(path): 516 | ida_path = path 517 | break 518 | 519 | # 如果没找到,提示用户指定路径 520 | if ida_path is None: 521 | ida_path = input("请输入 IDA Pro 可执行文件的完整路径 (例如: C:\\Program Files\\IDA Pro 9.1\\ida64.exe): ") 522 | if not os.path.exists(ida_path): 523 | print(f"错误: 无效的 IDA Pro 路径 '{ida_path}'") 524 | return 525 | else: 526 | # Linux/Mac 系统 527 | print("警告: 自动启动 IDA Pro 功能目前主要支持 Windows 系统") 528 | ida_path = "ida64" 529 | 530 | print(f"正在启动 IDA Pro ({ida_path}) 并加载文件 '{binary_path}'...") 531 | 532 | try: 533 | # 启动 IDA Pro 并加载二进制文件 534 | subprocess.Popen([ida_path, binary_path]) 535 | 536 | # 等待 IDA Pro 启动 537 | print("IDA Pro 已启动,正在等待加载完成...") 538 | time.sleep(10) # 等待 10 秒,让 IDA 有足够时间加载 539 | 540 | print("\n提示:\n") 541 | print("1. IDA Pro 已成功启动并加载了二进制文件") 542 | print("2. 请在 IDA Pro 中手动启动 MCP 插件 (Edit -> Plugins -> MCP 或按 Ctrl-Alt-M)") 543 | print("3. 启动 MCP 服务器以连接到 IDA Pro") 544 | print(" 命令: python -m ida_pro_mcp.server") 545 | 546 | except Exception as e: 547 | print(f"启动 IDA Pro 时出错: {e}") 548 | print("请确保 IDA Pro 已正确安装并且路径正确") 549 | 550 | def main(): 551 | global ida_host, ida_port 552 | parser = argparse.ArgumentParser(description="IDA Pro MCP Server") 553 | parser.add_argument("--install", action="store_true", help="安装 MCP 服务器和 IDA 插件") 554 | parser.add_argument("--uninstall", action="store_true", help="卸载 MCP 服务器和 IDA 插件") 555 | parser.add_argument("--generate-docs", action="store_true", help=argparse.SUPPRESS) 556 | parser.add_argument("--install-plugin", action="store_true", help=argparse.SUPPRESS) 557 | parser.add_argument("--transport", type=str, default="stdio", help="MCP 传输协议 (stdio 或 http://127.0.0.1:8744)") 558 | parser.add_argument("--ida-rpc", type=str, default=f"http://{ida_host}:{ida_port}", help=f"IDA RPC 服务器 (默认: http://{ida_host}:{ida_port})") 559 | parser.add_argument("--unsafe", action="store_true", help="启用不安全函数 (危险)") 560 | parser.add_argument("--config", action="store_true", help="生成 MCP 配置 JSON") 561 | parser.add_argument("--auto-run-ida", type=str, help="自动启动 IDA Pro 并加载指定的二进制文件") 562 | args = parser.parse_args() 563 | 564 | if args.install and args.uninstall: 565 | print("无法同时安装和卸载") 566 | return 567 | 568 | if args.install: 569 | install_mcp_servers() 570 | install_ida_plugin() 571 | return 572 | 573 | if args.uninstall: 574 | install_mcp_servers(uninstall=True) 575 | install_ida_plugin(uninstall=True) 576 | return 577 | 578 | # NOTE: Developers can use this to generate the README 579 | if args.generate_docs: 580 | generate_readme() 581 | return 582 | 583 | # NOTE: This is silent for automated Cline installations 584 | if args.install_plugin: 585 | install_ida_plugin(quiet=True) 586 | 587 | if args.config: 588 | print_mcp_config() 589 | return 590 | 591 | # 自动启动 IDA Pro 并加载二进制文件 592 | if args.auto_run_ida: 593 | auto_run_ida_and_load_file(args.auto_run_ida) 594 | return 595 | 596 | # Parse IDA RPC server argument 597 | ida_rpc = urlparse(args.ida_rpc) 598 | if ida_rpc.hostname is None or ida_rpc.port is None: 599 | raise Exception(f"无效的 IDA RPC 服务器: {args.ida_rpc}") 600 | ida_host = ida_rpc.hostname 601 | ida_port = ida_rpc.port 602 | 603 | # Remove unsafe tools 604 | if not args.unsafe: 605 | mcp_tools = mcp._tool_manager._tools 606 | for unsafe in UNSAFE_FUNCTIONS: 607 | if unsafe in mcp_tools: 608 | del mcp_tools[unsafe] 609 | 610 | try: 611 | if args.transport == "stdio": 612 | mcp.run(transport="stdio") 613 | else: 614 | url = urlparse(args.transport) 615 | if url.hostname is None or url.port is None: 616 | raise Exception(f"无效的传输 URL: {args.transport}") 617 | mcp.settings.host = url.hostname 618 | mcp.settings.port = url.port 619 | # NOTE: npx @modelcontextprotocol/inspector for debugging 620 | print(f"MCP 服务器在 http://{mcp.settings.host}:{mcp.settings.port}/sse 可用") 621 | mcp.settings.log_level = "INFO" 622 | mcp.run(transport="sse") 623 | except KeyboardInterrupt: 624 | pass 625 | 626 | if __name__ == "__main__": 627 | main() 628 | -------------------------------------------------------------------------------- /src/ida_pro_mcp/mcp-plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | 5 | # 修改Python版本要求,使其兼容IDA Pro环境 6 | # IDA Pro 通常使用Python 3.8或3.9 7 | if sys.version_info < (3, 8): 8 | print("[MCP] 警告: 建议使用Python 3.8或更高版本以获得最佳体验") 9 | # 不再直接抛出异常,而是给出警告并继续执行 10 | 11 | import json 12 | import struct 13 | import threading 14 | import http.server 15 | from urllib.parse import urlparse 16 | from typing import Any, Callable, get_type_hints, TypedDict, Optional, Annotated, TypeVar, Generic, NotRequired 17 | import re 18 | import time 19 | import tempfile 20 | import subprocess 21 | 22 | # 导入脚本生成工具模块 23 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 24 | try: 25 | import script_utils 26 | except ImportError: 27 | print("[MCP] 警告: 无法导入script_utils模块,将使用内置函数") 28 | script_utils = None 29 | 30 | # JSON-RPC 错误类 31 | class JSONRPCError(Exception): 32 | def __init__(self, code: int, message: str, data: Any = None): 33 | self.code = code 34 | self.message = message 35 | self.data = data 36 | 37 | # RPC 注册表,管理所有可调用方法 38 | class RPCRegistry: 39 | def __init__(self): 40 | self.methods: dict[str, Callable] = {} 41 | self.unsafe: set[str] = set() 42 | 43 | def register(self, func: Callable) -> Callable: 44 | self.methods[func.__name__] = func 45 | return func 46 | 47 | def mark_unsafe(self, func: Callable) -> Callable: 48 | self.unsafe.add(func.__name__) 49 | return func 50 | 51 | def dispatch(self, method: str, params: Any) -> Any: 52 | if method not in self.methods: 53 | raise JSONRPCError(-32601, f"方法 '{method}' 未找到") 54 | 55 | func = self.methods[method] 56 | hints = get_type_hints(func) 57 | 58 | # 移除返回值注解 59 | hints.pop("return", None) 60 | 61 | if isinstance(params, list): 62 | if len(params) != len(hints): 63 | raise JSONRPCError(-32602, f"参数数量错误: 期望 {len(hints)} 个,实际 {len(params)} 个") 64 | 65 | # 参数类型校验与转换 66 | converted_params = [] 67 | for value, (param_name, expected_type) in zip(params, hints.items()): 68 | try: 69 | if not isinstance(value, expected_type): 70 | value = expected_type(value) 71 | converted_params.append(value) 72 | except (ValueError, TypeError): 73 | raise JSONRPCError(-32602, f"参数 '{param_name}' 类型错误: 期望 {expected_type.__name__}") 74 | 75 | return func(*converted_params) 76 | elif isinstance(params, dict): 77 | if set(params.keys()) != set(hints.keys()): 78 | raise JSONRPCError(-32602, f"参数名错误: 期望 {list(hints.keys())}") 79 | 80 | # 参数类型校验与转换 81 | converted_params = {} 82 | for param_name, expected_type in hints.items(): 83 | value = params.get(param_name) 84 | try: 85 | if not isinstance(value, expected_type): 86 | value = expected_type(value) 87 | converted_params[param_name] = value 88 | except (ValueError, TypeError): 89 | raise JSONRPCError(-32602, f"参数 '{param_name}' 类型错误: 期望 {expected_type.__name__}") 90 | 91 | return func(**converted_params) 92 | else: 93 | raise JSONRPCError(-32600, "请求参数必须为数组或对象") 94 | 95 | rpc_registry = RPCRegistry() 96 | 97 | @jsonrpc 98 | @idaread 99 | def check_connection() -> dict: 100 | """ 101 | 标准MCP协议接口:检查与服务器的连接 102 | 用于客户端验证服务是否正常运行 103 | """ 104 | return { 105 | "status": "ok", 106 | "protocol": "MCP", 107 | "version": "1.6.0", 108 | "server": "IDA Pro MCP Plugin", 109 | "timestamp": time.time() 110 | } 111 | 112 | @jsonrpc 113 | @idaread 114 | def get_methods() -> list[dict]: 115 | """ 116 | 获取所有可用的JSON-RPC方法列表及其元数据 117 | 支持MCP协议的自描述功能 118 | """ 119 | methods_info = [] 120 | for method_name, func in rpc_registry.methods.items(): 121 | method_info = { 122 | "name": method_name, 123 | "description": func.__doc__ or "", 124 | "is_unsafe": method_name in rpc_registry.unsafe, 125 | "parameters": [] 126 | } 127 | 128 | # 尝试获取函数参数信息 129 | try: 130 | import inspect 131 | sig = inspect.signature(func) 132 | for param_name, param in sig.parameters.items(): 133 | param_info = { 134 | "name": param_name, 135 | "type": str(param.annotation) if param.annotation != inspect.Parameter.empty else "unknown" 136 | } 137 | method_info["parameters"].append(param_info) 138 | except: 139 | # 如果获取参数信息失败,继续处理其他方法 140 | pass 141 | 142 | methods_info.append(method_info) 143 | 144 | return methods_info 145 | 146 | # 注册 JSON-RPC 方法的装饰器 147 | def jsonrpc(func: Callable) -> Callable: 148 | global rpc_registry 149 | return rpc_registry.register(func) 150 | 151 | # 标记为不安全方法的装饰器 152 | def unsafe(func: Callable) -> Callable: 153 | return rpc_registry.mark_unsafe(func) 154 | 155 | # JSON-RPC 请求处理器 156 | class JSONRPCRequestHandler(http.server.BaseHTTPRequestHandler): 157 | def send_jsonrpc_error(self, code: int, message: str, id: Any = None): 158 | response = { 159 | "jsonrpc": "2.0", 160 | "error": { 161 | "code": code, 162 | "message": message 163 | } 164 | } 165 | if id is not None: 166 | response["id"] = id 167 | response_body = json.dumps(response).encode("utf-8") 168 | self.send_response(200) 169 | self.send_header("Content-Type", "application/json") 170 | self.send_header("Content-Length", len(response_body)) 171 | self.end_headers() 172 | self.wfile.write(response_body) 173 | 174 | def do_POST(self): 175 | global rpc_registry 176 | logger.info(f"收到POST请求: {self.path}") 177 | 178 | parsed_path = urlparse(self.path) 179 | # 同时支持/jsonrpc和/mcp路径,确保兼容性 180 | if parsed_path.path not in ["/jsonrpc", "/mcp"]: 181 | logger.error(f"无效的接口路径: {parsed_path.path}") 182 | self.send_jsonrpc_error(-32098, "无效的接口路径", None) 183 | return 184 | 185 | content_length = int(self.headers.get("Content-Length", 0)) 186 | if content_length == 0: 187 | logger.error("请求体缺失") 188 | self.send_jsonrpc_error(-32700, "请求体缺失", None) 189 | return 190 | 191 | request_body = self.rfile.read(content_length) 192 | try: 193 | request = json.loads(request_body) 194 | except json.JSONDecodeError: 195 | logger.error(f"JSON 解析错误: {request_body}") 196 | self.send_jsonrpc_error(-32700, "JSON 解析错误", None) 197 | return 198 | 199 | # 构造响应内容 200 | response = { 201 | "jsonrpc": "2.0" 202 | } 203 | if request.get("id") is not None: 204 | response["id"] = request.get("id") 205 | 206 | try: 207 | # 基本 JSON-RPC 校验 208 | if not isinstance(request, dict): 209 | logger.error(f"请求格式错误: {request}") 210 | raise JSONRPCError(-32600, "请求格式错误") 211 | if request.get("jsonrpc") != "2.0": 212 | logger.error(f"JSON-RPC 版本错误: {request.get('jsonrpc')}") 213 | raise JSONRPCError(-32600, "JSON-RPC 版本错误") 214 | if "method" not in request: 215 | logger.error("未指定方法名") 216 | raise JSONRPCError(-32600, "未指定方法名") 217 | 218 | method = request["method"] 219 | params = request.get("params", []) 220 | logger.info(f"处理API请求: {method}, 参数: {params}") 221 | 222 | # 分发方法调用 223 | result = rpc_registry.dispatch(method, params) 224 | response["result"] = result 225 | logger.info(f"API请求成功完成: {method}") 226 | 227 | except JSONRPCError as e: 228 | logger.error(f"JSONRPC错误: {e.code} - {e.message}") 229 | response["error"] = { 230 | "code": e.code, 231 | "message": e.message 232 | } 233 | if e.data is not None: 234 | response["error"]["data"] = e.data 235 | except IDAError as e: 236 | logger.error(f"IDA错误: {e.message}") 237 | response["error"] = { 238 | "code": -32000, 239 | "message": e.message, 240 | } 241 | except Exception as e: 242 | error_trace = traceback.format_exc() 243 | logger.error(f"内部错误: {str(e)}\n{error_trace}") 244 | response["error"] = { 245 | "code": -32603, 246 | "message": "内部错误(请反馈 bug)", 247 | "data": error_trace, 248 | } 249 | 250 | try: 251 | response_body = json.dumps(response).encode("utf-8") 252 | except Exception as e: 253 | traceback.print_exc() 254 | response_body = json.dumps({ 255 | "error": { 256 | "code": -32603, 257 | "message": "内部错误(请反馈 bug)", 258 | "data": traceback.format_exc(), 259 | } 260 | }).encode("utf-8") 261 | 262 | self.send_response(200) 263 | self.send_header("Content-Type", "application/json") 264 | self.send_header("Content-Length", len(response_body)) 265 | self.end_headers() 266 | self.wfile.write(response_body) 267 | 268 | def log_message(self, format, *args): 269 | # 屏蔽 HTTP 日志输出 270 | pass 271 | 272 | def get_config_file_path(): 273 | """ 274 | 获取配置文件路径 275 | 按优先级查找配置文件: 276 | 1. 当前工作目录下的mcp_config.json 277 | 2. 用户目录下的.mcp/mcp_config.json 278 | 3. IDA插件目录下的mcp_config.json 279 | """ 280 | # 尝试多个配置文件位置,按优先级返回第一个存在的 281 | import ida_idaapi 282 | import pathlib 283 | 284 | # 获取可能的配置文件路径列表 285 | config_paths = [] 286 | 287 | # 1. 当前工作目录 288 | config_paths.append(os.path.join(os.getcwd(), "mcp_config.json")) 289 | 290 | # 2. 用户目录下的.mcp文件夹 291 | user_home = os.path.expanduser("~") 292 | config_paths.append(os.path.join(user_home, ".mcp", "mcp_config.json")) 293 | 294 | # 3. IDA插件目录 295 | plugin_dir = ida_idaapi.idadir("plugins") 296 | config_paths.append(os.path.join(plugin_dir, "mcp_config.json")) 297 | 298 | # 返回第一个存在的配置文件 299 | for path in config_paths: 300 | if os.path.exists(path): 301 | logger.info(f"使用配置文件: {path}") 302 | return path 303 | 304 | # 如果都不存在,返回默认路径(IDA插件目录) 305 | default_path = os.path.join(plugin_dir, "mcp_config.json") 306 | logger.info(f"未找到配置文件,将使用默认配置。默认配置文件路径: {default_path}") 307 | return default_path 308 | 309 | def validate_config(config): 310 | """ 311 | 验证配置的有效性 312 | 返回(是否有效, 错误信息) 313 | """ 314 | # 检查必需的配置项 315 | required_fields = ["host", "port", "allow_port_override"] 316 | for field in required_fields: 317 | if field not in config: 318 | return False, f"缺少必需的配置项: {field}" 319 | 320 | # 验证端口号 321 | port = config.get("port") 322 | if not isinstance(port, int) or port < 1 or port > 65535: 323 | return False, f"无效的端口号: {port}" 324 | 325 | # 验证主机名 326 | host = config.get("host") 327 | if not isinstance(host, str) or not host: 328 | return False, "主机名不能为空" 329 | 330 | # 验证布尔类型配置 331 | if not isinstance(config.get("allow_port_override"), bool): 332 | return False, "allow_port_override 必须是布尔值" 333 | 334 | return True, "" 335 | 336 | def load_config(): 337 | """ 338 | 加载配置文件 339 | 返回验证后的配置字典,如果配置无效则返回默认配置 340 | """ 341 | default_config = { 342 | "host": "localhost", 343 | "port": 13337, 344 | "allow_port_override": True, 345 | "log_level": "INFO", 346 | "timeout": 30 347 | } 348 | 349 | config_path = get_config_file_path() 350 | user_config = {} 351 | 352 | if os.path.exists(config_path): 353 | try: 354 | with open(config_path, 'r', encoding='utf-8') as f: 355 | user_config = json.load(f) 356 | 357 | # 合并默认配置和用户配置 358 | merged_config = {**default_config, **user_config} 359 | 360 | # 验证配置 361 | is_valid, error_msg = validate_config(merged_config) 362 | if not is_valid: 363 | logger.error(f"配置验证失败: {error_msg},将使用默认配置") 364 | return default_config 365 | 366 | logger.info(f"成功加载并验证配置文件: {config_path}") 367 | return merged_config 368 | 369 | except json.JSONDecodeError as e: 370 | logger.error(f"配置文件格式错误: {e},将使用默认配置") 371 | except Exception as e: 372 | logger.error(f"加载配置文件失败: {e},将使用默认配置") 373 | 374 | # 从环境变量覆盖配置 375 | if "MCP_PORT" in os.environ: 376 | try: 377 | port = int(os.environ["MCP_PORT"]) 378 | if 1 <= port <= 65535: 379 | default_config["port"] = port 380 | logger.info(f"从环境变量MCP_PORT设置端口: {port}") 381 | else: 382 | logger.warning(f"环境变量MCP_PORT值无效: {port},必须在1-65535之间") 383 | except ValueError: 384 | logger.warning("环境变量MCP_PORT不是有效的整数") 385 | 386 | if "MCP_HOST" in os.environ: 387 | default_config["host"] = os.environ["MCP_HOST"] 388 | logger.info(f"从环境变量MCP_HOST设置主机: {default_config['host']}") 389 | 390 | return default_config 391 | 392 | # MCP HTTP 服务器 393 | class MCPHTTPServer(http.server.HTTPServer): 394 | allow_reuse_address = True # 允许端口重用 395 | 396 | # 服务器主类 397 | class Server: 398 | def __init__(self): 399 | self.server = None 400 | self.server_thread = None 401 | self.running = False 402 | # 从配置系统获取配置 403 | self.config = load_config() 404 | self.host = self.config.get("host", "localhost") 405 | self.port = self.config.get("port", 13337) 406 | 407 | def start(self): 408 | """ 409 | 启动MCP服务器 410 | 初始化并启动一个线程来运行服务器 411 | """ 412 | try: 413 | if self.running: 414 | print("[MCP] 服务器已在运行") 415 | logger.info("服务器已在运行") 416 | return 417 | 418 | # 确保之前的资源已释放 419 | if self.server is not None: 420 | try: 421 | self.server.server_close() 422 | except: 423 | pass 424 | self.server = None 425 | 426 | if self.server_thread is not None: 427 | try: 428 | if self.server_thread.is_alive(): 429 | logger.warning("检测到之前的服务器线程仍在运行,尝试停止") 430 | except: 431 | pass 432 | self.server_thread = None 433 | 434 | # 加载配置 435 | try: 436 | config = load_config() 437 | self.host = config["host"] 438 | self.port = config["port"] 439 | self.allow_port_override = config.get("allow_port_override", True) 440 | logger.info(f"加载配置完成 - 主机: {self.host}, 端口: {self.port}") 441 | except Exception as e: 442 | print(f"[MCP] 加载配置失败: {e}") 443 | logger.error(f"加载配置失败: {e}", exc_info=True) 444 | # 使用默认配置继续 445 | self.host = "localhost" 446 | self.port = 13337 447 | self.allow_port_override = True 448 | print("[MCP] 使用默认配置继续") 449 | 450 | # 创建并启动服务器线程 451 | self.server_thread = threading.Thread(target=self._run_server, daemon=True, name="MCP-Server-Thread") 452 | self.running = True 453 | self.server_thread.start() 454 | print("[MCP] 服务器启动中...") 455 | logger.info("服务器线程已启动") 456 | 457 | # 添加短暂延迟确保服务器有机会绑定端口 458 | time.sleep(0.5) 459 | 460 | except Exception as e: 461 | self.running = False 462 | print(f"[MCP] 服务器启动失败: {e}") 463 | logger.error(f"服务器启动失败: {e}", exc_info=True) 464 | 465 | def stop(self): 466 | """ 467 | 停止MCP服务器 468 | 安全地关闭服务器并释放资源 469 | """ 470 | try: 471 | if not self.running: 472 | print("[MCP] 服务器未运行") 473 | return 474 | 475 | print("[MCP] 正在停止服务器...") 476 | logger.info("正在停止服务器...") 477 | 478 | # 首先标记为非运行状态 479 | self.running = False 480 | 481 | # 优雅关闭服务器 482 | if self.server: 483 | try: 484 | self.server.shutdown() 485 | logger.info("服务器已关闭") 486 | except Exception as e: 487 | logger.warning(f"关闭服务器时出错: {e}") 488 | 489 | try: 490 | self.server.server_close() 491 | logger.info("服务器资源已释放") 492 | except Exception as e: 493 | logger.warning(f"释放服务器资源时出错: {e}") 494 | 495 | self.server = None 496 | 497 | # 等待线程结束 498 | if self.server_thread: 499 | try: 500 | # 设置超时以避免无限等待 501 | self.server_thread.join(timeout=5.0) 502 | if self.server_thread.is_alive(): 503 | logger.warning("服务器线程未能在超时时间内结束") 504 | else: 505 | logger.info("服务器线程已结束") 506 | except Exception as e: 507 | logger.warning(f"等待服务器线程结束时出错: {e}") 508 | 509 | self.server_thread = None 510 | 511 | print("[MCP] 服务器已成功停止") 512 | logger.info("服务器已成功停止") 513 | 514 | except Exception as e: 515 | print(f"[MCP] 停止服务器时出错: {e}") 516 | logger.error(f"停止服务器时出错: {e}", exc_info=True) 517 | # 确保状态被重置 518 | self.running = False 519 | self.server = None 520 | self.server_thread = None 521 | 522 | def _run_server(self): 523 | """ 524 | 运行MCP服务器 525 | 尝试绑定指定端口,如果失败则尝试自动选择可用端口 526 | """ 527 | try: 528 | # 尝试绑定指定端口,如果失败则尝试自动选择可用端口 529 | original_port = self.port # 保存原始端口 530 | current_port = original_port 531 | max_attempts = 100 # 最多尝试100个端口 532 | attempts = 0 533 | 534 | while attempts < max_attempts and self.running: 535 | try: 536 | self.server = MCPHTTPServer((self.host, current_port), JSONRPCRequestHandler) 537 | self.port = current_port # 更新实际使用的端口 538 | print(f"[MCP] 服务器已启动: http://{self.host}:{self.port}") 539 | logger.info(f"服务器已启动: http://{self.host}:{self.port}") 540 | 541 | # 如果使用的端口不是原始端口,保存到配置中 542 | if current_port != original_port and self.allow_port_override: 543 | self._save_port_config(current_port) 544 | 545 | # 启动服务器 546 | self.server.serve_forever() 547 | break # 成功启动后跳出循环 548 | 549 | except OSError as e: 550 | # 检查是否是端口被占用错误 551 | if "[WinError 10048]" in str(e) or "Address already in use" in str(e): 552 | if self.allow_port_override and self.running: 553 | attempts += 1 554 | current_port += 1 555 | logger.warning(f"端口 {current_port-1} 已被占用,尝试使用端口 {current_port}") 556 | time.sleep(0.1) # 短暂延迟,避免过快尝试 557 | else: 558 | print(f"[MCP] 启动服务器失败: {e}") 559 | logger.error(f"启动服务器失败: {e}") 560 | break 561 | else: 562 | print(f"[MCP] 启动服务器失败: {e}") 563 | logger.error(f"启动服务器失败: {e}", exc_info=True) 564 | break 565 | 566 | if attempts >= max_attempts and self.running: 567 | error_msg = f"[MCP] 启动服务器失败: 无法找到可用端口 (尝试了{max_attempts}次)" 568 | print(error_msg) 569 | logger.error(error_msg) 570 | 571 | except Exception as e: 572 | print(f"[MCP] 服务器运行错误: {e}") 573 | logger.error(f"服务器运行错误: {e}", exc_info=True) 574 | finally: 575 | # 确保在退出时更新运行状态 576 | if self.running: 577 | self.running = False 578 | print("[MCP] 服务器已停止运行") 579 | logger.info("服务器已停止运行") 580 | 581 | def _save_port_config(self, port): 582 | """ 583 | 保存端口配置到配置文件 584 | """ 585 | try: 586 | config_path = get_config_file_path() 587 | # 加载现有配置 588 | config = load_config() 589 | config["port"] = port 590 | 591 | # 保存配置 592 | with open(config_path, 'w', encoding='utf-8') as f: 593 | json.dump(config, f, indent=2, ensure_ascii=False) 594 | 595 | logger.info(f"已保存端口配置: {port}") 596 | except Exception as e: 597 | logger.warning(f"保存端口配置失败: {e}") 598 | # 即使保存配置失败,也不应该重复启动服务器 599 | 600 | # 一个帮助编写线程安全IDA代码的模块。 601 | # Based on: 602 | # https://web.archive.org/web/20160305190440/http://www.williballenthin.com/blog/2015/09/04/idapython-synchronization-decorator/ 603 | import os 604 | import queue 605 | import traceback 606 | import functools 607 | import logging.handlers 608 | 609 | import ida_hexrays 610 | import ida_kernwin 611 | import ida_funcs 612 | import ida_gdl 613 | import ida_lines 614 | import ida_idaapi 615 | import idc 616 | import idaapi 617 | import idautils 618 | import ida_nalt 619 | import ida_bytes 620 | import ida_typeinf 621 | import ida_xref 622 | import ida_entry 623 | import ida_idd 624 | import ida_dbg 625 | import ida_name 626 | import ida_ida 627 | import ida_frame 628 | 629 | class IDAError(Exception): 630 | def __init__(self, message: str): 631 | super().__init__(message) 632 | 633 | @property 634 | def message(self) -> str: 635 | return self.args[0] 636 | 637 | class IDASyncError(Exception): 638 | pass 639 | 640 | class DecompilerLicenseError(IDAError): 641 | pass 642 | 643 | # 重要提示:始终确保函数 f 的返回值是从 IDA 获取的数据的副本,而不是原始数据。 644 | # 645 | # 示例: 646 | # -------- 647 | # 648 | # 正确做法: 649 | # 650 | # @idaread 651 | # def ts_Functions(): 652 | # return list(idautils.Functions()) 653 | # 654 | # 错误做法: 655 | # 656 | # @idaread 657 | # def ts_Functions(): 658 | # return idautils.Functions() 659 | # 660 | 661 | def setup_logging(): 662 | """ 663 | 设置日志系统,支持配置化级别和日志滚动 664 | """ 665 | # 获取日志目录 666 | log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') 667 | os.makedirs(log_dir, exist_ok=True) 668 | 669 | # 从配置加载日志级别 670 | try: 671 | config = load_config() 672 | log_level = getattr(logging, config.get('log_level', 'INFO')) 673 | except (NameError, AttributeError): 674 | log_level = logging.INFO 675 | 676 | # 创建logger对象 677 | logger = logging.getLogger('MCP') 678 | logger.setLevel(log_level) 679 | 680 | # 清除已有的handler 681 | for handler in logger.handlers[:]: 682 | logger.removeHandler(handler) 683 | 684 | # 创建控制台handler 685 | console_handler = logging.StreamHandler() 686 | console_handler.setLevel(log_level) 687 | 688 | # 创建文件handler(支持日志滚动,最多保存5个文件,每个5MB) 689 | file_handler = logging.handlers.RotatingFileHandler( 690 | os.path.join(log_dir, 'ida_mcp_plugin.log'), 691 | maxBytes=5*1024*1024, 692 | backupCount=5 693 | ) 694 | file_handler.setLevel(log_level) 695 | 696 | # 设置日志格式 697 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 698 | console_handler.setFormatter(formatter) 699 | file_handler.setFormatter(formatter) 700 | 701 | # 添加handler到logger 702 | logger.addHandler(console_handler) 703 | logger.addHandler(file_handler) 704 | 705 | return logger 706 | 707 | # 初始化日志系统 708 | logger = setup_logging() 709 | 710 | # 安全模式枚举,数值越高表示越安全: 711 | class IDASafety: 712 | SAFE_NONE = ida_kernwin.MFF_FAST 713 | SAFE_READ = ida_kernwin.MFF_READ 714 | SAFE_WRITE = ida_kernwin.MFF_WRITE 715 | 716 | call_stack = queue.LifoQueue() 717 | 718 | def sync_wrapper(ff, safety_mode: IDASafety): 719 | """ 720 | 调用一个函数 ff 并指定一个特定的 IDA 安全模式。 721 | """ 722 | #logger.debug('sync_wrapper: {}, {}'.format(ff.__name__, safety_mode)) # 调试日志:同步包装器信息 723 | 724 | if safety_mode not in [IDASafety.SAFE_READ, IDASafety.SAFE_WRITE]: 725 | error_str = 'Invalid safety mode {} over function {}'\ 726 | .format(safety_mode, ff.__name__) 727 | logger.error(error_str) 728 | raise IDASyncError(error_str) 729 | 730 | # 未设置安全级别: 731 | res_container = queue.Queue() 732 | 733 | def runned(): 734 | #logger.debug('Inside runned') # 调试日志:进入运行状态 735 | 736 | # 确保我们不在sync_wrapper内部: 737 | if not call_stack.empty(): 738 | last_func_name = call_stack.get() 739 | error_str = ('Call stack is not empty while calling the ' 740 | 'function {} from {}').format(ff.__name__, last_func_name) 741 | #logger.error(error_str) # 错误日志:输出错误信息 742 | raise IDASyncError(error_str) 743 | 744 | call_stack.put((ff.__name__)) 745 | try: 746 | res_container.put(ff()) 747 | except Exception as x: 748 | res_container.put(x) 749 | finally: 750 | call_stack.get() 751 | #logger.debug('Finished runned') 752 | 753 | ret_val = idaapi.execute_sync(runned, safety_mode) 754 | res = res_container.get() 755 | if isinstance(res, Exception): 756 | raise res 757 | return res 758 | 759 | def idawrite(f): 760 | """ 761 | 标记一个函数为修改 IDB 的装饰器。 762 | 在主 IDA 循环中安排一个请求,以避免 IDB 损坏。 763 | """ 764 | @functools.wraps(f) 765 | def wrapper(*args, **kwargs): 766 | ff = functools.partial(f, *args, **kwargs) 767 | ff.__name__ = f.__name__ 768 | return sync_wrapper(ff, idaapi.MFF_WRITE) 769 | return wrapper 770 | 771 | def idaread(f): 772 | """ 773 | 标记一个函数为从 IDB 读取的装饰器。 774 | 在主 IDA 循环中安排一个请求,以避免 775 | 不一致的结果。 776 | MFF_READ 常量通过:http://www.openrce.org/forums/posts/1827 777 | """ 778 | @functools.wraps(f) 779 | def wrapper(*args, **kwargs): 780 | ff = functools.partial(f, *args, **kwargs) 781 | ff.__name__ = f.__name__ 782 | return sync_wrapper(ff, idaapi.MFF_READ) 783 | return wrapper 784 | 785 | def is_window_active(): 786 | """返回 IDA 当前是否处于活动状态""" 787 | try: 788 | from PyQt5.QtWidgets import QApplication 789 | except ImportError: 790 | return False 791 | 792 | app = QApplication.instance() 793 | if app is None: 794 | return False 795 | 796 | for widget in app.topLevelWidgets(): 797 | if widget.isActiveWindow(): 798 | return True 799 | return False 800 | 801 | class Metadata(TypedDict): 802 | path: str 803 | module: str 804 | base: str 805 | size: str 806 | md5: str 807 | sha256: str 808 | crc32: str 809 | filesize: str 810 | 811 | def get_image_size() -> int: 812 | try: 813 | # https://www.hex-rays.com/products/ida/support/sdkdoc/structidainfo.html 814 | info = idaapi.get_inf_structure() 815 | omin_ea = info.omin_ea 816 | omax_ea = info.omax_ea 817 | except AttributeError: 818 | import ida_ida 819 | omin_ea = ida_ida.inf_get_omin_ea() 820 | omax_ea = ida_ida.inf_get_omax_ea() 821 | # 一个不准确的图像大小(如果重定位在最后) 822 | image_size = omax_ea - omin_ea 823 | # 尝试从 PE 头中提取它 824 | header = idautils.peutils_t().header() 825 | if header and header[:4] == b"PE\0\0": 826 | image_size = struct.unpack(" Metadata: 832 | """获取当前 IDB 的元数据""" 833 | # Fat Mach-O 二进制文件可以返回 None 哈希: 834 | # https://github.com/mrexodia/ida-pro-mcp/issues/26 835 | def hash(f): 836 | try: 837 | return f().hex() 838 | except: 839 | return None 840 | 841 | return Metadata(path=idaapi.get_input_file_path(), 842 | module=idaapi.get_root_filename(), 843 | base=hex(idaapi.get_imagebase()), 844 | size=hex(get_image_size()), 845 | md5=hash(ida_nalt.retrieve_input_file_md5), 846 | sha256=hash(ida_nalt.retrieve_input_file_sha256), 847 | crc32=hex(ida_nalt.retrieve_input_file_crc32()), 848 | filesize=hex(ida_nalt.retrieve_input_file_size())) 849 | 850 | def get_prototype(fn: ida_funcs.func_t) -> Optional[str]: 851 | try: 852 | prototype: ida_typeinf.tinfo_t = fn.get_prototype() 853 | if prototype is not None: 854 | return str(prototype) 855 | else: 856 | return None 857 | except AttributeError: 858 | try: 859 | return idc.get_type(fn.start_ea) 860 | except: 861 | tif = ida_typeinf.tinfo_t() 862 | if ida_nalt.get_tinfo(tif, fn.start_ea): 863 | return str(tif) 864 | return None 865 | except Exception as e: 866 | print(f"Error getting function prototype: {e}") 867 | return None 868 | 869 | class Function(TypedDict): 870 | address: str 871 | name: str 872 | size: str 873 | 874 | def parse_address(address: str) -> int: 875 | try: 876 | return int(address, 0) 877 | except ValueError: 878 | for ch in address: 879 | if ch not in "0123456789abcdefABCDEF": 880 | raise IDAError(f"Failed to parse address: {address}") 881 | raise IDAError(f"Failed to parse address (missing 0x prefix): {address}") 882 | 883 | def get_function(address: int, *, raise_error=True) -> Function: 884 | fn = idaapi.get_func(address) 885 | if fn is None: 886 | if raise_error: 887 | raise IDAError(f"No function found at address {hex(address)}") 888 | return None 889 | 890 | try: 891 | name = fn.get_name() 892 | except AttributeError: 893 | name = ida_funcs.get_func_name(fn.start_ea) 894 | 895 | return Function(address=hex(address), name=name, size=hex(fn.end_ea - fn.start_ea)) 896 | 897 | DEMANGLED_TO_EA = {} 898 | 899 | def create_demangled_to_ea_map(): 900 | for ea in idautils.Functions(): 901 | # 获取函数名并进行解混淆 902 | # MNG_NODEFINIT 标志仅保留主名称,抑制其他信息 903 | # 默认解混淆会添加函数签名 904 | # 以及装饰器(如有) 905 | demangled = idaapi.demangle_name( 906 | idc.get_name(ea, 0), idaapi.MNG_NODEFINIT) 907 | if demangled: 908 | DEMANGLED_TO_EA[demangled] = ea 909 | 910 | 911 | def get_type_by_name(type_name: str) -> ida_typeinf.tinfo_t: 912 | # 8-bit integers 913 | if type_name in ('int8', '__int8', 'int8_t', 'char', 'signed char'): 914 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT8) 915 | elif type_name in ('uint8', '__uint8', 'uint8_t', 'unsigned char', 'byte', 'BYTE'): 916 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT8) 917 | 918 | # 16-bit integers 919 | elif type_name in ('int16', '__int16', 'int16_t', 'short', 'short int', 'signed short', 'signed short int'): 920 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT16) 921 | elif type_name in ('uint16', '__uint16', 'uint16_t', 'unsigned short', 'unsigned short int', 'word', 'WORD'): 922 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT16) 923 | 924 | # 32-bit integers 925 | elif type_name in ('int32', '__int32', 'int32_t', 'int', 'signed int', 'long', 'long int', 'signed long', 'signed long int'): 926 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT32) 927 | elif type_name in ('uint32', '__uint32', 'uint32_t', 'unsigned int', 'unsigned long', 'unsigned long int', 'dword', 'DWORD'): 928 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT32) 929 | 930 | # 64-bit integers 931 | elif type_name in ('int64', '__int64', 'int64_t', 'long long', 'long long int', 'signed long long', 'signed long long int'): 932 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT64) 933 | elif type_name in ('uint64', '__uint64', 'uint64_t', 'unsigned int64', 'unsigned long long', 'unsigned long long int', 'qword', 'QWORD'): 934 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT64) 935 | 936 | # 128-bit integers 937 | elif type_name in ('int128', '__int128', 'int128_t', '__int128_t'): 938 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT128) 939 | elif type_name in ('uint128', '__uint128', 'uint128_t', '__uint128_t', 'unsigned int128'): 940 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT128) 941 | 942 | # 浮点类型 943 | elif type_name in ('float', ): 944 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_FLOAT) 945 | elif type_name in ('double', ): 946 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_DOUBLE) 947 | elif type_name in ('long double', 'ldouble'): 948 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_LDOUBLE) 949 | 950 | # 布尔类型 951 | elif type_name in ('bool', '_Bool', 'boolean'): 952 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_BOOL) 953 | 954 | # 空类型 955 | elif type_name in ('void', ): 956 | return ida_typeinf.tinfo_t(ida_typeinf.BTF_VOID) 957 | 958 | # 如果不是标准类型,尝试获取命名类型 959 | tif = ida_typeinf.tinfo_t() 960 | if tif.get_named_type(None, type_name, ida_typeinf.BTF_STRUCT): 961 | return tif 962 | 963 | if tif.get_named_type(None, type_name, ida_typeinf.BTF_TYPEDEF): 964 | return tif 965 | 966 | if tif.get_named_type(None, type_name, ida_typeinf.BTF_ENUM): 967 | return tif 968 | 969 | if tif.get_named_type(None, type_name, ida_typeinf.BTF_UNION): 970 | return tif 971 | 972 | if tif := ida_typeinf.tinfo_t(type_name): 973 | return tif 974 | 975 | raise IDAError(f"Unable to retrieve {type_name} type info object") 976 | 977 | @jsonrpc 978 | @idaread 979 | def get_function_by_name( 980 | name: Annotated[str, "要获取的函数名称"] 981 | ) -> Function: 982 | """根据函数名称获取函数""" 983 | function_address = idaapi.get_name_ea(idaapi.BADADDR, name) 984 | if function_address == idaapi.BADADDR: 985 | # 如果映射尚未创建,则创建它 986 | if len(DEMANGLED_TO_EA) == 0: 987 | create_demangled_to_ea_map() 988 | # 尝试在映射中查找函数,否则抛出错误 989 | if name in DEMANGLED_TO_EA: 990 | function_address = DEMANGLED_TO_EA[name] 991 | else: 992 | raise IDAError(f"No function found with name {name}") 993 | return get_function(function_address) 994 | 995 | @jsonrpc 996 | @idaread 997 | def get_function_by_address( 998 | address: Annotated[str, "要获取的函数地址"], 999 | ) -> Function: 1000 | """根据函数地址获取函数""" 1001 | return get_function(parse_address(address)) 1002 | 1003 | @jsonrpc 1004 | @idaread 1005 | def get_current_address() -> str: 1006 | """获取用户当前选中的地址""" 1007 | return hex(idaapi.get_screen_ea()) 1008 | 1009 | @jsonrpc 1010 | @idaread 1011 | def get_current_function() -> Optional[Function]: 1012 | """获取用户当前选中的函数""" 1013 | return get_function(idaapi.get_screen_ea()) 1014 | 1015 | class ConvertedNumber(TypedDict): 1016 | decimal: str 1017 | hexadecimal: str 1018 | bytes: str 1019 | ascii: Optional[str] 1020 | binary: str 1021 | 1022 | @jsonrpc 1023 | def convert_number( 1024 | text: Annotated[str, "要转换的数字的文本表示"], 1025 | size: Annotated[Optional[int], "变量的大小(字节)"], 1026 | ) -> ConvertedNumber: 1027 | """将数字(十进制、十六进制)转换为不同表示""" 1028 | try: 1029 | value = int(text, 0) 1030 | except ValueError: 1031 | raise IDAError(f"Invalid number: {text}") 1032 | 1033 | # 估计数字的大小 1034 | if not size: 1035 | size = 0 1036 | n = abs(value) 1037 | while n: 1038 | size += 1 1039 | n >>= 1 1040 | size += 7 1041 | size //= 8 1042 | 1043 | # 将数字转换为字节 1044 | try: 1045 | bytes = value.to_bytes(size, "little", signed=True) 1046 | except OverflowError: 1047 | raise IDAError(f"Number {text} is too big for {size} bytes") 1048 | 1049 | # 将字节转换为 ASCII 1050 | ascii = "" 1051 | for byte in bytes.rstrip(b"\x00"): 1052 | if byte >= 32 and byte <= 126: 1053 | ascii += chr(byte) 1054 | else: 1055 | ascii = None 1056 | break 1057 | 1058 | return ConvertedNumber( 1059 | decimal=str(value), 1060 | hexadecimal=hex(value), 1061 | bytes=bytes.hex(" "), 1062 | ascii=ascii, 1063 | binary=bin(value), 1064 | ) 1065 | 1066 | T = TypeVar("T") 1067 | 1068 | class Page(TypedDict, Generic[T]): 1069 | data: list[T] 1070 | next_offset: Optional[int] 1071 | 1072 | def paginate(data: list[T], offset: int, count: int) -> Page[T]: 1073 | if count == 0: 1074 | count = len(data) 1075 | next_offset = offset + count 1076 | if next_offset >= len(data): 1077 | next_offset = None 1078 | return { 1079 | "data": data[offset:offset + count], 1080 | "next_offset": next_offset, 1081 | } 1082 | 1083 | def pattern_filter(data: list[T], pattern: str, key: str) -> list[T]: 1084 | if not pattern: 1085 | return data 1086 | 1087 | # TODO: implement /regex/ matching 1088 | 1089 | def matches(item: T) -> bool: 1090 | return pattern.lower() in item[key].lower() 1091 | return list(filter(matches, data)) 1092 | 1093 | @jsonrpc 1094 | @idaread 1095 | def list_functions( 1096 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1097 | count: Annotated[int, "要列出的函数数量 (100 是默认值,0 表示剩余)"], 1098 | ) -> Page[Function]: 1099 | """列出数据库中的所有函数(分页) 1100 | 1101 | 警告: 此API已弃用,请使用更通用的query_database API 1102 | """ 1103 | functions = [get_function(address) for address in idautils.Functions()] 1104 | return paginate(functions, offset, count) 1105 | 1106 | @jsonrpc 1107 | @idaread 1108 | def query_database( 1109 | entity_type: Annotated[str, "实体类型: 'functions', 'globals', 'strings', 'imports'"], 1110 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1111 | count: Annotated[int, "要列出的实体数量 (100 是默认值,0 表示剩余)"], 1112 | filter: Annotated[str, "可选的过滤模式,空字符串表示无过滤"], 1113 | ) -> Page[dict]: 1114 | """统一的数据库查询API,可查询各种实体类型 1115 | 1116 | 用于替代特定的list_xxx和list_xxx_filter函数 1117 | """ 1118 | if entity_type == 'functions': 1119 | functions = [get_function(address) for address in idautils.Functions()] 1120 | if filter: 1121 | functions = pattern_filter(functions, filter, "name") 1122 | return paginate(functions, offset, count) 1123 | elif entity_type == 'globals': 1124 | return list_globals_filter(offset, count, filter) 1125 | elif entity_type == 'strings': 1126 | return list_strings_filter(offset, count, filter) 1127 | elif entity_type == 'imports': 1128 | imports = [] 1129 | for i in idautils.Imports(): 1130 | for name, ordinal in idautils.Entries(i): 1131 | if name: 1132 | imports.append({ 1133 | "address": hex(ordinal), 1134 | "imported_name": name, 1135 | "module": idaapi.get_import_module_name(i), 1136 | }) 1137 | if filter: 1138 | imports = pattern_filter(imports, filter, "imported_name") 1139 | return paginate(imports, offset, count) 1140 | else: 1141 | raise JSONRPCError(400, f"不支持的实体类型: {entity_type}") 1142 | 1143 | class Global(TypedDict): 1144 | address: str 1145 | name: str 1146 | 1147 | @jsonrpc 1148 | @idaread 1149 | def list_globals_filter( 1150 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1151 | count: Annotated[int, "要列出的全局变量数量 (100 是默认值,0 表示剩余)"], 1152 | filter: Annotated[str, "要应用的过滤器 (必需参数,空字符串表示无过滤). 大小写不敏感包含或 /regex/ 语法"], 1153 | ) -> Page[Global]: 1154 | """列出数据库中的匹配全局变量(分页,过滤)""" 1155 | globals = [] 1156 | for addr, name in idautils.Names(): 1157 | # 跳过函数 1158 | if not idaapi.get_func(addr): 1159 | globals += [Global(address=hex(addr), name=name)] 1160 | 1161 | globals = pattern_filter(globals, filter, "name") 1162 | return paginate(globals, offset, count) 1163 | 1164 | @jsonrpc 1165 | def list_globals( 1166 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1167 | count: Annotated[int, "要列出的全局变量数量 (100 是默认值,0 表示剩余)"], 1168 | ) -> Page[Global]: 1169 | """列出数据库中的所有全局变量(分页) 1170 | 1171 | 警告: 此API已弃用,请使用更通用的query_database API 1172 | """ 1173 | return list_globals_filter(offset, count, "") 1174 | 1175 | class Import(TypedDict): 1176 | address: str 1177 | imported_name: str 1178 | module: str 1179 | 1180 | @jsonrpc 1181 | @idaread 1182 | def list_imports( 1183 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1184 | count: Annotated[int, "要列出的导入符号数量 (100 是默认值,0 表示剩余)"], 1185 | ) -> Page[Import]: 1186 | """ 列出所有导入符号及其名称和模块(分页) """ 1187 | nimps = ida_nalt.get_import_module_qty() 1188 | 1189 | rv = [] 1190 | for i in range(nimps): 1191 | module_name = ida_nalt.get_import_module_name(i) 1192 | if not module_name: 1193 | module_name = "" 1194 | 1195 | def imp_cb(ea, symbol_name, ordinal, acc): 1196 | if not symbol_name: 1197 | symbol_name = f"#{ordinal}" 1198 | 1199 | acc += [Import(address=hex(ea), imported_name=symbol_name, module=module_name)] 1200 | 1201 | return True 1202 | 1203 | imp_cb_w_context = lambda ea, symbol_name, ordinal: imp_cb(ea, symbol_name, ordinal, rv) 1204 | ida_nalt.enum_import_names(i, imp_cb_w_context) 1205 | 1206 | return paginate(rv, offset, count) 1207 | 1208 | class String(TypedDict): 1209 | address: str 1210 | length: int 1211 | string: str 1212 | 1213 | @jsonrpc 1214 | @idaread 1215 | def list_strings_filter( 1216 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1217 | count: Annotated[int, "要列出的字符串数量 (100 是默认值,0 表示剩余)"], 1218 | filter: Annotated[str, "要应用的过滤器 (必需参数,空字符串表示无过滤). 大小写不敏感包含或 /regex/ 语法"], 1219 | ) -> Page[String]: 1220 | """列出数据库中的匹配字符串(分页,过滤)""" 1221 | strings = [] 1222 | for item in idautils.Strings(): 1223 | try: 1224 | string = str(item) 1225 | if string: 1226 | strings += [ 1227 | String(address=hex(item.ea), length=item.length, string=string), 1228 | ] 1229 | except: 1230 | continue 1231 | strings = pattern_filter(strings, filter, "string") 1232 | return paginate(strings, offset, count) 1233 | 1234 | @jsonrpc 1235 | def list_strings( 1236 | offset: Annotated[int, "从 (0) 开始列出偏移量"], 1237 | count: Annotated[int, "要列出的字符串数量 (100 是默认值,0 表示剩余)"], 1238 | ) -> Page[String]: 1239 | """列出数据库中的所有字符串(分页) 1240 | 1241 | 警告: 此API已弃用,请使用更通用的query_database API 1242 | """ 1243 | return list_strings_filter(offset, count, "") 1244 | 1245 | @jsonrpc 1246 | @idaread 1247 | def list_local_types(): 1248 | """列出数据库中的所有本地类型""" 1249 | error = ida_hexrays.hexrays_failure_t() 1250 | locals = [] 1251 | idati = ida_typeinf.get_idati() 1252 | type_count = ida_typeinf.get_ordinal_limit(idati) 1253 | for ordinal in range(1, type_count): 1254 | try: 1255 | tif = ida_typeinf.tinfo_t() 1256 | if tif.get_numbered_type(idati, ordinal): 1257 | type_name = tif.get_type_name() 1258 | if not type_name: 1259 | type_name = f"" 1260 | locals.append(f"\nType #{ordinal}: {type_name}") 1261 | if tif.is_udt(): 1262 | c_decl_flags = (ida_typeinf.PRTYPE_MULTI | ida_typeinf.PRTYPE_TYPE | ida_typeinf.PRTYPE_SEMI | ida_typeinf.PRTYPE_DEF | ida_typeinf.PRTYPE_METHODS | ida_typeinf.PRTYPE_OFFSETS) 1263 | c_decl_output = tif._print(None, c_decl_flags) 1264 | if c_decl_output: 1265 | locals.append(f" C declaration:\n{c_decl_output}") 1266 | else: 1267 | simple_decl = tif._print(None, ida_typeinf.PRTYPE_1LINE | ida_typeinf.PRTYPE_TYPE | ida_typeinf.PRTYPE_SEMI) 1268 | if simple_decl: 1269 | locals.append(f" Simple declaration:\n{simple_decl}") 1270 | else: 1271 | message = f"\nType #{ordinal}: Failed to retrieve information." 1272 | if error.str: 1273 | message += f": {error.str}" 1274 | if error.errea != idaapi.BADADDR: 1275 | message += f"from (address: {hex(error.errea)})" 1276 | raise IDAError(message) 1277 | except: 1278 | continue 1279 | return locals 1280 | 1281 | def decompile_checked(address: int) -> ida_hexrays.cfunc_t: 1282 | if not ida_hexrays.init_hexrays_plugin(): 1283 | raise IDAError("Hex-Rays 反编译器不可用") 1284 | error = ida_hexrays.hexrays_failure_t() 1285 | cfunc: ida_hexrays.cfunc_t = ida_hexrays.decompile_func(address, error, ida_hexrays.DECOMP_WARNINGS) 1286 | if not cfunc: 1287 | if error.code == ida_hexrays.MERR_LICENSE: 1288 | raise DecompilerLicenseError("反编译器许可证不可用。请使用 `disassemble_function` 获取汇编代码。") 1289 | 1290 | message = f"Decompilation failed at {hex(address)}" 1291 | if error.str: 1292 | message += f": {error.str}" 1293 | if error.errea != idaapi.BADADDR: 1294 | message += f" (address: {hex(error.errea)})" 1295 | raise IDAError(message) 1296 | return cfunc 1297 | 1298 | @jsonrpc 1299 | @idaread 1300 | def decompile_function( 1301 | address: Annotated[str, "要反编译的函数地址"], 1302 | ) -> str: 1303 | """反编译给定地址的函数""" 1304 | address = parse_address(address) 1305 | cfunc = decompile_checked(address) 1306 | if is_window_active(): 1307 | ida_hexrays.open_pseudocode(address, ida_hexrays.OPF_REUSE) 1308 | sv = cfunc.get_pseudocode() 1309 | pseudocode = "" 1310 | for i, sl in enumerate(sv): 1311 | sl: ida_kernwin.simpleline_t 1312 | item = ida_hexrays.ctree_item_t() 1313 | addr = None if i > 0 else cfunc.entry_ea 1314 | if cfunc.get_line_item(sl.line, 0, False, None, item, None): 1315 | ds = item.dstr().split(": ") 1316 | if len(ds) == 2: 1317 | try: 1318 | addr = int(ds[0], 16) 1319 | except ValueError: 1320 | pass 1321 | line = ida_lines.tag_remove(sl.line) 1322 | if len(pseudocode) > 0: 1323 | pseudocode += "\n" 1324 | if not addr: 1325 | pseudocode += f"/* line: {i} */ {line}" 1326 | else: 1327 | pseudocode += f"/* line: {i}, address: {hex(addr)} */ {line}" 1328 | 1329 | return pseudocode 1330 | 1331 | class DisassemblyLine(TypedDict): 1332 | segment: NotRequired[str] 1333 | address: str 1334 | label: NotRequired[str] 1335 | instruction: str 1336 | comments: NotRequired[list[str]] 1337 | 1338 | class Argument(TypedDict): 1339 | name: str 1340 | type: str 1341 | 1342 | class DisassemblyFunction(TypedDict): 1343 | name: str 1344 | start_ea: str 1345 | return_type: NotRequired[str] 1346 | arguments: NotRequired[list[Argument]] 1347 | stack_frame: list[dict] 1348 | lines: list[DisassemblyLine] 1349 | 1350 | @jsonrpc 1351 | @idaread 1352 | def disassemble_function( 1353 | start_address: Annotated[str, "要反汇编的函数地址"], 1354 | ) -> DisassemblyFunction: 1355 | """获取函数汇编代码""" 1356 | start = parse_address(start_address) 1357 | func: ida_funcs.func_t = idaapi.get_func(start) 1358 | if not func: 1359 | raise IDAError(f"No function found containing address {start_address}") 1360 | if is_window_active(): 1361 | ida_kernwin.jumpto(start) 1362 | 1363 | lines = [] 1364 | for address in ida_funcs.func_item_iterator_t(func): 1365 | seg = idaapi.getseg(address) 1366 | segment = idaapi.get_segm_name(seg) if seg else None 1367 | 1368 | label = idc.get_name(address, 0) 1369 | func_name = idc.get_func_name(func.start_ea) 1370 | if label and label == func_name and address == func.start_ea: 1371 | label = None 1372 | if label == "": 1373 | label = None 1374 | 1375 | comments = [] 1376 | if comment := idaapi.get_cmt(address, False): 1377 | comments += [comment] 1378 | if comment := idaapi.get_cmt(address, True): 1379 | comments += [comment] 1380 | 1381 | raw_instruction = idaapi.generate_disasm_line(address, 0) 1382 | tls = ida_kernwin.tagged_line_sections_t() 1383 | ida_kernwin.parse_tagged_line_sections(tls, raw_instruction) 1384 | insn_section = tls.first(ida_lines.COLOR_INSN) 1385 | 1386 | operands = [] 1387 | for op_tag in range(ida_lines.COLOR_OPND1, ida_lines.COLOR_OPND8 + 1): 1388 | op_n = tls.first(op_tag) 1389 | if not op_n: 1390 | break 1391 | 1392 | op: str = op_n.substr(raw_instruction) 1393 | op_str = ida_lines.tag_remove(op) 1394 | 1395 | # 做很多工作来添加地址注释以获取符号 1396 | for idx in range(len(op) - 2): 1397 | if op[idx] != idaapi.COLOR_ON: 1398 | continue 1399 | 1400 | idx += 1 1401 | if ord(op[idx]) != idaapi.COLOR_ADDR: 1402 | continue 1403 | 1404 | idx += 1 1405 | addr_string = op[idx:idx + idaapi.COLOR_ADDR_SIZE] 1406 | idx += idaapi.COLOR_ADDR_SIZE 1407 | 1408 | addr = int(addr_string, 16) 1409 | 1410 | # 找到下一个颜色并切片直到那里 1411 | symbol = op[idx:op.find(idaapi.COLOR_OFF, idx)] 1412 | 1413 | if symbol == '': 1414 | # 我们无法确定符号,所以使用整个 op_str 1415 | symbol = op_str 1416 | 1417 | comments += [f"{symbol}={addr:#x}"] 1418 | 1419 | # 如果其类型可用,则打印其值 1420 | try: 1421 | value = get_global_variable_value_internal(addr) 1422 | except: 1423 | continue 1424 | 1425 | comments += [f"*{symbol}={value}"] 1426 | 1427 | operands += [op_str] 1428 | 1429 | mnem = ida_lines.tag_remove(insn_section.substr(raw_instruction)) 1430 | instruction = f"{mnem} {', '.join(operands)}" 1431 | 1432 | line = DisassemblyLine( 1433 | address=f"{address:#x}", 1434 | instruction=instruction, 1435 | ) 1436 | 1437 | if len(comments) > 0: 1438 | line.update(comments=comments) 1439 | 1440 | if segment: 1441 | line.update(segment=segment) 1442 | 1443 | if label: 1444 | line.update(label=label) 1445 | 1446 | lines += [line] 1447 | 1448 | prototype = func.get_prototype() 1449 | arguments: list[Argument] = [Argument(name=arg.name, type=f"{arg.type}") for arg in prototype.iter_func()] if prototype else None 1450 | 1451 | disassembly_function = DisassemblyFunction( 1452 | name=func.name, 1453 | start_ea=f"{func.start_ea:#x}", 1454 | stack_frame=get_stack_frame_variables_internal(func.start_ea), 1455 | lines=lines 1456 | ) 1457 | 1458 | if prototype: 1459 | disassembly_function.update(return_type=f"{prototype.get_rettype()}") 1460 | 1461 | if arguments: 1462 | disassembly_function.update(arguments=arguments) 1463 | 1464 | return disassembly_function 1465 | 1466 | class Xref(TypedDict): 1467 | address: str 1468 | type: str 1469 | function: Optional[Function] 1470 | 1471 | @jsonrpc 1472 | @idaread 1473 | def get_xrefs_to( 1474 | address: Annotated[str, "要获取交叉引用的地址"], 1475 | ) -> list[Xref]: 1476 | """获取给定地址的所有交叉引用""" 1477 | xrefs = [] 1478 | xref: ida_xref.xrefblk_t 1479 | for xref in idautils.XrefsTo(parse_address(address)): 1480 | xrefs += [ 1481 | Xref(address=hex(xref.frm), 1482 | type="code" if xref.iscode else "data", 1483 | function=get_function(xref.frm, raise_error=False)) 1484 | ] 1485 | return xrefs 1486 | 1487 | @jsonrpc 1488 | @idaread 1489 | def get_xrefs_to_field( 1490 | struct_name: Annotated[str, "结构体名称 (类型)"], 1491 | field_name: Annotated[str, "要获取交叉引用的字段名称 (成员)"], 1492 | ) -> list[Xref]: 1493 | """获取命名结构体字段 (成员) 的所有交叉引用""" 1494 | 1495 | # 获取类型库 1496 | til = ida_typeinf.get_idati() 1497 | if not til: 1498 | raise IDAError("Failed to retrieve type library.") 1499 | 1500 | # 获取结构体类型信息 1501 | tif = ida_typeinf.tinfo_t() 1502 | if not tif.get_named_type(til, struct_name, ida_typeinf.BTF_STRUCT, True, False): 1503 | print(f"Structure '{struct_name}' not found.") 1504 | return [] 1505 | 1506 | # 获取字段索引 1507 | idx = ida_typeinf.get_udm_by_fullname(None, struct_name + '.' + field_name) 1508 | if idx == -1: 1509 | print(f"Field '{field_name}' not found in structure '{struct_name}'.") 1510 | return [] 1511 | 1512 | # 获取类型标识符 1513 | tid = tif.get_udm_tid(idx) 1514 | if tid == ida_idaapi.BADADDR: 1515 | raise IDAError(f"Unable to get tid for structure '{struct_name}' and field '{field_name}'.") 1516 | 1517 | # 获取 tid 的交叉引用 1518 | xrefs = [] 1519 | xref: ida_xref.xrefblk_t 1520 | for xref in idautils.XrefsTo(tid): 1521 | 1522 | xrefs += [ 1523 | Xref(address=hex(xref.frm), 1524 | type="code" if xref.iscode else "data", 1525 | function=get_function(xref.frm, raise_error=False)) 1526 | ] 1527 | return xrefs 1528 | 1529 | @jsonrpc 1530 | @idaread 1531 | def get_entry_points() -> list[Function]: 1532 | """获取数据库中的所有入口点""" 1533 | result = [] 1534 | for i in range(ida_entry.get_entry_qty()): 1535 | ordinal = ida_entry.get_entry_ordinal(i) 1536 | address = ida_entry.get_entry(ordinal) 1537 | func = get_function(address, raise_error=False) 1538 | if func is not None: 1539 | result.append(func) 1540 | return result 1541 | 1542 | @jsonrpc 1543 | @idawrite 1544 | def set_comment( 1545 | address: Annotated[str, "要设置注释的函数地址"], 1546 | comment: Annotated[str, "注释文本"], 1547 | ): 1548 | """设置给定函数反汇编和伪代码中的注释""" 1549 | address = parse_address(address) 1550 | 1551 | if not idaapi.set_cmt(address, comment, False): 1552 | raise IDAError(f"Failed to set disassembly comment at {hex(address)}") 1553 | 1554 | if not ida_hexrays.init_hexrays_plugin(): 1555 | return 1556 | 1557 | # 参考:https://cyber.wtf/2019/03/22/using-ida-python-to-analyze-trickbot/ 1558 | # 检查地址是否对应于一行 1559 | try: 1560 | cfunc = decompile_checked(address) 1561 | except DecompilerLicenseError: 1562 | # 由于反编译器许可证错误,我们未能反编译函数 1563 | return 1564 | 1565 | # 特殊情况:函数入口注释 1566 | if address == cfunc.entry_ea: 1567 | idc.set_func_cmt(address, comment, True) 1568 | cfunc.refresh_func_ctext() 1569 | return 1570 | 1571 | eamap = cfunc.get_eamap() 1572 | if address not in eamap: 1573 | print(f"Failed to set decompiler comment at {hex(address)}") 1574 | return 1575 | nearest_ea = eamap[address][0].ea 1576 | 1577 | # 移除孤立注释 1578 | if cfunc.has_orphan_cmts(): 1579 | cfunc.del_orphan_cmts() 1580 | cfunc.save_user_cmts() 1581 | 1582 | # 尝试所有可能的项目类型设置注释 1583 | tl = idaapi.treeloc_t() 1584 | tl.ea = nearest_ea 1585 | for itp in range(idaapi.ITP_SEMI, idaapi.ITP_COLON): 1586 | tl.itp = itp 1587 | cfunc.set_user_cmt(tl, comment) 1588 | cfunc.save_user_cmts() 1589 | cfunc.refresh_func_ctext() 1590 | if not cfunc.has_orphan_cmts(): 1591 | return 1592 | cfunc.del_orphan_cmts() 1593 | cfunc.save_user_cmts() 1594 | print(f"Failed to set decompiler comment at {hex(address)}") 1595 | 1596 | def refresh_decompiler_widget(): 1597 | widget = ida_kernwin.get_current_widget() 1598 | if widget is not None: 1599 | vu = ida_hexrays.get_widget_vdui(widget) 1600 | if vu is not None: 1601 | vu.refresh_ctext() 1602 | 1603 | def refresh_decompiler_ctext(function_address: int): 1604 | error = ida_hexrays.hexrays_failure_t() 1605 | cfunc: ida_hexrays.cfunc_t = ida_hexrays.decompile_func(function_address, error, ida_hexrays.DECOMP_WARNINGS) 1606 | if cfunc: 1607 | cfunc.refresh_func_ctext() 1608 | 1609 | @jsonrpc 1610 | @idawrite 1611 | def rename_local_variable( 1612 | function_address: Annotated[str, "包含变量的函数地址"], 1613 | old_name: Annotated[str, "变量的当前名称"], 1614 | new_name: Annotated[str, "变量的新名称 (空表示默认名称)"], 1615 | ): 1616 | """重命名函数中的本地变量""" 1617 | func = idaapi.get_func(parse_address(function_address)) 1618 | if not func: 1619 | raise IDAError(f"No function found at address {function_address}") 1620 | if not ida_hexrays.rename_lvar(func.start_ea, old_name, new_name): 1621 | raise IDAError(f"Failed to rename local variable {old_name} in function {hex(func.start_ea)}") 1622 | refresh_decompiler_ctext(func.start_ea) 1623 | 1624 | @jsonrpc 1625 | @idawrite 1626 | def rename_global_variable( 1627 | old_name: Annotated[str, "全局变量的当前名称"], 1628 | new_name: Annotated[str, "全局变量的新名称 (空表示默认名称)"], 1629 | ): 1630 | """重命名全局变量""" 1631 | ea = idaapi.get_name_ea(idaapi.BADADDR, old_name) 1632 | if not idaapi.set_name(ea, new_name): 1633 | raise IDAError(f"Failed to rename global variable {old_name} to {new_name}") 1634 | refresh_decompiler_ctext(ea) 1635 | 1636 | @jsonrpc 1637 | @idawrite 1638 | def set_global_variable_type( 1639 | variable_name: Annotated[str, "全局变量的名称"], 1640 | new_type: Annotated[str, "变量的新类型"], 1641 | ): 1642 | """设置全局变量的类型""" 1643 | ea = idaapi.get_name_ea(idaapi.BADADDR, variable_name) 1644 | tif = get_type_by_name(new_type) 1645 | if not tif: 1646 | raise IDAError(f"Parsed declaration is not a variable type") 1647 | if not ida_typeinf.apply_tinfo(ea, tif, ida_typeinf.PT_SIL): 1648 | raise IDAError(f"Failed to apply type") 1649 | 1650 | @jsonrpc 1651 | @idaread 1652 | def get_global_variable_value_by_name(variable_name: Annotated[str, "全局变量的名称"]) -> str: 1653 | """ 1654 | 读取全局变量的值(如果编译时已知) 1655 | 1656 | 优先使用此函数,而不是 `data_read_*` 函数。 1657 | """ 1658 | ea = idaapi.get_name_ea(idaapi.BADADDR, variable_name) 1659 | if ea == idaapi.BADADDR: 1660 | raise IDAError(f"Global variable {variable_name} not found") 1661 | 1662 | return get_global_variable_value_internal(ea) 1663 | 1664 | @jsonrpc 1665 | @idaread 1666 | def get_global_variable_value_at_address(ea: Annotated[str, "全局变量的地址"]) -> str: 1667 | """ 1668 | 通过地址读取全局变量的值(如果编译时已知) 1669 | 1670 | 优先使用此函数,而不是 `data_read_*` 函数。 1671 | """ 1672 | ea = parse_address(ea) 1673 | return get_global_variable_value_internal(ea) 1674 | 1675 | def get_global_variable_value_internal(ea: int) -> str: 1676 | # 获取变量的类型信息 1677 | tif = ida_typeinf.tinfo_t() 1678 | if not ida_nalt.get_tinfo(tif, ea): 1679 | # 没有类型信息,也许我们可以通过名称推断其大小 1680 | if not ida_bytes.has_any_name(ea): 1681 | raise IDAError(f"Failed to get type information for variable at {ea:#x}") 1682 | 1683 | size = ida_bytes.get_item_size(ea) 1684 | if size == 0: 1685 | raise IDAError(f"Failed to get type information for variable at {ea:#x}") 1686 | else: 1687 | # 确定变量的大小 1688 | size = tif.get_size() 1689 | 1690 | # 根据大小读取值 1691 | if size == 0 and tif.is_array() and tif.get_array_element().is_decl_char(): 1692 | return_string = idaapi.get_strlit_contents(ea, -1, 0).decode("utf-8").strip() 1693 | return f"\"{return_string}\"" 1694 | elif size == 1: 1695 | return hex(ida_bytes.get_byte(ea)) 1696 | elif size == 2: 1697 | return hex(ida_bytes.get_word(ea)) 1698 | elif size == 4: 1699 | return hex(ida_bytes.get_dword(ea)) 1700 | elif size == 8: 1701 | return hex(ida_bytes.get_qword(ea)) 1702 | else: 1703 | # 对于其他大小,返回原始字节 1704 | return ' '.join(hex(x) for x in ida_bytes.get_bytes(ea, size)) 1705 | 1706 | 1707 | @jsonrpc 1708 | @idawrite 1709 | def rename_function( 1710 | function_address: Annotated[str, "要重命名的函数地址"], 1711 | new_name: Annotated[str, "函数的新名称 (空表示默认名称)"], 1712 | ): 1713 | """重命名函数""" 1714 | func = idaapi.get_func(parse_address(function_address)) 1715 | if not func: 1716 | raise IDAError(f"No function found at address {function_address}") 1717 | if not idaapi.set_name(func.start_ea, new_name): 1718 | raise IDAError(f"Failed to rename function {hex(func.start_ea)} to {new_name}") 1719 | refresh_decompiler_ctext(func.start_ea) 1720 | # 自动记录变更 1721 | record_incremental_change("rename_function", {"address": function_address, "new_name": new_name}) 1722 | 1723 | @jsonrpc 1724 | @idawrite 1725 | def set_function_prototype( 1726 | function_address: Annotated[str, "函数地址"], 1727 | prototype: Annotated[str, "新的函数原型"], 1728 | ): 1729 | """设置函数原型""" 1730 | func = idaapi.get_func(parse_address(function_address)) 1731 | if not func: 1732 | raise IDAError(f"No function found at address {function_address}") 1733 | try: 1734 | tif = ida_typeinf.tinfo_t(prototype, None, ida_typeinf.PT_SIL) 1735 | if not tif.is_func(): 1736 | raise IDAError(f"Parsed declaration is not a function type") 1737 | if not ida_typeinf.apply_tinfo(func.start_ea, tif, ida_typeinf.PT_SIL): 1738 | raise IDAError(f"Failed to apply type") 1739 | refresh_decompiler_ctext(func.start_ea) 1740 | except Exception as e: 1741 | raise IDAError(f"Failed to parse prototype string: {prototype}") 1742 | # 自动记录变更 1743 | record_incremental_change("set_function_prototype", {"address": function_address, "prototype": prototype}) 1744 | 1745 | class my_modifier_t(ida_hexrays.user_lvar_modifier_t): 1746 | def __init__(self, var_name: str, new_type: ida_typeinf.tinfo_t): 1747 | ida_hexrays.user_lvar_modifier_t.__init__(self) 1748 | self.var_name = var_name 1749 | self.new_type = new_type 1750 | 1751 | def modify_lvars(self, lvars): 1752 | for lvar_saved in lvars.lvvec: 1753 | lvar_saved: ida_hexrays.lvar_saved_info_t 1754 | if lvar_saved.name == self.var_name: 1755 | lvar_saved.type = self.new_type 1756 | return True 1757 | return False 1758 | 1759 | # 注意:这是一种非常不规范的方法,但为了从IDA中获取错误信息是必要的 1760 | def parse_decls_ctypes(decls: str, hti_flags: int) -> tuple[int, str]: 1761 | if sys.platform == "win32": 1762 | import ctypes 1763 | 1764 | assert isinstance(decls, str), "decls must be a string" 1765 | assert isinstance(hti_flags, int), "hti_flags must be an int" 1766 | c_decls = decls.encode("utf-8") 1767 | c_til = None 1768 | ida_dll = ctypes.CDLL("ida") 1769 | ida_dll.parse_decls.argtypes = [ 1770 | ctypes.c_void_p, 1771 | ctypes.c_char_p, 1772 | ctypes.c_void_p, 1773 | ctypes.c_int, 1774 | ] 1775 | ida_dll.parse_decls.restype = ctypes.c_int 1776 | 1777 | messages = [] 1778 | 1779 | @ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p) 1780 | def magic_printer(fmt: bytes, arg1: bytes): 1781 | if fmt.count(b"%") == 1 and b"%s" in fmt: 1782 | formatted = fmt.replace(b"%s", arg1) 1783 | messages.append(formatted.decode("utf-8")) 1784 | return len(formatted) + 1 1785 | else: 1786 | messages.append(f"unsupported magic_printer fmt: {repr(fmt)}") 1787 | return 0 1788 | 1789 | errors = ida_dll.parse_decls(c_til, c_decls, magic_printer, hti_flags) 1790 | else: 1791 | # 注意:上面的方法也可以在其他平台上工作,但未经过测试,并且存在变量参数ABI的差异。 1792 | errors = ida_typeinf.parse_decls(None, decls, False, hti_flags) 1793 | messages = [] 1794 | return errors, messages 1795 | 1796 | @jsonrpc 1797 | @idawrite 1798 | def declare_c_type( 1799 | c_declaration: Annotated[str, "类型C声明。示例包括:typedef int foo_t; struct bar { int a; bool b; };"], 1800 | ): 1801 | """从C声明创建或更新本地类型""" 1802 | # PT_SIL: 抑制警告对话框(虽然看起来在这里是不必要的) 1803 | # PT_EMPTY: 允许空类型(也可能是多余的?) 1804 | # PT_TYP: 打印带有结构体标签的状态消息 1805 | flags = ida_typeinf.PT_SIL | ida_typeinf.PT_EMPTY | ida_typeinf.PT_TYP 1806 | errors, messages = parse_decls_ctypes(c_declaration, flags) 1807 | 1808 | pretty_messages = "\n".join(messages) 1809 | if errors > 0: 1810 | raise IDAError(f"Failed to parse type:\n{c_declaration}\n\nErrors:\n{pretty_messages}") 1811 | return f"success\n\nInfo:\n{pretty_messages}" 1812 | # 自动记录变更 1813 | record_incremental_change("declare_c_type", {"c_declaration": c_declaration}) 1814 | 1815 | @jsonrpc 1816 | @idawrite 1817 | def set_local_variable_type( 1818 | function_address: Annotated[str, "要反编译的函数地址"], 1819 | variable_name: Annotated[str, "变量名称"], 1820 | new_type: Annotated[str, "变量的新类型"], 1821 | ): 1822 | """设置本地变量的类型""" 1823 | try: 1824 | # 某些版本的 IDA 不支持此构造函数 1825 | new_tif = ida_typeinf.tinfo_t(new_type, None, ida_typeinf.PT_SIL) 1826 | except Exception: 1827 | try: 1828 | new_tif = ida_typeinf.tinfo_t() 1829 | # parse_decl 需要分号来表示类型 1830 | ida_typeinf.parse_decl(new_tif, None, new_type + ";", ida_typeinf.PT_SIL) 1831 | except Exception: 1832 | raise IDAError(f"Failed to parse type: {new_type}") 1833 | func = idaapi.get_func(parse_address(function_address)) 1834 | if not func: 1835 | raise IDAError(f"No function found at address {function_address}") 1836 | if not ida_hexrays.rename_lvar(func.start_ea, variable_name, variable_name): 1837 | raise IDAError(f"Failed to find local variable: {variable_name}") 1838 | modifier = my_modifier_t(variable_name, new_tif) 1839 | if not ida_hexrays.modify_user_lvars(func.start_ea, modifier): 1840 | raise IDAError(f"Failed to modify local variable: {variable_name}") 1841 | refresh_decompiler_ctext(func.start_ea) 1842 | # 自动记录变更 1843 | record_incremental_change("set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type}) 1844 | 1845 | class StackFrameVariable(TypedDict): 1846 | name: str 1847 | offset: str 1848 | size: str 1849 | type: str 1850 | 1851 | @jsonrpc 1852 | @idaread 1853 | def get_stack_frame_variables( 1854 | function_address: Annotated[str, "要获取栈帧变量的反汇编函数地址"] 1855 | ) -> list[StackFrameVariable]: 1856 | """获取给定函数的栈帧变量""" 1857 | return get_stack_frame_variables_internal(parse_address(function_address)) 1858 | 1859 | def get_stack_frame_variables_internal(function_address: int) -> list[dict]: 1860 | func = idaapi.get_func(function_address) 1861 | if not func: 1862 | raise IDAError(f"No function found at address {function_address}") 1863 | 1864 | members = [] 1865 | tif = ida_typeinf.tinfo_t() 1866 | if not tif.get_type_by_tid(func.frame) or not tif.is_udt(): 1867 | return [] 1868 | 1869 | udt = ida_typeinf.udt_type_data_t() 1870 | tif.get_udt_details(udt) 1871 | for udm in udt: 1872 | if not udm.is_gap(): 1873 | name = udm.name 1874 | offset = udm.offset // 8 1875 | size = udm.size // 8 1876 | type = str(udm.type) 1877 | 1878 | members += [StackFrameVariable(name=name, 1879 | offset=hex(offset), 1880 | size=hex(size), 1881 | type=type) 1882 | ] 1883 | 1884 | return members 1885 | 1886 | 1887 | class StructureMember(TypedDict): 1888 | name: str 1889 | offset: str 1890 | size: str 1891 | type: str 1892 | 1893 | class StructureDefinition(TypedDict): 1894 | name: str 1895 | size: str 1896 | members: list[StructureMember] 1897 | 1898 | @jsonrpc 1899 | @idaread 1900 | def get_defined_structures() -> list[StructureDefinition]: 1901 | """返回所有定义的结构体列表""" 1902 | 1903 | rv = [] 1904 | limit = ida_typeinf.get_ordinal_limit() 1905 | for ordinal in range(1, limit): 1906 | tif = ida_typeinf.tinfo_t() 1907 | tif.get_numbered_type(None, ordinal) 1908 | if tif.is_udt(): 1909 | udt = ida_typeinf.udt_type_data_t() 1910 | members = [] 1911 | if tif.get_udt_details(udt): 1912 | members = [ 1913 | StructureMember(name=x.name, 1914 | offset=hex(x.offset // 8), 1915 | size=hex(x.size // 8), 1916 | type=str(x.type)) 1917 | for _, x in enumerate(udt) 1918 | ] 1919 | 1920 | rv += [StructureDefinition(name=tif.get_type_name(), 1921 | size=hex(tif.get_size()), 1922 | members=members)] 1923 | 1924 | return rv 1925 | 1926 | @jsonrpc 1927 | @idawrite 1928 | def rename_stack_frame_variable( 1929 | function_address: Annotated[str, "要设置栈帧变量的反汇编函数地址"], 1930 | old_name: Annotated[str, "变量的当前名称"], 1931 | new_name: Annotated[str, "变量的新名称 (空表示默认名称)"] 1932 | ): 1933 | """更改IDA函数中栈帧变量的名称""" 1934 | func = idaapi.get_func(parse_address(function_address)) 1935 | if not func: 1936 | raise IDAError(f"No function found at address {function_address}") 1937 | 1938 | frame_tif = ida_typeinf.tinfo_t() 1939 | if not ida_frame.get_func_frame(frame_tif, func): 1940 | raise IDAError("No frame returned.") 1941 | 1942 | idx, udm = frame_tif.get_udm(old_name) 1943 | if not udm: 1944 | raise IDAError(f"{old_name} not found.") 1945 | 1946 | tid = frame_tif.get_udm_tid(idx) 1947 | if ida_frame.is_special_frame_member(tid): 1948 | raise IDAError(f"{old_name} is a special frame member. Will not change the name.") 1949 | 1950 | udm = ida_typeinf.udm_t() 1951 | frame_tif.get_udm_by_tid(udm, tid) 1952 | offset = udm.offset // 8 1953 | if ida_frame.is_funcarg_off(func, offset): 1954 | raise IDAError(f"{old_name} is an argument member. Will not change the name.") 1955 | 1956 | sval = ida_frame.soff_to_fpoff(func, offset) 1957 | if not ida_frame.define_stkvar(func, new_name, sval, udm.type): 1958 | raise IDAError("failed to rename stack frame variable") 1959 | 1960 | @jsonrpc 1961 | @idawrite 1962 | def create_stack_frame_variable( 1963 | function_address: Annotated[str, "要设置栈帧变量的反汇编函数地址"], 1964 | offset: Annotated[str, "栈帧变量的偏移量"], 1965 | variable_name: Annotated[str, "栈变量名称"], 1966 | type_name: Annotated[str, "栈变量类型"] 1967 | ): 1968 | """对于给定的函数,在指定偏移量处创建一个栈变量并设置特定类型""" 1969 | 1970 | func = idaapi.get_func(parse_address(function_address)) 1971 | if not func: 1972 | raise IDAError(f"No function found at address {function_address}") 1973 | 1974 | offset = parse_address(offset) 1975 | 1976 | frame_tif = ida_typeinf.tinfo_t() 1977 | if not ida_frame.get_func_frame(frame_tif, func): 1978 | raise IDAError("No frame returned.") 1979 | 1980 | tif = get_type_by_name(type_name) 1981 | if not ida_frame.define_stkvar(func, variable_name, offset, tif): 1982 | raise IDAError("failed to define stack frame variable") 1983 | 1984 | @jsonrpc 1985 | @idawrite 1986 | def set_stack_frame_variable_type( 1987 | function_address: Annotated[str, "要设置栈帧变量的反汇编函数地址"], 1988 | variable_name: Annotated[str, "栈变量名称"], 1989 | type_name: Annotated[str, "栈变量类型"] 1990 | ): 1991 | """对于给定的反汇编函数,设置栈变量的类型""" 1992 | 1993 | func = idaapi.get_func(parse_address(function_address)) 1994 | if not func: 1995 | raise IDAError(f"No function found at address {function_address}") 1996 | 1997 | frame_tif = ida_typeinf.tinfo_t() 1998 | if not ida_frame.get_func_frame(frame_tif, func): 1999 | raise IDAError("No frame returned.") 2000 | 2001 | idx, udm = frame_tif.get_udm(variable_name) 2002 | if not udm: 2003 | raise IDAError(f"{variable_name} not found.") 2004 | 2005 | tid = frame_tif.get_udm_tid(idx) 2006 | udm = ida_typeinf.udm_t() 2007 | frame_tif.get_udm_by_tid(udm, tid) 2008 | offset = udm.offset // 8 2009 | 2010 | tif = get_type_by_name(type_name) 2011 | if not ida_frame.set_frame_member_type(func, offset, tif): 2012 | raise IDAError("failed to set stack frame variable type") 2013 | 2014 | @jsonrpc 2015 | @idawrite 2016 | def delete_stack_frame_variable( 2017 | function_address: Annotated[str, "要设置栈帧变量的函数地址"], 2018 | variable_name: Annotated[str, "栈变量名称"] 2019 | ): 2020 | """删除给定函数的命名栈变量""" 2021 | 2022 | func = idaapi.get_func(parse_address(function_address)) 2023 | if not func: 2024 | raise IDAError(f"No function found at address {function_address}") 2025 | 2026 | frame_tif = ida_typeinf.tinfo_t() 2027 | if not ida_frame.get_func_frame(frame_tif, func): 2028 | raise IDAError("No frame returned.") 2029 | 2030 | idx, udm = frame_tif.get_udm(variable_name) 2031 | if not udm: 2032 | raise IDAError(f"{variable_name} not found.") 2033 | 2034 | tid = frame_tif.get_udm_tid(idx) 2035 | if ida_frame.is_special_frame_member(tid): 2036 | raise IDAError(f"{variable_name} is a special frame member. Will not delete.") 2037 | 2038 | udm = ida_typeinf.udm_t() 2039 | frame_tif.get_udm_by_tid(udm, tid) 2040 | offset = udm.offset // 8 2041 | size = udm.size // 8 2042 | if ida_frame.is_funcarg_off(func, offset): 2043 | raise IDAError(f"{variable_name} is an argument member. Will not delete.") 2044 | 2045 | if not ida_frame.delete_frame_members(func, offset, offset+size): 2046 | raise IDAError("failed to delete stack frame variable") 2047 | 2048 | @jsonrpc 2049 | @idaread 2050 | def read_memory_bytes( 2051 | memory_address: Annotated[str, "要读取的字节地址"], 2052 | size: Annotated[int, "要读取的内存大小"] 2053 | ) -> str: 2054 | """ 2055 | 读取指定地址的字节。 2056 | 2057 | 仅当 `get_global_variable_at` 和 `get_global_variable_by_name` 2058 | 都失败时才使用此函数。 2059 | """ 2060 | return ' '.join(f'{x:#02x}' for x in ida_bytes.get_bytes(parse_address(memory_address), size)) 2061 | 2062 | @jsonrpc 2063 | @idaread 2064 | @unsafe 2065 | def dbg_get_registers() -> list[dict[str, str]]: 2066 | """获取所有寄存器及其值。此函数仅在调试时可用。""" 2067 | result = [] 2068 | dbg = ida_idd.get_dbg() 2069 | for thread_index in range(ida_dbg.get_thread_qty()): 2070 | tid = ida_dbg.getn_thread(thread_index) 2071 | regs = [] 2072 | regvals = ida_dbg.get_reg_vals(tid) 2073 | for reg_index, rv in enumerate(regvals): 2074 | reg_info = dbg.regs(reg_index) 2075 | reg_value = rv.pyval(reg_info.dtype) 2076 | if isinstance(reg_value, int): 2077 | try_record_dynamic_string(reg_value) 2078 | reg_value = hex(reg_value) 2079 | if isinstance(reg_value, bytes): 2080 | reg_value = reg_value.hex(" ") 2081 | regs.append({ 2082 | "name": reg_info.name, 2083 | "value": reg_value, 2084 | }) 2085 | result.append({ 2086 | "thread_id": tid, 2087 | "registers": regs, 2088 | }) 2089 | return result 2090 | 2091 | @jsonrpc 2092 | @idaread 2093 | @unsafe 2094 | def dbg_get_call_stack() -> list[dict[str, str]]: 2095 | """获取当前调用堆栈。""" 2096 | callstack = [] 2097 | try: 2098 | tid = ida_dbg.get_current_thread() 2099 | trace = ida_idd.call_stack_t() 2100 | 2101 | if not ida_dbg.collect_stack_trace(tid, trace): 2102 | return [] 2103 | for frame in trace: 2104 | frame_info = { 2105 | "address": hex(frame.callea), 2106 | } 2107 | try: 2108 | module_info = ida_idd.modinfo_t() 2109 | if ida_dbg.get_module_info(frame.callea, module_info): 2110 | frame_info["module"] = os.path.basename(module_info.name) 2111 | else: 2112 | frame_info["module"] = "" 2113 | 2114 | name = ( 2115 | ida_name.get_nice_colored_name( 2116 | frame.callea, 2117 | ida_name.GNCN_NOCOLOR 2118 | | ida_name.GNCN_NOLABEL 2119 | | ida_name.GNCN_NOSEG 2120 | | ida_name.GNCN_PREFDBG, 2121 | ) 2122 | or "" 2123 | ) 2124 | frame_info["symbol"] = name 2125 | 2126 | except Exception as e: 2127 | frame_info["module"] = "" 2128 | frame_info["symbol"] = str(e) 2129 | 2130 | callstack.append(frame_info) 2131 | 2132 | except Exception as e: 2133 | pass 2134 | return callstack 2135 | 2136 | def list_breakpoints(): 2137 | ea = ida_ida.inf_get_min_ea() 2138 | end_ea = ida_ida.inf_get_max_ea() 2139 | breakpoints = [] 2140 | while ea <= end_ea: 2141 | bpt = ida_dbg.bpt_t() 2142 | if ida_dbg.get_bpt(ea, bpt): 2143 | breakpoints.append( 2144 | { 2145 | "ea": hex(bpt.ea), 2146 | "type": bpt.type, 2147 | "enabled": bpt.flags & ida_dbg.BPT_ENABLED, 2148 | "condition": bpt.condition if bpt.condition else None, 2149 | } 2150 | ) 2151 | ea = ida_bytes.next_head(ea, end_ea) 2152 | return breakpoints 2153 | 2154 | 2155 | 2156 | @jsonrpc 2157 | @idaread 2158 | @unsafe 2159 | def dbg_control_process( 2160 | action: Annotated[str, "调试操作类型: 'start', 'exit', 'continue', 'run_to'"], 2161 | address: Annotated[Optional[str], "目标地址,仅run_to操作需要"] = None 2162 | ) -> str: 2163 | """统一的调试器控制接口 2164 | 2165 | Args: 2166 | action: 调试操作类型,支持'start', 'exit', 'continue', 'run_to' 2167 | address: 目标地址,仅在action为'run_to'时需要 2168 | 2169 | Returns: 2170 | 操作结果消息 2171 | """ 2172 | if action == 'start': 2173 | if idaapi.start_process("", "", ""): 2174 | return "Debugger started" 2175 | return "Failed to start debugger" 2176 | elif action == 'exit': 2177 | if idaapi.exit_process(): 2178 | return "Debugger exited" 2179 | return "Failed to exit debugger" 2180 | elif action == 'continue': 2181 | if idaapi.continue_process(): 2182 | return "Debugger continued" 2183 | return "Failed to continue debugger" 2184 | elif action == 'run_to': 2185 | if not address: 2186 | return "Error: Address required for run_to action" 2187 | ea = parse_address(address) 2188 | if idaapi.run_to(ea): 2189 | return f"Debugger run to {hex(ea)}" 2190 | return f"Failed to run to address {hex(ea)}" 2191 | else: 2192 | return f"Error: Invalid action '{action}'. Supported actions: start, exit, continue, run_to" 2193 | 2194 | @jsonrpc 2195 | @idaread 2196 | @unsafe 2197 | def dbg_start_process() -> str: 2198 | """启动调试器 (已弃用,请使用dbg_control_process)""" 2199 | logger.warning("dbg_start_process is deprecated. Please use dbg_control_process with action='start'") 2200 | return dbg_control_process('start') 2201 | 2202 | @jsonrpc 2203 | @idaread 2204 | @unsafe 2205 | def dbg_exit_process() -> str: 2206 | """退出调试器 (已弃用,请使用dbg_control_process)""" 2207 | logger.warning("dbg_exit_process is deprecated. Please use dbg_control_process with action='exit'") 2208 | return dbg_control_process('exit') 2209 | 2210 | @jsonrpc 2211 | @idaread 2212 | @unsafe 2213 | def dbg_continue_process() -> str: 2214 | """继续调试器 (已弃用,请使用dbg_control_process)""" 2215 | logger.warning("dbg_continue_process is deprecated. Please use dbg_control_process with action='continue'") 2216 | return dbg_control_process('continue') 2217 | 2218 | @jsonrpc 2219 | @idaread 2220 | @unsafe 2221 | def dbg_run_to( 2222 | address: Annotated[str, "运行调试器到指定地址"], 2223 | ) -> str: 2224 | """运行调试器到指定地址 (已弃用,请使用dbg_control_process)""" 2225 | logger.warning("dbg_run_to is deprecated. Please use dbg_control_process with action='run_to'") 2226 | return dbg_control_process('run_to', address) 2227 | 2228 | @jsonrpc 2229 | @idaread 2230 | @unsafe 2231 | def dbg_manage_breakpoint( 2232 | action: Annotated[str, "断点操作类型: 'list', 'set', 'delete', 'enable'"], 2233 | address: Annotated[Optional[str], "断点地址,仅set/delete/enable操作需要"] = None, 2234 | enable: Annotated[Optional[bool], "是否启用断点,仅enable操作需要"] = None 2235 | ) -> Union[str, list[dict[str, str]]]: 2236 | """统一的断点管理接口 2237 | 2238 | Args: 2239 | action: 断点操作类型,支持'list', 'set', 'delete', 'enable' 2240 | address: 断点地址,仅在action为'set', 'delete', 'enable'时需要 2241 | enable: 是否启用断点,仅在action为'enable'时需要 2242 | 2243 | Returns: 2244 | 操作结果消息或断点列表 2245 | """ 2246 | if action == 'list': 2247 | return list_breakpoints() 2248 | elif action in ['set', 'delete', 'enable']: 2249 | if not address: 2250 | return f"Error: Address required for {action} action" 2251 | ea = parse_address(address) 2252 | 2253 | if action == 'set': 2254 | if idaapi.add_bpt(ea, 0, idaapi.BPT_SOFT): 2255 | return f"Breakpoint set at {hex(ea)}" 2256 | breakpoints = list_breakpoints() 2257 | for bpt in breakpoints: 2258 | if bpt["ea"] == hex(ea): 2259 | return f"Breakpoint already exists at {hex(ea)}" 2260 | return f"Failed to set breakpoint at address {hex(ea)}" 2261 | elif action == 'delete': 2262 | if idaapi.del_bpt(ea): 2263 | return f"Breakpoint deleted at {hex(ea)}" 2264 | return f"Failed to delete breakpoint at address {hex(ea)}" 2265 | elif action == 'enable': 2266 | if enable is None: 2267 | return "Error: enable parameter required for enable action" 2268 | if idaapi.enable_bpt(ea, enable): 2269 | return f"Breakpoint {'enabled' if enable else 'disabled'} at {hex(ea)}" 2270 | return f"Failed to {'' if enable else 'disable '}breakpoint at address {hex(ea)}" 2271 | else: 2272 | return f"Error: Invalid action '{action}'. Supported actions: list, set, delete, enable" 2273 | 2274 | @jsonrpc 2275 | @idaread 2276 | @unsafe 2277 | def dbg_list_breakpoints(): 2278 | """列出程序中的所有断点 (已弃用,请使用dbg_manage_breakpoint)""" 2279 | logger.warning("dbg_list_breakpoints is deprecated. Please use dbg_manage_breakpoint with action='list'") 2280 | return dbg_manage_breakpoint('list') 2281 | 2282 | @jsonrpc 2283 | @idaread 2284 | @unsafe 2285 | def dbg_set_breakpoint( 2286 | address: Annotated[str, "在指定地址设置断点"], 2287 | ) -> str: 2288 | """在指定地址设置断点 (已弃用,请使用dbg_manage_breakpoint)""" 2289 | logger.warning("dbg_set_breakpoint is deprecated. Please use dbg_manage_breakpoint with action='set'") 2290 | return dbg_manage_breakpoint('set', address) 2291 | 2292 | @jsonrpc 2293 | @idaread 2294 | @unsafe 2295 | def dbg_delete_breakpoint( 2296 | address: Annotated[str, "del a breakpoint at the specified address"], 2297 | ) -> str: 2298 | """del a breakpoint at the specified address (已弃用,请使用dbg_manage_breakpoint)""" 2299 | logger.warning("dbg_delete_breakpoint is deprecated. Please use dbg_manage_breakpoint with action='delete'") 2300 | return dbg_manage_breakpoint('delete', address) 2301 | 2302 | @jsonrpc 2303 | @idaread 2304 | @unsafe 2305 | def dbg_enable_breakpoint( 2306 | address: Annotated[str, "Enable or disable a breakpoint at the specified address"], 2307 | enable: Annotated[bool, "Enable or disable a breakpoint"], 2308 | ) -> str: 2309 | """Enable or disable a breakpoint at the specified address (已弃用,请使用dbg_manage_breakpoint)""" 2310 | logger.warning("dbg_enable_breakpoint is deprecated. Please use dbg_manage_breakpoint with action='enable'") 2311 | return dbg_manage_breakpoint('enable', address, enable) 2312 | 2313 | 2314 | 2315 | def _is_valid_address(address_str: str) -> bool: 2316 | """ 2317 | 检查地址字符串是否有效 2318 | """ 2319 | try: 2320 | parse_address(address_str) 2321 | return True 2322 | except (ValueError, TypeError): 2323 | return False 2324 | 2325 | def _generate_angr_script_template(binary_path: str, func_address_var: str) -> str: 2326 | """ 2327 | 生成angr脚本基础模板 2328 | """ 2329 | return f""" 2330 | import angr 2331 | import claripy 2332 | import sys 2333 | import os 2334 | 2335 | # 设置Angr项目 2336 | proj = angr.Project('{binary_path}', auto_load_libs=False) 2337 | 2338 | # 函数地址 2339 | {func_address_var} 2340 | 2341 | # 创建初始状态 2342 | initial_state = proj.factory.entry_state() 2343 | 2344 | # 脚本主体 2345 | {{script_body}} 2346 | 2347 | # 运行求解器 2348 | {{script_execution}} 2349 | """ 2350 | 2351 | def _generate_angr_script_content(script_type: str, func_name: str, func_size: int, options: dict) -> tuple[str, str]: 2352 | """ 2353 | 根据脚本类型生成相应的脚本内容 2354 | """ 2355 | if script_type == 'symbolic_execution': 2356 | arg_count = options.get('arg_count', 1) 2357 | arg_size = options.get('arg_size', 32) 2358 | 2359 | # 生成符号参数 2360 | args_code = [] 2361 | call_args = [] 2362 | for i in range(arg_count): 2363 | arg_name = f'sym_arg{i+1}' 2364 | args_code.append(f'{arg_name} = claripy.BVS("{arg_name}", {arg_size}) # 参数{i+1}') 2365 | call_args.append(arg_name) 2366 | 2367 | args_code_str = "\n".join(args_code) 2368 | 2369 | script_body = f""" 2370 | # 创建函数参数的符号变量 2371 | {args_code_str} 2372 | 2373 | # 创建调用函数的状态 2374 | state = proj.factory.call_state({func_name}_addr, {', '.join(call_args)}) 2375 | 2376 | # 添加约束条件 2377 | # Create simulation manager 2378 | simgr = proj.factory.simgr(state) 2379 | """ 2380 | 2381 | script_execution = """ 2382 | # 运行符号执行 2383 | print("开始符号执行...") 2384 | simgr.explore() 2385 | 2386 | # 分析结果 2387 | if simgr.deadended: 2388 | print(f"找到 {len(simgr.deadended)} 个终止状态") 2389 | for i, state in enumerate(simgr.deadended): 2390 | print(f"状态 {i}:") 2391 | # 可以根据需要获取返回值或其他信息 2392 | # print(f" 返回值: {state.regs.eax}") 2393 | else: 2394 | print("未找到终止状态") 2395 | """ 2396 | 2397 | elif script_type == 'brute_force': 2398 | input_size = options.get('input_size', 32) 2399 | success_condition = options.get('success_condition', 'state.regs.eax == 1') 2400 | 2401 | script_body = f""" 2402 | # 设置目标函数地址作为 hook 点 2403 | def check_password(state): 2404 | # 检查密码是否正确 2405 | if state.solver.is_true({success_condition}): 2406 | print("找到正确密码!") 2407 | # 可以根据需要提取输入值 2408 | # 例如,如果密码存储在内存中的某个位置 2409 | # password_ptr = state.regs.esp + 8 # 假设参数在栈上 2410 | # password = state.memory.load(password_ptr, {input_size}) 2411 | # print(f"密码: {state.solver.eval(password, cast_to=bytes)}") 2412 | sys.exit(0) 2413 | 2414 | # 在函数返回前添加 hook 2415 | proj.hook({func_name}_addr + {hex(func_size)}, check_password) 2416 | 2417 | # 创建符号输入 2418 | sym_input = claripy.BVS('input', 8 * {input_size}) # 输入是{input_size}字节 2419 | 2420 | # 设置符号输入到合适的位置 2421 | state = proj.factory.entry_state() 2422 | # 例如,设置命令行参数 2423 | # state.argv = [proj.filename, sym_input] 2424 | 2425 | # 添加约束以加速求解 2426 | # state.solver.add(sym_input.get_byte(0) != 0) # 非空 2427 | 2428 | # 创建模拟管理器 2429 | simgr = proj.factory.simgr(state) 2430 | """ 2431 | 2432 | script_execution = """ 2433 | # 开始爆破 2434 | print("开始密码爆破...") 2435 | simgr.run() 2436 | print("未找到正确密码") 2437 | """ 2438 | 2439 | elif script_type == 'control_flow': 2440 | script_body = f""" 2441 | # 创建状态 2442 | def trace_path(state): 2443 | # 记录路径信息 2444 | path = state.history.bbl_addrs 2445 | print(f"路径长度: {{len(path)}}") 2446 | # 可以根据需要保存路径信息 2447 | 2448 | # 在函数返回前添加 hook 2449 | proj.hook({func_name}_addr + {hex(func_size)}, trace_path) 2450 | 2451 | # 创建状态 2452 | state = proj.factory.call_state({func_name}_addr) 2453 | 2454 | # 创建模拟管理器 2455 | simgr = proj.factory.simgr(state) 2456 | """ 2457 | 2458 | script_execution = """ 2459 | # 运行控制流分析 2460 | print("开始控制流分析...") 2461 | simgr.run() 2462 | """ 2463 | 2464 | else: 2465 | raise IDAError(f"不支持的脚本类型: {{script_type}}") 2466 | 2467 | return script_body, script_execution 2468 | 2469 | @jsonrpc 2470 | @idaread 2471 | def generate_angr_script( 2472 | function_address: Annotated[str, "要分析的函数地址"], 2473 | script_type: Annotated[str, "脚本类型: 'symbolic_execution', 'brute_force', 'control_flow'"], 2474 | options: Annotated[dict, "可选配置参数,例如输入约束、输出格式等"] = None 2475 | ) -> str: 2476 | """ 2477 | 生成angr符号执行或爆破脚本。 2478 | 参数: 2479 | function_address: 目标函数地址 2480 | script_type: 脚本类型 2481 | options: 可选配置 2482 | 返回: 2483 | 生成的Python脚本代码 2484 | """ 2485 | if options is None: 2486 | options = {} 2487 | 2488 | # 参数验证 2489 | if not _is_valid_address(function_address): 2490 | raise IDAError(f"无效的函数地址: {{function_address}}") 2491 | 2492 | supported_types = ['symbolic_execution', 'brute_force', 'control_flow'] 2493 | if script_type not in supported_types: 2494 | raise IDAError(f"不支持的脚本类型: {{script_type}}。支持的类型: {{', '.join(supported_types)}}") 2495 | 2496 | try: 2497 | # 获取函数信息 2498 | func = idaapi.get_func(parse_address(function_address)) 2499 | if not func: 2500 | raise IDAError(f"未找到函数: {{function_address}}") 2501 | 2502 | func_name = idc.get_func_name(func.start_ea) 2503 | if not func_name: 2504 | func_name = f"func_{hex(func.start_ea)}" 2505 | 2506 | # 获取二进制路径 2507 | binary_path = idaapi.get_input_file_path() 2508 | if not binary_path: 2509 | raise IDAError("无法获取二进制文件路径") 2510 | 2511 | # 函数地址变量定义 2512 | func_address_var = f"{func_name}_addr = {{hex(func.start_ea)}}" 2513 | 2514 | # 生成脚本内容 2515 | script_body, script_execution = _generate_angr_script_content( 2516 | script_type, func_name, func.size(), options 2517 | ) 2518 | 2519 | # 生成最终脚本 2520 | script = _generate_angr_script_template(binary_path, func_address_var) 2521 | script = script.replace("{{script_body}}", script_body) 2522 | script = script.replace("{{script_execution}}", script_execution) 2523 | 2524 | return script 2525 | 2526 | except Exception as e: 2527 | if isinstance(e, IDAError): 2528 | raise 2529 | logger.error(f"生成angr脚本时出错: {{str(e)}}") 2530 | raise IDAError(f"生成脚本失败: {{str(e)}}") 2531 | 2532 | @jsonrpc 2533 | @idaread 2534 | def generate_frida_script( 2535 | target: Annotated[str, "目标函数名或地址"], 2536 | script_type: Annotated[str, "脚本类型: 'hook', 'memory_dump', 'string_hook'"], 2537 | options: Annotated[dict, "可选配置参数"] = None 2538 | ) -> str: 2539 | """ 2540 | 生成frida动态分析脚本。 2541 | 参数: 2542 | target: 目标函数名或地址 2543 | script_type: 脚本类型 2544 | options: 可选配置 2545 | 返回: 2546 | 生成的JavaScript脚本代码 2547 | """ 2548 | # 参数验证 2549 | if options is None: 2550 | options = {} 2551 | 2552 | # 验证脚本类型 2553 | valid_script_types = ['hook', 'memory_dump', 'string_hook'] 2554 | if script_type not in valid_script_types: 2555 | raise IDAError(f"不支持的脚本类型: {script_type}。支持的类型: {', '.join(valid_script_types)}") 2556 | 2557 | try: 2558 | # 优先使用script_utils模块生成脚本 2559 | if script_utils is not None: 2560 | print(f"[MCP] 使用script_utils模块生成{script_type}类型的Frida脚本") 2561 | 2562 | # 根据脚本类型调用相应的生成函数 2563 | if script_type == 'hook': 2564 | script_content = script_utils._generate_hook_script(target, options) 2565 | elif script_type == 'memory_dump': 2566 | script_content = script_utils._generate_memory_dump_script(target, options) 2567 | elif script_type == 'string_hook': 2568 | script_content = script_utils._generate_string_hook_script(target, options) 2569 | 2570 | # 获取使用说明 2571 | usage_notes = script_utils._get_usage_notes() 2572 | 2573 | return usage_notes + "\n" + script_content 2574 | except Exception as e: 2575 | # 如果script_utils不可用,使用内置实现 2576 | print("[MCP] script_utils模块不可用,使用内置实现生成Frida脚本") 2577 | 2578 | # 检查target是否为地址 2579 | is_address = False 2580 | try: 2581 | if target.startswith('0x'): 2582 | int(target, 16) 2583 | is_address = True 2584 | except ValueError: 2585 | pass 2586 | 2587 | # 生成脚本模板 2588 | if is_address: 2589 | target_expr = f"ptr('{target}')" 2590 | else: 2591 | target_expr = f"'{target}'" 2592 | 2593 | # 应用类型,默认native(原生二进制) 2594 | app_type = options.get('app_type', 'native') 2595 | 2596 | # 根据应用类型生成不同的前缀和后缀 2597 | if app_type == 'java': 2598 | script_prefix = "Java.perform(function() {\n" 2599 | script_suffix = "\n});" 2600 | else: 2601 | script_prefix = "" # 原生二进制不需要Java环境 2602 | script_suffix = "" 2603 | 2604 | if script_type == 'hook': 2605 | script_content = f""" 2606 | console.log("开始Hook目标函数: {target}"); 2607 | 2608 | // 尝试不同的模块和导出方式 2609 | const moduleName = "{options.get('module', 'target')}"; 2610 | const module = Process.getModuleByName(moduleName); 2611 | 2612 | if (!module) {{ 2613 | console.log(`未找到模块: ${moduleName}`); 2614 | return; 2615 | }} 2616 | 2617 | let targetFunc = null; 2618 | try {{ 2619 | if ({is_address}) {{ 2620 | // 如果是地址,直接使用ptr 2621 | targetFunc = {target_expr}; 2622 | }} else {{ 2623 | // 如果是函数名,尝试在模块中查找导出函数 2624 | targetFunc = Module.findExportByName(moduleName, {target_expr}); 2625 | if (!targetFunc) {{ 2626 | console.log(`在模块{moduleName}中未找到导出函数: {target}`); 2627 | // 尝试使用模糊搜索查找函数 2628 | console.log("尝试模糊搜索函数..."); 2629 | const matches = Memory.scanSync(module.base, module.size, `[${' '.repeat(20)}]${target}${' '.repeat(20)}`); 2630 | if (matches.length > 0) {{ 2631 | console.log(`找到 ${matches.length} 个可能的匹配`); 2632 | targetFunc = matches[0].address; 2633 | console.log(`使用第一个匹配: ${targetFunc}`); 2634 | }} 2635 | }} 2636 | }} 2637 | 2638 | if (!targetFunc) {{ 2639 | console.log(`未找到目标函数: {target}`); 2640 | return; 2641 | }} 2642 | 2643 | Interceptor.attach(targetFunc, {{ 2644 | onEnter: function(args) {{ 2645 | console.log(`\n[+] 调用 {target}:`); 2646 | // 打印参数 - 可以根据函数签名调整 2647 | for (let i = 0; i < {options.get('arg_count', 4)}; i++) {{ 2648 | console.log(` 参数 ${i}:`, args[i]); 2649 | // 尝试将参数解析为字符串 2650 | try {{ 2651 | const str = Memory.readUtf8String(args[i]); 2652 | if (str) console.log(` 字符串值: ${str}`); 2653 | }} catch (e) {{}} 2654 | }} 2655 | // 保存参数供onLeave使用 2656 | this.args = args; 2657 | }}, 2658 | onLeave: function(retval) {{ 2659 | console.log(`[+] {target} 返回:`); 2660 | console.log(` 返回值:`, retval); 2661 | // 尝试解析返回值为字符串 2662 | try {{ 2663 | const str = Memory.readUtf8String(retval); 2664 | if (str) console.log(` 字符串值: ${str}`); 2665 | }} catch (e) {{}} 2666 | console.log("----------------------------------------"); 2667 | }} 2668 | }}); 2669 | }} catch (e) {{ 2670 | console.log(`Hook失败: ${e}`); 2671 | }} 2672 | """ 2673 | script = script_prefix + script_content + script_suffix 2674 | elif script_type == 'memory_dump': 2675 | script_content = f""" 2676 | console.log("开始内存监控..."); 2677 | 2678 | const targetAddr = {target_expr}; 2679 | const memSize = {options.get('size', 1024)}; // 要监控的内存大小 2680 | 2681 | console.log(`监控地址范围: ${targetAddr} - ${ptr(targetAddr).add(memSize)}`); 2682 | 2683 | try {{ 2684 | // 监控内存读写 2685 | Memory.protect(ptr(targetAddr), memSize, 'rwx'); 2686 | 2687 | // Windows下可能需要特殊处理 2688 | if (Process.platform === 'windows') {{ 2689 | console.log("Windows平台: 使用内存断点方式监控"); 2690 | // 为内存区域设置读写断点 2691 | Memory.watchpoint(ptr(targetAddr), memSize, {{ 2692 | read: true, 2693 | write: true, 2694 | onAccess: function(details) {{ 2695 | console.log(`\n[+] 内存访问:`); 2696 | console.log(` 地址: ${details.address}`); 2697 | console.log(` 类型: ${details.type}`); // 'read' 或 'write' 2698 | console.log(` 大小: ${details.size} 字节`); 2699 | 2700 | // 打印调用栈 2701 | const stack = Thread.backtrace(this.context, Backtracer.ACCURATE) 2702 | .map(DebugSymbol.fromAddress).join('\n'); 2703 | console.log(` 调用栈:\n${stack}`); 2704 | 2705 | // 对于写操作,尝试打印写入的值 2706 | if (details.type === 'write' && details.data) {{ 2707 | console.log(` 写入值:`, details.data); 2708 | }} 2709 | console.log("----------------------------------------"); 2710 | }} 2711 | }}); 2712 | }} else {{ 2713 | // 其他平台使用accessMonitor 2714 | Memory.accessMonitor.enable(); 2715 | 2716 | // 监听内存访问事件 2717 | Memory.accessMonitor.on('access', function(event) {{ 2718 | if (event.address.compare(ptr(targetAddr)) >= 0 && 2719 | event.address.compare(ptr(targetAddr).add(memSize)) < 0) {{ 2720 | console.log(`\n[+] 内存访问:`); 2721 | console.log(` 地址: ${event.address}`); 2722 | console.log(` 类型: ${event.type}`); // 'read' 或 'write' 2723 | console.log(` 大小: ${event.size} 字节`); 2724 | 2725 | // 打印调用栈 2726 | const stack = Thread.backtrace(event.thread, Backtracer.ACCURATE) 2727 | .map(DebugSymbol.fromAddress).join('\n'); 2728 | console.log(` 调用栈:\n${stack}`); 2729 | 2730 | // 对于写操作,尝试打印写入的值 2731 | if (event.type === 'write') {{ 2732 | try {{ 2733 | console.log(` 写入值:`, Memory.readByteArray(event.address, event.size)); 2734 | }} catch (e) {{}} 2735 | }} 2736 | console.log("----------------------------------------"); 2737 | }} 2738 | }}); 2739 | }} 2740 | 2741 | console.log("内存监控已启动。按Ctrl+C停止。"); 2742 | }} catch (e) {{ 2743 | console.log(`内存监控设置失败: ${e}`); 2744 | }} 2745 | """ 2746 | script = script_prefix + script_content + script_suffix 2747 | elif script_type == 'string_hook': 2748 | script_content = f""" 2749 | console.log("开始字符串监控..."); 2750 | 2751 | // 存储已收集的字符串 2752 | const collectedStrings = new Set(); 2753 | 2754 | // Hook常见的字符串处理函数 2755 | const stringFuncs = {{ 2756 | 'strcmp': Module.findExportByName(null, 'strcmp'), 2757 | 'strcpy': Module.findExportByName(null, 'strcpy'), 2758 | 'strlen': Module.findExportByName(null, 'strlen'), 2759 | 'memcpy': Module.findExportByName(null, 'memcpy'), 2760 | 'strcat': Module.findExportByName(null, 'strcat') 2761 | }}; 2762 | 2763 | // Hook目标函数附近的字符串操作 2764 | if ({is_address}) {{ 2765 | // 如果指定了地址,也监控该地址附近的内存读取 2766 | try {{ 2767 | const targetAddr = {target_expr}; 2768 | console.log(`监控地址附近的字符串: ${targetAddr}`); 2769 | Memory.scan(ptr(targetAddr).sub(0x1000), 0x2000, "[41-7a]{4,}", {{ 2770 | onMatch: function(address, size) {{ 2771 | try {{ 2772 | const str = Memory.readUtf8String(address); 2773 | if (str.length >= 4 && !collectedStrings.has(str)) {{ 2774 | collectedStrings.add(str); 2775 | console.log(`\n[+] 发现字符串:`); 2776 | console.log(` 地址: ${address}`); 2777 | console.log(` 内容: "${str}"`); 2778 | 2779 | // 获取调用栈 2780 | const stack = Thread.backtrace(Thread.currentThread(), Backtracer.ACCURATE) 2781 | .map(DebugSymbol.fromAddress).join('\n'); 2782 | console.log(` 访问栈:\n${stack}`); 2783 | }} 2784 | }} catch (e) {{}} 2785 | }}, 2786 | onComplete: function() {{}} 2787 | }}); 2788 | }} catch (e) {{ 2789 | console.log(`字符串扫描失败: ${e}`); 2790 | }} 2791 | }} 2792 | 2793 | // Hook字符串函数 2794 | for (const [name, func] of Object.entries(stringFuncs)) {{ 2795 | if (func) {{ 2796 | Interceptor.attach(func, {{ 2797 | onEnter: function(args) {{ 2798 | try {{ 2799 | // 尝试读取第一个参数作为字符串 2800 | const str = Memory.readUtf8String(args[0]); 2801 | if (str && str.length >= 3 && !collectedStrings.has(str)) {{ 2802 | collectedStrings.add(str); 2803 | console.log(`\n[+] 函数 {name} 使用字符串:`); 2804 | console.log(` 字符串: "${str}"`); 2805 | 2806 | // 获取调用栈 2807 | const stack = Thread.backtrace(this.context, Backtracer.ACCURATE) 2808 | .map(DebugSymbol.fromAddress).join('\n'); 2809 | console.log(` 调用栈:\n${stack}`); 2810 | }} 2811 | }} catch (e) {{}} 2812 | }} 2813 | }}); 2814 | }} 2815 | }} 2816 | 2817 | console.log("字符串监控已启动。按Ctrl+C停止。"); 2818 | console.log("已收集的字符串将自动去重并显示。"); 2819 | """ 2820 | script = script_prefix + script_content + script_suffix 2821 | else: 2822 | raise IDAError(f"不支持的脚本类型: {script_type}") 2823 | 2824 | usage_notes = """// 使用说明: 2825 | // 1. 确保已安装frida-tools: pip install frida-tools 2826 | // 2. 对于Windows原生程序,使用以下命令运行: 2827 | // frida -p <进程ID> -l <脚本文件> --no-pause 2828 | // 或附加到已运行的程序 2829 | // 3. 对于Java程序,使用: 2830 | // frida -U -f <包名> -l <脚本文件> --no-pause 2831 | // 4. 若要保存输出,可重定向到文件: 2832 | // frida -p <进程ID> -l <脚本文件> --no-pause > output.log 2833 | """ 2834 | 2835 | return usage_notes + "\n" + script 2836 | 2837 | @jsonrpc 2838 | @idawrite 2839 | def save_generated_script( 2840 | script_content: Annotated[str, "脚本内容"], 2841 | script_type: Annotated[str, "脚本类型: 'angr' 或 'frida'"], 2842 | file_name: Annotated[str, "保存的文件名(可选)"] = None 2843 | ) -> str: 2844 | """ 2845 | 保存生成的脚本到文件。 2846 | 参数: 2847 | script_content: 脚本内容 2848 | script_type: 脚本类型 2849 | file_name: 保存的文件名(可选) 2850 | 返回: 2851 | 保存的文件路径 2852 | """ 2853 | import os 2854 | 2855 | # 确定文件扩展名 2856 | if script_type == 'angr': 2857 | extension = '.py' 2858 | elif script_type == 'frida': 2859 | extension = '.js' 2860 | else: 2861 | raise IDAError(f"不支持的脚本类型: {script_type}") 2862 | 2863 | # 确定保存目录(使用临时目录或IDA当前目录) 2864 | save_dir = os.path.dirname(idaapi.get_input_file_path()) if idaapi.get_input_file_path() else os.getcwd() 2865 | 2866 | # 确定文件名 2867 | if not file_name: 2868 | import time 2869 | timestamp = time.strftime("%Y%m%d_%H%M%S") 2870 | file_name = f"{script_type}_script_{timestamp}{extension}" 2871 | elif not file_name.endswith(extension): 2872 | file_name += extension 2873 | 2874 | # 构建完整文件路径 2875 | file_path = os.path.join(save_dir, file_name) 2876 | 2877 | # 保存文件 2878 | try: 2879 | with open(file_path, 'w', encoding='utf-8') as f: 2880 | f.write(script_content) 2881 | return f"脚本已保存至: {file_path}\n\n使用以下命令运行:\n{get_run_command_hint(script_type, file_path)}" 2882 | except Exception as e: 2883 | raise IDAError(f"保存脚本失败: {str(e)}") 2884 | 2885 | @jsonrpc 2886 | @idawrite 2887 | def run_external_script( 2888 | script_path: Annotated[str, "脚本文件路径"], 2889 | script_type: Annotated[str, "脚本类型: 'angr' 或 'frida'"], 2890 | target_binary: Annotated[str, "目标二进制文件路径"] = None, 2891 | target_pid: Annotated[int, "目标进程ID(可选,用于frida)"] = None 2892 | ) -> str: 2893 | """ 2894 | 运行外部脚本(angr或frida)。 2895 | 参数: 2896 | script_path: 脚本文件路径 2897 | script_type: 脚本类型 2898 | target_binary: 目标二进制文件路径(可选) 2899 | target_pid: 目标进程ID(可选,用于frida附加到运行中的进程) 2900 | 返回: 2901 | 脚本执行的输出结果 2902 | """ 2903 | import subprocess 2904 | import tempfile 2905 | import os 2906 | 2907 | # 检查脚本文件是否存在 2908 | if not os.path.exists(script_path): 2909 | raise IDAError(f"脚本文件不存在: {script_path}") 2910 | 2911 | # 如果未提供目标二进制,使用当前加载的二进制 2912 | if target_binary is None: 2913 | target_binary = idaapi.get_input_file_path() 2914 | if not target_binary: 2915 | raise IDAError("未加载二进制文件,无法确定目标") 2916 | 2917 | # 构建命令 2918 | if script_type == 'angr': 2919 | # 确保angr模块可用 2920 | try: 2921 | import angr 2922 | except ImportError: 2923 | raise IDAError("未安装angr模块,请先安装: pip install angr") 2924 | 2925 | # 使用当前Python环境运行脚本 2926 | cmd = [sys.executable, script_path] 2927 | elif script_type == 'frida': 2928 | # 确保frida模块可用 2929 | try: 2930 | # 检查frida命令是否在PATH中 2931 | subprocess.run(['frida', '--version'], check=True, capture_output=True, text=True) 2932 | except (subprocess.SubprocessError, FileNotFoundError): 2933 | raise IDAError("未安装frida命令行工具,请先安装: pip install frida-tools") 2934 | 2935 | # 构建frida命令 2936 | if target_pid is not None: 2937 | # 附加到已运行的进程 2938 | cmd = ['frida', '-p', str(target_pid), '-l', script_path, '--no-pause'] 2939 | else: 2940 | # 在Windows上,使用spawn模式可能需要管理员权限 2941 | cmd = ['frida', '-f', target_binary, '-l', script_path, '--no-pause'] 2942 | # 添加Windows特有的提示 2943 | if sys.platform == 'win32': 2944 | print("注意:在Windows上以spawn模式运行frida可能需要管理员权限") 2945 | print("建议先手动启动目标程序,然后使用-p参数附加到进程") 2946 | else: 2947 | raise IDAError(f"不支持的脚本类型: {script_type}") 2948 | 2949 | try: 2950 | # 创建临时文件保存输出 2951 | with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.log', encoding='utf-8') as temp_log: 2952 | log_path = temp_log.name 2953 | 2954 | # 运行脚本并捕获输出 2955 | print(f"运行{script_type}脚本: {script_path} 目标: {target_binary} {f'PID: {target_pid}' if target_pid else ''}") 2956 | 2957 | # 对于frida,考虑到它可能需要用户交互,我们不使用capture_output,而是直接输出 2958 | if script_type == 'frida': 2959 | output = f"正在运行frida脚本,请在终端中查看输出...\n" 2960 | output += f"命令: {' '.join(cmd)}\n\n" 2961 | output += "注意:\n" 2962 | output += "1. frida脚本在Windows上可能需要管理员权限\n" 2963 | output += "2. 建议使用以下方式手动运行,以获得更好的交互体验:\n" 2964 | output += f" {' '.join(cmd)}\n\n" 2965 | output += "3. 脚本执行结果将不会在此处显示,请在独立终端中运行以查看完整输出" 2966 | 2967 | # 在后台启动frida进程,但不等待其完成 2968 | try: 2969 | subprocess.Popen( 2970 | cmd, 2971 | creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0 2972 | ) 2973 | except Exception as e: 2974 | output += f"\n启动进程失败: {str(e)}" 2975 | 2976 | return output 2977 | else: 2978 | # 对于angr,我们可以捕获输出 2979 | result = subprocess.run( 2980 | cmd, 2981 | capture_output=True, 2982 | text=True, 2983 | timeout=300 # 增加超时时间到5分钟,因为angr分析可能需要较长时间 2984 | ) 2985 | 2986 | # 保存输出到日志文件 2987 | with open(log_path, 'w', encoding='utf-8') as f: 2988 | f.write(f"命令: {' '.join(cmd)}\n\n") 2989 | f.write(f"返回码: {result.returncode}\n\n") 2990 | f.write("标准输出:\n") 2991 | f.write(result.stdout) 2992 | f.write("\n\n标准错误:\n") 2993 | f.write(result.stderr) 2994 | 2995 | # 返回执行结果摘要 2996 | output = f"脚本执行 {'成功' if result.returncode == 0 else '失败'}。\n" 2997 | output += f"返回码: {result.returncode}\n" 2998 | output += f"输出日志已保存至: {log_path}\n\n" 2999 | 3000 | # 显示部分输出 3001 | if result.stdout: 3002 | output += "标准输出 (前5行):\n" 3003 | output += '\n'.join(result.stdout.split('\n')[:5]) + '\n...\n\n' 3004 | 3005 | if result.stderr: 3006 | output += "标准错误:\n" 3007 | output += result.stderr 3008 | 3009 | return output 3010 | except subprocess.TimeoutExpired: 3011 | return f"脚本执行超时(>5分钟)。\n请手动运行脚本以查看完整输出。" 3012 | except Exception as e: 3013 | raise IDAError(f"运行脚本时发生错误: {str(e)}") 3014 | 3015 | @idaread 3016 | def get_run_command_hint(script_type: str, script_path: str) -> str: 3017 | """ 3018 | 获取运行脚本的命令提示。 3019 | """ 3020 | if script_type == 'angr': 3021 | return f"python {script_path}" 3022 | elif script_type == 'frida': 3023 | # 获取当前加载的二进制文件路径 3024 | binary_path = idaapi.get_input_file_path() 3025 | if binary_path: 3026 | return f"frida -f {binary_path} -l {script_path} --no-pause # 启动新进程\n或\nfrida -p <进程ID> -l {script_path} --no-pause # 附加到已运行进程" 3027 | else: 3028 | return f"frida -f <二进制文件路径> -l {script_path} --no-pause # 启动新进程\n或\nfrida -p <进程ID> -l {script_path} --no-pause # 附加到已运行进程" 3029 | 3030 | class MCP(idaapi.plugin_t): 3031 | """MCP插件主类,增强版实现""" 3032 | flags = idaapi.PLUGIN_KEEP 3033 | comment = "MCP Plugin" 3034 | help = "MCP" 3035 | wanted_name = "MCP" 3036 | wanted_hotkey = "Ctrl-Alt-M" 3037 | 3038 | def init(self): 3039 | """初始化插件,创建服务器实例""" 3040 | try: 3041 | # 创建服务器实例,增加错误处理 3042 | try: 3043 | self.server = Server() 3044 | self.is_initialized = True 3045 | except Exception as e: 3046 | print(f"[MCP] 警告: 创建服务器实例时出错: {str(e)}") 3047 | print("[MCP] 尝试使用备用初始化...") 3048 | # 创建一个最小的服务器对象作为后备 3049 | class FallbackServer: 3050 | def __init__(self): 3051 | self.running = False 3052 | def start(self): 3053 | print("[MCP] 警告: 使用备用服务器实现") 3054 | self.running = True 3055 | def stop(self): 3056 | self.running = False 3057 | self.server = FallbackServer() 3058 | self.is_initialized = True 3059 | 3060 | # 格式化热键显示 3061 | hotkey = MCP.wanted_hotkey.replace("-", "+") 3062 | if sys.platform == "darwin": 3063 | hotkey = hotkey.replace("Alt", "Option") 3064 | elif sys.platform == "win32": 3065 | hotkey = hotkey.replace("Ctrl", "Ctrl") 3066 | 3067 | # 显示插件加载信息 3068 | print(f"[MCP] Plugin loaded, use Edit -> Plugins -> MCP ({hotkey}) to start the server") 3069 | print("[MCP] 版本增强: 添加了自动错误恢复机制和详细日志记录") 3070 | 3071 | return idaapi.PLUGIN_KEEP 3072 | 3073 | except Exception as e: 3074 | # 捕获所有初始化异常 3075 | import traceback 3076 | print(f"[MCP] 错误: 插件初始化失败: {str(e)}") 3077 | print("[MCP] 详细错误:") 3078 | traceback.print_exc() 3079 | # 即使初始化失败,也尽量保持插件可用 3080 | return idaapi.PLUGIN_KEEP 3081 | 3082 | def run(self, args): 3083 | """运行插件,启动服务器,增加错误处理和状态检查""" 3084 | try: 3085 | if not hasattr(self, 'server') or self.server is None: 3086 | print("[MCP] 错误: 服务器实例不存在") 3087 | return 3088 | 3089 | print("[MCP] 正在启动服务器...") 3090 | 3091 | # 尝试启动服务器 3092 | try: 3093 | self.server.start() 3094 | 3095 | # 短暂延迟以确保服务器有时间初始化 3096 | import time 3097 | time.sleep(0.3) 3098 | 3099 | # 检查服务器状态 3100 | if hasattr(self.server, 'running') and self.server.running: 3101 | status = "已启动成功" 3102 | # 显示端口信息 3103 | if hasattr(self.server, 'port'): 3104 | status += f",监听端口: {self.server.port}" 3105 | else: 3106 | status = "启动状态未知,请检查日志" 3107 | 3108 | print(f"[MCP] 服务器{status}") 3109 | 3110 | except Exception as e: 3111 | print(f"[MCP] 错误: 启动服务器时出错: {str(e)}") 3112 | print("[MCP] 建议检查端口占用情况或重启IDA Pro") 3113 | 3114 | except Exception as e: 3115 | print(f"[MCP] 错误: 执行run方法时发生未知错误: {str(e)}") 3116 | 3117 | def term(self): 3118 | """终止插件,停止服务器,增加优雅关闭和错误恢复""" 3119 | try: 3120 | if hasattr(self, 'server') and self.server is not None: 3121 | print("[MCP] 正在停止服务器...") 3122 | try: 3123 | self.server.stop() 3124 | print("[MCP] 服务器已停止") 3125 | except Exception as e: 3126 | print(f"[MCP] 警告: 停止服务器时出错: {str(e)}") 3127 | except Exception as e: 3128 | # 即使停止出错也不要影响IDA的关闭 3129 | print(f"[MCP] 警告: 终止插件时出错: {str(e)}") 3130 | 3131 | 3132 | def PLUGIN_ENTRY(): 3133 | """插件入口点,增加错误处理""" 3134 | try: 3135 | plugin = MCP() 3136 | return plugin 3137 | except Exception as e: 3138 | print(f"[MCP] 错误: 创建插件实例失败: {str(e)}") 3139 | # 返回一个最小的插件实例作为后备 3140 | class MinimalPlugin(idaapi.plugin_t): 3141 | flags = idaapi.PLUGIN_KEEP 3142 | comment = "MCP (Minimal)" 3143 | help = "MCP Minimal Version" 3144 | wanted_name = "MCP" 3145 | wanted_hotkey = "" 3146 | 3147 | def init(self): 3148 | print("[MCP] 已加载最小化版本,请检查完整版本的安装") 3149 | return idaapi.PLUGIN_KEEP 3150 | 3151 | def run(self, args): 3152 | print("[MCP] 最小化版本不提供完整功能,请重新安装插件") 3153 | 3154 | def term(self): 3155 | pass 3156 | 3157 | return MinimalPlugin() 3158 | 3159 | @jsonrpc 3160 | @idaread 3161 | def get_function_call_graph( 3162 | start_address: Annotated[str, "起始函数地址"], 3163 | depth: Annotated[int, "递归深度"] = 3, 3164 | mermaid: Annotated[bool, "是否返回 mermaid 格式"] = False 3165 | ) -> dict: 3166 | """ 3167 | 获取函数调用图。 3168 | 参数: 3169 | start_address: 起始函数地址(字符串) 3170 | depth: 递归深度,默认3 3171 | mermaid: 是否返回 mermaid 格式(默认False,返回邻接表) 3172 | 返回: 3173 | {"graph": 邻接表或mermaid字符串, "nodes": 节点列表, "edges": 边列表} 3174 | """ 3175 | visited = set() 3176 | edges = set() 3177 | nodes = set() 3178 | def dfs(addr, d): 3179 | if d < 0 or addr in visited: 3180 | return 3181 | visited.add(addr) 3182 | func = idaapi.get_func(parse_address(addr)) 3183 | if not func: 3184 | return 3185 | nodes.add(addr) 3186 | for ref in idautils.CodeRefsFrom(func.start_ea, 1): 3187 | callee_func = idaapi.get_func(ref) 3188 | if callee_func: 3189 | callee_addr = hex(callee_func.start_ea) 3190 | edges.add((addr, callee_addr)) 3191 | dfs(callee_addr, d-1) 3192 | start_addr = hex(parse_address(start_address)) 3193 | dfs(start_addr, depth) 3194 | nodes = list(nodes) 3195 | edges = list(edges) 3196 | if mermaid: 3197 | mermaid_lines = ["graph TD"] 3198 | for src, dst in edges: 3199 | mermaid_lines.append(f' "{src}" --> "{dst}"') 3200 | return {"graph": "\n".join(mermaid_lines), "nodes": nodes, "edges": edges} 3201 | else: 3202 | adj = {n: [] for n in nodes} 3203 | for src, dst in edges: 3204 | adj[src].append(dst) 3205 | return {"graph": adj, "nodes": nodes, "edges": edges} 3206 | 3207 | 3208 | 3209 | @jsonrpc 3210 | @idaread 3211 | def get_analysis_report() -> dict: 3212 | """ 3213 | 自动生成结构化分析报告。 3214 | """ 3215 | report = { 3216 | "functions": [], 3217 | "globals": [], 3218 | "strings": [], 3219 | "entry_points": [], 3220 | } 3221 | for f in idautils.Functions(): 3222 | func = get_function(f, raise_error=False) 3223 | if func: 3224 | report["functions"].append(func) 3225 | for g in idautils.Names(): 3226 | if not idaapi.get_func(g[0]): 3227 | report["globals"].append({"address": hex(g[0]), "name": g[1]}) 3228 | for s in idautils.Strings(): 3229 | report["strings"].append({"address": hex(s.ea), "string": str(s)}) 3230 | report["entry_points"] = get_entry_points() 3231 | return report 3232 | 3233 | @jsonrpc 3234 | @idaread 3235 | def get_incremental_changes() -> list: 3236 | """ 3237 | 返回自上次分析以来的增量变更。 3238 | """ 3239 | global _incremental_changes 3240 | changes = _incremental_changes.copy() 3241 | _incremental_changes.clear() 3242 | return changes 3243 | 3244 | @jsonrpc 3245 | @idaread 3246 | def get_dynamic_string_map() -> dict: 3247 | """ 3248 | 动态字符串解密映射(静态+动态分析结果)。 3249 | """ 3250 | string_map = {} 3251 | for s in idautils.Strings(): 3252 | string_map[hex(s.ea)] = str(s) 3253 | # 合并动态字符串 3254 | string_map.update(_dynamic_strings) 3255 | return string_map 3256 | 3257 | @jsonrpc 3258 | @idaread 3259 | def generate_analysis_report_md() -> str: 3260 | """ 3261 | 一键生成结构化 markdown 报告,帮助用户快速理解程序核心逻辑。 3262 | """ 3263 | import hashlib 3264 | # 基本信息 3265 | meta = get_metadata() 3266 | md = [f"# 程序自动分析报告\n"] 3267 | md.append(f"## 基本信息\n- 文件名: {meta['module']}\n- MD5: {meta['md5']}\n- 入口点: {meta['base']}\n") 3268 | 3269 | # 入口点分析 3270 | entry_points = get_entry_points() 3271 | md.append(f"## 入口点分析\n- 入口点数量: {len(entry_points)}\n" + "\n".join([f"- {f['name']} @ {f['address']}" for f in entry_points])) 3272 | 3273 | # 导入表分析 3274 | suspicious_apis = ["virtualalloc", "getprocaddress", "loadlibrary", "system", "exec", "winexec", "createthread", "writeprocessmemory", "readprocessmemory", "openprocess", "socket", "connect", "recv", "send"] 3275 | imports = [] 3276 | suspicious_imports = [] 3277 | for i in range(0, 1000, 100): 3278 | page = list_imports(i, 100) 3279 | for imp in page["data"]: 3280 | imports.append(f"- {imp['imported_name']} ({imp['module']}) @ {imp['address']}") 3281 | if any(api in imp['imported_name'].lower() for api in suspicious_apis): 3282 | suspicious_imports.append(f"- {imp['imported_name']} ({imp['module']}) @ {imp['address']}") 3283 | if not page["next_offset"]: 3284 | break 3285 | md.append(f"\n## 导入表分析\n- 导入API总数: {len(imports)}\n- 可疑API: {len(suspicious_imports)}\n" + ("\n".join(suspicious_imports) if suspicious_imports else "无")) 3286 | 3287 | # 关键/可疑函数 3288 | keywords = ["flag", "ctf", "check", "verify", "rc4", "base64", "tea", "debug", "tls", "anti", "success", "congrat"] 3289 | suspicious_funcs = [] 3290 | algo_funcs = [] 3291 | anti_debug_funcs = [] 3292 | obfuscated_funcs = [] 3293 | func_lens = [] 3294 | branch_counts = [] 3295 | for f in idautils.Functions(): 3296 | func = get_function(f, raise_error=False) 3297 | if not func: 3298 | continue 3299 | name = func["name"].lower() 3300 | code = decompile_function(func["address"]) 3301 | func_len = int(func["size"], 16) 3302 | func_lens.append(func_len) 3303 | # 统计分支数 3304 | try: 3305 | flowchart = list(ida_gdl.FlowChart(idaapi.get_func(f))) 3306 | branch_count = sum(len(list(block.succs())) for block in flowchart) 3307 | branch_counts.append(branch_count) 3308 | except: 3309 | branch_counts.append(0) 3310 | for kw in keywords: 3311 | if kw in name or kw in code.lower(): 3312 | suspicious_funcs.append(f"- {func['name']} ({func['address']})") 3313 | break 3314 | # 算法检测(升级:展示所有检测到的算法和置信度) 3315 | algo_info = get_algorithm_signature(func["address"]) 3316 | if algo_info["algorithm"] != "unknown": 3317 | algo_funcs.append(f"- {func['name']} ({func['address']}) : {algo_info['algorithm']} (置信度: {algo_info['confidence']:.2f})") 3318 | # 反调试检测 3319 | anti_keywords = ["isdebuggerpresent", "checkremotedebuggerpresent", "tls", "int 0x2d", "peb", "beingdebugged"] 3320 | if any(ak in code.lower() for ak in anti_keywords): 3321 | anti_debug_funcs.append(f"- {func['name']} ({func['address']})") 3322 | # 混淆检测 3323 | obf = detect_obfuscation(func["address"]) 3324 | if obf.get("flattening") or obf.get("string_encryption"): 3325 | obfuscated_funcs.append(f"- {func['name']} ({func['address']}) : {obf}") 3326 | md.append("\n## 关键/可疑函数\n" + ("\n".join(suspicious_funcs) if suspicious_funcs else "无")) 3327 | md.append("\n## 检测到的加密/编码/哈希算法\n" + ("\n".join(algo_funcs) if algo_funcs else "无")) 3328 | md.append("\n## 反调试相关函数\n" + ("\n".join(anti_debug_funcs) if anti_debug_funcs else "无")) 3329 | md.append("\n## 检测到的混淆/加密函数\n" + ("\n".join(obfuscated_funcs) if obfuscated_funcs else "无")) 3330 | 3331 | # 关键字符串 3332 | suspicious_strs = [] 3333 | for s in idautils.Strings(): 3334 | sval = str(s).lower() 3335 | if any(kw in sval for kw in keywords): 3336 | suspicious_strs.append(f"- {sval} @ {hex(s.ea)}") 3337 | md.append("\n## 关键字符串\n" + ("\n".join(suspicious_strs) if suspicious_strs else "无")) 3338 | 3339 | # flag 逻辑与长度 3340 | flag_info = [] 3341 | for f in idautils.Functions(): 3342 | func = get_function(f, raise_error=False) 3343 | if not func: 3344 | continue 3345 | code = decompile_function(func["address"]) 3346 | if any(kw in code.lower() for kw in ["flag", "ctf", "check", "verify"]): 3347 | constraints = get_function_constraints(func["address"]) 3348 | if constraints: 3349 | flag_info.append(f"- {func['name']} ({func['address']}): {constraints}") 3350 | md.append("\n## flag 逻辑与长度\n" + ("\n".join(flag_info) if flag_info else "无")) 3351 | 3352 | # 代码段/数据段分布 3353 | segs = [] 3354 | for seg in idaapi.get_segm_qty() and [idaapi.getnseg(i) for i in range(idaapi.get_segm_qty())]: 3355 | segs.append(f"- {idaapi.get_segm_name(seg)}: {hex(seg.start_ea)} ~ {hex(seg.end_ea)} (大小: {hex(seg.end_ea - seg.start_ea)}) 类型: {seg.type}") 3356 | md.append("\n## 代码段/数据段分布\n" + ("\n".join(segs) if segs else "无")) 3357 | 3358 | # 代码复杂度 3359 | if func_lens: 3360 | md.append(f"\n## 代码复杂度\n- 函数总数: {len(func_lens)}\n- 平均函数长度: {sum(func_lens)//len(func_lens)} 字节\n- 最大函数长度: {max(func_lens)} 字节\n- 最小函数长度: {min(func_lens)} 字节\n- 平均分支数: {sum(branch_counts)//len(branch_counts) if branch_counts else 0}\n") 3361 | else: 3362 | md.append("\n## 代码复杂度\n无") 3363 | 3364 | # 交叉引用热点 3365 | xref_func_count = {} 3366 | for f in idautils.Functions(): 3367 | xrefs = get_xrefs_to(hex(f)) 3368 | xref_func_count[f] = len(xrefs) 3369 | top_funcs = sorted(xref_func_count.items(), key=lambda x: x[1], reverse=True)[:5] 3370 | md.append("\n## 交叉引用热点(函数)\n" + "\n".join([f"- {get_function(f, raise_error=False)['name']} ({hex(f)}): {cnt} 处引用" for f, cnt in top_funcs])) 3371 | 3372 | # 结构体/类型定义 3373 | structs = get_defined_structures() 3374 | md.append(f"\n## 结构体/类型定义\n- 总数: {len(structs)}\n" + ("\n".join([f"- {s['name']} (大小: {s['size']})" for s in structs[:5]]) if structs else "无")) 3375 | 3376 | # 主执行流程图(入口点递归3层) 3377 | if entry_points: 3378 | entry_addr = entry_points[0]["address"] 3379 | call_graph = get_function_call_graph(entry_addr, 3, True) 3380 | md.append("\n## 主执行流程图\n```mermaid\n" + call_graph["graph"] + "\n```") 3381 | else: 3382 | md.append("\n## 主执行流程图\n无入口点") 3383 | 3384 | # 其它自动分析结论 3385 | md.append("\n## 其它自动分析结论\n") 3386 | # 反调试点补充 3387 | anti_debug_points = [] 3388 | for f in idautils.Functions(): 3389 | func = get_function(f, raise_error=False) 3390 | if not func: 3391 | continue 3392 | patch_points = get_patch_points(func["address"]) 3393 | for pt in patch_points: 3394 | if pt["mnem"] in ("anti-debug", "int", "tls"): 3395 | anti_debug_points.append(f"- {func['name']} {pt['address']} : {pt['mnem']}") 3396 | if anti_debug_points: 3397 | md.append("### 反调试点\n" + "\n".join(anti_debug_points)) 3398 | else: 3399 | md.append("### 反调试点\n无") 3400 | # 未命名函数比例 3401 | unnamed = [f for f in idautils.Functions() if get_function(f, raise_error=False) and get_function(f, raise_error=False)["name"].startswith("sub_")] 3402 | md.append(f"\n- 未命名函数数量: {len(unnamed)} / {len(func_lens)}\n") 3403 | return "\n".join(md) 3404 | --------------------------------------------------------------------------------