1.2 反编译阅读


1.2 反编译阅读

本节摘要:本节练全书最常用的手艺:读反编译产物。先讲伪 C 窗口的正确预期——它是反编译器重建的近似源码,不是原始源码——以及怎么读懂 local_、DAT_、undefined 这些噪音符号;函数列表与字符串怎么当「地图比例尺」用。然后是逆向最锋利的工具:交叉引用(xref),从 main 找到 check、从字符串找到使用点,只沿引用链走、不顺序硬啃。再把理解固化下来:重命名、注释、书签三类操作让工程越读越清楚而不是越读越乱。最后是完整走查:对 stripped 版 demo_stripped,在没有函数名的情况下,从入口出发找到许可校验函数、认出异或常量 0x5a、算出正确密钥 license,并把每一步的理解写回工程——这也是第 03 章模型将要替你做的动作清单。

学习目标

  • 对伪 C 建立正确预期:说得出它与原始源码的三点差异。
  • 认得 local_、DAT_、undefined 等常见噪音符号,不被它们吓住。
  • 用函数列表与字符串锚点快速建立程序结构认知,定位可疑函数。
  • 用交叉引用回答「谁调用了它」「它调用了谁」「字符串在哪被用」。
  • 熟练使用重命名、注释、书签,把阅读成果写回工程。
  • 独立完成 demo_stripped 的完整走查,恢复核心逻辑与命名。

一、伪 C 窗口:它是什么、不是什么

Decompiler 窗口把当前函数重建成类似 C 的代码。三件事必须心里有数:

  1. 它是重建,不是还原。 反编译器从指令推断结构:循环边界、类型、变量个数都是推断结果。编译器把源码变成机器码时丢掉的信息(类型、变量名、结构),反编译器只能猜。
  2. 变量名是无意义的。 你会看到 local_18、param_1、FUN_00401189——名字丢了,语义要靠上下文找回。
  3. 类型是可编辑的假设。 把 param_1 从 int 改成 char *,伪 C 立即按新类型重新解释——这个操作在走查里立大功。

伪 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:那串异或过的字节在数据区里,反编译器不知道它是什么,只知道地址。走查时我们会回来收拾它。

二、函数列表与符号树:先看地图比例尺

拿到程序别急着逐行读,先花两分钟看结构:

  • Functions 窗口按 Size 排序:最大的函数往往是 main 或初始化逻辑;只有几条指令的小函数多是包装(wrapper)。
  • 符号树 Imports:导入表暴露能力面——有 WSAStartup 说明碰网络,有 CryptDecrypt 说明碰加密。demo 只有 strcmp、printf,攻击面一眼见底。
  • 符号树 Strings:字符串是最快的语义锚点。demo 里的「[FAIL] invalid license key」直接指出许可校验的存在。

方法:字符串锚点 → 双击跳到使用处 → 顺着使用它的函数展开。比从入口顺序啃快得多。这个「锚点优先」的读法在第 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_stripped

现在忘掉 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 像是去混淆」)也要走同样的验证路。

走查效率的三条习惯,从第一天就养成:

  • 每跳一层记一行:假设是「H2 校验函数内还调了解码器」,就顺手写进注释草稿;走查结束时这些行就是现成的理解记录。
  • 入口、锚点、结论三处打书签:走查的路径比结论更难复现,书签是路径的脚印。
  • 卡住超过十分钟就换锚点:字符串走不通换导入表,导入表走不通换大函数——锚点是多维的,别在一条引用链上吊死。

常见坑提醒三条:改了函数签名(类型)后伪 C 变得离谱,多半是改错了,撤销重来;引用面板为空时先确认对象选的是函数名而不是某条指令;字符串在 Ghidra 里默认只显示已定义的,没找到时用 Search → For Strings 重新扫描。

本节要点回顾

  • 伪 C 是重建不是还原:名字丢了、类型是可编辑的假设、结构靠推断;噪音符号表帮你读得动。
  • 阅读顺序:函数列表与字符串锚点先行,xref 按需跳转,不顺序硬啃。
  • xref 三问:谁调用它、它调用谁、数据在哪被用;一次只追一层。
  • 重命名写语义、注释写证据、书签管回访、类型也算落盘——理解必须写回工程。
  • 走查五步法完整跑通 demo_stripped,密钥 license 验证闭环;双样本对照是练习的加速器。

一次走查读一个程序,来了一百个样本怎么办?下一节把这套动作变成脚本:analyzeHeadless 批处理与 PyGhidra 库模式,让函数清单以 JSON 的形式批量流出来。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U