从零开始的 Pwn 之旅 - 深入了解 elf 文件

总结摘要
ELF 文件即 Linux 下的可执行文件

ELF 文件初识

ELF 文件即 Linux 下的可执行文件,ELF 是 Executable and Linkable Format 的缩写。它是一个标准的二进制文件格式,用于存储可执行文件、目标代码、共享库和核心转储等。

使用 Linux 内置命令 file 可以轻松查看文件类型:

1
2
3
4
5
$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=d1f6561268de19201ceee260d3a4f6662e1e70dd, for GNU/Linux 4.4.0,
stripped
  • ELF 64-bit LSB pie executable 部分表示这是一个 64 位的 ELF 文件,LSB 表示低位字节序,pie 表示位置无关可执行文件(Position Independent Executable)。
  • x86-64 表示这是一个 amd64(x86-64) 架构的可执行文件。
  • dynamically linked 表示这个可执行文件是动态链接的,意味着它依赖于共享库。
  • interpreter /lib64/ld-linux-x86-64.so.2 表示这个可执行文件使用的动态链接器是 /lib64/ld-linux-x86-64.so.2
  • BuildID[sha1]=d1f6561268de19201ceee260d3a4f6662e1e70dd 是一个唯一的标识符,用于标识这个可执行文件的构建版本。
  • for GNU/Linux 4.4.0 表示这个可执行文件是为 GNU/Linux 4.4.0 版本构建的。
  • stripped 表示这个可执行文件已经被剥离了调试信息。

当 ELF 文件被赋予可执行属性时,直接在命令行中输入 ELF 文件路径即可执行该文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 可执行文件在当期目录下
$ ./binary_file

# 可执行文件在其他目录下(使用绝对路径)
$ /bin/binary_file
# 可执行文件在其他目录下(使用相对路径)
$ ../path/to/binary_file

# 可执行文件在环境变量 PATH 中
$ binary_file

二进制文件的编译过程

当我们编写一个 C 语言程序并使用 gcc 编译时,gcc 会将源代码编译成目标文件(.o 文件),然后链接成可执行文件。这个过程可以分为以下几个步骤:

1
2
3
4
5
6
7
// hello.c
#include <stdio.h>

int main() {
  printf("Hello, World!\n");
  return 0;
}
  • 预处理:将源代码中的宏定义、头文件等进行处理,生成中间文件。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -E hello.c -o hello.i

$ cat hello.i
# 这里预处理后的代码有 800 多行
# 这里只截取 printf 函数的部分
extern int printf (const char *__restrict __format, ...);
# ...
int main() {
    printf("Hello, World!\n");
    return 0;
}

可以看到预处理会将 hello.c 中的头文件包含和宏定义展开,生成一个中间文件 hello.i。

  • 编译:将预处理后的中间文件转换为汇编代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ gcc -S hello.i -o hello.s

$ cat hello.s
	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello, World!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	call	puts@PLT
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 15.1.1 20250425"
	.section	.note.GNU-stack,"",@progbits
  • 汇编:将汇编代码转换为机器码,生成目标文件(.o 文件)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ gcc -c hello.s -o hello.o

$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

$ chmod +x ./hello.o
'./hello.o' 的模式已由 0644 (rw-r--r--) 更改为 0755 (rwxr-xr-x)

$ ./hello.o
zsh: 可执行文件格式错误: ./hello.o
可重定位文件会在后续文章中更进一步讲解

程序在这一步执行会报错是因为 ELF 64-bit LSB relocatable 表示这是一个可重定位文件(relocatable file),它不能直接执行。可重定位文件是编译器生成的中间文件,包含了机器码和符号信息,但还没有链接成可执行文件。

  • 链接:将目标文件和所需的库文件链接成可执行文件。
1
2
3
4
5
6
7
$ gcc hello.o -o hello

$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fff430a3b4def9f6c9b6b37917f6e7a44e1fedd5, for GNU/Linux 4.4.0, not stripped

$ ./hello
Hello, World!

dynamically linked 表示这个可执行文件是动态链接的,意味着它依赖于共享库。 同样的静态链接的可执行文件也可以通过 gcc -static 命令生成:

1
2
3
4
$ gcc -static hello.o -o hello_static

$ file hello_static
hello_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=86962ce4b2b5f4686ce66e7b8c2dd33a9b61410a, for GNU/Linux 4.4.0, not stripped

statically linked 表示这个可执行文件是静态链接的,意味着它不依赖于共享库。

ELF 文件结构

通常 ELF 文件结构如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+------------------------------+
|         ELF HEADER           |
+------------------------------+
|     PROGRAM HEADER TABLE     |
+------------------------------+
|         SECTION 1            |
+------------------------------+
|             ...              |
+------------------------------+
|         SECTION 2            |
+------------------------------+
|             ...              |
+------------------------------+
|    SECTION HEADER TABLE      |
+------------------------------+

简单的 ELF 文件结构通常至少包括以下几个部分:

1
2
3
4
5
6
7
8
9
+-------------------------------------------+
|           FILE HEADER (ELF HEADER)        |
+-------------------------------------------+
|           .text SECTION (代码段)          |
+-------------------------------------------+
|           .data SECTION (数据段)          |
+-------------------------------------------+
|        .bss SECTION (未初始化数据段)      |
+-------------------------------------------+

以下收集了一些常见的 ELF 文件结构的部分:

序号结构/段名中文名称/类型描述
1ELF HEADERELF文件头文件头,包含ELF文件的基本信息,如类型、架构、入口点等。
2PROGRAM HEADER TABLE程序头表描述可执行文件各个段(Segment)的信息,如内存地址、大小、权限等。
3SECTION HEADER TABLE节头表描述文件中各个节(Section)的信息,如名称、类型、大小等。
4.interp SECTION解释器节包含动态链接器(解释器)的路径。
5.text SECTION代码段存放程序的机器指令(可执行代码)。
6.rodata SECTION只读数据段存放只读数据(如常量字符串等)。
7.data SECTION已初始化数据段存放已初始化的全局变量和静态变量。
8.bss SECTION未初始化数据段存放未初始化的全局变量和静态变量,运行时会被初始化为0。
9.tdata SECTION线程数据段存放线程局部变量的初始值(TLS)。
10.tbss SECTION线程未初始化数据段存放线程局部变量的未初始化值(TLS),运行时会被初始化为0。
11.jcr SECTIONJava类注册表用于Java相关的运行时(很少见,一般可略)。
12.ctors SECTION构造函数数组存放程序启动时需要调用的构造函数指针数组。
13.dtors SECTION析构函数数组存放程序结束时需要调用的析构函数指针数组。
14.preinit_array SECTION预初始化数组存放在构造函数前执行的初始化函数数组。
15.init_array SECTION初始化数组存放程序启动时需要调用的初始化函数数组。
16.fini_array SECTION终止数组存放程序结束时需要调用的终止(清理)函数数组。
17.init SECTION初始化代码段存放启动时执行的初始化代码。
18.fini SECTION终止代码段存放退出时执行的清理代码。
19.plt SECTION过程链接表用于动态链接时的函数调用跳转表(Procedure Linkage Table)。
20.got SECTION全局偏移表动态链接时全局变量地址表(Global Offset Table)。
21.plt.got SECTION过程链接表/全局偏移表合并某些架构下的混合段。
22.got.plt SECTION全局偏移表/过程链接表合并某些架构下的混合段。
23.dynamic SECTION动态节存放动态链接所需的信息,如依赖库、符号表地址等。
24.hash SECTION哈希表段存放符号表的哈希索引,加速符号查找。
25.symtab SECTION符号表存放符号信息(如函数、变量名及其地址等),主要用于静态链接和调试。
26.dynsym SECTION动态符号表用于动态链接的符号表,只包含动态链接需要的符号。
27.strtab SECTION字符串表存放符号表对应的字符串(如符号名等)。
28.dynstr SECTION动态字符串表动态链接用的符号名字符串表。
29.rel.text SECTION / .rela.text代码段重定位信息代码段中需要重定位的地址和符号信息。
30.rel.data SECTION / .rela.data数据段重定位信息数据段中需要重定位的地址和符号信息。
31.rel.dyn SECTION / .rela.dyn动态段重定位信息用于动态链接时的重定位信息。
32.debug SECTION调试信息段存放调试信息,如符号、源代码行号等,仅用于调试。
33.note SECTION注释段存放额外信息,如编译器、平台、版本等。
可以使用 readelf 命令查看 ELF 文件的详细结构信息:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ readelf -WS /bin/ls
There are 28 section headers, starting at offset 0x223c0:

节头:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.property NOTE            0000000000000350 000350 000050 00   A  0   0  8
  [ 2] .note.gnu.build-id NOTE            00000000000003a0 0003a0 000024 00   A  0   0  4
  [ 3] .interp           PROGBITS        00000000000003c4 0003c4 00001c 00   A  0   0  1
  [ 4] .gnu.hash         GNU_HASH        00000000000003e0 0003e0 00004c 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          0000000000000430 000430 000be8 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000001018 001018 0005f3 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          000000000000160c 00160c 0000fe 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0000000000001710 001710 0000d0 00   A  6   1  8
  [ 9] .rela.dyn         RELA            00000000000017e0 0017e0 000b10 18   A  5   0  8
  [10] .relr.dyn         RELR            00000000000022f0 0022f0 000050 08   A  0   0  8
  [11] .init             PROGBITS        0000000000003000 003000 00001b 00  AX  0   0  4
  [12] .text             PROGBITS        0000000000003040 003040 015893 00  AX  0   0 64
  [13] .fini             PROGBITS        00000000000188d4 0188d4 00000d 00  AX  0   0  4
  [14] .rodata           PROGBITS        0000000000019000 019000 005270 00   A  0   0 32
  [15] .eh_frame_hdr     PROGBITS        000000000001e270 01e270 0005dc 00   A  0   0  4
  [16] .eh_frame         PROGBITS        000000000001e850 01e850 001a50 00   A  0   0  8
  [17] .note.ABI-tag     NOTE            00000000000202a0 0202a0 000020 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      0000000000021fd0 020fd0 000008 08  WA  0   0  8
  [19] .fini_array       FINI_ARRAY      0000000000021fd8 020fd8 000008 08  WA  0   0  8
  [20] .data.rel.ro      PROGBITS        0000000000021fe0 020fe0 000a50 00  WA  0   0 32
  [21] .dynamic          DYNAMIC         0000000000022a30 021a30 0001f0 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000022c20 021c20 0003c8 08  WA  0   0  8
  [23] .data             PROGBITS        0000000000023000 022000 000278 00  WA  0   0 32
  [24] .bss              NOBITS          0000000000023280 022278 001300 00  WA  0   0 32
  [25] .comment          PROGBITS        0000000000000000 022278 00001b 01  MS  0   0  1
  [26] .gnu_debuglink    PROGBITS        0000000000000000 022294 000010 00      0   0  4
  [27] .shstrtab         STRTAB          0000000000000000 0222a4 000119 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)
PROGRAM HEADER TABLE 指示的是段(segment),而 SECTION HEADER TABLE 指示的是节(section)。两者的区别在于:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
                       ELF 文件
+-------------------------------------------------------------+
| ELF Header | Program Header Table | Section 1 | Section 2 | ...
+-------------------------------------------------------------+

Program Header Table (段) --> 用于运行时加载
   |
   |--- Segment 1: 可能覆盖 Section 1 + Section 2
   |--- Segment 2: 可能只覆盖 Section 3

Section Header Table (节) --> 用于链接/调试
   |
   |--- Section 1: .text
   |--- Section 2: .data
   |--- Section 3: .bss
   |--- Section 4: .symtab
   ...

segment 揭示了程序在内存中的布局,而 section 则揭示了程序的逻辑结构。 segment 会装载多个权限相同的 section,从而在内存中形成一个连续的区域,节约资源和进行更好的权限控制

静态链接和动态链接

C 语言编译器是如何将多个文件联合编译出来一个可执行文件呢?

答案是通过链接(linker), 那么如何组织程序的链接结构?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// main.c
#include <stdio.h>

extern int add(int a, int b);

int main() {
  int a = 1;
  int b = 2;
  int result = add(a, b);
  printf("The result of adding %d and %d is %d\n", a, b, result);
}

// func.c
int add(int a, int b) {
    return a + b;
}
1
2
3
4
$ gcc main.c func.c -o main

$ ./main
The result of adding 1 and 2 is 3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
目标文件1                目标文件2
+-----------+           +-----------+
| .text     |           | .text     |
+-----------+           +-----------+
| .data     |           | .data     |
+-----------+           +-----------+
| .bss      |           | .bss      |
+-----------+           +-----------+

       \                     /
        \                   /
         \                 /
          \               /
           \             /
            \           /
             \         /
              \       /
               \     /
                \   /
                 \ /
         +-------------------+
         |    链接器(ld)    |
         +-------------------+
                 |
                 v
+---------------------------+
|      .text (合并后)       |
+---------------------------+
|      .data (合并后)       |
+---------------------------+
|      .bss  (合并后)       |
+---------------------------+

两个程序中相同的 section 会被合并成一个 section, 从而更好的利用资源和内存空间

系统拥有许多的库文件(类似 C 语言的标准库), 通过在运行时链接还是编译时链接分为动态链接和静态链接

序号动态链接(Dynamic Linking)静态链接(Static Linking)说明对比
1运行时完成链接编译时完成链接链接时机
2使用共享库(.so, .dll)使用静态库(.a, .lib)所用库类型
3多个程序可共享内存中的库代码每个程序独立拥有一份库代码和数据内存/代码复用
4运行时加载库(延迟加载可能)编译时加载库(不可延迟)加载方式
5可通过升级库文件直接影响所有相关程序升级库文件需重新编译所有相关程序升级与维护性
6节省磁盘空间(库只存一份)占用更多磁盘空间(每个程序都包含库)存储占用
7程序启动和运行时依赖外部库存在程序本身独立,无需外部库运行依赖
8兼容性较弱(库版本不一致易出问题)兼容性较强(所有依赖已固定)兼容性
9运行时调用库函数,性能略低于静态链接库代码已集成,运行时性能更高性能对比
10可实现库的动态升级/热更新需重新编译和部署,无法动态升级维护方式
11适用于大型、需频繁升级的应用适用于小型、发行版程序或嵌入式开发典型使用场景

字节序

  • 低位字节序(Little Endian)/ 小端序 :低位字节存储在低地址,高位字节存储在高地址。
1
2
3
4
5
1234 在内存中的存储方式:
\x01\x02\x03\x04

"abcd" 字符串在内存中的存储方式:
\x61\x62\x63\x64

gdb 查看

1
2
3
4
pwndbg> x/1gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000000064636261
                ^                ^
              高地址           低地址
  • 高位字节序(Big Endian)/ 大端序 :高位字节存储在低地址,低位字节存储在高地址。
1
2
1234 在内存中的存储方式:
\x04\x03\x02\x01