☰
同源样本逆向分析:从静态拆解到动态调试的完整流程
2026/10/19 6:11:43 网站建设 项目流程

刚拿到figugegl.1和figugegl.2这两个样本的时候,我第一反应是:这不像是正常软件交付的命名习惯,更像是某个内部项目里,同一份程序在不同构建阶段留下的两个产物。后缀的.1和.2多半代表版本迭代或构建批次。作为逆向分析任务,有价值的地方恰恰在这里——我们可以通过静态与动态两种手段,把两个样本各自的功能边界、代码结构和行为差异摸清楚,再对比它们之间到底改了什么、加了什么、修了什么。

这篇文章我打算按照我平时做二进制样本分析的完整流程来写,从环境准备、静态拆解、动态调试,到算法还原和版本差异对比。过程中会穿插一些我实际踩过的坑。内容适合刚接触逆向、想完整走一遍流程的人,也适合已经会一些基础操作、但遇到“两个相似样本怎么快速定位差异”这类问题的人。

1. 样本初识与情报铺垫

1.1 先别急着双击:从文件身份开始

我拿到任何样本,第一步永远不是把它跑起来,而是先搞清楚它的文件身份。file命令也好,PE查看工具也好,先回答几个基础问题:这是什么架构的程序?是PE还是ELF?编译时间是什么时候?有没有加壳?如果我们连这些底数都没摸清,后面很容易被误导。

用工具查看后,我手里的这两个样本都是32位PE格式,文件大小接近但哈希完全不同,说明它们不是同一个文件改了后缀,而是内容上确实有差异。比较有意思的是编译时间戳:

项目figugegl.1figugegl.2
架构32位 PE32位 PE
文件大小约 48 KB约 51 KB
MD5不同不同
加壳状态无壳疑似UPX壳
编译时间戳时间较早比 .1 晚一些

基本信息一出来,分析路径就清晰了很多:.1是未加壳的原始版本,可以直接进反编译器;.2带了壳,说明在上线或分发之前做了一层压缩或混淆处理,需要先脱壳再分析。这种“一个裸奔、一个加壳”的组合,恰好适合用来体会逆向工程中的两条常见路线——静态优先,还是动态优先。

提示:样本来源必须合法,最好来自授权测试、CTF题目或者自己编译的程序。没有授权就去分析别人的软件,不管出发点是什么,都有法律风险。我这批样本来自一次内部攻防演练,研究完即销毁,别拿这个流程去做不该做的事。

1.2 隔离环境与工具链准备

分析样本之前,环境隔离是第一优先级。我个人的习惯是:用虚拟机做分析,虚拟机里只装逆向工具,不登录任何个人账号,不开共享文件夹,网络用虚拟网卡模拟。分析前打一个干净快照,每次跑完样本直接回滚快照,保证宿主不受污染,也保证下次分析时环境是全新的。

工具链方面,我常用的组合如下:

  • 文件识别与查壳:DIE(Detect It Easy),兼顾PE解析和壳特征识别
  • 静态分析:IDA 或 Ghidra
  • 动态调试:x64dbg,配合 ScyllaHide 过反调试
  • 行为监控:Process Monitor,记录文件、注册表、进程操作
  • 网络模拟:FakeNet,配合 Wireshark 看流量
  • 哈希工具:简单点就用 Python 的 hashlib,也能顺手写小脚本

这些工具各有侧重,但有一个共同原则:能自动化的尽量自动化,能和我后续手写脚本配套的优先选。比如 DIE 的 CLI 输出可以直接进我自己的脚本做批量判断,Ghidra 支持用 Python 脚本批量提取字符串和导入函数,后期做两个样本的 diff 时非常方便。

2. 静态分析:从外壳到底裤

2.1 查壳与脱壳:先看入口点是否可信

.1没有壳,直接拖进 Ghidra 能看到正常的导入表和控制流,分析体验比较舒服。.2就麻烦一点,DIE 提示 UPX 特征,也就是说程序真正的代码被压缩了,程序启动后先执行一段解压代码,把原代码还原到内存,再跳转到真正入口点执行。

脱壳我推荐用“ESP 定律”:程序在加壳入口处(通常是 pushad)停下后,单步到 popad 执行完,栈顶寄存器会指向原来的栈位置。这时候下硬件断点,等程序跳转到原入口点(OEP),再用工具 dump 内存。原理其实不复杂,加壳程序为了保留上下文,一定会在某个地方把寄存器的值释放出来,ESP 定律抓的就是这个释放时刻。

实际操作中我发现 UPX 壳比较规矩,入口特征很好认:第一句往往是pushad,后面跟着call跳转。用 x64dbg 在入口处设断,F8 走几步,看到寄存器窗口里 ESP 突然变成和栈顶差不多的值,就说明壳已经解压到关键节点了。具体脱壳步骤我放在动态调试那节展开,这里先记住一个原则:脱壳不是目的,拿到干净可分析的代码才是目的。脱壳过程不要过度纠结工具,关键是理解壳的“先解压、后跳转”机制。

2.2 字符串、导入表和交叉引用里的线索

拿到.1的干净代码后,我先看字符串窗口。字符串是逆向分析里回报率最高的信息源,它直接暴露程序的配置路径、提示信息、可能调用的库等。figugegl.1里我看到了这些关键词:

  • config.dat
  • debug output
  • C:\ProgramData\
  • [dbg]

这些字符串指向一个很典型的路径:程序启动后会去读取某个配置文件,并且带一个调试输出功能。顺着字符串的交叉引用(xref)就能跳到使用它的函数,快速锁定核心逻辑。

figugegl.2在脱壳后的字符串里多出了几项:

  • update_check
  • http://开头的地址
  • mutex_privilege
  • 一些疑似日志格式的字符串

看到这些,我大致能判断:.2比.1新增了网络请求和互斥体逻辑,可能是一个带更新检查或授权判断的新版本。单凭字符串还不能下结论,但这些字符串就是后续分析的地图。

导入表也是重要信号。.1的导入表主要是CreateFile、ReadFile、WriteFile、RegOpenKey这些文件和注册表 API,没有网络相关函数。.2脱壳后能看到WinHttpOpen、WinHttpConnect、WinHttpSendRequest等,网络功能的存在得到了进一步证实。

2.3 反编译主逻辑:把伪代码读成故事

静态分析不能停留在字符串层面,要深入主逻辑。用 Ghidra 反编译.1后,定位入口函数和主处理函数。这个程序的入口逻辑不复杂,大致是:

  1. 打开config.dat
  2. 读入一段配置内容
  3. 对配置内容做一次简单变换(这里先不剧透,后面算法还原再展开)
  4. 根据变换结果决定写日志还是做其他处理

.2的反编译结果明显多了一个分支:先检查互斥体是否存在,如果存在就退出;如果不存在,创建互斥体,然后启动网络请求线程。看到这里,两个版本的整体定位已经比较清晰了:

  • figugegl.1:一个带配置文件读取功能的本地程序,逻辑相对简单
  • figugegl.2:同一个程序的增强版,增加了网络通信和单实例保护

到这里,静态分析提供了足够多的“假设”,接下来要让动态调试来验证这些假设。

3. 动态调试与行为追踪

3.1 用调试器把静态猜想变成事实

静态分析擅长看“代码长什么样”,动态分析擅长看“代码跑起来会发生什么”。我一般不是先动态后静态,而是先静态建立假设,再用动态逐条验证。验证工具我选了 x64dbg,对付 32 位程序手感最顺。

对.1,加载后直接在config.dat的读取函数入口下断点,然后看它读文件后做了什么。断点命中后,我看到了一个熟悉的内存拷贝模式:先分配缓冲区,把文件内容读进去,然后调用一个循环对字节逐个处理。这个循环里有一个xor指令,目标值是0x7C,我推测这是对配置文件做异或解密。

对.2,因为多了一层壳,我的做法是先把 OEP 找到,再用 Scylla 修复导入表。这里有个很容易踩的坑:手动脱壳后 dll 导入表经常是残缺的,直接跑会崩。Scylla 能从内存中重建导入表,选中OEP后自动找到 IAT,修复后还要用dump功能导出新的文件。修复后的.2跑起来稳定很多,这时我再设断点观察它的网络请求逻辑。

3.2 API 监控、文件操作与网络行为

动态调试能精准看到代码执行流程,但在宏观行为上,Process Monitor 和 FakeNet 的视角更直观。

我用 Process Monitor 分别记录了.1和.2的行为,两组记录差异很明显:

行为维度figugegl.1figugegl.2
文件操作读取config.dat,创建日志文件读取config.dat,创建日志文件,释放临时文件
注册表操作无读取启动项相关注册表位置
网络行为无向固定地址发起 HTTP 请求
进程行为单进程退出创建新线程执行网络任务

.2有网络请求,这在静态分析阶段已经有预判,但动态验证后看到了更多细节:它发起请求前会先创建互斥体,如果互斥体已存在,直接自杀退出。这个行为模式在授权管理场景里很常见,就是典型的单实例保护。哦对了,.2还释放了一个临时文件到%TEMP%,这个在静态字符串里没直接体现,是动态监控抓出来的。

网络模拟我用了 FakeNet,把所有外部请求重定向到本地,用一个伪造的服务端响应。这样我可以在不出网的环境下完整观察请求内容,包括 URL、User-Agent、POST 数据等。这里提一句:别在真实网络环境里直接跑带网络行为的样本,有条件就上流量模拟工具。

3.3 沙箱报告与手工分析怎么互相印证

有些人会直接丢进沙箱看报告,然后就不管了。我的习惯是沙箱报告和手工分析对照着看。自动沙箱擅长快速筛出可疑行为,但经常漏掉逻辑细节,比如某个文件的写入条件是“特定时间后才生效”,沙箱默认观察时间不够就测不出来。

这次我把样本丢进某在线沙箱测试,沙箱报告显示figugegl.2有网络请求行为,但没识别出请求的具体参数。我用手工抓包和断点分析后发现,那个请求里携带了从config.dat解密出来的内容。自动沙箱能告诉我“有网络行为”,但“请求里带了什么、为什么带这些内容”还是得靠手工逆向。两手都抓,才能形成完整证据链。

4. 核心机制解密与版本差异分析

4.1 从内存dump里还原一段加密配置

前面提到.1会对config.dat内容做异或变换,这里展开讲讲怎么还原的。程序读文件后有一个循环,逐字节对数据做异或运算。我在调试器里设了断点,把原始的密文和运行时的明文都抓了下来:

  • 密文:6D 65 78 70 6F 69 6E 74
  • 明文:44 65 62 75 67 4D 6F 64
  • 异或密钥:两串相减就能看出来,密钥是0x29

这种单字节异或是最基础的混淆方式,但确实大量存在于实际样本中。确认密钥后,我用 Python 写了个小脚本,直接对config.dat批量解密:

import os def xor_decode(data: bytes, key: int) -> bytes: return bytes(b ^ key for b in data) raw = open("config.dat", "rb").read() decode = xor_decode(raw, 0x29) print(decode)

脚本跑完,config.dat里的明文就出来了,内容是一段带路径和参数的配置项。这个过程核心价值不在于异或本身多难,而在于“如何在调试器中快速定位到关键变换函数”。我的思路是:在读取文件后调用的第一个自定义函数上下断点,观察输入输出差异。

4.2 两个版本的功能迭代与改动点

到这里,两个版本的差异已经比较明确了。为了让对比更直观,我列了一张功能区对比表:

功能点figugegl.1figugegl.2
配置文件读取有有,且解析逻辑更复杂
数据变换单字节异或同样使用异或,但密钥不同
单实例保护无有互斥体机制
网络请求无有,携带配置内容
持久化行为写入固定日志释放临时文件,检查注册表启动项
崩溃处理无增加了异常处理分支

.2对比.1的升级方向很清楚:从孤立运行变成带网络同步的单实例程序,并且增加了异常处理和启动项自检。这种差异模式在很多软件的更新迭代里都能看到,也算是一个正常的“功能演进”范本。

功能差异之外,我还会对比一下编译选项导致的代码结构差异。.1代码里大量使用直接内存拷贝,.2则用了更多的间接跳转和线程同步,说明编译时优化级别可能调高过,或者引入了额外的运行时库。这种结构性差异会影响反汇编阅读难度,但对最终行为没有决定性影响。

注意:两个样本虽然存在差异,但核心逻辑一脉相承。分析这类“同源不同版”的样本时,先找出共同点,再圈定差异点,比从头到尾硬啃两个文件效率高得多。我一般在脱完壳后先用工具做一次二进制 diff,能秒杀很多肉眼对比工作量。

5. 常见问题与排查技巧实录

5.1 脱壳后“跑不动”的典型场景

figugegl.2脱壳过程中我遇到过一次典型问题:用 x64dbg 脱壳 dump 出来的文件一运行就弹错。原因不是壳没脱干净,而是导入表修复不完整。这几乎是手动脱壳的必经之路。

排查思路:先用 DIE 检查脱壳后的文件,如果显示壳特征已清除但导入表异常,就重新用 Scylla 加载原始进程,再次扫描 IAT,然后手动指定 OEP 重建。如果还不行,观察错误码:访问违例往往是因为重定位表缺失,可以用 PE 工具重新生成重定位;找不到 dll 入口则是 IAT 地址写错了。

我自己后来养成了一个习惯:脱壳后先用静态工具的“快速验证功能”检查入口点是否合理,再跑一次行为监控确认程序能正常执行。别直接双击测试,隔离环境里也尽量不要双击运行可疑程序的脱壳产物。

5.2 静态分析走进了死胡同怎么办

分析.1时我出现过一次卡壳:某个关键函数的调用关系在静态反编译里看是断的,因为该地址是通过call [寄存器]间接调用的。如果死磕静态,可能要多花几个小时。

我的解决办法是转向动态调试:在间接调用的上一条指令断点,运行后看寄存器实际值是多少,直接定位真正被调用的函数。这就是一个典型的“静态不行就动态补”的场景。

另外,硬件断点比软件断点在对抗反调试时更有用。如果程序检测到0xCC断点字节就崩溃,硬件断点不会修改代码,可以绕开检测。ScyllaHide 插件则能隐藏调试寄存器标志、PEB 的调试标记等,能拦住大部分常见反调试。

5.3 行为监控一片空白时别急

有朋友遇到过跑样本时 Process Monitor 什么操作都记不到,第一反应是样本太高级了。但很多时候原因朴素得很:程序在等待某个定时器触发,或者需要特定命令行参数。

这次分析.2时也出现过假象,一开始 FakeNet 没有抓到任何请求。后来才发现程序把网络请求放在了一个延迟执行的线程里,主线程跑完退出后,子线程才发起连接。我在调试器里把时间相关的延迟函数改短,甚至直接跳过 sleep,请求就立刻发出来了。

整理一下我常用的排查速查表:

异常现象可能原因排查手法
脱壳后程序崩溃IAT 缺失 / 重定位损坏Scylla 修复导入表,重建重定位
静态反编译看不懂间接跳转函数指针地址来自运行时计算断点 + 查看寄存器实际值
行为监控无记录定时器延迟 / 等待用户输入改时间参数 / 模拟交互
网络请求抓不到线程延迟、请求在子线程跳过 sleep,断点定位线程入口
样本自动退出互斥体冲突 / 已存在实例找到互斥体创建代码,改名字或跳过

5.4 对比分析时怎么提高效率

最后分享一个我对比两个相似样本时常用的技巧。别急着同时开两个反编译窗口,而是先给.1生成一份“基准报告”,包括字符串表、导入函数表、关键函数地址列表;再给.2生成同样格式的报告,然后用 diff 工具对比。差异部分通常就是版本迭代的功能点。

字符串差异最直观,加上导入函数差异就能锁定“新增了网络功能”“新增了互斥体”这类结论。进一步确认某个具体改动时,再回到反编译器里看对应函数。这样既不会漏信息,也不会把时间浪费在重复分析上。

我个人在实际操作中最深的感受是:逆向分析更像“假设-验证-修正”的循环。静态分析提供假设,动态分析验证假设,两者有时会打架,但打架的地方恰恰是问题最值得深挖的地方。figugegl.1 和 figugegl.2 的分析过程并不算难,可它把逆向的完整链条走了一遍——从文件指纹、查壳脱壳、字符串线索、动态行为、算法还原到版本对比。这套流程跑熟练之后,再遇到更复杂的样本,思路也会清晰很多。

最后再说一个小技巧:每做完一个样本的分析,记得写一份只给自己看的“分析日志”,记录每个关键断点的地址、关键寄存器的值、异或密钥、脱壳的坑。下个样本很可能用得上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询