逆向工程基本原理03
编译过程
1 | pre |
预编译
1 | gcc main.c -E -o main.i |
生成汇编代码
1 | gcc main.i -S -o main.s |
可重定位文件
1 | gcc main.s -c -o main.o |
生成可执行文件
1 | gcc main.o -o main |
查看 gcc 编译的详细过程
1 | gcc main.c -v -o main |
预编译
将源代码中的一些特定指令或宏展开成实际的代码
#include:将头文件的内容插入到源代码中- 宏定义:将宏定义展开成实际的代码
- 条件编译:根据条件编译指令选择性地编译代码
- 注释:去掉源代码中的注释
- 编译指令处理:处理一些特定的编译指令,如
#pragma等
预处理器 - cppreference.cn - C++参考手册
生成汇编代码
运行时存储管理
静态内存分配
- 局部变量 -> 栈内存
- 全局/静态变量 -> 全局数据段(.data)
- 不需要手动释放
动态内存分配
- 通过
malloc、calloc、realloc等函数在堆内存中分配内存 - 需要显示释放以保证不会内存泄漏
中间代码生成
提升编译器的可扩展性,先将源代码转换成中间代码表示,在中间代码(IR)上进行各种优化,最后将优化后的中间代码编译为机器码
编译优化
编译优化可能会改变高级语言代码与机器级表示的关联,参考 2.5 节
生成可重定位文件
- 指令翻译:按照特定架构中机器码的定义,逐条将汇编指令文本转化为对应的机器码
- 符号:汇编语言中对代码、数据的引用
- 符号类型:代码符号、数据符号
汇编中维护了一个符号表
第一次使用某个符号时在符号表中记录该符号,后续汇编过程中再次使用该符号时直接从符号表中获取对应符号偏移
例如:
1 | .global_var_1 |
变为符号表:
| 符号名 | 符号偏移 |
|---|---|
| .global_var_1 | 0x0 |
| .func | 0x100 |
| .func_2 | 0x300 |
变为
1 | .global_var_1 |
生成可执行文件
链接:将多个目标文件(.o)和库文件(.a、.so)合并成一个可执行文件
- 符号解析:将目标文件中未定义的符号引用与其他目标文件中符号定义绑定
- 重定位:将多个目标文件中合并为一个可执行文件,确定各符号的地址
二进制文件结构
设计原则
- 可移植性:不同平台和操作系统之间的兼容性
- 多架构支持:支持不同的处理器架构,如 x86、ARM 等
- 支持动态链接:允许程序在运行时加载和链接共享库
- 文件体积尽可能小
- 支持调试、符号等信息
二进制文件中的元数据
- 文件头:包含文件基本信息
- 长度:代表文件的总大小
- 标志位:代表文件状态或特性
- 时间戳:文件的创建时间、修改时间
- 校验和:用于验证文件完整性
- 结构描述:文件中各结构的信息描述,如字段的名称、大小、数据类型
二进制文件结构不同意,不同操作系统采用的二进制可执行文件格式也不同,如 Windows 的 PE 格式,Linux 的 ELF 格式,macOS 的 Mach-O 格式等
PE 文件结构
PE(Portable Executable,可移植可执行)文件是 Windows 操作系统下可执行文件(.exe)、动态链接库(.dll)、驱动文件(.sys)等的标准格式
具体参考 逆向工程核心原理03 | exdoubled’s blog
Mach-O 文件结构
Mach-O(Mach Object)是 macOS、iOS、iPadOS、watchOS 等 Apple 平台上的可执行文件、动态库和对象文件的标准格式。其名称源于 Mach 内核,该内核是 macOS 等 Darwin 系统的核心组成部分
ELF 文件结构
ELF 用于多种 Linux 二进制文件,例如可重定位文件(.o)、可执行文件(.elf)和共享库(.so)
示例命令:
1 | # 查看 ELF Header |
ELF 文件头
1 | ELF Header: |
如图可以看到 ELF 的结构
1 | typedef struct { |
| 字段 | 说明 |
|---|---|
| e_ident[16] | 魔数(0x7F 'E' 'L' 'F')、位数(32/64)、字节序、ELF 版本等 |
| e_type | 文件类型:ET_REL(可重定位文件,.o)、ET_EXEC(可执行文件)、ET_DYN(共享库,.so)、ET_CORE(核心转储) |
| e_machine | 目标架构:EM_386(x86)、EM_X86_64、EM_ARM、EM_AARCH64 等 |
| e_version | ELF 版本,通常为 1 |
| e_entry | 程序入口点的虚拟地址(ET_EXEC 和 ET_DYN 有效) |
| e_phoff | Program Header Table 在文件中的偏移量 |
| e_shoff | Section Header Table 在文件中的偏移量 |
| e_flags | 处理器特定标志 |
| e_ehsize | ELF Header 的大小 |
| e_phentsize | 每个 Program Header 条目的大小 |
| e_phnum | Program Header 条目的数量 |
| e_shentsize | 每个 Section Header 条目的大小 |
| e_shnum | Section Header 条目的数量 |
| e_shstrndx | Section 名称字符串表在 Section Header Table 中的索引 |
程序表头 Program Header Table
用来描述二进制文件中所有的段(Segment),每个段包含一系列相关的节(Section)
包含段包含的页面大小、虚拟地址以及段大小,描述可执行文件的布局、加载时如何加载到对应的虚拟地址空间
1 | struct Elf64_Phdr { |
Program Header 的关键字段:
| 字段 | 说明 |
|---|---|
| p_type | 段类型 |
| p_offset | 段在文件中的偏移量 |
| p_vaddr | 段在内存中的虚拟地址 |
| p_paddr | 物理地址(通常忽略) |
| p_filesz | 段在文件中的大小 |
| p_memsz | 段在内存中的大小(通常 >= p_filesz,多出的部分为 BSS 区,初始化为零) |
| p_flags | 权限:PF_R(读)、PF_W(写)、PF_X(执行) |
| p_align | 对齐要求(通常为页大小 0x1000 或 0x200000) |
Segment 的类型:
| 类型 | 说明 |
|---|---|
PT_LOAD | 可加载段,必须被映射到内存。通常有代码段(可读可执行)和数据段(可读可写) |
PT_DYNAMIC | 动态链接信息,指向 .dynamic section,供动态链接器使用 |
PT_INTERP | 解释器路径(如 /ld-linux.so.2),指定动态链接器的位置 |
PT_NOTE | 附加信息(如 ABI 版本、构建 ID) |
PT_GNU_STACK | 栈权限控制,标记栈是否可执行(用于防御缓冲区溢出) |
PT_GNU_RELRO | 将某些数据段在动态链接后设置为只读(Relocation Read-Only) |
节头表 Section Header Table
用来描述二进制文件中所有的节(Section),每个节包含一系列相关的数据,如代码、数据、符号表、字符串表等
并非所有的节对于程序运行都有作用,例如,去除调试信息相关节之后程序仍可正常运行
1 | struct Elf64_Shdr { |
Section Header 的关键字段:
| 字段 | 说明 |
|---|---|
| sh_name | Section 名称在 .shstrtab 中的偏移 |
| sh_type | Section 类型(数据、符号表、字符串表、重定位表等) |
| sh_flags | 标志位(是否可写、是否在内存中分配等) |
| sh_addr | 若被加载,该 Section 在内存中的地址 |
| sh_offset | Section 在文件中的偏移 |
| sh_size | Section 的大小 |
| sh_link | 关联的其他 Section 索引 |
| sh_info | 附加信息(取决于类型) |
| sh_addralign | 对齐要求 |
| sh_entsize | 条目大小(如果 Section 包含固定大小的条目) |
节的类型
| SHT_NULL | 对应 ELF 文件中的第一个 NULL section |
|---|---|
| SHT_PROGBITS | 程序数据,二进制的机器码 |
| SHT_SYMTAB | 符号表 |
| SHT_STRTAB | 字符串表 |
| SHT_RELA | 重定位偏移信息 |
| SHT_HASH | 符号对应的哈希表 |
| SHT_DYNAMIC | 动态链接相关信息 |
| SHT_NOTE | 用于标记文件的信息 |
| SHT_NOBITS | 该节在ELF文件中不占用空间 |
| SHT_REL | 重定位信息 |
| SHT_DYNSYM | 动态链接符号表 |
| … | … |
常见 section:
| Section | 类型 | 说明 |
|---|---|---|
.text | SHT_PROGBITS | 可执行代码(机器指令) |
.data | SHT_PROGBITS | 已初始化的全局变量和静态变量 |
.bss | SHT_NOBITS | 未初始化的全局变量(在文件中不占空间,运行时分配并清零) |
.rodata | SHT_PROGBITS | 只读数据(字符串常量、const 变量) |
.init | SHT_PROGBITS | 构造函数表(C/C++ 全局对象构造) |
.fini | SHT_PROGBITS | 析构函数表 |
.plt | SHT_PROGBITS | 程序链接表(用于延迟绑定) |
.got / .got.plt | SHT_PROGBITS | 全局偏移表(存储外部函数地址) |
.dynamic | SHT_DYNAMIC | 动态链接信息(依赖库、符号表位置等) |
.dynsym | SHT_DYNSYM | 动态符号表(用于动态链接) |
.dynstr | SHT_STRTAB | 动态符号表对应的字符串表 |
.symtab | SHT_SYMTAB | 完整符号表(包含本地符号,可用于调试,可被 strip) |
.strtab | SHT_STRTAB | 符号表对应的字符串表 |
.rel.text / .rela.text | SHT_REL / SHT_RELA | 重定位信息(记录需要修正的地址) |
.debug_\* | SHT_PROGBITS | DWARF 调试信息 |
.shstrtab | SHT_STRTAB | Section 名称字符串表 |
符号表 Symbol Table 和 重定位表 .rel
符号表记录了程序中所有符号(函数、变量等)的名称、地址、大小、类型等信息,这些符号在后续过程中需要进行链接或重定位
重定位表记录了需要在链接阶段修正的地址信息,例如函数调用、全局变量访问等,用于辅助后续链接与重定位过程
符号绑定
决定了符号的行为和可见性
简单来说:当你的代码调用 printf 时,编译器并不知道 printf 在内存中的哪个位置,符号绑定就是动态链接器(loader)在运行时或加载时,把这个调用指向真正的 printf 函数地址的过程
ELF 通过 PLT(Procedure Linkage Table,程序链接表)和 GOT(Global Offset Table,全局偏移表)实现延迟绑定
1 | 首次调用 printf 时: |
如果动态链接器无法找到符号,会导致 undefined symbol 错误(ELF),程序无法启动或运行时报错
如果 GOT 表可写,攻击者可以通过缓冲区溢出覆写 GOT 条目,劫持程序控制流
链接与加载
程序拆分为多个功能明确的源文件,而不是一个体积巨大的源文件
解耦、降低代码之间的依赖性、提高可维护性,将常用的函数封装为库文件
每个.o文件可以并行编译,只需重新编译被修改的源文件,然后重新链接
源文件中无需包含共享库函数的源码,直接调用即可,可执行文件和运行时内存中只需包含所调用函数的代码而不需要包含整个共享库
链接
- 静态链接:将多个可执行文件或静态链接库文件链接为一个可执行文件,在编译时进行
- 动态链接:将可执行文件运行所需的函数动态加载到内存中进行链接,在运行时进行
库文件
GNU C Library (glibc),包括了最基本、最常用的 C 标准库函数和系统函数
可以分为静态链接库和动态链接库
静态链接库
- 静态链接库中的代码和数据,在编译链接时被复制到新的可执行文件中
- 以静态链接的方式 (例如ar指令),将多个.o文件打包,以.a结尾
动态链接库
- 以动态链接的方式,将多个.o文件打包,以.so结尾
- 可被多个可执行文件共享(编译、运行时)
- 动态链接库中的代码和数据,在编译链接时被记录为依赖,运行时被动态加载链接
- 可执行文件使用GOT、PLT作为跳转表
- 通过跳转表解析并记录动态链接库中函数加载后的真实地址
静态链接步骤
- 符号解析
顺序扫描所有输入文件中的符号表,解析未定义符号与定义符号的关系,确定每个符号的最终地址
- 重定位
将目标文件相同类型的节合并,对定义符号重新定位(确定符号新地址),对引用符号进行重定位(修正指令中的地址)
动态链接步骤
从DYNAMIC段获取依赖的动态链接库信息,每个可执行文件中会记录使用到的动态链接库文件名(在DYNAMIC段)
将可执行文件使用的动态链接库加载到内存中,动态链接器会根据可执行文件中记录的动态链接库文件名,在系统的库路径中查找对应的动态链接库文件,并将其加载到内存中
解析符号地址,动态链接器会解析可执行文件中使用的函数符号,并将其与动态链接库中的实际函数地址进行绑定,更新可执行文件中的跳转表(GOT、PLT)
动态链接的延迟绑定
动态链接的延迟绑定(Lazy Binding)是指在程序运行时,只有当某个函数被首次调用时,才会解析该函数的地址并进行绑定。这种机制可以提高程序的启动速度,因为不需要在程序启动时解析所有函数的地址
主要通过 PLT(Procedure Linkage Table,程序链接表)和 GOT(Global Offset Table,全局偏移表)实现
加载
在运行前将二进制文件加载到内存中的过程
解析头文件信息,判断加载对应程序所需内存
申请对应的内存,构建物理内存与虚拟地址空间地址映射
将二进制文件中可加载的段读入到对应的地址空间
清空.bss段所在内存中的内容
为可执行程序创建栈空间
设置运行时信息,包括程序参数和环境变量
运行程序