2. STM32架构基础:Cortex-M内核架构、存储器映射、启动流程与向量表

做固件逆向,说白了就是跟芯片的「底裤」打交道。你如果不了解STM32怎么启动、怎么访问内存、中断怎么跳转,那拿到一个固件bin文件基本就是两眼一抹黑。我个人习惯,拿到一个陌生固件的第一件事,就是先看它的向量表——这玩意儿能告诉你芯片型号、启动模式,甚至能帮你定位入口函数。

这一章我们就把Cortex-M内核的几个核心概念掰开揉碎。嗯,都是后面做混淆识别和逻辑还原的硬基础。

2.1 Cortex-M内核架构概览

STM32全系列都基于ARM Cortex-M内核。从M0到M7,指令集和性能有差异,但内核架构的骨架是一样的。

特性 Cortex-M0/M0+ Cortex-M3 Cortex-M4 Cortex-M7
指令集 ARMv6-M (Thumb/Thumb-2子集) ARMv7-M (Thumb-2) ARMv7E-M (Thumb-2 + DSP + FPU) ARMv7E-M (Thumb-2 + 双精度FPU)
流水线 3级 3级 + 分支预测 3级 + 分支预测 6级 + 分支预测 + 缓存
中断延迟 16周期 12周期 12周期 12周期
位带操作 不支持 支持 支持 支持
MPU 可选 标配 标配 标配

你想想看,M0和M7虽然都叫Cortex-M,但逆向分析时的关注点完全不同。M0的固件通常更小、更直接,没有复杂的缓存和分支预测干扰;M7的固件则可能包含大量浮点运算指令,反汇编时一眼就能看到VLDR、VADD这些指令。

个人经验: 我在逆向一个M4固件时,发现大量VSQRT指令,一开始以为是数学库,后来才发现是攻击者用浮点运算做控制流混淆。所以看到FPU指令别急着跳过,它可能是混淆的一部分。

2.2 存储器映射——固件在哪儿,数据在哪儿

STM32的存储器映射是固定的。说白了,就是芯片出厂时已经把4GB地址空间划分好了,每个区域干什么用,写死在硬件里。你改不了,但可以利用这个规律来定位固件。

以STM32F4系列为例,典型的映射是这样的:

起始地址 大小 区域 说明
0x0000 0000 1GB Code区 别名映射,实际指向Flash或SRAM
0x2000 0000 512MB SRAM区 运行时数据、堆栈
0x4000 0000 512MB 外设区 GPIO、USART、TIM等寄存器
0xE000 0000 512MB 系统区 NVIC、MPU、SysTick等内核外设

这里有个关键点:0x0000 0000开始的Code区其实是个别名映射。芯片上电后,它映射到Flash(通常是0x0800 0000)还是SRAM(0x2000 0000),由BOOT引脚决定。我见过不少逆向新手,看到固件里地址是0x0800 xxxx就以为是绝对地址,其实它和0x0000 xxxx是同一块物理存储。

避坑指南: 我曾经在分析一个IAP(在应用编程)固件时,发现代码里大量使用0x0801 0000这样的地址。一开始以为是Flash基址偏移,后来才发现这是Bootloader跳转到App的入口。如果你不熟悉存储器映射,很容易把跳转地址当成普通函数调用。

2.3 启动流程——从复位到main()的三步走

Cortex-M的启动流程比传统ARM简单得多。没有复杂的Bootloader,没有汇编启动代码的噩梦。说白了,就是三步:

  1. 读取向量表:从0x0000 0000取栈顶指针(MSP),从0x0000 0004取复位向量(Reset_Handler地址)。
  2. 执行Reset_Handler:初始化全局变量(.data段复制、.bss段清零),调用SystemInit()配置时钟。
  3. 跳转到main():调用__libc_init_array()(如果有C++全局构造),最后进入main()。

嗯,这里要注意:复位向量必须满足LSB=1,因为Cortex-M使用Thumb指令集,地址最低位必须是1表示Thumb状态。如果你在逆向时看到复位向量是偶数,那这个固件要么是损坏的,要么是被人为篡改过——我就在一个恶意固件里见过这种手法,攻击者把复位向量改成了奇数地址,导致芯片上电后直接跑飞。

逆向实战要点: 拿到一个固件bin文件,先看0x0000 0004处的4字节。如果它指向0x0800 xxxx且LSB=1,那基本可以确定是标准STM32固件。如果指向0x2000 xxxx,说明是从SRAM启动,常见于调试或Bootloader场景。

2.4 向量表——固件的「目录页」

向量表是Cortex-M启动和中断响应的核心。它从0x0000 0000开始,每4字节一个入口,前16个是系统异常,后面是外设中断。

标准向量表布局如下:

偏移 异常/中断 说明
0x00 栈顶指针 (MSP) 初始堆栈地址
0x04 复位 (Reset) 上电/复位后第一条指令
0x08 NMI 不可屏蔽中断
0x0C 硬错误 (HardFault) 未处理的异常都会落在这里
0x10 MemManage MPU违规(M3/M4/M7)
0x14 总线错误 (BusFault) 指令预取或数据访问错误
0x18 用法错误 (UsageFault) 未定义指令、对齐错误等
... ... ...
0x40 SVCall 系统调用(SVC指令触发)
0x44 调试监视器 (DebugMon) 调试断点/观察点
0x48 PendSV 可挂起系统调用(常用于RTOS上下文切换)
0x4C SysTick 系统滴答定时器
0x50+ 外设中断 (IRQ0~IRQn) 芯片外设中断入口

我个人习惯,拿到固件后先提取向量表,看看HardFault_Handler和PendSV这两个入口。为什么?因为混淆代码经常故意触发异常来跳转,或者用PendSV做任务切换。如果HardFault入口指向一个非对齐地址,那基本可以断定固件被篡改过。

技巧: 用Python解析固件时,可以这样提取向量表:
import struct

with open('firmware.bin', 'rb') as f:
    data = f.read()

# 读取前16个异常向量
for i in range(16):
    addr = struct.unpack('<I', data[i*4:(i+1)*4])[0]
    print(f'Vector[{i}]: 0x{addr:08X}')
注意检查每个地址的LSB是否为1,如果不是,说明固件可能被混淆或损坏。

2.5 核心知识体系图

下面这张图把本章的核心知识点串起来了。你可以把它当作逆向分析的「地图」——从内核架构到存储器映射,再到启动流程和向量表,每一步都对应着固件分析的关键线索。

STM32 Cortex-M 核心知识体系 STM32 Cortex-M 核心知识体系 Cortex-M 内核架构 M0/M3/M4/M7 差异 存储器映射 Code/SRAM/外设/系统区 启动流程 复位→初始化→main() 向量表(固件目录页) 0x0000 0000 开始,每4字节一个入口 系统异常 (前16个) Reset/HardFault/SVCall 外设中断 (IRQ) GPIO/USART/TIM等 特殊入口 PendSV/SysTick 逆向关注点 LSB=1检查 | 异常入口定位 | 存储器别名映射 | 启动模式识别 提示:每个节点都对应固件逆向中的关键线索,建议结合实战固件反复对照

2.6 实战:从向量表识别固件类型

最后,我们用一个真实案例把知识串起来。我曾经拿到一个未知的.bin文件,大小64KB。按照上面的方法,先提取向量表:

偏移 0x00: 0x2000 1000  → 栈顶指针,指向SRAM,正常
偏移 0x04: 0x0800 01C5  → 复位向量,LSB=1,指向Flash
偏移 0x08: 0x0800 01C9  → NMI,LSB=1
偏移 0x0C: 0x0800 01CD  → HardFault,LSB=1
...
偏移 0x4C: 0x0800 02A1  → SysTick,LSB=1
偏移 0x50: 0x0800 02A5  → IRQ0,LSB=1

从栈顶指针0x2000 1000可以推断SRAM大小为4KB以上(常见于STM32F0系列)。复位向量指向0x0800 01C5,说明固件基址在0x0800 0000,且代码段起始偏移约0x1C4。嗯,这里要注意:复位向量指向的地址减去1才是真正的指令地址(因为LSB=1表示Thumb状态)。所以实际入口是0x0800 01C4。

再结合向量表里只有16个系统异常和少量外设中断(IRQ只到IRQ10),基本可以确定这是一个STM32F030系列的固件,没有RTOS(因为PendSV和SysTick的入口很常规)。

总结一下: 向量表就是固件的「指纹」。通过它,你能在5分钟内判断出芯片型号、启动模式、是否跑RTOS、甚至有没有被篡改。后面做混淆识别时,我们还会反复用到这个技巧。