资讯详情

资讯详情

用大模型给Ghidra伪代码加语义翻译:AI辅助二进制逆向分析实践

先抛一个问题你拿到一个陌生的二进制文件丢进 Ghidra反编译窗口里吐出一屏伪代码函数名全是FUN_00102a48变量全是local_18、param_2注释为零。你盯着它看了三分钟脑子里其实什么都没装进去。这不是能力问题是所有基于静态分析的工具链都卡在同一个地方——它给你的是“语法”不是“语义”。GhidraGPT 这类方案想解决的就是给这堆干巴巴的伪代码补上一层自然的语义翻译让机器帮你先把函数猜想写好你再去做验证和深挖。这篇文章我会从传统逆向分析的痛点讲起解释为什么大模型天然适合做二进制分析里的“翻译层”然后给出一套可落地的集成方案怎么把 Ghidra 的反编译结果抽出来、怎么构造提示词喂给大模型、怎么把模型输出的函数重命名和注释写回工程。适合正在做漏洞挖掘、恶意样本分析、固件逆向的从业者也适合想用 AI 工具链提升分析效率但还没找到切入点的同学。全程以实操为主不堆理论。1. 传统逆向分析的瓶颈不是工具不行是“翻译层”缺位1.1 伪代码可读性差的根源在哪里很多人第一次用 Ghidra 的 Decompiler会觉得“这已经很接近 C 了为什么分析还是慢”实际上伪代码的“类 C”只是表象。反编译器做的工作是把汇编指令还原成结构化的控制流和变量操作但它不负责告诉你“这段代码在做什么业务”。比如下面这段典型的 Ghidra 输出undefined8 FUN_00102a48(long param_1, int param_2) { int iVar1; size_t sVar2; char local_38[32]; iVar1 strcmp(*(char **)(param_1 0x10), admin); if (iVar1 0) { sVar2 strlen(*(char **)(param_1 0x10)); memcpy(local_38, *(char **)(param_1 0x10), sVar2); return FUN_00101220(local_38, param_2); } return 0; }你能看出它和“登录校验”“字符串拼接”“配置读取”有什么关系吗看不出。param_1 0x10是什么结构体成员FUN_00101220做什么local_38缓冲区有多大风险这些信息分散在十几个函数、几百条交叉引用里需要分析师逐个跳转、记录、脑内建图。传统工作流里这个过程靠的是个人经验积累一个熟练的分析师一天能手动重命名、补注释的函数数量是有限的而且每换一个二进制样本这套手搓语义的工作又得从头来一遍。1.2 分析师的时间都消耗在哪里了我算过一笔账假设一个中型固件里有 300 个需要人工关注的函数。每个函数从跳转交叉引用、猜测参数含义、对照字符串引用、确认调用约定到最后写注释平均耗时 10 到 15 分钟。这意味着光“给函数起一个像样的名字”就要花掉 30 到 75 个小时。而这还没算那些恶意代码里故意混淆过的函数它们会让这个时间翻倍。传统辅助手段也不是没有Ghidra 有插件可以做函数签名匹配用 FLIRT/Signature 库识别库函数也有脚本能根据字符串引用自动加注释还有数据流分析插件帮忙标记参数来源。但这些手段都是“规则驱动”的——它们只能在已知模式内工作一旦遇到自定义加密、混淆分发器、或代码被刻意打乱的样本规则的覆盖能力立刻见底。真正消耗分析师精力的不是识别“这是什么库函数”而是理解“这段库函数调用组合起来构成了什么业务逻辑”。这也是为什么大家开始把目光投向大模型。规则不够就用统计学习到的海量语义来凑。下面进入正题。2. 大模型为什么能切入二进制分析2.1 逆向分析本质上是“翻译”任务如果把逆向分析的链路拆开它其实是这样的二进制字节 → 汇编指令 → 伪代码 → 人类可理解的业务语义。前两步 Ghidra 已经做得很好最后一步是天然的语言模型任务。大模型接受过海量 C/C 代码、开源项目、安全博客、漏洞报告的训练它见过无数种“代码长什么样、注释怎么说”的对应关系。你给它一段伪代码它可以用极高的概率猜出这段代码的意图——因为这种意图在它见过的开源代码、固件驱动、恶意样本分析报告里有大量相似的原型。有人可能会质疑大模型没有真正“执行”代码它怎么保证猜得对这个质疑合理。但这里的关键是我们本来也不是让模型做精确证明而是让它生成一个“优先假设”。分析师不需要一个 100% 正确的答案需要一个可以在几分钟内验证的方向。实际使用中模型对常见逻辑模式的联想能力非常强strcmp 固定字符串 分支跳转 → 大概率是登录校验或者权限判断memcpy 栈缓冲区 长度来自外部参数 → 大概率是潜在溢出点一堆异或运算 循环 → 大概率是自定义解密。这种联想能力恰恰是传统工具缺失的。2.2 大模型补的是“上下文拼图”还有一个容易被忽略的点Ghidra 的反编译是函数粒度的但业务语义往往是跨函数的。一个函数只做了解压的一小步它的输入来自上一个函数输出又喂给下一个函数。传统分析里分析师要在多个函数之间来回跳脑内维护一张“上下文拼图”。大模型不一样你可以把整条调用链上的反编译结果都塞进上下文里让它一次性给出跨函数的语义总结。比如从入口函数开始把FUN_00102a48和它调用的FUN_00101220的伪代码拼在一起模型就能告诉你“这组函数共同完成了一次配置文件的解析其中包含对 key 的权限检查”。这就是 GhidraGPT 这类方案的核心价值不是替代 Ghidra 的反编译引擎而是在 Ghidra 之上加一个语义理解层。传统插件的局限在于“利用已知规则”大模型的价值在于“根据海量先验知识推断未知规则”。两者的组合覆盖的场景会宽很多。不过在动手集成之前先想清楚一件事你选的模型和调用方式决定了整个方案的效果上限。下面看具体怎么搭。3. Ghidra 与大模型集成落地的完整路径3.1 为什么选 Ghidra 而不是闭源工具说句公道话很多商业逆向工具的插件生态也不差但 Ghidra 有几个天然优势让它特别适合做大模型集成的基座。第一它开源Python API 完整可以做 headless 命令行批处理这意味着你可以在服务器上批量跑整个固件目录而不需要打开 GUI 点来点去。第二Ghidra 的反编译结果是结构化的函数对象、参数列表、调用图、交叉引用都可以直接通过 API 拿到方便程序化拼 prompt。第三它的工程文件是导出的分析结果可以被脚本自由读写重命名、加注释这些操作都可以批量写回。如果你是做自动化分析流水线的GUI 工具很难嵌入到 CI/CD 里Ghidra 的 headless 模式几乎是唯一解。后面所有示例都会基于 Ghidra 的 Python API 来做用的版本是 Ghidra 11.x语言是 Python 3。3.2 整体架构与三种接入模式先看清楚整体流程再往下看代码就不会晕。整个系统分四层抽取层用 Ghidra headless 加载二进制枚举函数拿到反编译伪代码和函数属性地址、大小、调用数量、交叉引用。构造层按一定策略筛选函数拼接反编译结果、调用图信息、字符串引用等生成 prompt。推理层调用大模型 API可以是在线服务也可以用本地部署的模型拿到返回的重命名建议和注释。回写层解析模型输出调用 Ghidra API 批量重命名函数、添加注释、设置标记。接入模式有三种看你的使用场景选模式适用场景优点缺点在线 API如 OpenAI / 国内大模型接口样本量不大临时分析效果好无需本地GPU数据出域风险按量收费本地模型如 Qwen、DeepSeek 蒸馏版、Llama 系列敏感样本、批量处理数据不出域可长期运行需要较好显卡效果略逊混合模式涉密样本用本地、普通样本用在线兼顾效果与安全链路复杂需要做路由切换从实际项目的经验看在线 API 做函数重命名这种短文本任务效果最稳尤其是对常见架构x86、ARM的代码理解能力明显强于小尺寸本地模型。本地模型的好处是可以 7x24 小时跑批不需要担心限流和费率。我的建议是先拿在线 API 跑通流程再根据需求决定是否切换本地推理。3.3 核心实现抽取、调用、写回三步走下面是整套方案里最核心的一段 Python 脚本运行在 Ghidra 的 headless 环境里。逻辑不复杂我加了详细注释。# ghidra_gpt_core.py # 运行方式analyzeHeadless project_dir project_name -import target_binary -postScript ghidra_gpt_core.py # 注意脚本运行在 Ghidra 内置的 Jython/Python 环境里API 用 Ghidra 提供的模块 import os import json import time import urllib.request # 需要根据实际模型服务地址修改 API_URL os.environ.get(LLM_API_URL, http://127.0.0.1:8000/v1/chat/completions) MODEL_NAME os.environ.get(LLM_MODEL_NAME, qwen2.5:14b) API_TIMEOUT 60 def get_function_decompilation(func): 使用 Ghidra Decompiler 获取函数伪代码 try: decompiler DecompInterface() decompiler.openProgram(currentProgram) result decompiler.decompileFunction(func, 120, monitor) if result and result.decompileCompleted(): return result.getDecompiledFunction().getC() except Exception as e: print([!] Decompile failed: {} - {}.format(func.getName(), e)) return None def call_llm(func_name, decomp_code, extra_context): 调用大模型接口返回重命名建议和注释 prompt build_prompt(func_name, decomp_code, extra_context) payload { model: MODEL_NAME, messages: [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: prompt} ], temperature: 0.2, max_tokens: 512 } req urllib.request.Request( API_URL, datajson.dumps(payload).encode(utf-8), headers{Content-Type: application/json} ) for attempt in range(3): try: with urllib.request.urlopen(req, timeoutAPI_TIMEOUT) as resp: data json.loads(resp.read().decode(utf-8)) return data[choices][0][message][content] except Exception as e: print([!] LLM call error (attempt {}): {}.format(attempt 1, e)) time.sleep(2) return None def parse_llm_output(output): 解析模型输出提取函数名和注释 # 这里约定模型按 JSON 格式返回例如 # {name: check_admin_login, comment: 校验用户是否为管理员, confidence: high} try: # 清理可能的 markdown 噪音 output output.strip() if output.startswith(): output output.split(\n, 1)[1] if output.endswith(): output output[:-3] return json.loads(output) except json.JSONDecodeError: print([!] Failed to parse LLM output: {}.format(output[:200])) return None def write_back(func, parsed): 把模型结果写回 Ghidra重命名函数、加注释 if not parsed: return new_name parsed.get(name) comment parsed.get(comment) confidence parsed.get(confidence, medium) if new_name: try: # 重命名后的名字加上前缀标明是AI生成的方便追溯 new_name ai_ new_name func.setName(new_name, SourceType.USER_DEFINED) except Exception as e: print([!] Rename failed: {} - {}.format(func.getName(), e)) if comment: # 在函数头部写入多行注释标明置信度 full_comment [AI] {} (confidence: {}).format(comment, confidence) func.setComment(full_comment) print([] {} - {}.format(func.getEntryPoint(), new_name)) def build_prompt(func_name, decomp_code, extra_context): prompt 以下是来自二进制文件的一个函数反编译结果。\n prompt 函数原名: {}\n.format(func_name) prompt 关联调用信息:\n{}\n.format(extra_context[:1500]) prompt 反编译伪代码:\n{}\n.format(decomp_code[:4000]) prompt 请分析这个函数的作用严格按JSON格式返回name、comment、confidence。 return prompt def main(): # 获取所有函数过滤掉太小的和纯库函数 fm currentProgram.getFunctionManager() all_funcs list(fm.getFunctions(True)) results [] for func in all_funcs: # 函数太小没有分析价值 if func.getBody().getNumAddresses() 5: continue # 跳过已知库函数 if func.isExternal() or printf in func.getName() or malloc in func.getName(): continue decomp_code get_function_decompilation(func) if not decomp_code: continue # 收集调用者对当前函数的调用上下文让模型理解它是在什么场景下被调用的 refs getReferencesTo(func.getEntryPoint()) call_context for ref in refs: caller fm.getFunctionContaining(ref.getFromAddress()) if caller: call_context caller: {}\n.format(caller.getName()) if len(call_context) 800: break output call_llm(func.getName(), decomp_code, call_context) parsed parse_llm_output(output) if parsed: write_back(func, parsed) results.append({ addr: str(func.getEntryPoint()), orig: func.getName(), ai: parsed.get(name), comment: parsed.get(comment) }) # 输出汇总报告 report_path os.environ.get(REPORT_PATH, ./ai_analysis_report.json) with open(report_path, w) as f: json.dump(results, f, ensure_asciiFalse, indent2) print([*] Done. Processed {} functions. Report saved to {}.format(len(results), report_path)) main()这段代码里有两个地方值得展开说。第一是getReferencesTo获取的调用上下文。如果只单独看一个函数模型很容易把它的作用理解偏。但如果你告诉它“这个函数是被FUN_00102a48调用的而FUN_00102a48又读取了字符串admin”模型的推断准确率会明显上升。在实际测试中带调用上下文的 prompt 比不带上下文的准确率能高出二到三成。第二是返回结果的解析。模型输出默认是自然语言这对接下来的程序化处理不友好。所以在 prompt 里强制约定 JSON 格式并且加了 markdown 代码块的清理逻辑避免模型输出带格式噪声。这里有个小技巧把temperature调低到 0.2 左右。逆向分析任务需要的是保守、一致性的推断不是创造性发散过高的温度会让模型起名越来越离谱。代码里我用的是urllib.request做最朴素的 HTTP 请求实际工程中你可以换用requests或 OpenAI SDK。如果接入的是本地 Ollama 服务API_URL填http://127.0.0.1:11434/v1/chat/completions就行兼容性比我这里默认的地址更好Ollama 的 OpenAI 兼容接口可以直接用同一套代码。4. 提示词与上下文工程效果好坏的分水岭4.1 函数级提示词的设计要点很多人以为直接把伪代码丢给大模型就完事了跑完发现输出的一堆函数名全是sub_xxx_do_thing这种毫无区分度的东西于是得出结论“AI 逆向不靠谱”。其实问题多半出在提示词上。一个合格的提示词至少要有四个部分角色设定、反编译代码、调用上下文、约束条件。角色设定让模型进入“安全分析师”模式约束条件让输出格式可控。下面是一个我在实际项目中验证过的提示词模板可以直接抄你是一名资深二进制安全分析师擅长从反编译伪代码中推断函数的真实用途。 请根据下面给出的反编译代码和调用上下文完成以下任务 1. 为函数起一个简洁、准确的名字使用 snake_case动词开头不超过20个字符 2. 用一句话说明函数的作用不超过50个字 3. 标注你对这个推断的置信度high / medium / low 4. 如果代码中存在缓冲区溢出、命令注入、路径穿越等明显的安全风险点列出风险类型 函数原名{func_name} 调用上下文当前函数被以下函数调用 {call_context} 反编译伪代码 {decompiled_code} 严格按照以下JSON格式返回不要输出其他内容 {name: , comment: , confidence: , risk: }注意几个细节。第一“动词开头”这个约束非常关键。如果不约束模型容易返回名词短语比如admin_check而不是check_admin_permission后者在函数列表里排序、搜索的体验远好于前者。第二“不超过20个字符”是硬性限制因为 Ghidra 的函数名窗口和交叉引用列表的空间有限超长名字会导致分析界面拥挤。第三“置信度标注”设计得很巧妙它让模型在高不确定时有机会表达“我不确定”而不是硬编一个误导性的名字这比你拿到一堆错误命名然后逐个验证要高效得多。4.2 跨函数上下文注入的正确姿势前面提到过单独分析一个函数的效果有限但把整个程序的代码全塞进上下文也不现实受限于模型的上下文窗口长度。我试过把一个 5000 行的大函数拆成多段塞进去效果反而更差——模型在后半段忘掉了前半段的关键变量。比较实用的策略是“两级上下文”一级上下文当前函数的伪代码 其直接调用者的名字和摘要。二级上下文当前函数引用的字符串列表 当前函数调用的子函数名字。字符串引用极其重要。二进制里硬编码的字符串通常是业务语义的“锚点”比如一个函数里出现login success、invalid password、session expired那它的用途不言自明。我专门写了一个预处理逻辑把函数中所有字符串引用的偏移和值抽出来拼到 prompt 末尾该函数引用的字符串常量 0x10210: login_success 0x10230: invalid_password 0x10245: session_token抽样测试表明加字符串引用后模型的“高置信度”回答比例从 40% 提升到 65% 左右。成本几乎为零但效果立竿见影。原因也很简单——字符串是最接近人类语义的二进制残留物它比汇编指令更容易让模型建立联想。4.3 批量处理与重命名策略批量跑完整个固件后你会得到一堆ai_前缀的新函数名。我统一加这个前缀是有意的它明确标识了这些名字是机器生成的猜测未来人工审计时一眼就能区分哪些是已验证的、哪些是待定的。如果你直接覆盖原有FUN_xxx命名出了问题想回溯就麻烦了。批量处理时还有几个实操建议先从最小的函数开始跑。小函数逻辑简单、模型推断准确率高先跑通整个流程、建立信心再处理复杂函数。可以用函数体规模做分级策略小于 20 行的函数提示词用简化版20 到 200 行的用标准版超过 200 行的不建议直接整个塞给模型而是先让 Ghidra 自动拆分或者先用注释标记关键分支。结果写回时保持幂等性。做了重命名的函数下次再跑会被跳过避免重复覆盖人工修改结果。我在脚本里加了一个判断如果函数名已经是ai_开头就跳过。5. 实际使用中踩过的坑与排查方案5.1 模型幻觉导致的错误命名最大的坑是“看起来合理、实际上完全错误”的命名。模型会根据某些局部特征强行推断一个语义比如看到字符串key就把函数命名为get_key但实际上这个函数只是在做字符串拼接并不是获取密钥。我试过更宽松的提示词让模型输出“可能的作用 依据”发现这种自由格式输出的幻觉率反而更高。后来在提示词里强制加了一句“如果不确定函数意图请在 confidence 字段返回 low不要猜测”。副作用是 low 置信度的答案比例有所上升但剩下的 high 置信度回答变得相当可靠。建议处理策略是只对 confidencehigh 的结果自动写回medium 的存入备注low 的直接丢弃。这样你至少不会把错误的函数名带进后续分析。fake 函数名一旦建立靠人工排查成本很高宁可少自动标注也不要让错误命名污染工程。5.2 反编译代码过长导致上下文溢出遇到过最离谱的情况是一个几十 KB 的反编译函数直接把模型请求打到超时。这种巨型函数在固件里其实不少见尤其是一些未经优化的驱动代码或者被混淆过的分发器。我的解决办法是“前置降级”。反编译结果超过 4000 字符就触发分级先取函数的前 30% 代码片段再取包含所有if/else分支头部的摘要最后取函数尾部三段拼接后再喂给模型。实测证明丢失中段的具体操作对意图推断影响不大因为模型的判断依据更多来自调用关系、字符串引用和关键分支结构而不是每一行赋值语句。你也可以在 Ghidra 里用“只反编译函数签名 局部变量 第一个基本块”的方式压缩输入但那样做丢的信息太多不推荐。5.3 限流、超时与成本失控批量处理一个几百函数的固件时在线 API 的限流几乎是必然遇到的。脚本里我做了三重防护每次请求之间加 delay、失败自动重试 3 次、超时时间拉长到 60 秒。但真正治本的方案是先把所有函数的反编译结果缓存到本地再分批调用模型这样即使中途断了也能断点续跑不用重新加载二进制。缓存的 key 用函数地址。成本方面我做过一个量化一个中等复杂度函数反编译代码大约 1000 字符加上提示词模板大约 1500 token。按一次批量 300 个函数计算总 token 数约 50 万用在线 API 的费用大概在几块钱到几十块钱区间。这个开销对一次严肃的逆向分析任务来说可以忽略不计但如果你每天跑几百个样本还是建议切成本地推理或批量夜间跑。5.4 一个常见问题速查表现象可能原因排查与解决模型返回空结果上下文过长或 API 超时检查函数体大小开启降级模式增大 timeout函数重命名失败命名冲突或名称含非法字符检查 model 输出是否有空格、特殊符号给setName加 try/except大量 low 置信度样本风格与模型训练分布差异大换用更大的模型或给模型补充更多上下文字符串、调用链批量跑得很慢在线 API 限流加 sleep、开并发注意限流上限、改用本地模型分析结果不可复现模型输出随机性temperature 调到 0固定 seed部分服务支持6. 效果上限在哪里实测对比与适用边界6.1 一个直观的对比数据我在某大型 CTF 模拟题和一个固件样本上分别做了测试。测试方法很简单选 50 个功能明确的函数人工标注标准答案再用 GhidraGPT 的流程跑一遍看模型给出的重命名和注释与标准答案的语义匹配度。结果大致如下函数类型数量语义完全匹配部分匹配完全不匹配简单工具函数字符串格式化、内存拷贝包装201631协议解析相关15753自定义加密/解密函数10244状态机/分发器5122数字很直观简单工具函数上模型表现接近完美协议解析上可用但需要人工复核自定义加密和复杂状态机基本只能做弱提示离可用还有距离。我的体感是GhidraGPT 这类方案目前最适合做“函数列表的第一层标注”帮助你在一小时内快速建立一个陌生样本的初步功能地图而不是替代深入分析。6.2 适用场景建议与风险提示哪些场景收益最大我认为有三类恶意样本分析。样本量大、函数功能碎片化用 AI 预标注后分析师只需要聚焦可疑点。固件安全审计。固件里大量配置解析、字符串处理、外设驱动函数语义模式高度重复模型准确率很高。闭源组件摸底。拿到一个陌生的第三方库先自动标注一遍能快速定位关键入口函数。哪些场景不建议用混淆壳、自修改代码、高度虚拟化的样本。这类代码的反编译结果本身就已经失真模型基于失真输入做出的推断自然不可靠。还有一类是超大型函数几百行的反编译结果已经超出模型的注意力极限强跑只会得到一堆空洞的命名。另外提醒一句自动重命名会改变你的工作记忆锚点。当你习惯了ai_check_admin_permission这个名字如果它是错的你对整个模块的理解都会被带偏。所以我每次跑完批量标注后都会花半小时随机抽 10 个函数人工验证确认置信度阈值设得合适再做后续分析。在这个项目上我个人的体会是别指望大模型直接告诉你漏洞在哪它最大的价值是帮你把前面 80% 的“这个函数是干嘛的”的体力活干掉把精力集中到最有疑点的 20% 上。这套方案的演进方向也很清晰——把调用图嵌入、结构化输出、人工反馈闭环加进去做成一个真正能在日常分析流水线里长期跑的基础设施。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →