RISC-V 在 IoT 和嵌入式领域的市场份额持续增长,逆向分析 RISC-V 固件的需求也随之而来。Ghidra 从早期版本就内置了 RISC-V 支持,本文梳理用 Ghidra 分析 RISC-V 固件的完整流程。
RISC-V 指令集速览
RISC-V 是模块化的指令集架构,由基础指令集 + 标准扩展组成:
- RV32I / RV64I — 基础整数指令集(32/64位),约 40 条指令
- M — 乘除法扩展
- A — 原子操作扩展
- F / D — 单/双精度浮点
- C — 压缩指令(16位短指令),类似 ARM Thumb
常见组合是 RV32IMAC(嵌入式 MCU)和 RV64GC(应用级处理器,G = IMAFD)。
逆向时需要关注的特点:
- 固定长度 vs 压缩 — 基础指令 32 位对齐,C 扩展引入 16 位指令,反汇编器需要处理混合长度
- 寄存器 ABI 名 — x0-x31 有 ABI 别名(ra, sp, a0-a7, t0-t6 等),Ghidra 默认使用 ABI 名
- PC 相对寻址 —
auipc+jalr组合实现长跳转,反编译器需要识别这个模式 - relaxation — 链接器优化可能将远跳转收缩为近跳转,需注意
Ghidra 的 RISC-V 支持
Ghidra 内置的 RISC-V 处理器模块覆盖:
- RV32 / RV64
- IMACFD 标准扩展
- 特权指令(CSR 读写)
- 压缩指令(C 扩展)
创建项目时选择处理器:RISCV -> RV32 或 RV64,变体选择对应的扩展组合(如 RV32GC)。
Ghidra 的 SLEIGH 反汇编框架对 RISC-V 的支持比较完善,标准指令的反汇编和反编译准确度高。但部分厂商自定义扩展(如 ESP32-C3 的 Espressif 扩展)需要手动添加 SLEIGH 定义。
固件加载与内存映射
RISC-V 嵌入式固件通常是裸二进制(raw binary)或 ELF 格式。
ELF 固件
ELF 格式相对简单,Ghidra 可以自动解析段和符号。导入时选择 ELF 格式,Ghidra 会自动设置正确的内存布局。
如果固件 strip 了符号,仍然可以从 section header 和 dynamic 信息中恢复部分结构。
Raw Binary 固件
裸二进制需要手动指定加载地址和内存映射。步骤:
- 确定加载基址 — 查阅芯片 datasheet 的 memory map。例如 ESP32-C3 的 Flash 映射地址是
0x42000000,IRAM 起始是0x40380000 - 导入设置 — 选
Raw Binary,Language 选RISCV:LE:32:RV32GC(以 32 位小端为例),Options 中设置 Base Address - 追加内存区域 — 在 Memory Map 窗口手动创建 RAM、外设寄存器等区域
典型的 RISC-V MCU 内存布局:
| 区域 | 地址范围 | 说明 |
|---|---|---|
| Flash/ROM | 0x2000_0000 - 0x2FFF_FFFF | 代码和只读数据 |
| SRAM | 0x8000_0000 - 0x8001_FFFF | 运行时内存 |
| Peripheral | 0x1000_0000 - 0x1FFF_FFFF | MMIO 外设寄存器 |
| CLINT | 0x0200_0000 | Core Local Interruptor |
| PLIC | 0x0C00_0000 | Platform-Level Interrupt Controller |
注意:不同厂商的地址映射差异很大,一定要查对应芯片的手册。
外设寄存器标注
嵌入式固件逆向中,标注外设寄存器是理解功能的关键。
手动标注
从 datasheet 或 SVD(System View Description)文件中获取寄存器定义。例如某 UART 外设:
在 Ghidra 中:
- 在 Memory Map 中创建对应外设地址范围的 block
- 在对应地址上创建 Label(如
UART0_TXDATA、UART0_RXDATA) - 定义数据类型(通常为 uint32)
SVD 自动导入
社区有 Ghidra 脚本可以解析 SVD/XML 文件并自动创建所有外设寄存器标签:
- SVD-Loader — 解析 CMSIS-SVD 文件,虽然 SVD 主要面向 ARM,但很多 RISC-V 芯片厂商(如 GigaDevice GD32VF103)也提供 SVD 文件
- 手动映射 — 对于没有 SVD 的芯片,可以从头文件(
.h)中提取寄存器定义,用 Ghidra Script 批量创建
标注完成后,反编译视图中的裸地址访问会变成有意义的寄存器名,可读性大幅提升。
分析流程
一个完整的 RISC-V 固件逆向流程:
1. 信息收集
- 识别芯片型号(丝印、FCC ID、拆解照片)
- 获取 datasheet 和 memory map
- 确定指令集变体(RV32IMAC?RV64GC?)
2. 固件提取
- SPI Flash 读取(CH341A 编程器 + flashrom)
- JTAG/SWD 调试口 dump
- OTA 固件包解包
- UART bootloader dump
3. Ghidra 导入与初始配置
- 设置正确的处理器和加载地址
- 配置内存映射
- 运行 Auto Analysis
4. 入口点定位
RISC-V 的复位向量通常在固定地址(如 0x80000000 或 0x20000000),也可能有中断向量表。查看起始地址的代码:
- 设置
sp(栈指针) - 清零
.bss段 - 复制
.data段到 RAM - 跳转到
main()
5. 功能模块识别
- 中断处理 — 搜索
mtvecCSR 的写入,定位中断向量表 - 外设初始化 — 通过外设寄存器标签追踪初始化顺序
- 通信协议 — UART/SPI/I2C 的发送接收函数通常有特征模式
- 加密/签名 — 搜索常量(如 AES S-Box、SHA 初始值)
6. 交叉引用与重命名
Ghidra 的交叉引用是核心武器。对已识别的函数和数据进行重命名,逐步构建对固件的整体理解。
与 ARM 逆向的对比
| 维度 | RISC-V | ARM (Cortex-M) |
|---|---|---|
| 指令集规则性 | 非常规则,编码清晰 | 较复杂(ARM/Thumb/Thumb-2混合) |
| Ghidra 支持成熟度 | 好,标准指令完善 | 非常好,生态最成熟 |
| 调试工具 | OpenOCD 支持,但不如 ARM 丰富 | J-Link/ST-Link/OpenOCD 全覆盖 |
| 厂商碎片化 | 较少,多数遵循标准 | 标准化程度高(CMSIS) |
| 自定义扩展 | 常见,增加逆向难度 | 少见,指令集固定 |
| 参考资料 | 增长中,但远不如 ARM | 非常丰富 |
RISC-V 逆向的主要挑战在于厂商自定义扩展和相对较少的社区经验积累。但 RISC-V 指令集的规则性实际上让反汇编和反编译更容易——没有 ARM 的 Thumb 模式切换问题,编码格式统一。
实用技巧
- 始终检查 C 扩展 — 很多 RISC-V MCU 使用 RV32IMAC,如果用 RV32IMA 导入会导致 16 位压缩指令被错误解析
- 关注
auipc+addi模式 — Ghidra 通常能正确处理,但在数据引用中偶尔会漏掉 - 利用字符串 — 嵌入式固件中的调试打印字符串是定位功能模块的捷径
- 比较 SDK — 如果芯片有开源 SDK(如 ESP-IDF、RT-Thread),编译一份对比分析,可以快速识别库函数
- RISC-V 特权规范 — 理解 CSR 寄存器(mstatus, mtvec, mepc 等)对分析中断和异常处理至关重要