第四章 静态分析工具链:Ghidra 与 IDA Pro 的安装与配置、反汇编引擎(Capstone)的使用、固件加载脚本编写
好,咱们进入第四章。这一章我打算聊聊静态分析工具链。说白了,就是把你手里的固件二进制,变成人能读懂的汇编代码。我见过不少新手,拿到固件直接扔进 hex 编辑器里看十六进制,那效率太低了。咱们得用对工具。
我个人习惯,桌面放两个反汇编器:IDA Pro 和 Ghidra。IDA 是老牌劲旅,Ghidra 是 NSA 开源的神器。两个都装上,互相印证。你想想看,一个工具分析不出来的地方,另一个可能就搞定了。
4.1 Ghidra 的安装与配置
Ghidra 是 Java 写的,所以你得先装 JDK 11 或更高版本。我建议装 JDK 17,稳定一些。下载 Ghidra 的 release 包,解压就能用,不需要安装。
配置方面,有几个关键点:
- 内存分配: 修改
ghidra/ghidraRun.bat(Windows)或ghidraRun(Linux)中的-Xmx参数。我一般设成-Xmx8G,分析 32MB 以下的固件绰绰有余。 - 处理器模块: Ghidra 自带大量处理器定义。但如果你遇到冷门 MCU,比如某国产 RISC-V 变种,可能需要手动加载 .sla 文件。我曾经在分析一个智能电表固件时,就遇到过这种情况,折腾了半天才找到正确的处理器模块。
- 插件管理: 我推荐装一个
GhidraEmu插件,可以在 Ghidra 里直接模拟执行代码片段,调试起来很方便。
4.2 IDA Pro 的安装与配置
IDA Pro 是商业软件,价格不菲。但它的反汇编质量确实高,尤其是 F5 插件,能把汇编转成伪 C 代码,阅读起来舒服很多。
安装 IDA 时,注意选择正确的版本:
| 版本 | 适用场景 | 我的建议 |
|---|---|---|
| IDA Pro 7.7 | 通用逆向,支持 ARM、x86、MIPS 等主流架构 | 主力版本,稳定 |
| IDA Pro 8.x | 新增了更现代的 UI 和调试器 | 尝鲜可以,但部分旧插件不兼容 |
| IDA Free | 只支持 x86,功能受限 | 不推荐,不如用 Ghidra |
4.3 反汇编引擎 Capstone 的使用
Ghidra 和 IDA 都是集成环境。但有时候,你需要自己写脚本,批量反汇编固件片段。这时候 Capstone 就派上用场了。
Capstone 是一个轻量级、多平台的反汇编框架。它支持 ARM、x86、MIPS、PowerPC 等主流架构。我常用它来写固件加载脚本。
下面是一个简单的 Python 示例,用 Capstone 反汇编一段 ARM Thumb 代码:
from capstone import *
# 固件中的一段二进制数据
CODE = b"\x10\xb5\x04\x46\x0c\x48\x0d\x49"
ADDRESS = 0x1000
# 创建 Capstone 实例,指定架构为 ARM,模式为 Thumb
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
# 开始反汇编
for insn in md.disasm(CODE, ADDRESS):
print("0x%x:\t%s\t%s" % (insn.address, insn.mnemonic, insn.op_str))
输出结果:
0x1000: push {r4, lr}
0x1002: mov r4, r0
0x1004: ldr r0, [pc, #0x30]
0x1006: ldr r1, [pc, #0x34]
你看,就这么几行代码,就能把二进制变成可读的汇编指令。我在写固件加载脚本时,经常用 Capstone 来验证加载地址是否正确。
Cs 类需要两个参数:架构和模式。ARM 架构下,模式可以是 CS_MODE_ARM(32位 ARM 指令)或 CS_MODE_THUMB(16位 Thumb 指令)。搞错了模式,反汇编出来的指令就是错的。
4.4 固件加载脚本编写
固件加载脚本,说白了就是告诉反汇编器:这个二进制文件是什么结构,代码从哪个地址开始,数据段在哪里。没有正确的加载脚本,反汇编器就是一盘散沙。
我以 Ghidra 的 Python 脚本为例,展示如何编写一个简单的固件加载器:
# 加载固件到 Ghidra
from ghidra.program.model.listing import Program
from ghidra.program.model.address import Address
from ghidra.program.model.mem import MemoryBlock
from ghidra.util.task import TaskMonitor
def load_firmware():
# 获取当前程序
program = getCurrentProgram()
# 获取内存管理器
memory = program.getMemory()
# 创建代码段,起始地址 0x08000000,大小 0x10000
code_block = memory.createInitializedBlock(
".text",
addr(0x08000000),
0x10000,
"Firmware Code",
"Firmware",
True,
False,
TaskMonitor.DUMMY
)
# 设置代码段为可读可执行
code_block.setPermissions(True, False, True)
# 创建数据段,起始地址 0x20000000,大小 0x4000
data_block = memory.createInitializedBlock(
".data",
addr(0x20000000),
0x4000,
"Firmware Data",
"Firmware",
True,
False,
TaskMonitor.DUMMY
)
# 设置数据段为可读可写
data_block.setPermissions(True, True, False)
print("固件加载完成!")
# 执行加载
load_firmware()
这个脚本做了三件事:
- 创建了一个名为
.text的代码段,起始地址 0x08000000(这是 STM32 的 Flash 起始地址)。 - 创建了一个名为
.data的数据段,起始地址 0x20000000(这是 STM32 的 SRAM 起始地址)。 - 设置了内存块的权限:代码段可读可执行,数据段可读可写。
下面这张图,展示了固件加载脚本的核心逻辑:
嗯,这里要注意。加载脚本不是万能的。有些固件用了自定义的压缩或加密,你得先解压或解密,才能用加载脚本。我遇到过一种情况,固件头里有个标志位,指示固件是否加密。如果标志位为 1,就需要先 XOR 一个密钥,才能得到真正的代码。
好了,这一章的内容就这些。工具链搭好了,脚本写好了,下一步就是真正的逆向分析了。记住,工具只是辅助,关键还是你对架构和协议的理解。多动手,多踩坑,自然就熟练了。