什么是ELF文件?
英文全称就是Executable Linkable Format,即可执行可链接格式,Linux系统上所运行的就是ELF格式的文件,相关定义在“/usr/include/elf.h”文件里。
1. 编写示例代码
这里我们编写下面的示例代码,用来编译生成ELF文件
#include <stdio.h>
int global_init_var = 10;
int global_uninit_var;
void func(int sum) {
printf("%d\n",sum);
}
void main(void) {
static int local_static_init_var = 20;
static int local_static_uninit_var;
int local_init_val = 30;
int local_uninit_var;
func(global_init_var + local_init_val + local_static_init_var);
}
稍微解释一下上述代码,就是分别在全局创建可变变量,main函数内部创建静态变量和可变变量,然后在main函数内对变量进行相加并输出。
接下来分别使用以下命令对示例代码进行编译:
gcc elfDemo.c -o elfDemo.exec //生成动态可执行文件
gcc -static elfDemo.c -o elfDemo_static.exec //生成静态可执行文件
gcc -c elfDemo.c -o elfDemo.rel //生成可重定位文件
gcc -c -fPIC elfDemo.c -o elfDemo_pic.rel && gcc -shared elfDemo_pic.rel -o elfDemo.dyn //生成动态链接库文件(共享目标文件)
分别解释一下上述gcc参数的作用:
-static:生成静态可执行文件,使用静态链接;
-c:编译、汇编指定的源文件,但是不进行链接;
-fPIC:产生位置无关码(position independent code) ;
-shared:生成共享库(动态链接库);
让我们使用file命令来看看生成的究竟是什么妖孽
➜ file elfDemo.exec
elfDemo.exec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f9dc99d717e41298f465ac3be2db2775aede666, for GNU/Linux 3.2.0, not stripped
dynamically linked说明第一个文件属于动态可链接文件
➜ file elfDemo_static.exec
elfDemo_static.exec: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=af7a8c76ceca3b151e669aabb38f753002d4a394, for GNU/Linux 3.2.0, not stripped
statically linked说明第二个文件属于静态可链接文件
➜ file elfDemo.rel
elfDemo.rel: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
relocaltable说明第三个文件属于可重定位文件
➜ file elfDemo.dyn
elfDemo.dyn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d43a76d4eeb0d10e1d7fe26e929ae8e669f87346, not stripped
shared object说明第四个文件属于可共享文件
通过上述案例我们可以发现ELF文件可以分为三种类型:可执行文件(.exec)、可重定位文件(.rel)和共享目标文件(.dyn)
- 可执行文件:经过链接的、可执行的目标文件,通常也被称为程序;
- 可重定位文件:由源文件编译而成且尚未链接的目标文件,通常以“.o”作为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置独立的代码;
- 共享目标文件:动态链接库文件。用于在链接过程中与其他动态链接库或可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。
2. ELF文件结构
我们讨论ELF文件结构通常有两个不同的视角:链接视角,即用户视角,通过节(Section)来进行划分ELF文件;另一种是运行视角,即操作系统的视角,通过段(Segment)来进行划分。相信大家在学习计算机组成原理的时候也有了解过这两个概念。
在网上找来了一张图,可以更直观的看出来两种视角的结构
2.1 ELF文件总体结构
在进行PWN时,我们通常是以用户的视角来找到二进制漏洞的,因此我们先学习链接视角下ELF的文件结构👇
在链接视角下,通常目标文件都会包含以下三个部分:
- 代码节(.text):用于保存可执行的机器指令;
- 数据节(.data):用于保存已初始化的全局变量和局部静态变量;
- BSS节(.bss):用于保存未初始化的全局变量和局部静态变量。
为什么要将目标文件分成一个个节呢❓❓❓
从安全的角度来说,将程序指令和程序数据分开进行存储,由于数据区域对于进程而言是可读写的,而指令区域对于进程而言是只读的,两块区域权限分别为可读写和只读,这样可以有效地防止程序的指令被改写和利用。
在这部分,我们最后说明一下节和段的关系👇
相同权限的节会放入同一个段中,例如.text和.rodata节;一个段包含许多节,一个节可以属于多个段。(.rodata节是一个只读的数据节)
2.2 ELF文件头
在一个ELF文件中,除了具有上述三个节外,还应包含一个文件头(LEF header)。
ELF文件头位于目标文件最开始的位置,包含描述整个文件的一些基本信息,如ELF文件类型、版本/ABI版本、目标机器、程序入口、段表和节表的位置和长度等。
在文件头部存在魔术字符(7f 45 4c 46),即字符串“\177ELF”,当文件被映射到内存时,可以通过搜索该字符串确定映射地址,这个方法通常用于dump内存。
下面将展示一下这个方法👇
➜ readelf -h elfDemo.rel
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
从上面可以看见,Magic前四个字节内容为7f 45 4c 46。
2.3 节头表 (链接视角)
一个目标文件中包含许多节,这些节的信息保存在节头表中。
表的每一项都记录了节的名称、长度、偏移、读写权限等信息。
节头表的位置记录在文件头的e_shoff域中。
节头表对于程序运行而言并不是必须的,因为它与程序内存布局无关,所以常有程序去除节头表,以增加反编译器的分析难度。
下面将展示一下程序的节头表长什么样👇
➜ readelf -S elfDemo.rel
There are 14 section headers, starting at offset 0x400:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000005b 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000002e0
0000000000000078 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 0000009c
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a4
0000000000000008 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a4
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a8
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000d4
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.pr[...] NOTE 0000000000000000 000000d8
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000f8
0000000000000058 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000358
0000000000000030 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000150
0000000000000120 0000000000000018 12 7 8
[12] .strtab STRTAB 0000000000000000 00000270
0000000000000070 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000388
0000000000000074 0000000000000000 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)
我们不难发现,上述程序具有.text、.rela.text、.data、.bss、.rodata、.comment等节。
2.3.1 .text节详解
接下来仔细分析一下.text节,也就是代码节👇
➜ objdump -x -s -d elfDemo.rel
Idx Name Size VMA LMA File off Algn
0 .text 0000005b 0000000000000000 0000000000000000 00000040 2**0
Contents of section .text:
0000 f30f1efa 554889e5 4883ec10 897dfc8b ....UH..H....}..
0010 45fc89c6 488d0500 00000048 89c7b800 E...H......H....
0020 000000e8 00000000 90c9c3f3 0f1efa55 ...............U
0030 4889e548 83ec10c7 45fc1e00 00008b15 H..H....E.......
0040 00000000 8b45fc01 c28b0500 00000001 .....E..........
0050 d089c7e8 00000000 90c9c3
Disassembly of section .text:
0000000000000000 <func>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: 89 7d fc mov %edi,-0x4(%rbp)
f: 8b 45 fc mov -0x4(%rbp),%eax
12: 89 c6 mov %eax,%esi
14: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 1b <func+0x1b>
17: R_X86_64_PC32 .rodata-0x4
1b: 48 89 c7 mov %rax,%rdi
1e: b8 00 00 00 00 mov $0x0,%eax
23: e8 00 00 00 00 call 28 <func+0x28>
可以看到,Contents of section .text部分是.text数据的十六进制形式,最左边一列是偏移量,中间四列是内容,最右边一列是ASCII码形式;Disassembly of section .text部分则是反汇编的结果。
2.3.2 .data节和.rodata节详解
接下来仔细分析一下.text和.rodata节,也就是数据节和只读数据节👇
➜ objdump -x -s -d elfDemo.rel
Sections:
Idx Name Size VMA LMA File off Algn
1 .data 00000008 0000000000000000 0000000000000000 0000009c 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .rodata 00000004 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
Contents of section .data:
0000 0a000000 14000000 ........
Contents of section .rodata:
0000 25640a00 %d..
容易看到.data节保存已经初始化的全局变量和局部静态变量。在源代码中共有两个这样的变量:global_init_var(0a000000)和local_static_init_var(14000000),每个变量4个字节,一共8个字节。
.rodata节保存只读数据,包括只读变量和字符串常量。源代码中调用printf()函数时,用到了一个字符串“%d\n”,它是一种只读数据,因此保存在.rodata节中,可以看到字符串常量的ASCII形式,以“\0”结尾。
2.3.3 .bss节详解
最后分析一下.bss节,也就是BSS节👇
➜ objdump -x -s -d elfDemo.rel
Sections:
Idx Name Size VMA LMA File off Algn
2 .bss 00000008 0000000000000000 0000000000000000 000000a4 2**2
ALLOC
BSS节是用于保存未初始化的全局变量和局部静态变量。BSS节没有CONTENTS属性,表示该节在文件中实际上不存在,只是为变量预留了位置而已,因此该节的sh_offset域也没有意义。
2.3.4 其他常见的节
除了上述节外,还有一些常用的节,下面进行简单的列举,以后用到再细嗦
节名 | 说明 |
.got | 全局偏移量表(global offset table),用于保存全局变量引用的地址 |
.got.plt | 全局偏移量表,用于保存函数引用的地址 |
.plt | 过程链接表(procedure linkage table),用于延迟绑定 |
2.4 可执行文件的装载(运行视角)
在上述内容都是以链接视角对目标文件进行解读,接下来我们以运行视角进行审视目标文件。
可执行文件是如何装载到内存中的❓❓❓
首先运行一个可执行文件
然后将该文件和动态链接库装载到进程空间中
最后形成一个进程镜像,每个进程都拥有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头决定的。ELF文件头的e_phoff域给出了段头表的位置
下面将展示一个可执行文件的程序头👇
➜ readelf -l elfDemo.exec
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001b1 0x00000000000001b1 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000114 0x0000000000000114 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000260 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002008 0x0000000000002008 0x0000000000002008
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
容易看到一个段内包含了一个节或者多个节,即段就是对这些节进行分组。
分段的目的👇
随着节的数量增多,在进行内存映射时就产生空间和资源的浪费。实际上系统并不关心每个节的内容,而是关心这些节的权限(读、写、执行),通过将不同权限的节分组,即可同时装载多个节,从而节省资源。
2.4.1 常见段详解
PT_LOAD段——每个可执行文件至少有一个PT_LOAD段,用于描述可装载的节,而动态链接的可执行文件则包含两个,将.data和.text分开存放。
动态段PT_DYNAMIC——包含一些动态链接器所必须的信息,如共享库列表、GOT表、重定位表等。
PT_NONE段——保存了系统相关的附加信息,但并不是程序运行所需要的。
PT_INTERP段——将位置和大小信息存放在一个字符串中,是对程序解释器位置的描述。
PT_PHDR段——保存了程序头表本身的位置和大小。