在没有源代码的前提下弄清一个产物怎么运转,过去依赖少数熟手的手工推断。把这件事拆成可编排的链路之后,它更像一条流水线:二进制接入、符号映射、调用追踪、等价性校验。四段串起来,回答一个具体问题------怎样把「读懂」这件事从手感变成流程。
本文按这条链路逐段展开。每段都会说明它产出什么、以什么作为判据、以及在没有达到判据时应该停下来补什么。

一、二进制接入:先把产物变成可分析对象
问题的起点是编译产物会丢失符号信息。函数名与变量名在优化后基本不可恢复,剩下的只有地址与指令序列。源码面向人编写,符号在优化阶段被清除,可复现的线索只剩地址与指令序列。
接入这一步的目标不是还原源码,而是把产物变成结构化的中间数据:段信息、导入导出表、可定位的入口点。
bin_ingest:产出可分析对象,不做任何还原假设
def bin_ingest(path, out_dir):
obj = load_binary(path) # 只读取段表与导出表
meta = {
"arch": obj.arch,
"entry": obj.entry_point,
"segments": \[s.name, s.va, s.size for s in obj.segments],
"imports": obj.imports, # 外部调用面,逆向的入手处
}
write_json(out_dir + "/ingest.json", meta)
return meta
关键点在于这一层只描述「有什么」,不猜「是什么」。把猜测留到后面,前一段的产出才可复现。如果在这一层就开始给地址起名字,后面的结果会随着一次重跑全部变化,链路的可比性就没了。
二、符号映射:给匿名地址挂上意义
接入之后的产物是一堆无语义的地址。符号映射要做的是给这些地址挂上可读的名字与类型------也就是人工逆向里耗时的大头。
这一层的价值在于可追溯:每个命名都要能指回一条证据,而不是凭感觉。
symbol_map:命名必须能指回证据
def symbol_map(meta, hints):
table = {}
for addr in meta"call_targets":
ev = hints.evidence(addr) # 字符串常量、调用上下文、系统调用
tableaddr = {
"name": hints.suggest(addr),
"type": hints.signature(addr),
"evidence": ev,
}
return table
这一步留下的证据字段,是后面校验环节能自动比对的前提。命名一旦脱离证据,整条链路就退回成手工笔记------看上去有了结构化数据,实际上仍然无法复核。
命名粒度也值得权衡。名字太细,维护成本压过收益;名字太粗,复核时等于没有。一个可用的标准是:换个人照着这份表能还原出同样的行为描述,就够了。
三、调用追踪:把散点连成关系
有了符号,下一步是把它们连起来。追踪的目标是给出调用关系,让「谁触发谁」变成可查询的数据。
call_trace:以入口为根做可达性分析
def call_trace(meta, table, root):
seen, stack, edges = set(), root, \[\]
while stack:
cur = stack.pop()
for callee in resolve_calls(meta, cur):
edges.append((table.get(cur, {}).get("name", cur),
table.get(callee, {}).get("name", callee)))
if callee not in seen:
seen.add(callee)
stack.append(callee)
return edges
到这一步,一份产物的功能轮廓才真正显形。此前需要逐段推断的工作,现在变成对关系图的检索。检索能力带来的另一个好处是可重复:同一份产物跑两遍,得到的关系图应当一致。
这里有一个容易被忽略的细节:关系图不是越全越好。从入口不可达的部分,参与判定的价值有限,却会显著抬高维护成本。先把可达部分理干净,比追求覆盖率更实际。
四、等价性校验:证明行为一致
前面三段产出的是理解结果,这一段负责证明这个理解站得住。校验不能只看输出对得上,还要覆盖输入边界。
python -m parity_check --ingest ./ingest.json --trace ./trace.json \
--cases ./cases/ --report http://your-domain.test/reports/parity.json
校验的意义在于把「我觉得一样」换成「在给定的用例集合上一样」。用例集合覆盖不到的地方,要显式写进报告,而不是默认通过。
这一点决定了整条链路的可信度。前面三段做得再细,只要校验环节放水,得到的仍然只是印象,而不是证据。
五、四个踩坑记录
-
把命名与推测混在一起。结果是每次重跑,命名都不一样。
-
跳过证据字段。短时间看不出问题,等到需要复核时无从下手。
-
只测主路径。边界输入没覆盖,行为差异在真实使用中才暴露。
-
把分析结果写进业务代码。产物一更新,链路就得重改一遍。
前两条尤其常见。它们不会让链路报错,只会让链路的结果不可信------而不可信的结果,比没有结果更麻烦。
第三条和第四条通常成对出现。因为只测了主路径,所以改动的影响面被低估;因为把结果写进了业务代码,所以每次产物更新都要连着改业务流程。这两件事一起发生的时候,返工量会成倍上升。
六、观测指标
| 指标 | 说明 |
| 可命名地址占比 | 挂上名字与类型的地址比例,偏低说明证据不足 |
| 关系图覆盖率 | 从入口可达的调用边占全部调用边的比例 |
| 用例通过率 | 等价性校验中行为一致的用例比例 |
| 重跑一致率 | 同一产物连续两次分析结果一致的比例 |
排序上,重跑一致率应该先看。这一项不达标,其余指标的比较意义有限。
把四项指标并排看,还能看出问题的位置:重跑一致率低,说明证据字段不完整;可命名地址占比低,说明接入层粒度不对;用例通过率低而关系图覆盖率高,说明理解方向出了偏差;关系图覆盖率低,则多半是入口选择有问题。
七、怎么判断这条链路是否可用
看三件事:产物是否能被结构化读取;命名是否能指回证据;行为是否有用例覆盖。三者缺一,先补那一环,不要急着扩用例规模。
顺序反了,得到的只是一个能跑出手感、却经不起复核的脚本。