固件分析之加载基地址与符号恢复
字数 3760
更新时间 2026-04-15 13:39:12

固件分析之加载基地址与符号恢复教学文档

第一章:核心概念与背景

1.1 基地址定义

基地址通常是指程序在内存中的起始地址。在嵌入式系统安全分析中,准确获取固件在内存中的加载基地址是进行逆向工程、漏洞分析及功能理解的基础前提。

1.2 应用场景区分

  • Linux系统设备:通常具有固定的内存布局,内核、用户空间及其他关键数据区域的位置在系统启动时即已确定,一般不需要进行基地址恢复。
  • RTOS/ECOS系统设备:特别是运行VxWorks的嵌入式设备,固件在内存中的加载地址不是固定的。加载地址的偏差会导致对绝对地址(如跳转函数表、字符串表)的引用全部失效。因此,在研究此类固件时,基地址与符号表的恢复是至关重要的第一步

第二章:基地址恢复方法详解

2.1 方法一:跳转表推理法

本方法主要利用switch跳转表来确定固件的加载地址,尤其适用于ARM架构的VxWorks固件。

操作步骤:

  1. 架构确认:使用binwalk -A命令分析固件,确认目标系统架构。
  2. 定位跳转表:在反汇编器(如IDA Pro、Ghidra)中搜索典型的switch代码结构,寻找包含绝对地址跳转指令的跳转表。
  3. 计算基地址
    • 在跳转表中选取一个绝对跳转地址,例如0x8B2BE0
    • 查看该地址指令在固件二进制文件中的偏移,例如0xB0BEC
    • 初步计算:基址 = 绝对跳转地址 - 文件偏移,即 0x8B2BE0 - 0xB0BEC = 0x801FF4
    • 进行页对齐(通常为0x1000):0x801FF4 & 0xFFF000 = 0x801000。此即推测的加载基地址。
  4. 验证与优化
    • 使用计算出的基地址对固件进行rebase(重定位)操作。
    • 检查跳转表中的一个地址,跳转后是否指向函数的起始位置。如果指向函数中间,则说明单一跳转表推测存在误差。
    • 核心要点:编译器优化可能导致跳转表地址对齐或变形,因此必须选取多个跳转表作为样本进行交叉验证,以找到最佳匹配的基地址。

自动化工具:

  • ArmBaseFinder:Github上的IDA插件,包含通过跳转表寻找基址的算法。
  • basefind:对上述算法用C语言重写以提升效率的工具。

2.2 方法二:(字符串表+符号表)恢复法

这是针对VxWorks固件最常用、最可靠的基地址恢复方法,其原理是利用固件内建的函数符号表。

实例流程(以Schneider M241为例):

  1. 初步分析
    • 使用binwalk查看固件信息,尝试获取架构和可能的符号表地址提示(注:binwalk解析结果可能存在误差,需人工确认)。
    • 使用IDA Pro加载固件,加载地址暂设为0x0。此时所有函数均显示为sub_xxxx的形式。
  2. 定位符号表
    • 使用vxhunter插件或Ghidra的VxWorksSymtab_Finder脚本,扫描固件,获取符号表的起始地址。即使符号恢复失败,通常也能得到此地址。
  3. 解析符号表结构
    • 导航到符号表起始地址。VxWorks的符号表条目是一个结构体(不同版本大小不同,常见为20字节)。
    • 每个结构体通常包含:函数名指针地址函数入口地址符号类型等字段。
    • 关键观察:符号表条目通常按函数名在字符串表中的顺序进行绑定。需要确定绑定顺序是正序(第一个符号绑定字符串表第一个字符串)还是逆序。
  4. 定位字符串表
    • 使用IDA的字符串视图(Strings window)或系统的strings命令,找到字符串表的第一个字符串,并记录其在文件中的偏移地址(例如0xE8CC8)。
  5. 计算基地址
    • 在符号表第一个条目中,读取函数名指针地址(例如0x8EAC88,注意字节序)。
    • 计算基址:基址 = 符号表中的函数名指针地址 - 字符串表第一个字符串的文件偏移,即0x8EAC88 - 0xE8CC8 = 0x801FC0
  6. 恢复符号
    • 在IDA中修改镜像的加载基地址为计算出的0x801FC0
    • 运行符号恢复脚本(附录提供),将符号表中的函数名与地址关联,完成重命名。

注意事项(以Schneider NOE-771为例):

  • binwalk可能给出符号表地址,但需用010editor等工具人工验证其准确性。
  • 某些固件的符号表绑定顺序可能是逆序的,即符号表第一个条目绑定的是字符串表的最后一个字符串。此时需要用字符串表最后一个字符串的偏移来进行基址计算。

2.3 方法三:SP栈指针获取基址

本方法适用于PowerPC(PPC)等架构的VxWorks固件,依据的是VxWorks系统的启动流程内存布局。

原理与步骤:

  1. VxWorks设备启动后,执行的第一个C函数是usrInit()
  2. 根据VxWorks官方文档,usrInit()函数的初始化栈(initial stack)与固件镜像在内存中是相邻的,且栈顶指针(SP)指向初始化栈的底部。
  3. 操作
    • 分析固件入口代码(通常是_sysInit函数),找到设置栈指针(SP) 的指令。例如,在PPC架构中,指令lis r1, 1可能将SP设置为0x10000
    • 这个SP的值(0x10000)即为初始化栈的地址,根据文档描述,此地址即为固件的加载基地址

2.4 方法四:BSS段数据清零获取基址

此方法同样基于usrInit()函数的执行逻辑。

原理与步骤:

  1. usrInit()函数的第一步操作是清除.bss段(存储未初始化全局/静态变量的内存区域)。
  2. 操作
    • 在反汇编代码中定位负责清零.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

脚本使用说明:

  1. 根据固件架构(ARM/PPC,大端/小端)调整ida_bytes.get_32bit的字节序,或使用get_32bit_le/get_32bit_be
  2. 准确填写loadaddresseaStarteaEndENTRY_SIZEENTRY_SIZE需根据VxWorks版本确定(参考附录结构体)。
  3. 在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等手段直接获取运行时的内存地址。

第六章:实践流程建议

  1. 信息收集:首先使用binwalkfile等工具了解固件架构、操作系统、是否包含符号表提示。
  2. 方法选择
    • 如果发现明显的switch跳转表,可尝试方法一进行快速推测,并用其他方法验证。
    • 如果binwalk提示或有迹象表明存在符号表,优先采用方法二,这是最标准的方法。
    • 对于PPC等架构,可查看入口函数,尝试方法三方法四
  3. 交叉验证:使用不同方法计算出的基地址应相互印证。例如,用方法二计算出基址后,可以验证跳转表地址是否已对齐。
  4. 符号恢复:确定基地址后,使用IDA Pro修改镜像基址(Edit -> Segments -> Rebase program),然后运行恢复脚本。
  5. 分析验证:查看恢复出的函数名(如taskCreateprintf等系统函数)是否合理,确认恢复成功。
相似文章
相似文章
 全屏