0%

ELF文件基本结构

5f8e8b8e50326.jpg

ret2_dl_runtime_resolve学习第一步

ELF文件结构

之前学习ret2_dl_runtime_resolve看了ELF文件后,看的稀里糊涂,然后就被劝退了,现在做题碰到题不会写,现在被迫学,呜呜呜~~

WIKI上直接讲的利用方法,在学之前需要学习ELF文件基本结构和动态链接过程,我这里就记录一下自己的学习过程。

image-20201029212859066.png

注意:段(Segment)与节(Section)的区别。很多地方对两者有所混淆。段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中(像.data节和.bss节就被整合到了一起)。相比而言,节的粒度更小。Segment是告诉操作系统应该将段加载虚拟内存的那个位置

如图所示,为ELF文件的基本结构,其主要由四部分组成:

  • ELF Header
  • ELF Program Header Table (或称Program Headers、程序头)
  • ELF Section Header Table (或称Section Headers、节头表)
  • ELF Sections

从图中,我们就能看出它们各自的数据结构以及相互之间的索引关系。下面我们依次进行介绍。

ELF Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
L0ne1y@L0ne1y:~/pwn/ROP/ret2libc3$ readelf -h ret2libc3
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x80484d0
程序头起点: 52 (bytes into file)
Start of section headers: 6436 (bytes into file)
标志: 0x0
本头的大小: 52 (字节) //Size of this header
程序头大小: 32 (字节) //Size of program headers
Number of program headers: 9 //程序头数量
节头大小: 40 (字节) //单个节头大小
节头数量: 35
字符串表索引节头: 32 //Section Header字符串表在Section Header Table中的索引

这里着重说几个ELF Header的成员的含义。

ELF魔数

一般可执行文件的开头的前四个字节,被称做魔数(Magic Number),像ELF可执行文件的前四个字节是7f 45 4c 46也就是0x7f,e,l,f

程序入口点

入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位指令一般没有入口地址,则该值为0

ELF文件类型

ELF文件主要有三种类型,可以通过ELF Header中的e_type成员进行区分。

  • 可重定位文件(Relocatable File):ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。
  • 可执行文件(Executable File):ET_EXEC。可以直接执行的程序。
  • 共享目标文件(Shared Object File):ET_DYN。一般为.so文件。有两种情况可以使用。
    • 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
    • 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像

ELF Program Header Table

program header用于描述segment的特性,重定位文件(.o)没有program header 因为它不可以运行。ELF可执行文件是由Program Header Table的。

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
L0ne1y@L0ne1y:~/pwn/ROP/ret2libc3$ readelf -l ret2libc3

Elf 文件类型为 EXEC (可执行文件)
入口点 0x80484d0
共有 9 个程序头,开始于偏移量 52

程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00884 0x00884 R E 0x1000
LOAD 0x000f08 0x08049f08 0x08049f08 0x00134 0x001dc RW 0x1000
DYNAMIC 0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x000780 0x08048780 0x08048780 0x00034 0x00034 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1

Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got

可以看到,一共有9个segment,只有类型为LOAD的段是运行时真正需要的。我们可以看到第一个LOAD的执行权限是R(可读)W(可读)的,它的编号是03,在这个segment中包含多个section,包括了.init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 。到这里我们就可以明白了一个segment可能包含多个section,这也就是segment和section的映射关系。

ELF Section Header Table

image-20201029222446358.png

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
39
40
41
42
43
44
L0ne1y@L0ne1y:~/pwn/ROP/ret2libc3$ readelf -S ret2libc3
共有 35 个节头,从偏移量 0x1924 开始:

节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 00002c 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481d8 0001d8 0000e0 10 A 6 1 4
[ 6] .dynstr STRTAB 080482b8 0002b8 00008f 00 A 0 0 1
[ 7] .gnu.version VERSYM 08048348 000348 00001c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048364 000364 000030 00 A 6 1 4
[ 9] .rel.dyn REL 08048394 000394 000018 08 A 5 0 4
[10] .rel.plt REL 080483ac 0003ac 000050 08 A 5 12 4
[11] .init PROGBITS 080483fc 0003fc 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048420 000420 0000b0 04 AX 0 0 16
[13] .text PROGBITS 080484d0 0004d0 000242 00 AX 0 0 16
[14] .fini PROGBITS 08048714 000714 000014 00 AX 0 0 4
[15] .rodata PROGBITS 08048728 000728 000056 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 08048780 000780 000034 00 A 0 0 4
[17] .eh_frame PROGBITS 080487b4 0007b4 0000d0 00 A 0 0 4
[18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000034 04 WA 0 0 4
[24] .data PROGBITS 0804a034 001034 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a040 00103c 0000a4 00 WA 0 0 32
[26] .comment PROGBITS 00000000 00103c 00002b 01 MS 0 0 1
[27] .debug_aranges PROGBITS 00000000 001067 000020 00 0 0 1
[28] .debug_info PROGBITS 00000000 001087 000329 00 0 0 1
[29] .debug_abbrev PROGBITS 00000000 0013b0 0000f8 00 0 0 1
[30] .debug_line PROGBITS 00000000 0014a8 0000c5 00 0 0 1
[31] .debug_str PROGBITS 00000000 00156d 000270 01 MS 0 0 1
[32] .shstrtab STRTAB 00000000 0017dd 000146 00 0 0 1
[33] .symtab SYMTAB 00000000 001e9c 000530 10 34 50 4
[34] .strtab STRTAB 00000000 0023cc 000305 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

节类型(sh_type)

节名是一个字符串,只是在链接和编译过程中有意义,但它并不能真正地表示节的类型。对于编译器和链接器来说,主要决定节的属性是节的类型(sh_type)和节的标志位(sh_flags)。

节的类型相关常量以SHT_开头,上述readelf -S命令执行的结果省略了该前缀。常见的节类型如下表所示:

常量 含义
SHT_NULL 0 无效节
SHT_PROGBITS 1 程序节。代码节、数据节都是这种类型。
SHT_SYMTAB 2 符号表
SHT_STRTAB 3 字符串表
SHT_RELA 4 重定位表。该节包含了重定位信息。
SHT_HASH 5 符号表的哈希表
SHT_DYNAMIC 6 动态链接信息
SHT_NOTE 7 提示性信息
SHT_NOBITS 8 表示该节在文件中没有内容。如.bss
SHT_REL 9 该节包含了重定位信息
SHT_SHLIB 10 保留
SHT_DNYSYM 11 动态链接的符号表

节标志位(sh_flag)

节标志位表示该节在进程虚拟地址空间中的属性。如是否可写、是否可执行等。相关常量以SHF_开头。常见的节标志位如下表所示:

常量 含义
SHF_WRITE 1(W) 表示该节在进程空间中可写
SHF_ALLOC 2 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。
SHF_EXECINSTR 4(X) 表示该节在进程空间中可以被执行

ELF Sections

节的分类

上述ELF Section Header Table部分已经简单介绍了节类型。接下来我们来介绍详细一些比较重要的节。

.text节

.text节是保存了程序代码指令的代码节一段可执行程序,如果存在Phdr,则.text节就会存在于text段中。由于.text节保存了程序代码,所以节类型为SHT_PROGBITS

.rodata节

rodata节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,所以节类型为SHT_PROGBITS

.plt节(过程链接表)

.plt节也称为过程链接表(Procedure Linkage Table)其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt节保存了代码,所以节类型为SHT_PROGBITS

.data节

.data节存在于data段中,其保存了初始化的全局变量等数据。由于.data节保存了程序的变量数据,所以节类型为SHT_PROGBITS

.bss节

.bss节存在于data段中,占用空间不超过4字节,仅表示这个节本省的空间。.bss节保存了未进行初始化的全局数据。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS

.got.plt节(全局偏移表-过程链接表)

.got节保存了全局偏移表.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt节与程序执行有关,所以节类型为SHT_PROGBITS

.dynsym节(动态链接符号表)

.dynsym节保存在text段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM

.dynstr节(动态链接字符串表)

.dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。

.symtab节(符号表)

.symtab节是一个ElfN_Sym的数组,保存了符号信息。节类型为SHT_SYMTAB

.strtab节(字符串表)

.strtab节保存的是符号字符串表,表中的内容会被.symtabElfN_Sym结构中的st_name引用。节类型为SHT_STRTAB

符号表

节的分类中我们介绍了.dynsym节和.symtab节,两者都是符号表。那么它们到底有什么区别呢?存在什么关系呢?

符号是对某些类型的数据或代码(如全局变量或函数)的符号引用,函数名或变量名就是符号名。例如,printf()函数会在动态链接符号表.dynsym中存有一个指向该函数的符号项(以Elf_Sym数据结构表示)。在大多数共享库和动态链接可执行文件中,存在两个符号表。即.dynsym.symtab

.dynsym保存了引用来自外部文件符号的全局符号。如printf库函数。.dynsym保存的符号是.symtab所保存符合的子集,.symtab中还保存了可执行文件的本地符号。如全局变量,代码中定义的本地函数等。

既然.dynsym.symtab的子集,那为何要同时存在两个符号表呢?

通过readelf -S命令可以查看可执行文件的输出,一部分节标志位(sh_flags)被标记为了A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。其中,.dynsym被标记为ALLOC,而.symtab则没有标记。

ALLOC表示有该标记的节会在运行时分配并装载进入内存,而.symtab不是在运行时必需的,因此不会被装载到内存中。.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号.dynsym对于动态链接可执行文件的执行是必需的,而.symtab只是用来进行调试和链接的。image-20201029230253710.png上图所示为通过符号表索引字符串表的示意图。符号表中的每一项都是一个Elf_Sym结构,对应可以在字符串表中索引得到一个字符串。

字符串表

类似于符号表,在大多数共享库和动态链接可执行文件中,也存在两个字符串表。即.dynstr.strtab,分别对应于.dynsymsymtab。此外,还有一个.shstrtab的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name进行索引。

ELF文件中所有字符表的结构基本一致,如上图所示。

参考链接

http://chuquan.me/2018/05/21/elf-introduce/

https://www.cnblogs.com/jiqingwu/p/elf_format_research_01.html