Ghidra逆向RISC-V固件

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)。

逆向时需要关注的特点:

  1. 固定长度 vs 压缩 — 基础指令 32 位对齐,C 扩展引入 16 位指令,反汇编器需要处理混合长度
  2. 寄存器 ABI 名 — x0-x31 有 ABI 别名(ra, sp, a0-a7, t0-t6 等),Ghidra 默认使用 ABI 名
  3. PC 相对寻址auipc + jalr 组合实现长跳转,反编译器需要识别这个模式
  4. relaxation — 链接器优化可能将远跳转收缩为近跳转,需注意

Ghidra 的 RISC-V 支持

Ghidra 内置的 RISC-V 处理器模块覆盖:

  • RV32 / RV64
  • IMACFD 标准扩展
  • 特权指令(CSR 读写)
  • 压缩指令(C 扩展)

创建项目时选择处理器:RISCV -> RV32RV64,变体选择对应的扩展组合(如 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 固件

裸二进制需要手动指定加载地址和内存映射。步骤:

  1. 确定加载基址 — 查阅芯片 datasheet 的 memory map。例如 ESP32-C3 的 Flash 映射地址是 0x42000000,IRAM 起始是 0x40380000
  2. 导入设置 — 选 Raw Binary,Language 选 RISCV:LE:32:RV32GC(以 32 位小端为例),Options 中设置 Base Address
  3. 追加内存区域 — 在 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 中:

  1. 在 Memory Map 中创建对应外设地址范围的 block
  2. 在对应地址上创建 Label(如 UART0_TXDATAUART0_RXDATA
  3. 定义数据类型(通常为 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 的复位向量通常在固定地址(如 0x800000000x20000000),也可能有中断向量表。查看起始地址的代码:

  • 设置 sp(栈指针)
  • 清零 .bss
  • 复制 .data 段到 RAM
  • 跳转到 main()

5. 功能模块识别

  • 中断处理 — 搜索 mtvec CSR 的写入,定位中断向量表
  • 外设初始化 — 通过外设寄存器标签追踪初始化顺序
  • 通信协议 — 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 模式切换问题,编码格式统一。

实用技巧

  1. 始终检查 C 扩展 — 很多 RISC-V MCU 使用 RV32IMAC,如果用 RV32IMA 导入会导致 16 位压缩指令被错误解析
  2. 关注 auipc+addi 模式 — Ghidra 通常能正确处理,但在数据引用中偶尔会漏掉
  3. 利用字符串 — 嵌入式固件中的调试打印字符串是定位功能模块的捷径
  4. 比较 SDK — 如果芯片有开源 SDK(如 ESP-IDF、RT-Thread),编译一份对比分析,可以快速识别库函数
  5. RISC-V 特权规范 — 理解 CSR 寄存器(mstatus, mtvec, mepc 等)对分析中断和异常处理至关重要