第4章 固件加载与初步分析:把nRF52请进反汇编工具

拿到一份nRF52的固件,第一件事是什么?不是急着看代码,而是把它正确地加载到反汇编工具里。这一步要是错了,后面全是白费功夫。我见过不少新手,固件加载完发现反汇编出来的指令全是乱码,还以为是固件加密了——其实就是架构选错了。

这一章,我带你走一遍完整的加载流程。咱们用Ghidra和IDA Pro都演示一下,毕竟这两个工具在实际项目中都会用到。

4.1 选择正确的处理器架构

nRF52系列用的是ARM Cortex-M4内核,支持Thumb-2指令集。这里有个坑:Cortex-M4只支持Thumb模式,不支持ARM模式。你加载固件时如果选了ARM模式,反汇编出来的代码全是错的。

关键点:nRF52832、nRF52840、nRF52810等所有nRF52系列芯片,处理器架构统一选择ARM Cortex-M4,指令集选Thumb-2。

我个人习惯在Ghidra里这样配置:

Language: ARM
Variant: Cortex-M4
Endian: Little
Instruction Set: Thumb-2

在IDA Pro里,新建工程时选择:

Processor type: ARM
CPU: ARMv7-M (Cortex-M4)
Instruction set: Thumb-2

为什么一定要选Little Endian?nRF52的Flash和RAM都是小端模式。你想想看,如果选了大端,读出来的32位数据高低字节是反的,那还分析什么?

4.2 内存映射配置:让工具认识芯片的“地图”

固件不是凭空运行的,它依赖特定的内存布局。nRF52的内存映射是固定的,我们需要告诉反汇编工具:Flash在哪、RAM在哪、外设寄存器在哪。

先看一张我手绘的内存映射图,这样你脑子里有个整体印象:

nRF52 内存映射结构图 Flash (Code) 0x00000000 - 0x0007FFFF 512KB (nRF52840) 存储固件代码、常量、FICR/UICR RAM (Data) 0x20000000 - 0x2003FFFF 256KB (nRF52840) 堆、栈、全局变量、BSS段 外设寄存器 0x40000000 - 0x400FFFFF GPIO、UART、SPI、I2C、TIMER NVIC、SysTick、RADIO、AES 配置要点 1. Flash基址: 0x00000000 大小根据型号设置 2. RAM基址: 0x20000000 大小根据型号设置 3. 外设寄存器: 0x40000000 通常映射为只读 4. 向量表在Flash起始 0x00000000处 5. 入口点由向量表决定 不是固定地址

4.3 在Ghidra中配置内存映射

Ghidra加载固件后,默认不会自动识别nRF52的内存布局。你需要手动添加内存块。我一般按这个步骤来:

  1. 打开Memory Map面板:Window → Memory Map,或者按快捷键Ctrl+Shift+M。
  2. 删除默认块:Ghidra会把整个固件当成一个连续块,先删掉它。
  3. 添加Flash块:点击Add Block,设置如下:
参数值
NameFLASH
Start Address0x00000000
Length0x80000 (512KB,以nRF52840为例)
PermissionsRead, Execute
TypeDEFAULT
  1. 添加RAM块:
参数值
NameRAM
Start Address0x20000000
Length0x40000 (256KB)
PermissionsRead, Write, Execute
TypeRAM
  1. 添加外设寄存器区域(可选,但强烈建议):
参数值
NamePERIPHERALS
Start Address0x40000000
Length0x100000 (1MB)
PermissionsRead, Write
TypeOTHER

我的经验:外设寄存器区域建议映射为Read-Write,但不要勾Execute。有些固件会通过函数指针跳转到外设地址,如果勾了Execute,Ghidra会尝试反汇编那些寄存器地址,产生大量垃圾代码。

4.4 在IDA Pro中配置内存映射

IDA Pro的操作稍微不同。加载固件时,IDA会弹出一个对话框让你设置ROM和RAM的起始地址和大小。我通常这样填:

ROM start address: 0x00000000
ROM size: 0x80000
RAM start address: 0x20000000
RAM size: 0x40000

如果加载完成后发现还需要调整,可以进Edit → Segments → Rebase Program来修改。不过我个人建议一开始就填对,后面改来改去容易出问题。

IDA Pro还有一个好处:它内置了ARM Cortex-M的外设寄存器描述文件(SVD文件)。你可以通过File → Load File → Load SVD File来加载Nordic官方提供的nRF52840.svd。这样反汇编时,外设寄存器地址会自动显示为寄存器名称,比如0x40000500会显示为UART0->TXD,分析起来方便很多。

注意:如果你用的是破解版IDA Pro,SVD加载功能可能被阉割。我遇到过这种情况,最后只能手动给寄存器地址加注释。嗯,有条件的话还是支持正版吧。

4.5 固件入口点分析:找到程序从哪里开始跑

固件加载完了,内存映射也配好了,接下来最关键的一步:找到入口点。

Cortex-M4的启动流程和ARM7、ARM9不一样。它不是从0x00000000开始执行第一条指令,而是从0x00000000读取栈指针(MSP),从0x00000004读取复位向量(Reset_Handler)。说白了,真正的入口点在向量表的第二个字里。

我一般这样找入口点:

  1. 跳到地址0x00000000,看到的是栈指针的初始值,比如0x20004000。
  2. 跳到地址0x00000004,看到的是复位向量的地址,比如0x00000123。
  3. 注意:这个地址的最低位必须是1(表示Thumb模式)。如果看到0x00000122,说明固件有问题或者加载错了。
  4. 跳转到复位向量地址(去掉最低位的1,即0x00000122 → 0x00000122 & 0xFFFFFFFE = 0x00000122),这里就是固件的入口点。

举个例子,我手头有一个nRF52840的固件,加载后看到:

0x00000000:  0x20004000  (栈指针初始值)
0x00000004:  0x00000123  (复位向量,Thumb模式)

那么入口点就是0x00000122。跳过去一看,果然是复位处理函数的开头:

0x00000122:  ldr   r0, =0x40000400
0x00000124:  ldr   r1, [r0]
0x00000126:  orr   r1, r1, #1
0x00000128:  str   r1, [r0]
...  (时钟初始化代码)

核心要点:入口点不是0x00000000,也不是0x00000004,而是向量表第二个字指向的地址。这个地址的最低位必须是1,否则固件无法启动。

4.6 验证加载是否正确

配置完成后,别急着开始分析。先做几个快速验证,确保工具认识你的固件:

  • 检查向量表:0x00000000到0x00000040之间应该是连续的异常向量,每个都是合法的Thumb地址(最低位为1)。如果看到0xFFFFFFFF或者0x00000000,说明固件可能被截断了。
  • 检查代码密度:Cortex-M4的Thumb-2指令是16位或32位混合的。随便看几个地址,应该能看到合理的指令序列,而不是大片的0x00或0xFF。
  • 检查字符串:在固件里搜索常见的ASCII字符串,比如"nRF52"、"SoftDevice"、"Nordic"等。如果能找到,说明加载基本正确。

我曾经遇到过一个固件,加载后向量表全是0xFFFFFFFF。折腾了半天才发现是固件文件本身被加密了,需要先解密才能分析。嗯,那是另一个故事了。

4.7 本章小结

固件加载这一步,说白了就是让反汇编工具认识你的芯片。架构选Cortex-M4 Thumb-2,Flash从0x00000000开始,RAM从0x20000000开始,入口点从向量表里找。这三件事做对了,后面的分析才能顺利进行。