固件分析之加载基地址与符号恢复
字数 3760
更新时间 2026-04-15 13:39:12
固件分析之加载基地址与符号恢复教学文档
第一章:核心概念与背景
1.1 基地址定义
基地址通常是指程序在内存中的起始地址。在嵌入式系统安全分析中,准确获取固件在内存中的加载基地址是进行逆向工程、漏洞分析及功能理解的基础前提。
1.2 应用场景区分
- Linux系统设备:通常具有固定的内存布局,内核、用户空间及其他关键数据区域的位置在系统启动时即已确定,一般不需要进行基地址恢复。
- RTOS/ECOS系统设备:特别是运行VxWorks的嵌入式设备,固件在内存中的加载地址不是固定的。加载地址的偏差会导致对绝对地址(如跳转函数表、字符串表)的引用全部失效。因此,在研究此类固件时,基地址与符号表的恢复是至关重要的第一步。
第二章:基地址恢复方法详解
2.1 方法一:跳转表推理法
本方法主要利用switch跳转表来确定固件的加载地址,尤其适用于ARM架构的VxWorks固件。
操作步骤:
- 架构确认:使用
binwalk -A命令分析固件,确认目标系统架构。 - 定位跳转表:在反汇编器(如IDA Pro、Ghidra)中搜索典型的
switch代码结构,寻找包含绝对地址跳转指令的跳转表。 - 计算基地址:
- 在跳转表中选取一个绝对跳转地址,例如
0x8B2BE0。 - 查看该地址指令在固件二进制文件中的偏移,例如
0xB0BEC。 - 初步计算:
基址 = 绝对跳转地址 - 文件偏移,即0x8B2BE0 - 0xB0BEC = 0x801FF4。 - 进行页对齐(通常为
0x1000):0x801FF4 & 0xFFF000 = 0x801000。此即推测的加载基地址。
- 在跳转表中选取一个绝对跳转地址,例如
- 验证与优化:
- 使用计算出的基地址对固件进行
rebase(重定位)操作。 - 检查跳转表中的一个地址,跳转后是否指向函数的起始位置。如果指向函数中间,则说明单一跳转表推测存在误差。
- 核心要点:编译器优化可能导致跳转表地址对齐或变形,因此必须选取多个跳转表作为样本进行交叉验证,以找到最佳匹配的基地址。
- 使用计算出的基地址对固件进行
自动化工具:
- ArmBaseFinder:Github上的IDA插件,包含通过跳转表寻找基址的算法。
- basefind:对上述算法用C语言重写以提升效率的工具。
2.2 方法二:(字符串表+符号表)恢复法
这是针对VxWorks固件最常用、最可靠的基地址恢复方法,其原理是利用固件内建的函数符号表。
实例流程(以Schneider M241为例):
- 初步分析:
- 使用
binwalk查看固件信息,尝试获取架构和可能的符号表地址提示(注:binwalk解析结果可能存在误差,需人工确认)。 - 使用IDA Pro加载固件,加载地址暂设为
0x0。此时所有函数均显示为sub_xxxx的形式。
- 使用
- 定位符号表:
- 使用
vxhunter插件或Ghidra的VxWorksSymtab_Finder脚本,扫描固件,获取符号表的起始地址。即使符号恢复失败,通常也能得到此地址。
- 使用
- 解析符号表结构:
- 导航到符号表起始地址。VxWorks的符号表条目是一个结构体(不同版本大小不同,常见为20字节)。
- 每个结构体通常包含:
函数名指针地址、函数入口地址、符号类型等字段。 - 关键观察:符号表条目通常按函数名在字符串表中的顺序进行绑定。需要确定绑定顺序是正序(第一个符号绑定字符串表第一个字符串)还是逆序。
- 定位字符串表:
- 使用IDA的字符串视图(
Strings window)或系统的strings命令,找到字符串表的第一个字符串,并记录其在文件中的偏移地址(例如0xE8CC8)。
- 使用IDA的字符串视图(
- 计算基地址:
- 在符号表第一个条目中,读取
函数名指针地址(例如0x8EAC88,注意字节序)。 - 计算基址:
基址 = 符号表中的函数名指针地址 - 字符串表第一个字符串的文件偏移,即0x8EAC88 - 0xE8CC8 = 0x801FC0。
- 在符号表第一个条目中,读取
- 恢复符号:
- 在IDA中修改镜像的加载基地址为计算出的
0x801FC0。 - 运行符号恢复脚本(附录提供),将符号表中的函数名与地址关联,完成重命名。
- 在IDA中修改镜像的加载基地址为计算出的
注意事项(以Schneider NOE-771为例):
binwalk可能给出符号表地址,但需用010editor等工具人工验证其准确性。- 某些固件的符号表绑定顺序可能是逆序的,即符号表第一个条目绑定的是字符串表的最后一个字符串。此时需要用字符串表最后一个字符串的偏移来进行基址计算。
2.3 方法三:SP栈指针获取基址
本方法适用于PowerPC(PPC)等架构的VxWorks固件,依据的是VxWorks系统的启动流程内存布局。
原理与步骤:
- VxWorks设备启动后,执行的第一个C函数是
usrInit()。 - 根据VxWorks官方文档,
usrInit()函数的初始化栈(initial stack)与固件镜像在内存中是相邻的,且栈顶指针(SP)指向初始化栈的底部。 - 操作:
- 分析固件入口代码(通常是
_sysInit函数),找到设置栈指针(SP) 的指令。例如,在PPC架构中,指令lis r1, 1可能将SP设置为0x10000。 - 这个SP的值(
0x10000)即为初始化栈的地址,根据文档描述,此地址即为固件的加载基地址。
- 分析固件入口代码(通常是
2.4 方法四:BSS段数据清零获取基址
此方法同样基于usrInit()函数的执行逻辑。
原理与步骤:
usrInit()函数的第一步操作是清除.bss段(存储未初始化全局/静态变量的内存区域)。- 操作:
- 在反汇编代码中定位负责清零
.bss段的函数。 - 分析该函数的参数。例如,在PPC架构中,r3、r4寄存器通常传递起始地址和长度。由此可获知
.bss段的起始地址和结束地址。 - 计算基址:
基址 = .bss段结束地址 - 固件镜像文件长度。 - 固件镜像文件长度可通过文件大小或
binwalk -e提取后查看获得。
- 在反汇编代码中定位负责清零
第三章:符号表恢复脚本
以下是适用于IDA Pro的Python脚本,用于在正确设置基地址后,解析符号表并重命名函数。
from idaapi import *
import ida_bytes
import ida_ua
import ida_funcs
import ida_segment
import idc
# ====== 用户需修改的配置 ======
loadaddress = 0x801FC0 # 计算出的固件加载基址
eaStart = 0x + loadaddress # 符号表起始地址(虚拟地址)
eaEnd = 0x + loadaddress # 符号表结束地址(虚拟地址)
ENTRY_SIZE = 20 # 符号表每个条目的大小(字节),常见为20
# ==============================
ea = eaStart
while ea < eaEnd:
# 读取符号名地址和函数地址 (注意字节序,根据固件调整get_32bit/64bit)
str_addr = ida_bytes.get_32bit(ea + 4) # 假设name*在偏移4字节处
eaFunc = ida_bytes.get_32bit(ea + 8) # 假设value*在偏移8字节处
# 调试输出
print("[DEBUG] EA: 0x{:X}, str_addr=0x{:X}, eaFunc=0x{:X}".format(ea, str_addr, eaFunc))
# 跳过无效条目
if str_addr in (0, 0xFFFFFFFF) or eaFunc in (0, 0xFFFFFFFF):
ea += ENTRY_SIZE
continue
# 检查段映射
if ida_segment.getseg(str_addr) is None:
print("[WARN] str_addr 0x{:X} not in any segment".format(str_addr))
ea += ENTRY_SIZE
continue
# 获取符号名
func_name = idc.get_strlit_contents(str_addr, -1, idc.ASCSTR_C)
if func_name:
func_name = func_name.decode('utf-8', errors='ignore').strip('\\x00')
# 创建函数(如果地址尚未被识别为函数)
ida_funcs.add_func(eaFunc)
# 重命名函数
idc.set_name(eaFunc, func_name, idc.SN_NOWARN)
print("[+] Renamed 0x{:X} to {}".format(eaFunc, func_name))
else:
print("[-] Failed to get name at 0x{:X}".format(str_addr))
ea += ENTRY_SIZE
脚本使用说明:
- 根据固件架构(ARM/PPC,大端/小端)调整
ida_bytes.get_32bit的字节序,或使用get_32bit_le/get_32bit_be。 - 准确填写
loadaddress、eaStart、eaEnd和ENTRY_SIZE。ENTRY_SIZE需根据VxWorks版本确定(参考附录结构体)。 - 在IDA的Python脚本窗口中运行此脚本。
第四章:VxWorks符号表结构体参考
不同版本的VxWorks,其内核符号表结构体有所不同,恢复时需对应。
VxWorks 5.x 符号表结构体:
typedef struct symEntry5x {
char *name; // 符号名字符串首地址(4字节)
void *value; // 符号值/函数地址(4字节)
int type; // 符号类型(4字节)
struct symEntry5x *next; // 链表下一个节点(4字节)
} SYM_ENTRY5X; // 总计16字节
VxWorks 6.x 符号表结构体:
typedef struct symbol6x {
char *name; // 符号名字符串首地址(4字节)
void *value; // 符号值/函数地址(4字节)
unsigned short type; // 符号类型(2字节)
unsigned short flags; // 标志位(2字节)
struct module *mod; // 所属模块(4字节)
} SYMBOL6X; // 总计16字节
typedef struct symTbl6x {
SYMBOL6X *symbols; // 符号数组起始地址(4字节)
int nSymbols; // 符号数量(4字节)
} SYMTAB6X;
常见符号类型宏:N_TEXT(函数)、N_DATA(已初始化数据)、N_BSS(未初始化数据)等。
第五章:方法总结与备选方案
5.1 方法对比
| 方法 | 优点 | 缺点/注意事项 | 适用场景 |
|---|---|---|---|
| 跳转表推理 | 无需符号表,直接分析代码 | 存在编译器优化误差,需多样本校验 | ARM架构VxWorks固件 |
| 符号表恢复 | 最准确、可恢复函数名 | 依赖符号表未被剥离,需确定结构体 | 大部分VxWorks固件 |
| SP栈指针 | 原理清晰,直接 | 依赖对启动代码的准确识别 | PPC等架构,有清晰启动代码 |
| BSS段计算 | 另一种推导方式 | 需准确识别bss清零函数及参数 | 可作为验证或补充手段 |
5.2 其他备选方法
- 查看设备用户手册或技术文档:厂商文档可能直接给出内存映射信息。
- 分析设备串口调试输出:在Bootloader阶段,串口日志可能打印出加载地址。
- 固件魔术头分析:某些固件头部包含加载地址信息。
- 动态调试:如果设备支持调试,可通过JTAG等手段直接获取运行时的内存地址。
第六章:实践流程建议
- 信息收集:首先使用
binwalk、file等工具了解固件架构、操作系统、是否包含符号表提示。 - 方法选择:
- 如果发现明显的
switch跳转表,可尝试方法一进行快速推测,并用其他方法验证。 - 如果
binwalk提示或有迹象表明存在符号表,优先采用方法二,这是最标准的方法。 - 对于PPC等架构,可查看入口函数,尝试方法三或方法四。
- 如果发现明显的
- 交叉验证:使用不同方法计算出的基地址应相互印证。例如,用方法二计算出基址后,可以验证跳转表地址是否已对齐。
- 符号恢复:确定基地址后,使用IDA Pro修改镜像基址(
Edit -> Segments -> Rebase program),然后运行恢复脚本。 - 分析验证:查看恢复出的函数名(如
taskCreate、printf等系统函数)是否合理,确认恢复成功。
相似文章
相似文章