静态分析基础:PE/ELF文件格式解析、文件头与节区表、导入导出表分析
各位同学,咱们今天聊点硬核的。静态分析,说白了就是对着二进制文件“看”,不动手跑它。你想想看,一个恶意软件你哪敢随便双击?所以,读懂PE和ELF文件格式,是逆向工程师的基本功。我个人习惯,拿到一个样本,第一件事就是拿十六进制编辑器或者工具把它的结构扒开看看。
PE文件格式:Windows世界的身份证
PE(Portable Executable)是Windows下的可执行文件格式。我刚开始学的时候,觉得这东西很复杂,其实拆开来看,就那么几块。
PE文件的核心结构是这样的:
+-------------------+
| DOS头 (64字节) |
+-------------------+
| DOS Stub |
+-------------------+
| PE签名 (4字节) |
+-------------------+
| COFF文件头 |
+-------------------+
| 可选头 |
+-------------------+
| 节区表 |
+-------------------+
| 节区1 (.text) |
| 节区2 (.data) |
| 节区3 (.rdata) |
| ... |
+-------------------+
嗯,这里要注意,DOS头虽然古老,但它指向了PE签名的位置。我见过有人直接跳过DOS头去解析,结果偏移算错了,整个分析都跑偏。
文件头与节区表
PE签名之后紧跟着的是COFF文件头。这里面有几个关键字段:
| 字段 | 含义 | 我的经验 |
|---|---|---|
| Machine | 目标CPU架构(0x14C是x86,0x8664是x64) | 有一次我拿到一个样本,Machine字段被篡改成0xFFFF,工具直接报错。这就是个反分析的小花招。 |
| NumberOfSections | 节区数量 | 这个值如果异常大,说明文件可能被加壳或者被篡改过。 |
| SizeOfOptionalHeader | 可选头的大小 | PE32+(64位)的可选头比PE32大,解析时一定要区分。 |
节区表是PE文件的“目录”。每个节区表项占40字节,记录了节区的名称、虚拟地址、原始数据偏移等信息。我建议你重点关注这几个节区:
- .text:代码段,可执行。逆向分析的核心区域。
- .data:已初始化的全局变量。
- .rdata:只读数据,导入导出表通常在这里。
- .rsrc:资源段,图标、字符串、对话框都在里面。
导入导出表分析
导入表(Import Table)告诉你这个程序调用了哪些外部函数。比如调用了MessageBoxA、CreateFile等等。导出表(Export Table)则相反,它告诉别人这个DLL提供了哪些函数。
导入表的结构有点绕,我当年也花了不少时间才理清楚。它本质上是一个IMAGE_IMPORT_DESCRIPTOR数组,每个描述符对应一个DLL。结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT (Import Name Table)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL名称的RVA
DWORD FirstThunk; // IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;
这里有个关键点:OriginalFirstThunk和FirstThunk都指向一个IMAGE_THUNK_DATA数组。前者指向函数名称的RVA,后者在加载后会被系统填充为函数的实际地址。我曾经在分析一个被Hook的样本时,发现IAT被篡改了,但INT还是原始的。通过对比两者,我找到了Hook点。
ELF文件格式:Linux世界的通行证
ELF(Executable and Linkable Format)是Linux下的标准格式。和PE相比,ELF的设计更简洁,但核心思想是一样的。
ELF文件的结构:
+-------------------+
| ELF头 |
+-------------------+
| 程序头表 |
| (可选,用于加载) |
+-------------------+
| 节区头表 |
+-------------------+
| 节区 |
| .text |
| .data |
| .symtab |
| .strtab |
| ... |
+-------------------+
ELF头的前4个字节是魔数0x7F 'E' 'L' 'F'。我习惯用readelf -h命令快速查看ELF头信息。
节区表与关键节区
ELF的节区头表相当于PE的节区表。每个节区头表项描述了节区的名称、类型、偏移、大小等。常用的节区有:
- .text:代码段。
- .data:已初始化数据。
- .bss:未初始化数据(不占文件空间)。
- .symtab:符号表,包含函数名和变量名。
- .strtab:字符串表,符号名称的字符串都在这。
- .plt和.got:动态链接的关键,相当于PE的IAT。
.symtab和.strtab是你的好朋友。如果程序没有被strip(剥离符号),你能直接看到函数名,分析起来事半功倍。如果被strip了,那就得靠函数特征来识别了。
导入导出表(动态链接)
ELF没有像PE那样明确的导入导出表结构。它的动态链接信息分散在.dynamic节区中。.dynamic节区包含了一系列的Elf64_Dyn结构,其中:
- DT_NEEDED:依赖的共享库(相当于PE的导入DLL)。
- DT_SYMTAB:动态符号表的地址。
- DT_STRTAB:动态字符串表的地址。
- DT_PLTGOT:GOT(全局偏移表)的地址。
说白了,ELF的导入解析是通过PLT(过程链接表)和GOT配合完成的。第一次调用函数时,PLT跳转到GOT,GOT指向PLT的解析代码;解析完成后,GOT被更新为函数的真实地址。我建议你用objdump -d查看PLT段,能直观看到这个过程。
知识体系总览
下面这张图,是我自己总结的PE和ELF静态分析的核心逻辑。你把它记在脑子里,分析任何样本都不会迷路。
好了,这一章的内容就到这里。PE和ELF的格式细节很多,但核心就是文件头、节区表、导入导出表这三板斧。你把这些搞明白了,静态分析就算入门了。下一章我们会聊到如何用符号执行来动态求解,那才是真正有意思的地方。