二进制分析基础:ELF/PE/BIN文件格式深度解析
做固件逆向这些年,我有个很深的体会:不懂文件格式,逆向就是盲人摸象。你想想看,连目标程序怎么组织的都不知道,怎么下手分析?今天我们就来啃下这块硬骨头。
二进制文件格式,说白了就是操作系统和可执行程序之间的"契约"。ELF是Linux的,PE是Windows的,BIN是裸机固件的。它们各有各的脾气,但核心逻辑是相通的。
一、ELF文件格式:Linux世界的基石
ELF(Executable and Linkable Format)是我接触最多的格式。当年我第一次逆向一个嵌入式Linux设备时,就是靠读ELF头找到入口点的。
ELF文件结构其实不复杂,就三部分:
- 文件头(ELF Header):描述文件基本信息
- 程序头(Program Header):告诉系统怎么加载
- 节区头(Section Header):描述各个节区的信息
来看一个典型的ELF头结构:
typedef struct {
unsigned char e_ident[16]; // 魔数、类别、字节序等
uint16_t e_type; // 文件类型(可重定位/可执行/共享库)
uint16_t e_machine; // 架构(x86/ARM/MIPS等)
uint32_t e_version; // 版本
uint64_t e_entry; // 入口地址
uint64_t e_phoff; // 程序头偏移
uint64_t e_shoff; // 节区头偏移
uint32_t e_flags; // 标志位
uint16_t e_ehsize; // ELF头大小
uint16_t e_phentsize; // 程序头表项大小
uint16_t e_phnum; // 程序头表项数量
uint16_t e_shentsize; // 节区头表项大小
uint16_t e_shnum; // 节区头表项数量
uint16_t e_shstrndx; // 节区名称字符串表索引
} Elf64_Ehdr;
关键点:e_ident[0]~e_ident[3]必须是0x7f 'E' 'L' 'F',这是识别ELF文件的魔数。我见过有人把BIN文件硬改成ELF头,结果加载器直接报错——魔数不对,一切免谈。
二、PE文件格式:Windows的"老江湖"
PE(Portable Executable)格式,说白了就是Windows版的ELF。但它的结构更复杂一些,因为要兼容DOS时代的遗产。
PE文件的结构层次:
- DOS头:以"MZ"开头,包含DOS存根
- PE头:以"PE\0\0"签名开始
- 节区表:描述各个节区的位置和属性
- 节区数据:实际的代码和数据
我记得有一次分析一个恶意软件样本,它把真正的PE头藏在文件中间,前面全是垃圾数据。当时我手动解析DOS头里的e_lfanew字段,才找到真正的PE签名位置。
// PE可选头中的关键字段
typedef struct {
uint16_t magic; // 0x10b (PE32) 或 0x20b (PE32+)
uint8_t major_linker_version;
uint8_t minor_linker_version;
uint32_t size_of_code;
uint32_t size_of_initialized_data;
uint32_t size_of_uninitialized_data;
uint32_t address_of_entry_point; // 入口点RVA
uint32_t base_of_code;
uint64_t image_base; // 首选加载基址
uint32_t section_alignment;
uint32_t file_alignment;
// ... 还有很多字段
} IMAGE_OPTIONAL_HEADER;
实战技巧:解析PE文件时,我习惯先检查e_magic是否为0x5A4D("MZ"),再检查e_lfanew指向的位置是否有"PE\0\0"签名。两步验证,基本不会出错。
三、BIN文件格式:裸机世界的"原始人"
BIN文件,说白了就是内存的"快照"。没有头,没有节区表,就是纯粹的二进制数据。嵌入式固件大多用这种格式。
BIN文件的特点:
- 无结构:没有文件头,直接就是代码和数据
- 固定加载地址:通常烧录到特定地址(如0x08000000)
- 大小即内容:文件大小就是固件大小
我曾经遇到一个坑:某款MCU的BIN文件里混入了0xFF填充字节,我以为是正常数据,结果反汇编出来全是非法指令。后来才发现,那是Flash的擦除状态,不是有效代码。
注意:BIN文件没有入口点信息,你需要从芯片手册或链接脚本中获取入口地址。我一般先用binwalk扫描,看看有没有已知的固件特征。
四、节区与段:程序的组织方式
节区(Section)和段(Segment)是两个容易混淆的概念。我简单解释一下:
- 节区:链接视角,用于组织代码和数据
- 段:加载视角,用于描述内存映射
常见的节区类型:
| 节区名 | 内容 | 属性 |
|---|---|---|
| .text | 可执行代码 | 读+执行 |
| .data | 已初始化全局变量 | 读+写 |
| .bss | 未初始化全局变量 | 读+写(不占文件空间) |
| .rodata | 只读数据(字符串常量等) | 读 |
| .plt | 过程链接表(动态链接用) | 读+执行 |
嗯,这里要注意:.bss节区在文件中不占空间,但加载时会分配内存。我见过有人分析固件时,发现文件大小和加载后内存大小对不上,其实就是忽略了.bss。
五、符号表与重定位表:链接的"桥梁"
符号表(Symbol Table)记录了程序中所有的符号信息——函数名、全局变量名等。重定位表(Relocation Table)则记录了需要"修补"的地址。
为什么需要重定位?举个例子:
// 源代码
extern void foo(void);
void bar() {
foo(); // 编译时不知道foo的地址
}
编译时,编译器不知道foo函数的地址,只能生成一个占位符。链接器通过重定位表,找到这些占位符,填入实际的地址。
符号表的结构:
typedef struct {
uint32_t st_name; // 符号名在字符串表中的索引
uint8_t st_info; // 符号类型和绑定信息
uint8_t st_other; // 可见性
uint16_t st_shndx; // 所在节区索引
uint64_t st_value; // 符号值(地址或偏移)
uint64_t st_size; // 符号大小
} Elf64_Sym;
实战经验:我在逆向一个IoT设备固件时,发现符号表被strip掉了。但通过分析重定位表,我仍然能推断出哪些地址是函数调用点,进而还原出控制流图。重定位表是逆向的"金矿"。
六、文件头解析实战:手撕ELF头
光说不练假把式。我们来写一个简单的ELF头解析器:
#include <stdio.h>
#include <stdint.h>
#include <elf.h>
void parse_elf_header(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (!fp) {
perror("fopen");
return;
}
// 读取ELF头
Elf64_Ehdr ehdr;
fread(&ehdr, sizeof(ehdr), 1, fp);
// 验证魔数
if (ehdr.e_ident[0] != 0x7f ||
ehdr.e_ident[1] != 'E' ||
ehdr.e_ident[2] != 'L' ||
ehdr.e_ident[3] != 'F') {
printf("不是有效的ELF文件\n");
fclose(fp);
return;
}
printf("文件类型: %d\n", ehdr.e_type);
printf("架构: %d\n", ehdr.e_machine);
printf("入口地址: 0x%lx\n", ehdr.e_entry);
printf("程序头数量: %d\n", ehdr.e_phnum);
printf("节区头数量: %d\n", ehdr.e_shnum);
// 读取节区头表
fseek(fp, ehdr.e_shoff, SEEK_SET);
for (int i = 0; i < ehdr.e_shnum; i++) {
Elf64_Shdr shdr;
fread(&shdr, sizeof(shdr), 1, fp);
printf("节区%d: 偏移=0x%lx, 大小=0x%lx\n",
i, shdr.sh_offset, shdr.sh_size);
}
fclose(fp);
}
这段代码虽然简单,但涵盖了ELF解析的核心逻辑。我建议你动手跑一遍,看看实际文件的结构。
调试技巧:用readelf -a命令可以查看完整的ELF信息。我经常用它来验证自己的解析结果。如果readelf的输出和你的解析一致,那基本就对了。
七、知识体系总览
下面这张图总结了本章的核心内容:
这张图把本章的知识点串起来了。你可以看到,ELF、PE、BIN三种格式虽然不同,但都围绕着"如何组织代码和数据"这个核心问题。节区与段是理解程序结构的钥匙,符号表与重定位表则是逆向分析的利器。
好了,二进制分析基础就讲到这里。记住:文件格式是逆向的"语法",不懂语法,就看不懂程序。多动手解析几个文件,你会有更深的体会。