本节摘要:本节练全书最常用的手艺:读反编译产物。先讲伪 C 窗口的正确预期——它是反编译器重建的近似源码,不是原始源码——以及怎么读懂 local_、DAT_、undefined 这些噪音符号;函数列表与字符串怎么当「地图比例尺」用。然后是逆向最锋利的工具:交叉引用(xref),从 main 找到 check、从字符串找到使用点,只沿引用链走、不顺序硬啃。再把理解固化下来:重命名、注释、书签三类操作让工程越读越清楚而不是越读越乱。最后是完整走查:对 stripped 版 demo_stripped,在没有函数名的情况下,从入口出发找到许可校验函数、认出异或常量 0x5a、算出正确密钥 license,并把每一步的理解写回工程——这也是第 03 章模型将要替你做的动作清单。
Decompiler 窗口把当前函数重建成类似 C 的代码。三件事必须心里有数:
local_18、param_1、FUN_00401189——名字丢了,语义要靠上下文找回。伪 C 里最常见的「噪音符号」先认个脸熟,走查时不至于卡住:
| 符号 | 含义 | 你要做什么 |
|---|---|---|
| local_xx | 栈上局部变量 | 按用途重命名 |
| param_x | 参数,类型常是 undefined 或 int | 修类型后再读 |
| FUN_xxxxxxxx | 无名函数 | 走查后重命名 |
| DAT_xxxxxxxx | 数据区常量 | 看原始字节,判断是什么 |
| undefined / undefined8 | 未确定类型(1 / 8 字节) | 修类型 |
伪 C 与编译器各阶段的对应关系(词法、语法、IR、目标码)可回看《build-your-own-x》第 9 章《造操作系统与编译器》;这里只需要记住:伪 C 站在「目标码反推回源码」的半路上。
// decompiled_demo.c —— demo 的 check_key 反编译结果(示意,实际以你的环境为准) undefined4 check_key(char *param_1) { char local_28 [32]; int local_18; local_18 = deobfuscate(&DAT_00402008, local_28); return (int)(strcmp(param_1, local_28) == 0); }
注意 DAT_00402008:那串异或过的字节在数据区里,反编译器不知道它是什么,只知道地址。走查时我们会回来收拾它。
拿到程序别急着逐行读,先花两分钟看结构:
WSAStartup 说明碰网络,有 CryptDecrypt 说明碰加密。demo 只有 strcmp、printf,攻击面一眼见底。方法:字符串锚点 → 双击跳到使用处 → 顺着使用它的函数展开。比从入口顺序啃快得多。这个「锚点优先」的读法在第 03 章会原样传给模型——枚举(列清单)永远先于聚焦(读函数)。
xref 回答三类问题,全在右键菜单(Show References to / Show References from):
| 问题 | 操作 | demo 中的例子 |
|---|---|---|
| 谁调用了这个函数 | 对函数名查 References to | check_key 只有 main 一个调用者 |
| 它内部调用了谁 | 查 References from | check_key 调用 deobfuscate 与 strcmp |
| 这个字符串 / 数据在哪被用 | 对数据查引用 | [FAIL] 串只出现在 deny 函数 |
xref 的价值在于把「顺序阅读」变成「按需跳转」:你不需要读完 500 个函数,只需要沿着引用链走 5 步。走引用链的两条纪律:一次只追一层,跳过去先看函数签名再决定要不要展开;引用数为零的函数(没人调用)可能是回调或死代码,别急着深挖。
第 03 章 GhidraMCP 把 xref 做成了模型可直接调用的工具——原因正是它是人用得最勤的查询。
读出来的理解必须落盘,否则下次打开全部清零。三类操作各管一件事:
| 操作 | 快捷习惯(以界面实际为准) | 用途 | demo 演练 |
|---|---|---|---|
| 重命名 | 伪 C 里对函数 / 变量按 L | 给无语义的名字换上语义 | FUN_00401189 改 deobfuscate |
| 注释 | Listing 里按分号键 | 记录推理依据与待办 | 在 DAT 处注「XOR 0x5a 后得期望密钥」 |
| 书签 | 右键 Add Bookmark | 标记需要回访的位置 | 入口、校验函数打 Type 标签 |
纪律一:命名可改,证据要留——重命名旁边用注释写「依据:strcmp 与局部缓冲区比较」,三个月后的你才敢信这个名字。纪律二:书签少而准,十个以上就该整理了。纪律三:改类型(undefined 改 char *)也算落盘的一部分,工程里类型与名字一样值钱。
现在忘掉 demo 的符号表,打开 bin/demo_stripped(工程里双击切换,流程见 1.1 节)。函数列表里只有 entry、FUN_ 开头的无名函数和少量库导入——这才是真实场景。
第一步:从入口定位 main。 双击 entry,伪 C 里能看到它把某个 FUN_ 地址传给启动例程后调用——那个被传的函数就是 main。 Ghidra 的分析通常已自动给它标上 main 或 ENTRY。
第二步:从字符串锚点找校验。 符号树 Strings 里双击「[FAIL] invalid license key」,查引用,落在某个函数里——记为 deny 候选。它被谁调用?References to 指向的上层函数记为 main 候选。在 main 伪 C 里看到条件分支:成立走 grant(打印 [OK]),不成立走 deny。
第三步:攻校验函数。 main 调用的那个无名函数(记为 FUN_check)伪 C 大致是:
// decompiled_stripped.c —— stripped 版校验函数反编译(示意) undefined4 FUN_00401160(char *param_1) { char local_28 [32]; int local_18; local_18 = FUN_00401189(&DAT_00402008, local_28); return (int)(strcmp(param_1, local_28) == 0); }
与带符号版对照(1.1 节双样本布局的价值在此兑现):结构完全一致,只是名字全丢。点进 FUN_00401189:一个循环,逐字节取输入、与 0x5a 异或、写入输出——典型异或去混淆。
第四步:算出密钥并验证。 打开 DAT_00402008(在 Listing 里点地址,看原始字节):36 33 39 3f 34 29 3f。逐字节异或 0x5a:36 异或 5a 得 6c(即字符 l),33 得 69(i),39 得 63(c),3f 得 65(e),34 得 6e(n),29 得 73(s),3f 得 65(e)——期望密钥是 license。到终端跑 ./bin/demo_stripped license,输出 [OK],证据闭环。
第五步:把理解写回工程。 重命名 FUN_00401160 为 check_key、FUN_00401189 为 deobfuscate;在 DAT_00402008 加注释「异或 0x5a 后为 license」;给入口与校验函数打书签。保存工程——这个工程在第 03 章会被模型直接接管。
回看五步:入口定位、字符串锚点、结构对照、常量计算、落库固化。第 0.1 节的「静态出假设、验证做闭环」在此完整走了一遍;换成大模型,它提的每个假设(「FUN_00401189 像是去混淆」)也要走同样的验证路。
走查效率的三条习惯,从第一天就养成:
常见坑提醒三条:改了函数签名(类型)后伪 C 变得离谱,多半是改错了,撤销重来;引用面板为空时先确认对象选的是函数名而不是某条指令;字符串在 Ghidra 里默认只显示已定义的,没找到时用 Search → For Strings 重新扫描。
一次走查读一个程序,来了一百个样本怎么办?下一节把这套动作变成脚本:analyzeHeadless 批处理与 PyGhidra 库模式,让函数清单以 JSON 的形式批量流出来。