逆向工程基本原理03

逆向工程基本原理03

exdoubled Lv4

编译过程

1
2
3
		pre
processor cc1 as ld
hello.c------>hello.i------>hello.s------>hello.o------>hello

预编译

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)
  • 不需要手动释放

动态内存分配

  • 通过 malloccallocrealloc 等函数在堆内存中分配内存
  • 需要显示释放以保证不会内存泄漏

中间代码生成

提升编译器的可扩展性,先将源代码转换成中间代码表示,在中间代码(IR)上进行各种优化,最后将优化后的中间代码编译为机器码

编译优化

编译优化可能会改变高级语言代码与机器级表示的关联,参考 2.5 节

生成可重定位文件

  • 指令翻译:按照特定架构中机器码的定义,逐条将汇编指令文本转化为对应的机器码
  • 符号:汇编语言中对代码、数据的引用
  • 符号类型:代码符号、数据符号

汇编中维护了一个符号表

第一次使用某个符号时在符号表中记录该符号,后续汇编过程中再次使用该符号时直接从符号表中获取对应符号偏移

例如:

1
2
3
4
5
6
7
8
.global_var_1
0x12 .long 0x12345678

.func
0x20 ldr w0, [global_var_1]

.func_2
0x30 call func

变为符号表:

符号名符号偏移
.global_var_10x0
.func0x100
.func_20x300

变为

1
2
3
4
5
6
7
8
.global_var_1
0x12 .long 0x12345678

.func
0x20 ldr w0, [0x0]

.func_2
0x30 call 0x100

生成可执行文件

链接:将多个目标文件(.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看 ELF Header
readelf -h a.out

# 查看 Program Header(内存映射视图)
readelf -l a.out

# 查看 Section Header
readelf -S a.out

# 查看动态链接信息
readelf -d a.out

# 查看符号表
readelf -s a.out

# 查看重定位表
readelf -r a.out

# 查看依赖库
ldd a.out

ELF 文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0xf00
Start of program headers: 64 (bytes into file)
Start of section headers: 91688 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 36
Section header string table index: 35

如图可以看到 ELF 的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct { 
unsigned char e_ident[EI_NIDENT];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shtrndx;
} Elf64_Ehdr;
字段说明
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_64EM_ARMEM_AARCH64
e_versionELF 版本,通常为 1
e_entry程序入口点的虚拟地址(ET_EXECET_DYN 有效)
e_phoffProgram Header Table 在文件中的偏移量
e_shoffSection Header Table 在文件中的偏移量
e_flags处理器特定标志
e_ehsizeELF Header 的大小
e_phentsize每个 Program Header 条目的大小
e_phnumProgram Header 条目的数量
e_shentsize每个 Section Header 条目的大小
e_shnumSection Header 条目的数量
e_shstrndxSection 名称字符串表在 Section Header Table 中的索引

程序表头 Program Header Table

用来描述二进制文件中所有的段(Segment),每个段包含一系列相关的节(Section)

包含段包含的页面大小、虚拟地址以及段大小,描述可执行文件的布局、加载时如何加载到对应的虚拟地址空间

1
2
3
4
5
6
7
8
9
10
11
struct Elf64_Phdr {
Elf64_Word p_type; // Type of segment
Elf64_Word p_flags; // Segment flags
  Elf64_Off p_offset; // File offset where segment is located
  Elf64_Addr p_vaddr; // Virtual address of beginning of segment
  Elf64_Addr p_paddr; // Physical addr of beginning of segment
  Elf64_Xword p_filesz; // Num. of bytes in file image of segment
  Elf64_Xword p_memsz; // Num. of bytes in mem image of segment
  Elf64_Xword p_align; // Segment alignment constraint
}; 

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
2
3
4
5
6
7
8
9
10
11
12
struct Elf64_Shdr {
Elf64_Word sh_name; // Section name (string tbl index)
Elf64_Word sh_type; // Section type
Elf64_Xword sh_flags; // Section flags
Elf64_Addr sh_addr; // Section virtual addr at execution
Elf64_Off sh_offset; // Section file offset
Elf64_Xword sh_size; // Section size in bytes
Elf64_Word sh_link; // Link to another section
Elf64_Word sh_info; // Additional section information
Elf64_Xword sh_addralign; // Section alignment
Elf64_Xword sh_entsize; // Entry size if section holds table
};

Section Header 的关键字段:

字段说明
sh_nameSection 名称在 .shstrtab 中的偏移
sh_typeSection 类型(数据、符号表、字符串表、重定位表等)
sh_flags标志位(是否可写、是否在内存中分配等)
sh_addr若被加载,该 Section 在内存中的地址
sh_offsetSection 在文件中的偏移
sh_sizeSection 的大小
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类型说明
.textSHT_PROGBITS可执行代码(机器指令)
.dataSHT_PROGBITS已初始化的全局变量和静态变量
.bssSHT_NOBITS未初始化的全局变量(在文件中不占空间,运行时分配并清零)
.rodataSHT_PROGBITS只读数据(字符串常量、const 变量)
.initSHT_PROGBITS构造函数表(C/C++ 全局对象构造)
.finiSHT_PROGBITS析构函数表
.pltSHT_PROGBITS程序链接表(用于延迟绑定)
.got / .got.pltSHT_PROGBITS全局偏移表(存储外部函数地址)
.dynamicSHT_DYNAMIC动态链接信息(依赖库、符号表位置等)
.dynsymSHT_DYNSYM动态符号表(用于动态链接)
.dynstrSHT_STRTAB动态符号表对应的字符串表
.symtabSHT_SYMTAB完整符号表(包含本地符号,可用于调试,可被 strip)
.strtabSHT_STRTAB符号表对应的字符串表
.rel.text / .rela.textSHT_REL / SHT_RELA重定位信息(记录需要修正的地址)
.debug_\*SHT_PROGBITSDWARF 调试信息
.shstrtabSHT_STRTABSection 名称字符串表

符号表 Symbol Table 和 重定位表 .rel

符号表记录了程序中所有符号(函数、变量等)的名称、地址、大小、类型等信息,这些符号在后续过程中需要进行链接或重定位

重定位表记录了需要在链接阶段修正的地址信息,例如函数调用、全局变量访问等,用于辅助后续链接与重定位过程

符号绑定

决定了符号的行为和可见性

简单来说:当你的代码调用 printf 时,编译器并不知道 printf 在内存中的哪个位置,符号绑定就是动态链接器(loader)在运行时或加载时,把这个调用指向真正的 printf 函数地址的过程

ELF 通过 PLT(Procedure Linkage Table,程序链接表)和 GOT(Global Offset Table,全局偏移表)实现延迟绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首次调用 printf 时:

调用方调用 printf@plt

printf@plt 中的代码跳转到 GOT 中 printf 的条目

GOT 条目初始指向 plt 中的第二段代码(动态链接器入口)

动态链接器解析符号,找到 printf 的实际地址

动态链接器将地址写入 GOT

跳转到 printf 执行

后续调用 printf 时:
调用方调用 printf@plt

GOT 已包含 printf 的实际地址

直接跳转到 printf

如果动态链接器无法找到符号,会导致 undefined symbol 错误(ELF),程序无法启动或运行时报错

如果 GOT 表可写,攻击者可以通过缓冲区溢出覆写 GOT 条目,劫持程序控制流

链接与加载

程序拆分为多个功能明确的源文件,而不是一个体积巨大的源文件

解耦、降低代码之间的依赖性、提高可维护性,将常用的函数封装为库文件

每个.o文件可以并行编译,只需重新编译被修改的源文件,然后重新链接

源文件中无需包含共享库函数的源码,直接调用即可,可执行文件和运行时内存中只需包含所调用函数的代码而不需要包含整个共享库

链接

  • 静态链接:将多个可执行文件或静态链接库文件链接为一个可执行文件,在编译时进行
  • 动态链接:将可执行文件运行所需的函数动态加载到内存中进行链接,在运行时进行

库文件

GNU C Library (glibc),包括了最基本、最常用的 C 标准库函数和系统函数

可以分为静态链接库和动态链接库

  • 静态链接库

    • 静态链接库中的代码和数据,在编译链接时被复制到新的可执行文件中
    • 以静态链接的方式 (例如ar指令),将多个.o文件打包,以.a结尾
  • 动态链接库

    • 以动态链接的方式,将多个.o文件打包,以.so结尾
    • 可被多个可执行文件共享(编译、运行时)
    • 动态链接库中的代码和数据,在编译链接时被记录为依赖,运行时被动态加载链接
    • 可执行文件使用GOT、PLT作为跳转表
    • 通过跳转表解析并记录动态链接库中函数加载后的真实地址

静态链接步骤

  1. 符号解析

顺序扫描所有输入文件中的符号表,解析未定义符号与定义符号的关系,确定每个符号的最终地址

  1. 重定位

将目标文件相同类型的节合并,对定义符号重新定位(确定符号新地址),对引用符号进行重定位(修正指令中的地址)

动态链接步骤

从DYNAMIC段获取依赖的动态链接库信息,每个可执行文件中会记录使用到的动态链接库文件名(在DYNAMIC段)

将可执行文件使用的动态链接库加载到内存中,动态链接器会根据可执行文件中记录的动态链接库文件名,在系统的库路径中查找对应的动态链接库文件,并将其加载到内存中

解析符号地址,动态链接器会解析可执行文件中使用的函数符号,并将其与动态链接库中的实际函数地址进行绑定,更新可执行文件中的跳转表(GOT、PLT)

动态链接的延迟绑定

动态链接的延迟绑定(Lazy Binding)是指在程序运行时,只有当某个函数被首次调用时,才会解析该函数的地址并进行绑定。这种机制可以提高程序的启动速度,因为不需要在程序启动时解析所有函数的地址

主要通过 PLT(Procedure Linkage Table,程序链接表)和 GOT(Global Offset Table,全局偏移表)实现

加载

在运行前将二进制文件加载到内存中的过程

  • 解析头文件信息,判断加载对应程序所需内存

  • 申请对应的内存,构建物理内存与虚拟地址空间地址映射

  • 将二进制文件中可加载的读入到对应的地址空间

  • 清空.bss段所在内存中的内容

  • 为可执行程序创建栈空间

  • 设置运行时信息,包括程序参数和环境变量

  • 运行程序

同分类文章

Comments