ELF 文件格式
😊
1 目标文件
1.1 目标文件
ELF 是 Executable Linkable Format 的简称,是为 Executable files(可执行文件), object files(目标文件), shared libraries(动态链接库), 和 core dumps(内核转储)准备的标准文件格式。 Linux 和很多类 Unix 操作系统都使用这个格式。
在 ELF 规范中,ELF 文件统称为目标文件,主要分为以下几类:
- 共享目标文件(shared object file)。即动态链接库文件,以
.so
为文件扩展名(PE 动态链接库文件是dll
)。 - 可重定位文件(relocatable file)。通过编译和汇编,以
.o
扩展名结尾(还未链接为可执行文件,即可执行文件的前身)。可以和其他目标文件链接后生成可执行文件或动态链接库。 - 可执行文件(executable file)。已经经过链接的,可直接执行的文件(Linux 下可不指定文件扩展名,Windows 下为
.exe/.dll/.sys
等)。 - 核心转储文件(Core dump file)。当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些信息转储到核心转储文件。
Linux 下文件编译、汇编、链接过程如下图:
1.2 ELF 文件格式概览
ELF 文件有两种视角可供选择,一种是链接视角,通过节(Section)来进行划分;另一种是运行视角,通过段(Segment)来进行划分。(ELF 文件格式定义在 /usr/include/elf.h
)
- File header。必须位于文件的最开始处,包含有整个文件的结构信息。其结构定义为
Elf32_Ehdr/Elf64_Ehdr
。 - Section header table(节表头)。用来描述重定位文件各个节的信息。对于重定位文件来说是必须的,对可执行文件来说是可选的。
- Program header table(程序表头)。该表描述了加载程序准备执行所需的可加载段和其他数据结构。
- Section/Segment。包括可加载数据、重定位以及字符串和符号表。
常用的查看 ELF 文件的几种方式:
- 使用
readelf
命令。可查看 ELF 文件整体格式。 - 使用
hexdump
命令。 - 使用
file
命令。
关于 hexdump
命令:
1 | hexdump [选项] [文件] |
ELF 文件格式概览如下:
参考:
1.3 ELF 中的数据类型
ELF 文件格式定义在 /usr/include/elf.h
中,文件中分别为 ELF32、ELF64 的文件定义了很多数据类型(如 ELF64_Addr
),这些数据类型是使用的 uint32_t
等数据类型进行定义的(uint32_t
等类型在 /usr/include/stdint.h
中进行定义)。
2 ELF 文件格式解析
2.1 File Header
File Header 文件头必须位于 ELF 文件的开头,含有整个文件的一些基本信息。ELF32 对应的文件头为 Elf32_Ehdr
结构,ELF64 对应的文件头为 Elf64_Ehdr
结构。
1 | typedef struct |
2.2 e_ident 数组
e_ident
是一个 16 字节的数组,它将文件标识为 ELF 目标文件,并提供有关目标文件结构的数据表示的信息。 该数组最后剩余字节保留供将来使用,并且应设置为 0
。 数组的每个字节都使用 EI_
开头名称作为索引:
1 | /* /usr/include/elf */ |
解释:
索引名称 | 值 |
---|---|
EI_MAG0(0)~EI_MAG3(3) | 文件的最前面 4 字节 eident[EI_MAGO]~eident[EI_MAG3] 的内容被称为“魔数(Magic)”,用于标识这是一个 ELF 文件。这四个字节存放的内容是固定的: |
EI_CLASS(4) | e_ident[EI_CLASS] 指明文件是 ELF32 还是 ELF64 可执行文件。 |
EI_DATA(5) | e_ident[EI_DATA] 指明了目标文件中的数据编码格式,小端序大端序 。在 Intel 架构中,e_ident[EI_DATA] 取值为 ELFDATA2LSB 。 |
EI_VERSION(6) | e_ident[EI_VERSION] 指明 ELF 文件头的版本,目前这个版本号是 EV_CURRENT(1) 。 |
EI_OSABI(7) | e_ident[El_OSABI] 指示该 ELF 文件适用的操作系统和 ABI 规范。 |
EI_ABIVERSION(8) | e_ident[El_ABIVERSION] 标识该对象文件的 ABI 的版本。 该字段用于区分 ABI 的不兼容版本。 此版本号的解释取决于 El_OSABI 字段标识的 ABI。对于符合 System V ABl 第三版的应用程序,该字段应为 0 。 |
EI_PAD(9)~EI_NIDENT-1(15) | 这 7 个字节目前暂时不使用,留作以后扩展,在实际的文件中应被填 0 补充。 |
2.3 其余字段
e_type
:指示本目标文件属于哪种类型。ET_LOPROC~ ET_HIPROC(Oxff00~Oxffff)
这一范围内的文件类型是为特定处理器而保留的,如果需要为某种处理器专门设定文件格式,可以从这一范围内选取一个做为标识。1
2
3
4
5
6
7
8
9
10
11
12/* Legal values for e_type (object file type). */
e_machine
:指定该文件适用的处理器体系结构。Intel x86 结构对应于EM_386(3)
,x86-64(Intel/AMD)都是EM_X86_64(62)
。1
2
3
4
5
6
7
8
9
10
11
...e_version
:此字段指明目标文件的版本。相同于e_ident[EI_VERSION]
,当前取值为EV_CURRENT(1)
。1
2
3
4
5/* Legal values for e_version (version). */
e_entry
:程序入口的虚拟地址(EOP
)。即当文件被加载到进程空间里后,入口程 序在进程地址空间里的地址。对于可执行程序文件来说,当 ELF 文件完成加载之 后,程序将从这里开始运行;而对于其它文件来说,这个值应该是0
。e_phoff
:指示程序头表(program header table)开始处在文件中的偏移量。如果没有程序头表,该值应设为0
。e_shoff
:指示节头表(section header table)开始处在文件中的偏移量。如果没有节头表,该值应设为0
。e_flags
:处理器特定的标志位。对于 Intel 架构的处理器来说,它没有定义任何标志位,所以e_flags == 0
。e_ehsize
:指明 ELF文件头(File Header)的大小,以字节为单位。e_phentsize
:指明在程序头表中每一个表项条目(Entry)的大小,以字节为单位。e_phnum
:指明程序头表中总共有多少个表项条目。如果一个目标文件中没有程序头表,该值应设为0
。e_shentsize
:指明节头表中每一个表项条目(Entry)的大小,以字节为单位。e_shnum
:指明节头表中总共有多少个表项条目。如果一个目标文件中没有节头表, 该值应设为0
。e_shstrndx
:String Table Index,该值是一个索引值。在节区表中有一个存储各节区名称的节区.shstrtab
(通常是最后一个),这里表示名称表在第几个节区。如果文件没有节名称表,此值应设置为SHN_UNDEF(0)
。1
2
3
4
5
6
7
8
9
10
11
12
13
14/* Special section indices. */
举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14[root@centos-7 ~]
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
...
Section header string table index: 29 // 索引值为 29
// 查看节区,可以看到索引值 29 对应的节区名称为 .shstrtab
[root@centos-7 ~]
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[29] .shstrtab STRTAB 0000000000000000 00001819
0000000000000108 0000000000000000 0 0 1
3 Sections
在目标文件中可以包含很多“节”(section),所有这些“节”都登记在一张称为 “节头表”(section header table)的数组里。节头表的每一个表项是一个 Elf32_Shdr/Elf64_Shdr
结构,通过每一个表项可以定位到对应的节。
除了文件头、程序头表、节表的信息外,ELF 文件的其他所有信息都包含在节中。ELF 文件所有节的信息都保存在一个节表数组中,数组中的每一项对应一个节,该项的成员描述对应的节。
节表是一个数组,节表在文件中的位置由 Elf64_Ehdr.e_shoff
指向(从文件开头的文件偏移),节表数组的项数由 Elf64_Ehdr.e_shnum
指定,节表每一项的大小由 Elf64_Ehdr.e_shentsize
指定。
3.1 节索引号(Section indices)
节表是一个数组,数组中的每一项都有一个对应的索引值,从 0
开始(项数由 Elf64_Ehdr.e_shnum
决定)。但是 0
、0xFF00~0xFFFF
等索引值是保留的,具有特殊意义,如下表:
1 | /* Special section indices. */ |
注释:
- 节表数组的第一项(索引为
0
)必须保留,且该项为0
。 - 节表数组
0xFF00-0xFFFF
这个区间的索引号是保留值。
如下查看 ELF 文件的节头表,可以看到索引区间为 0-29
。
1 | [root@centos-7 ~] |
3.2 节头表(数组)
节区主要存放各种特定类型的信息,比如程序的正文区(代码)、数据区(初始化和未初始化的数据)、调试信息、以及用于动态链接的一些节区,比如解释器(.interp
)节区将指定程序动态装载 /
链接器 ld-linux.so
的位置,而过程链接表(plt
)、全局偏移表(got
)、重定位表则用于辅助动态链接过程。
https://github.com/tinyclub/open-c-book/blob/master/zh/chapters/02-chapter4.markdown
节表中的每一项都是一个条目(节表数组的项数由 Elf64_Ehdr.e_shnum
指定),每个条目的结构如下:
1 | typedef struct |
成员 | 解释 |
---|---|
sh_name | 本节名字的在“节名称表”中的索引值。整个名字的字符串并不存储在这里,它仅是一个索引号,指向“字符串表”节中的某个位置,那里存储了一个以 \0 结尾的字符串。 |
sh_type | 本节的类型。如下代码 3-1。 |
sh_flags | 本节的一些属性,由一系列标志比特位组成,各个比特定义了节的不同属性,当某种属性被设置时,相应的标志位被设为 1,反之则设为 0。如下代码 3-2。 |
sh_addr | 如果本节的内容需要映射到进程空间中去,此成员指定映射的起始地址;如果不需要映射,此值为 0。 |
sh_offset | 该节的文件偏移。该值是节的第一个字节在文件中的位置,即相对于文件开头的偏移量。单位是字节。 |
sh_size | 本节的大小,单位是字节。 |
sh_link | 此成员是一个索引值,指向节头表中本节所在的位置。根据节的类型不同,本成员的意义也有所不同。如下表 3-1. |
sh_info | 本节的附加信息,根据节的类型不同,本成员的意义也有所不同。如下表 3-1. |
sh_addralign | 指示 sh_addr 虚拟地址应该向多少字节对齐。ELF64 文件对齐值必须是 2 的倍数。如果该成员为 0/1 ,则表示该虚拟地址没有对齐的要求。 |
sh_entsize | 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。 如果节区中并不包含固定长度表项的表格,此成员取值为 0。 |
节表每一项的大小由 Elf64_Ehdr.e_shentsize
指定,ELF64 每个节表条目的大小为 64 bytes。
节的类型 sh_type
,代码 3-1.
1 | /* Legal values for sh_type (section type). */ |
每个节都有相关的属性,如下代码 3-2.
1 |
表 3-1.
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 表示该节所使用的字符串表在节表中的下标(索引)。 类似于 shstrtab 节使用到的字符串在节表的索引值为 e_shstrndx 。 |
0 |
SHT_HASH | 表示该节所使用的字符串表在节表中的下标(索引)。 | 0 |
SHT_RELA,SHT_RE | 相应符号表在节头表中的索引值 | 本重定位节所所作用的节在节头表中的索引值(需要被重定位的数据所在的节的索引) |
SHT_SYMTAB,SHT_DYNSYM | 相关字符串表的节头素引 | 符号表中最后一个本地符号的索引值加 1 |
其他 | SHN_UNDEF(0) | 0 |
参考:ELF:Sections。
3.3 系统使用的节
更多节信息可查看 Various sections hold program and control information。
- 以“.”开头的节区名称是系统保留的。应用程序可以使用没有前缀的节区名称,以避免与系统节区冲突。
- 目标文件中也可以包含多个名字相同的节区。
- 保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写 + 节区名称。
- 处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 街区是由 FOO 体系结构定义的 psect 节区。
常见的表的属性:
名字 | 类型 | 属性 | 意义 |
---|---|---|---|
.init | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进 入主函数之前执行这一节中的代码。 |
.fini | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节包含进程终止时要执行的程序指令。当程序正常退出时,系统会执行这 一节中的代码。 |
.bss | SHT_NOBITS | SHF_ALLOC+SHF_WRITE | 本节中包含目标文件中未初始化的全局变量。一般情况下,可执行程序在开 始运行的时候,系统会把这一段内容清零。但是,在运行期间的 bss 段是由系统初 始化而成的,在目标文件中.bss 节并不包含任何内容,其长度为 0,所以它的节类 型为 SHT_NOBITS。 |
.comment | SHT_PROGBITS | 无 | 本节包含版本控制信息 |
.data/.data1 | SHT_PROGBITS | SHF_ALLOC+SHF_WRITE | 这两个节用于存放程序中被初始化过的全局变量。在目标文件中,它们是占 用实际的存储空间的,与.bss 节不同。 |
.debug | SHT_PROGBITS | 无 | 调试信息,内容格式没有统一规定。所有以”.debug”为前缀的节名 字都是保留 |
.line | SHT_PROGBITS | 无 | 本节也是一个用于调试的节,它包含那些调试符号的行号,为程序指令码与 源文件的行号建立起联系。其内容格式没有统一规定。 |
.dynamic | SHT_DYNAMIC | 见下文 | 本节包含动态连接信息,并且可能有 SHF_ALLOC 和 SHF_WRITE 等属性。 是否具有 SHF_WRITE 属性取决于操作系统和处理器。 |
.dynstr | SHT_STRTAB | SHF_ALLOC | 此节含有用于动态连接的字符串,一般是那些与符号表相关的名字 |
.dynsym | SHT_DYNSYM | SHF_ALLOC | 此节含有动态连接符号表 |
.got | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE | 此节包含全局偏移量表 |
.hash | SHT_HASH | SHF_ALLOC | 本节包含一张符号哈希表 |
.interp | SHT_PROGBITS | 见下文 | 此节含有 ELF 程序解析器的路径名。如果此节被包含在某个可装载的段中, 那么本节的属性中应置 SHF_ALLOC 标志位,否则不置此标志。 |
.note | SHT_NOTE | 无 | 注释节 |
.plt | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节包含函数连接表 |
.relname/.relaname | SHT_REL/SHT_RELA | 见下文 | 这两个节含有重定位信息。如果此节被包含在某个可装载的段中,那么本节 的属性中应置 SHF_ALLOC 标志位,否则不置此标志。注意,这两个节的名字 中”name”是可替换的部分,执照惯例,对哪一节做重定位就把”name”换成哪一节 的名字。比如,.text 节的重定位节的名字将是.rel.text 或.rela.text。 |
.rodata/.rodata1 | SHT_PROGBITS | SHF_ALLOC | 本节包含程序中的只读数据,在程序装载时,它们一般会被装入进程空间中 那些只读的段中去 |
.shstrtab | SHT_STRTAB | 无 | 本节是“节名字表”,含有所有其它节的名字 |
.strtab | SHT_STRTAB | 见下文 | 本节用于存放字符串,主要是那些符号表项的名字。如果一个目标文件有一 个可装载的段,并且其中含有符号表,那么本节的属性中应该有 SHF_ALLOC |
.symtab | SHT_SYMTAB | 见下文 | 本节用于存放符号表。如果一个目标文件有一个可载入的段,并且其中含有 符号表,那么本节的属性中应该有 SHF_ALLOC。 |
.text | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 本节包含程序指令代码 |
.dynsym & .symtab 符号表
- 符号表记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab,前者是后者的子集。
- .dynsym保存了引用自外部文件的符号,只能在运行时被解析
- .symtab还保存了本地符号,用于调试和链接。
- 目标文件通过一个符号在表中的索引值来使用该符号。索引值从0开始计数,但值为0的表项不具有实际的意义,它表示未定义的符号STN_UNDEF。每个符号都有一个符号值(symbol value),对于变量和函数,该值就是符号的地址。
3.4 字符串表
ELF 文件中的字符串表是包含有若干个 null
字符的序列,即字符串表。在 ELF 文件中,这些字符串通常上节的名字或符号的名字。在目标文件的其他位置,当需要使用到某个字符串时,只需要提供该字符串在字符串表中的起始位置(索引)即可。
字符串表的特征:字符串表的第一个字节永远是空(\0
即 null
),由于每一个字符串都是以 null
结尾,所以字符串表的最后一字节也是 null
。在节的类型(sh_type
)中,有 SHT_STRTAB
标志的节都是字符串表,都具有这一特征。
如下图,即为一个长度为 22 字节的字符串表:
索引 1
表示的字符串为 helloworld
,索引 12
表示的字符串为 Myvariable
。
字符串表所在的节带有 SHT_STRTAB
类型标志,常用的两个字符串表所在的节:.shstrtab
、.strtab
。
.shstrtab
,为STRTAB
类型。保存所有节表的名称,如.text
、.data
等。文件头的e_shstrtab
成员是一个索引值,指出.shstrtab
节在节头表数组的索引下标。每个节表条目中sh_name
成员都是一个索引,使用该索引到该节指定的位置即可找到对应的字符串名称。.strtab
,为STRTAB
类型。保存符号字符串,其字符串表中的内容会被.symtab
中每个条目的st_name
成员进行索引,使用该索引到该节指定的位置即可找到对应的字符串名称。.dynstr
,为STRTAB
类型。保存动态链接符号字符串。其字符串表中的内容会被.dynsym
中每个条目的st_name
成员进行索引。
3.5 符号表(数组)
对于可执行文件除了编译器引入的一些符号外,主要就是用户自定义的全局变量,函数等。而对于可重定位文件仅仅包含用户自定义的一些符号。
在 ELF文件中一般有两节 .symtab
、.dynsym
保存符号表,节的类型为 SHT_SYMTAB
。我们将函数、变量等统称为符号(Symbol),函数名、变量名统称为符号名(Symbol name)。符号的引入是便于动态链接、静态链接,比如 A 文件使用到 B 文件中定义的一个函数,A 文件正是通过自身的符号表找到对应的引用函数。
每一个目标文件都会有符号表,表里面记录了该目标文件所使用到的所有符号,可以通过符号表中的 符号值
定位到符号保存的地址。
符号的分类:本地全局符号(在本文件中定义的全局符号)、外部符号(在其他文件中定义的符号,在本文件中引用)、节名/段名、局部符号、行号(源代码的代码行和指令对应的关系)。
类型为 SHT_SYMTAB、SHT_DYNSYM
的节表示符号表,ELF64 符号表的结构如下:
1 | typedef struct |
符号表也是一个数组,每一个元素都是 Elf64_Sym
结构,其中数组下标 0
的元素为 0
。每一个 Elf64_Sym
结构对应一个符号。
st_name
:指明当前符号在字符串表中的索引。对于.symtab(SHT_SYMTAB)
的节,其对应的字符串表在Elf64_Shdr.sh_link
为索引的节中。st_shndx
:索引值。如果该结构对应的符号是在本文件中定义的,st_shndx
表示该符号所在的节在节头表中的索引。如果该结构对应的符号不是在本文件中定义的,st_shndx
表示一个特殊索引值:SHN_ABS(0xfff1)
:表示该值是一个绝对的值,不会因为重定位前后发生改变。SHN_COMMON(0xfff2)
:一般表示未初始化的全局变量符号。该符号一般位于.bss
节中。SHN_UNDEF(0)
:符号未定义。表示该符号在本文件中进行引用,但是在其他文件中进行定义的。
st_value
:一个数值或地址,具体如下:- 在重定位文件中,如果
st_shndx
索引指向的节类型为SHN_COMMON(0xfff2)
,该值为索引指向的这个节的对齐值。如果st_shndx
索引指向的节类型不为SHN_COMMON(0xfff2)
,该值表示对应的符号在以st_shndx
为索引的节中的偏移量。 - 在可执行文件或共享库文件中,
st_value
即表示该符号的虚拟地址。
- 在重定位文件中,如果
st_size
:符号所占空间大小(字节单位)。 比如double
类型的变量占用8
字节,地址也是占用8
字节,char
字符串长度等。0
表示大小为0
或未知。st_info
:符号绑定(以STB_
开头)和符号类型(以STT_
开头)。低 4 位表示符号类型,高 4 位表示绑定信息。提取 st_info 中的绑定信息和类型:
1 | /* How to extract and insert information held in the st_info field. */ |
符号表是一个 Elf64_Sym
结构的数组,name应该如何计算该数组元素的个数呢?
- 找到
.symtab
符号表所在的节。 - 该节
Elf64_Shdr.sh_entsize
表示每个元素的大小,Elf64_Shdr.sh_size
表示该节的总大小。则$(Elf64\_Shdr.sh\_size) / (Elf64\_Shdr.sh\_entsize)$ 为元素的个数。
符号表 .symtab
和 .dynsym
的区别:
.symtab
:保存有本文件所有的符号。.dynsym
:保存的是外部符号。是.symtab
的子集。
3.6 重定位表(数组)
重定位文件存在静态链接、动态链接,都涉及到重定位。
静态链接时重定位由链接器来完成,装载时重定位是在模块被装载时,操作系统来完成。
当某个可执行文件由多个目标文件编译链接时,目标文件中的部分数据需要进行静态重定位,比如:
1
2 00040010: e8 fc ff ff ff call 7
00040014:链接器会对偏移为 7 的地方进行重定位(根据重定位表中对应的条目),当目标文件被链接成为可执行文件后,对应的代码可能会被重定位成如下代码:
1 080480de:e8 05 00 00 00 call 080480e8
静态链接的工作由链接器来完成,实际上我们并不关心具体的实现静态重定位逻辑,在构造利用代码时也基本用不到。而动态链接工作是在目标文件被加载时,会对目标文件数据段或代码段中引用的绝对地址等进行重定位,我们关注的是动态重定位。
重定位是将符号引用与符号定义进行链接的过程。因此链接是处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。
链接又分为静态链接和动态链接,前者是程序开发阶段程序员用 ld
(gcc
实际上在后台调用了 ld
)静态链接器手动链接的过程,实际上我们并不关心具体的实现静态重定位逻辑,在构造利用代码时也基本用不到。而动态链接则是程序运行期间系统调用动态链接器(ld-linux.so
)自动链接的过程,动态链接工作是在目标文件被加载时,会对目标文件数据段或代码段中引用的绝对地址等进行重定位,我们关注的是动态重定位。我们关注的是动态重定位。
——Gcc 编译的背后-链接。
目标文件中,有专门的节来保存文件重定位时需要被修正的地方,这样的节叫做重定位表(节类型为 SHT_REL
或 SHT_RELA
)。
重定位表是一个数组,数组的每个元素为 Elf64_Rel
或 Elf64_Rela
结构,一个元素对应一个需要被修正的数据。重定位表分两种类型,包含使用显示加数(r_addend
)的 Elf64_Rela
结构,和使用隐式加数的 Elf64_Rel
结构(x86 仅使用 Elf32_Rel
重定位项,64 位的可执行文件一般只使用 Elf64_Rela
结构)。
1 | /* Relocation table entry without addend (in section of type SHT_REL). */ |
成员 | 说明 |
---|---|
r_offset | 可重定位文件:是一个偏移值,相对于要重定位数据所在的节的文件起始位置。 可执行文件或共享动态链接文件:要被重定位的位置的虚拟地址。 |
r_info | ELF64 中,高 32 位指明要重定位的符号在符号表数组中的下标(不是在节头表中的索引),低 32 位指明重定位的类型(说明如何修改重定位的位置)。 |
r_addend | 是一个常量加数,用来计算要重定位的位置。 |
关于 r_info
:
1 | /* How to extract and insert information held in the r_info field. */ |
重定位的类型 ELF64_R_TYPE(r_info)
如下:
1 | /* AMD x86-64 relocations. /usr/include/elf.h line 2850 */ |
重定位节会引用其他两个节:符号表(由 sh_link
进行索引)和要修改的节(由 sh_info
进行索引)。
重定位计算:以下表示法用于说明重定位计算。
- A—用于计算可重定位字段的值的加数。
- B—执行过程中将共享目标文件装入内存的基本地址。通常,生成的共享目标文件的基本虚拟地址为 0。但是,共享目标文件的执行地址不相同。请参见程序头。
- G—执行过程中,重定位项的符号地址所在的全局偏移表中的偏移。请参见全局偏移表(特定于处理器)。
GOT—全局偏移表的地址。请参见全局偏移表(特定于处理器)。
- L—符号的过程链接表项的节偏移或地址。请参见过程链接表(特定于处理器)。
- P—使用 r_offset 计算出的重定位的存储单元的节偏移或地址。
- S—索引位于重定位项中的符号的值。
- Z—索引位于重定位项中的符号的大小。
名称 | 值 | 字段 | 计算 |
---|---|---|---|
R_AMD64_NONE |
0 |
无 | 无 |
R_AMD64_64 |
1 |
word64 |
S + A |
R_AMD64_PC32 |
2 |
word32 |
S + A - P |
R_AMD64_GOT32 |
3 |
word32 |
G + A |
R_AMD64_PLT32 |
4 |
word32 |
L + A - P |
R_AMD64_COPY |
5 |
无 | 请参阅此表后面的说明。 |
R_AMD64_GLOB_DAT |
6 |
word64 |
S |
R_AMD64_JUMP_SLOT |
7 |
word64 |
S |
R_AMD64_RELATIVE |
8 |
word64 |
B + A |
R_AMD64_GOTPCREL |
9 |
word32 |
G + GOT + A - P |
R_AMD64_32 |
10 |
word32 |
S + A |
R_AMD64_32S |
11 |
word32 |
S + A |
R_AMD64_16 |
12 |
word16 |
S + A |
R_AMD64_PC16 |
13 |
word16 |
S + A - P |
R_AMD64_8 |
14 |
word8 |
S + A |
R_AMD64_PC8 |
15 |
word8 |
S + A - P |
R_AMD64_PC64 |
24 |
word64 |
S + A - P |
3.7 Hash 表
ELF 文件中节类型为 SHT_SYMTAB、SHT_DYNSYM
的节表包含大量的符号(后者是前者的子集),在进行共享库的加载、或符号查找时,如果对符号进行线性搜索是非常慢的。所以在生成可执行文件或共享库文件时,链接器会生成对应的 SHT_HASH
或 SHT_GNU_HASH
哈希表,以便从 ELF 的符号表中快速查找符号,加快链接。
哈希(散列)表的原理:在链接生成可执行文件或共享库文件时,对符号表每一个字符串进行哈希运算(不包含 \0
字符)得到对应的哈希值,然后将所有哈希值保存在哈希表节中。之后有对字符串查找的需求时,使用相同的散列算法生成哈希值,拿着哈希值从 SHT_SYMTAB、SHT_DYNSYM
找到对应的符号。
SHT_HASH
与 SHT_GNU_HASH
是有区别的(后者从 2006 年开始使用),后者是对前者的改进,但是需要注意的是:DT_HASH
格式由 System V ABI 规范强制执行的,而 ABI 规范中没有对 DT_GNU_HASH
格式进行记录(非强制使用),但是在后来的 ELF 文件中基本都是使用 DT_GNU_HASH
而启用 SHT_HASH
。
一、SHT_HASH
在 System V ABI 规范 Hash Table 中规定,哈希表结构如下(x86 和 x86-64 通用):
1 | struct elf_hash_table { |
这里有一个规定:数组 chain
的项数 nchain
,必须和符号表 SHT_SYMTAB
数组的项数相等,且一一对应。
计算字符串哈希值的函数如下:
1 | unsigned long elf_Hash(const unsigned char *name) // 参数为输入的字符串 |
字符串查找的过程:给定一个符号名字,输入到函数 elf_Hash
返回一个哈希值 x
,然后由 bucket[x%nbucket]
得到一个符号表索引 y
,如果索引 y
对应的符号表项不是想要的符号,则由 chain[y]
得到下一个符号表索引 z
,如果仍不是想要的符号,继续 chain[z]
…直到 chain[xxx] == STN_UNDEF
,表示目标文件中不包含想要查找的符号。伪代码如下:
1 | unsigned long x = 0; |
DT_HASH
的缺点:虽然通过 DT_HASH
可以找到现有的符号,但它对不存在的符号查找表现不佳。对于不存在的符号,可能需要遍历 chain
链多次并比较字符串,直到碰撞撞到 STN_UNDEF
才停止。这在现实生活中变得更加糟糕,动态加载链接时需要在多个共享库中搜索符号,因此必须走过多个随机链。
1990s 年代使用 DT_HASH
,此后引入了一个名为 DT_GNU_HASH
的新哈希表,该表现在几乎无处不在,不再使用 DT_HASH
。可惜的是,DT_GNU_HASH
实际上没有标准化,除了BFD(二进制文件描述符库)源代码外,甚至没有在任何地方进行描述。
参考:
- ELF: symbol lookup via DT_HASH
- https://elixir.bootlin.com/glibc/glibc-2.31/source/sysdeps/generic/dl-hash.h#L28
- 散列表节
- The Cost Of ELF Symbol Hashing
二、SHT_GNU_HASH
在几乎任何 Linux 发行版的系统中,使用 gcc 或 clang 编译的程序中都会使用到到 DT_GNU_HASH
。但是除了在 GNU binutils 和 glibc 源代码外,没有在任何地方记录它。引入了 Bloom filter。
引入 DT_GNU_HASH
的主要目的是解决对不存在符号查找的优化,提升符号查找的效率。
参考 Re: GNU_HASH section format 的描述,x86-64 下 DT_GNU_HASH
的结构为:
1 | struct gnu_hash_table { |
计算字符串哈希值的函数如下(该函数可以在 bfd_elf_gnu_hash 或 dl_new_hash 中找到):
1 |
|
字符串过滤(目的:判断目标字符串是否存在于符号表中):
ELFCLASS32
和ELFCLASS64
二进制文件查找计算不同,先定义变量ELFCLASS_BITS
。- ELFCLASS32:
ELFCLASS_BITS = 32
。 - ELFCLASS64:
ELFCLASS_BITS = 64
。
- ELFCLASS32:
计算字符串哈希值:
hash = gnu_hash(name)
。计算字符串过滤值:
filter_value = bloom[(hash / ELFCLASS_BITS) % bloom_size]
。计算掩码值:
bitmask = (1 << (hash % ELFCLASS_BITS)) | (1 << ((hash >> bloom_shift) % ELFCLASS_BITS))
。如果字符串过滤值没有对应的掩码位,则字符串一定不存在于符号表中。
1
if ((filter_value & bitmask) != bitmask) return null;
过滤完之后如果不返回,说明符号表中存在要查找的字符串。先判断从 buckets
桶取得的符号表的初始索引值,该索引值不应该小于 symoffset
。
1 | uint32_t symix = buckets[namehash % nbuckets]; |
最后才是查找字符串(即遍历 chain
链)。
参考:
- ELF: better symbol lookup via DT_GNU_HASH
- [翻译]GNU Hash ELF Sections
- 从实例分析ELF格式的.gnu.hash区与glibc的符号查找
- GNU Hash ELF Sections
4 Segments
4.1 程序头表(数组)
可执行文件和动态链接库文件的程序头表是一个数组(类似于节头表),每个元素对应于一个段(Segment),每个段对应一个或多个节,程序头只针对可执行文件和动态链接库(shared object file)有意义,其他类型的目标文件可忽略。
文件头 Elf64_Ehdr.e_phoff
指出程序头表数组起始的文件偏移位置, Elf64_Ehdr.e_phnum
指出程序头表数组元素的个数。
程序头表数组类型如下:
1 | typedef struct |
p_type
:段的类型。名称 值 含义 PT_NULL 0 该数组元素未使用。 PT_LOAD 1 可加载的段 PT_DYNAMIC 2 指示该段是动态链接信息的段,指出动态链接时要做的事。主要包括以下三类信息:
运行时需要链接的动态链接库列表(名称)。
全局偏移表(GOT)的地址。
重定位条目的相关信息。PT_INTERP 3 该段的内容( p_offset
)是一个null
结尾的字符串,该字符串将被当作目标程序加载器调用(一般是[/lib64/ld-linux-x86-64.so.2](https://ld-linux.so/)
的完整路径)。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中最多只能出现一次。如果存在这种类型的段,它必须在所有可加载段的前面。PT_NOTE 4 该段指出 SHT_NOTE
节的位置和大小,包含与特定供应商或者系统相关的附加消息。PT_SHLIB 5 此段类型被保留,语义未指定。包含这种类型的段的程序与 ABI不符。 PT_PHDR 6 如果存在,则指定程序头表本身在文件和程序内存映像中的位置和大小。此段类型在一个文件中只能出现一次。而且,只有当程序头表是程序的内存映像的一部分时才可能发生。如果它存在,它必须在任何可加载段项之前。 PT_TLS 7 本地线程存储段。参考链接程序和库指南 。 PT_NUM 8 已经定义的类型总数(Number of defined types) PT_LOOS - PT_HIOS 0x60000000~0x6fffffff 此范围内的值保留用于特定于操作系统。 PT_LOPROC
- PT_HIPROC0x70000000~0x7fffffff 此范围内的值(包括这两个值)保留用于特定于处理器。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/* Legal values for p_type (segment type). */
p_offset
:该段的文件偏移(相对于文件起始位置)。p_vaddr
:段在内存中的虚拟地址。p_paddr
:段的物理地址,物理地址是不确定的,大部分操作系统保留该值。p_filesz
:该段数据在文件中的大小。p_memsz
:该段数据在内存中的大小。p_align
:指定本段内容如何在文件、内存中进行对齐。此成员可提供一个值,用于在内存和文件中根据该值对齐各段。值 0 和 1 表示无需对齐。另外,p_align 应为 2 的正整数幂,并且 p_vaddr 和 p_offset应该同余(以 p_align 为模数)。p_flags
:该段的相关属性。系统将可装入段加载入内存映像时,将会授予如p_flags
成员中所指定的访问权限。名称 值 含义 PF_X
0x1
执行 PF_W
0x2
写 PF_R
0x4
读 PF_MASKOS
0x0ff00000
未指定,所有位都保留用于特定操作系统 PF_MASKPROC
0xf0000000
未指定,所有位都保留用于特定处理器 如果权限值为
0
,表示无任何权限。实际的读写权限还要依赖于内存管理器,在不同的操作系统上,内存管理单元的做法可能会不同。但是如果 p_flags 中没有指定 PF_W 的话,系统一定不会给出写权限。下表给出了在一些权限组合的情况:
权限总结:
- 可读与可执行是通用的,有其中一个就等于也有了另一个。
- 可写权限是最高权限,可以覆盖另外两个,有了可写权限,所有权限就都有了。
4.2 动态段
需要进行动态链接的目标文件,在程序头中都会包含一个 PT_DYNAMIC
动态链接段。动态链接段提供很多链接信息,比如当前目标文件需要依赖哪些共享目标文件、动态链接符号表的位置、动态链接重定位表的位置等。动态段包含所有的 SHT_DYNAMIC
动态节,主要是 .dynamic
节。
SHT_DYNAMIC
动态节是一个数组,数组的类型为 Elf64_Dyn
:
1 | typedef struct |
d_tag
控制着 d_un
的解析,d_tag
不同的取值决定着 d_un
的取值:
d_tag | 值 | d_un | 可执行文件 | 共享目标文件 | 意义 |
---|---|---|---|---|---|
DT_NULL | 0 | 忽略 | 必需 | 必需 | 用于标记 DYNAMIC 数组的结束 |
DT_NEEDED | 1 | d_val | 可选 | 可选 | 此元素指明了一个所依赖的库的名字。不过此元素本身并不是一个字符串,它是一个索引值,是由 DT_STRTAB 所标记的字符串表中的索引,在该字符串表中,此索引处是一 个以 null 结尾的字符串,这个字符串就是库的名字。在动态数组中可以包含若干个此类型的项,这些项出现的相对顺序是不能随意调换的。 |
DT_PLTRELSZ | 2 | d_val | 可选 | 可选 | 此元素含有与函数链接表(PLT)相关联的所有重定位项的总大小,以字节为单位。如果存在 DT_JMPREL 类型的条目,必须有与之配合的 DT_PLTRELSZ 条目。 |
DT_PLTGOT | 3 | d_ptr | 可选 | 可选 | 此元素包含与函数链接表或全局偏移量表相应的虚拟地址(动态库和可执行文件)。在 Intel 架构中,这一项的 d_ptr 成员给出全局偏移量表(GOT)中第一项的地址。如下文所述,全局偏移量表中前三项都是保留的,其中两项用于持有函数连接表信息。got[0] :本ELF动态段(.dynamic 段)的装载地址got[1] :本ELF的 link_map 数据结构描述符地址got[2] :_dl_runtime_resolve 函数的地址 |
DT_HASH | 4 | d_ptr | 必需 | 必需 | 此元素含有符号哈希表的地址(对于共享库文件:字符串表的文件偏移,对于可执行文件:字符串表的虚拟地址)。这里所指的哈希表与 DT_SYMTAB 所指的哈希表是同一个。 |
DT_STRTAB | 5 | d_ptr | 必需 | 必需 | 对于共享库文件:字符串表的文件偏移,对于可执行文件:字符串表的虚拟地址。此表中包含符号名、库名等等。 |
DT_SYMTAB | 6 | d_ptr | 必需 | 必需 | 此元素包含符号表的地址(对于共享库文件:字符串表的文件偏移,对于可执行文件:字符串表的虚拟地址),符号表数组类型为 Elf64_Sym 。 |
DT_RELA | 7 | d_ptr | 必需 | 可选 | 此元素包含一个重定位表的地址,在重定位表中存储的是显式的“加数”, 比如对于 32 位文件来说,这种加数就是 Elf32_Rela 。在一个目标文件中可以存在多个重定位节,当为可执行文件或共享目标文件创建重定位表的时候,连接编辑器会把这些重定位节连接在一起,最后形成一张大的重定位表。当连接编辑器为一个可执行文件创建进程空间,或者把一个共享目标添加到进程空间中去的时候,它会去读重定位表并执行相应的操作。如果在动态结构中包含有 DT_RELA 元素的话, 就必须同时还包含 DT_RELASZ 和 DT_RELEANT 元素。如果一个文件需要重定位的话, DT_RELA 或 DT_REL 至少要出现一个。 |
DT_RELASZ | 8 | d_val | 必需 | 可选 | 此元素持有 DT_RELA 相应的重定位表的大小,以字节为单位。 |
DT_RELAENT | 9 | d_val | 必需 | 可选 | 此元素持有 DT_RELA 相应的重定位表项的大小,以字节为单位。 |
DT_STRSZ | 10 | d_val | 必需 | 必需 | 此元素持有字符串表的大小,以字节为单位。 |
DT_SYMENT | 11 | d_val | 必需 | 必需 | 此元素持有符号表项的大小,以字节为单位。 |
DT_INIT | 12 | d_ptr | 可选 | 可选 | 此元素持有初始化函数的地址。参见下文”初始化和终止函数”内容。 |
DT_FINI | 13 | d_ptr | 可选 | 可选 | 此元素持有终止函数的地址。参见下文”初始化和终止函数”内容。 |
DT_SONAME | 14 | d_val | 忽略 | 可选 | 是一个字符串表中的偏移量(索引),该位置存储了一个以 ’null’ 结尾的字符串,是一个共享目标的名字。相应的字符串表由 DT_STRTAB 指定。 |
DT_RPATH | 15 | d_val | 可选 | 忽略 | 是一个字符串表中的偏移量(索引),该位置存储了一个以 ’null’ 结尾的字符串,是一个用于搜索库文件的路径名。相应的字符串表由 DT_STRTAB 指定。它的用途已被 DT_RUNPATH 所取代。https://docs.oracle.com/cd/E26926_01/html/E25910/chapter6-42444.html https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html |
DT_SYMBOLIC | 16 | 忽略 | 忽略 | 可选 | 在共享目标文件中,此元素的出现与否决定了动态连接器解析符号时所用的算法。如果此元素不出现的话,动态连接器先搜索可执行文件再搜索库文件;如果此元素出现的话,顺序刚好相反,动态连接器会先从本共享目标文件开始,后搜索可执行文件。 |
DT_REL | 17 | d_ptr | 必需 | 可选 | 此元素与 DT_RELA 相似,只是它所指向的重定位表中,“加数”是隐含的而不是显式的。 |
DT_RELSZ | 18 | d_val | 必需 | 可选 | 此元素持有 DT_REL 相应的重定位表的大小,以字节为单位。 |
DT_RELENT | 19 | d_val | 必需 | 可选 | 此元素持有 DT_REL 相应的重定位表项的大小,以字节为单位。 |
DT_PLTREL | 20 | d_val | 可选 | 可选 | 本成员指明了函数链接表(PLT)所引用的重定位项的类型。d_val 成员含有 DT_REL 或 DT_RELA 。函数链接表中的所有重定位类型都是相同的。 |
DT_DEBUG | 21 | d_ptr | 可选 | 忽略 | 本成员用于调试,格式未明确定义。 |
DT_TEXTREL | 22 | 忽略 | 可选 | 可选 | 如果此元素出现的话,在重定位过程中如果需要修改的是只读段的话,连接 编辑器可以做相应的修改;而如果此元素不出现的话,在重定位过程中,即使需 要,也不能修改只读段。 |
DT_JMPREL | 23 | d_ptr | 可选 | 可选 | 此类型元素如果存在的话,其 d_ptr 成员包含与函数链接表(PLT)关联的重定位项地址。把多个重定位项分开可以让动态链接器在初始化的时候忽略它们(以便延迟加载),当然前提条件是“后期绑定”是激活的。如果此元素存在的话,DT_PLTRELSZ 和 DT_PLTREL 也应该出现。实际上就是指示着有导入表,实际上就是 .rela.plt 节的地址(Elf64_Rela 类型)。.rela.plt = (Elf64_Rela*)d_ptr 。 |
DT_BIND_NOW | 24 | 忽略 | 可选 | 可选 | 如果此元素存在的话,动态链接器必须在程序开始执行以前,完成所有包含此项的目标的重定位工作。如果此元素存在,即使程序应用了“后期绑定”,它对于此项所指定的目标也不适用,动态连接器仍需事先做好重定位。 |
DT_INIT_ARRAY | 25 | d_ptr | 可选 | 可选 | 初始化函数的指针数组的地址。此元素要求同时存在 DT_INIT_ARRAYSZ 元素。请参见初始化节和终止节。 |
DT_FINI_ARRAY | 26 | d_ptr | 可选 | 可选 | 终止函数的指针数组的地址。此元素要求同时存在 DT_FINI_ARRAYSZ 元素。请参见初始化节和终止节。 |
DT_INIT_ARRAYSZ | 27 | d_val | 可选 | 可选 | DT_INIT_ARRAY 数组的总大小(以字节为单位)。 |
DT_FINI_ARRAYSZ | 28 | d_val | 可选 | 可选 | DT_FINI_ARRAY 数组的总大小(以字节为单位)。 |
DT_RUNPATH | 29 | d_val | 可选 | 可选 | 以空字符结尾的库搜索路径字符串的 DT_STRTAB 字符串表偏移。请参见运行时链接程序搜索的目录。 |
DT_FLAGS | 30 | d_val | 可选 | 可选 | 特定于此目标文件的标志值。请参见表 13-9 |
DT_ENCODING | 32 | 未定义 | 未定义 | 未定义 | 大于或等于 DT_ENCODING 、小于或等于 DT_LOOS 的动态标记值遵循 d_un 联合的解释规则。 |
DT_LOOS | 0x6000000d | 未定义 | 未定义 | 未定义 | DT_LOOS - DT_HIOS 此范围内包含的值(包括这两个值)保留用于特定于操作系统的语义。所有这类值都遵循 d_un 联合的解释规则。 |
DT_HIOS | 0x6ffff000 | 未定义 | 未定义 | 未定义 | DT_LOOS - DT_HIOS 此范围内包含的值(包括这两个值)保留用于特定于操作系统的语义。所有这类值都遵循 d_un 联合的解释规则。 |
DT_LOPROC | 0x70000000 | 未定义 | 未定义 | 未定义 | 这一区间的值是为处理器保留的。 |
DT_HIPROC | 0x7fffffff | 未定义 | 未定义 | 未定义 | 这一区间的值是为处理器保留的。 |
需要特别说明的几个字段:
d_un
取d_ptr
成员时,有以下两种情况:- 对于
d_tag = DT_PLTGOT、DT_INIT_ARRAY、DT_FINI_ARRAY
时,对于共享库和可执行文件都是虚拟地址; - 其余
d_tag
取值,对于共享库来说是文件偏移,对于可执行文件来说是虚拟地址。
- 对于
- 对于
DT_NEEDED
、DT_SONAME
两个取值和DT_SONAME
的关系,可参考《Linux 编程基础—3.3 SO-NAME》。
注意:在解析动态段时,计算 Elf64_Dyn
数组元素的个数时,不是像节 Section那样( 元素个数 = 节大小/每一项大小
)。在动态段中,当数组 Elf64_Dyn
元素的值为 DT_NULL(0)
时,即表示数组最后一个元素(最后一个元素为全 0
)。
4.3 动态加载
需要进行动态链接的可执行文件,有一个 PT_INTERP
类型的程序头项(实际上就是 .interp
节)。该节/段的内容是一个路径字符串,表示ELF解析器。当执行一个程序时,系统函数 exec(BA_OS)
会被调用,这个函数会去读取 PT_INTERP
段获取解析器。系统去初始化该解析器的进程镜像,把进程空间暂时借给解析器,然后解析器继续执行(通过解释程序文件段创建初始进程映像,解释程序负责从系统接收控制并为应用程序提供环境)。
关于解析器:
- 一般是一个共享目标文件,且段内容位置不相关,一般系统会用
mmap
在动态区域为解析器创建段镜像。 - 它也可以是独立的可执行文件,那系统就要按照此可执行文件的程序头来加载它。
1.
2. 将可执行文件依赖(使用)的共享文件(动态链接库)的 PT_LOAD
段到加载到进行空间
3. 将可执行文件、动态链接库进行重定位
4. 关闭可执行文件 的文件描述符
5. 把控制权交给程序,从可执行文件入口点开始执行
其中第 2 步,加载依赖的动态链接库,从哪里加载,加载哪些库?Windows 下,因为有导入表的存在,这些都很方便。但是在 Linux 下没有导入表,必须使用依靠 PT_DYNAMIC
动态链接段(该段内容对应于 .dynamic
节)来找到相关的信息。
4.4 GOT、PLT
上一节讲了动态加载可执行文件的简单过程,对于可执行文件中,引用的外部函数,同样需要进行动态加载。ELF 可执行文件动态加载外部函数必须借助 PLT、GOT 表(Windows 借助函数导入表)。
要想获取外部函数的地址:
- 一个用来存储外部函数地址的数据段(GOT)
- 一段用来加载外部函数的代码(PLT)
PE 文件中,导入表中内容在函数导入前后不一样,当函数被引用后,导入表中对应的位置就是函数的地址。在 ELF 文件中没有导入表,使用 PLT、GOT 表来使用导入函数。
- GOT(Global Offset Table),每一个表项条目用来存储导入函数或全局变量的地址。
GOT
表一般被拆成了两个节(Section),不需要延迟绑定,用于存储全局变量,加载到内存中只需要被读取的.got
,以及存储外部函数地址的.got.plt
。 - PLT(Procedure Linkage Table),用来获取导入函数的地址,然后将地址填充到 GOT 表中。PLT 表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。程序对某个函数的访问都被调整为对 PLT 入口的访问。一个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址。
静态分析
举例代码:
1 |
|
编译成 x64 代码:
查看
printf()
函数。可以看到函数的调用被汇编成call puts@plt
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19[root@centos-7 program]
Disassembly of section .text:
...
000000000040052d <main>:
40052d: 55 push rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83 ec 20 sub rsp,0x20
400535: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400538: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi
40053c: 48 89 55 e8 mov QWORD PTR [rbp-0x18],rdx
400540: 48 8d 3d a9 00 00 00 lea rdi,[rip+0xa9] // 4005f0 <__dso_handle+0x8>
400547: e8 c4 fe ff ff call 400410 <puts@plt> // printf() 函数
40054c: b8 00 00 00 00 mov eax,0x0
400551: c9 leave
400552: c3 ret
400553: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
40055a: 00 00 00
40055d: 0f 1f 00 nop DWORD PTR [rax]继续查看
puts@plt(0x400410)
地址处的代码。1
2
3
4
5
6
7
8[root@centos-7 program]
Disassembly of section .plt:
0000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmp QWORD PTR [rip+0x200c02] // 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 push 0x0
40041b: e9 e0 ff ff ff jmp 400400 <.plt>可以看到,代码位于
.plt
节。反汇编整个
.plt
节的代码(-d
反汇编,-j
指定节)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23[root@centos-7 program]
Disassembly of section .plt:
0000000000400400 <.plt>:
400400: ff 35 02 0c 20 00 push QWORD PTR [rip+0x200c02] // 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400406: ff 25 04 0c 20 00 jmp QWORD PTR [rip+0x200c04] // 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40040c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmp QWORD PTR [rip+0x200c02] // 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 push 0x0
40041b: e9 e0 ff ff ff jmp 400400 <.plt>
0000000000400420 <__libc_start_main@plt>:
400420: ff 25 fa 0b 20 00 jmp QWORD PTR [rip+0x200bfa] // 601020 <__libc_start_main@GLIBC_2.2.5>
400426: 68 01 00 00 00 push 0x1
40042b: e9 d0 ff ff ff jmp 400400 <.plt>
0000000000400430 <__gmon_start__@plt>:
400430: ff 25 f2 0b 20 00 jmp QWORD PTR [rip+0x200bf2] // 601028 <__gmon_start__>
400436: 68 02 00 00 00 push 0x2
40043b: e9 c0 ff ff ff jmp 400400 <.plt>可以看到,
.plt
节的特征:- 表项,每个引用的函数都被反汇编为一个片段(或者叫做表项),也叫做
stub
。 - 公共 common 表项,需要特别注意的是第一个函数表项(名称为
.plt
),是一个公共的表项,会被后面的每一个其他表项调用。
- 表项,每个引用的函数都被反汇编为一个片段(或者叫做表项),也叫做
继续分析 PLT 中
0x400410<puts@plt>
。1
2
3
40000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmp QWORD PTR [rip+0x200c02] // 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 push 0x0
40041b: e9 e0 ff ff ff jmp 400400 <.plt>可以看到,跳转到
0x601018 = 0x200C02 + 0x400416
,注意:x86-64 中,使用 RIP-relative 寻址,即编码x = 跳转目标 - 返回地址
。并不是0x200c02 + 0x400410
。查看目标地址
0x601018
的内容。1
2
3
4
5
6
7
8
9
10[root@centos-7 program]
Disassembly of section .got.plt:
0000000000601018 <_GLOBAL_OFFSET_TABLE_+0x18>:
601018: 16 (bad)
601019: 04 40 add al,0x40
60101b: 00 00 add BYTE PTR [rax],al
60101d: 00 00 add BYTE PTR [rax],al
...可以看到目标地址位于
.got.plt
节,属于数据区,反汇编出来的内容是乱码。则查看整个.got.plt
区域内容:1
2
3
4
5
6[root@centos-7 program]
Contents of section .got.plt:
601000 280e6000 00000000 00000000 00000000 (.`.............
601010 00000000 00000000 16044000 00000000 ..........@.....
601020 26044000 00000000 36044000 00000000 &.@.....6.@.....可以看到,地址
0x601018
里面存储的值为0x400416
。在上面步骤 4,400410: ff 25 02 0c 20 00
,指令ff 25
为间接跳转,即取地址0x601018
存储的值0x400416
,并跳转。目标地址为
0x400416
,则又跳转回去.plt
节中,且刚好是400410: ff 25 02 0c 20 00
的下一条指令。继续分析
<puts@plt>
在400416
地址处的代码。1
2
3
40000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmp QWORD PTR [rip+0x200c02] // 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 push 0x0
40041b: e9 e0 ff ff ff jmp 400400 <.plt>这里
push
的值(参考链接):- x86-64,为目标函数在
.rela.plt
节中所在条目的索引值。 - x86,为目标函数在
.rela.plt
节中的偏移值。
1
2
3
4
5
6
7
8
9
10
11[root@centos-7 program]
重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
重定位节 '.rela.plt' 位于偏移量 0x398 含有 3 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0最后跳转到 common 公共表项
jmp 400400 <.plt>
。- x86-64,为目标函数在
公共表项分析。
1
2
3
40000000000400400 <.plt>:
400400: ff 35 02 0c 20 00 push QWORD PTR [rip+0x200c02] // 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400406: ff 25 04 0c 20 00 jmp QWORD PTR [rip+0x200c04] // 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40040c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]这里入栈的地址为
.got.plt[1]
,然后间接跳转.got.plt[2]
。.got.plt[0]
:本 ELF 动态段(.dynamic
)的装载地址 。.got.plt[1]
:本 ELF 的 link_map 数据结构描述符地址。.got.plt[2]
:_dl_runtime_resolve
函数的地址。
参考《PLT&GOT》。
分析地址
0x601010
,即.got.plt[2]
。1
2
3
4
5
6[root@centos-7 program]
Contents of section .got.plt:
601000 280e6000 00000000 00000000 00000000 (.`.............
601010 00000000 00000000 16044000 00000000 ..........@.....
601020 26044000 00000000 36044000 00000000 &.@.....6.@.....可以看到,
0x601008(.got.plt[1])
和0x601010(.got.plt[2])
目前都是0
。经过动态分析,.got.plt[1] = link_map
、.got.plt[2] = _dl_runtime_resolve
,即跳转到_dl_runtime_resolve
函数。后面将进行动态分析
_dl_runtime_resolve
函数。
总结:
- 每个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址 ,该 GOT 项初始值为 PLTn项中的 push 指令地址(即 jmp 的下一条指令,所以第 1 次跳转没有任何作用),待符号解析完成后存放符号的真正地址。
第一次调用外部函数:
之后再次调用:
动态调试
搭建远程调试。
参考IDA远程动态调试(linux & Windows)搭建,参考由一道逆向题而引发,IDA调试ELF文件需要关闭 Linux 防火墙。
动态调试常用命令:
IDA快捷键 功能 F7 单步步进,遇到call/指令跟进 F8 单步步过,遇到call指令不跟进 F4 运行到光标所在的行 Ctrl + F7 知道该函数返回时才停止 F9 运行程序 Ctrl+F2 终止一个正在运行的进程 F2 设置断点 找到
call puts
函数,动态跟踪。View—Open subviews—Functions(shift+F3)—main:
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.text:000000000040052D
.text:000000000040052D // Attributes: bp-based frame
.text:000000000040052D
.text:000000000040052D // int __cdecl main(int argc, const char **argv, const char **envp)
.text:000000000040052D public main
.text:000000000040052D main proc near // DATA XREF: _start+1D↑o
.text:000000000040052D
.text:000000000040052D var_18= qword ptr -18h
.text:000000000040052D var_10= qword ptr -10h
.text:000000000040052D var_4= dword ptr -4
.text:000000000040052D
.text:000000000040052D // __unwind {
.text:000000000040052D push rbp
.text:000000000040052E mov rbp, rsp
.text:0000000000400531 sub rsp, 20h
.text:0000000000400535 mov [rbp+var_4], edi
.text:0000000000400538 mov [rbp+var_10], rsi
.text:000000000040053C mov [rbp+var_18], rdx
.text:0000000000400540 lea rdi, aHelloWorld // "Hello world."
.text:0000000000400547 call sub_400410 // call <puts@plt>
.text:000000000040054C mov eax, 0
.text:0000000000400551 leave
.text:0000000000400552 retn
.text:0000000000400552 ; } // starts at 40052D
.text:0000000000400552 main endp在地址
0x400547
设置断点(F2),然后运行(F9),程序断在断点处。接着单步步进(F7)如下图,进入到 PLT 的<puts@plt>
处。在左下角数据区域可以看到地址0x601018
里面存储的值是0x400416
(即又回到<puts@plt>
的下一条指令)。继续 F7 步进到 common 公共表项。
可以看到,
.got.plt[1] == link_map == 0x7FDB'6BFD3150
、.got.plt[2] == _dl_runtime_resolve == 0x7FDB'6BDC6A30
。动态分析
_dl_runtime_resolve
函数。在虚拟机中使用
ldd --version
查看使用的glibc
版本(对应于/lib64/libc-2.17.so
),和 IDA 反汇编结果一样都是2.17
稳定版。但是反汇编出来的代码和version >= 2.27
的代码一致。(glibc 源码下载链接)1
2
3
4
5
6[root@centos-7 program]
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
由 Roland McGrath 和 Ulrich Drepper 编写。glibc >= 2.27
时,_dl_runtime_resolve
函数的源代码在 glibc/sysdeps/x86_64/dl-trampoline.h 中进行定义,在 glibc/sysdeps/x86_64/dl-trampoline.S 中,如果使用了USE_XSAVEC
则会使用_dl_runtime_resolve_xsavec
函数,如下:1
2
3
4
5
6
7所以我们以
glibc >= 2.27
代码进行分析。源代码是基于 AT&T 风格的汇编代码,可读性较差,我习惯 Intel 风格 反汇编,所以直接使用了 IDA 反汇编出来的代码: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
45
46
47
48
49
50
51
52
53
54
55
56ld_2.17.so:00007FDB6BDC6A30 _dl_runtime_resolve_xsavec proc near
ld_2.17.so:00007FDB6BDC6A30
ld_2.17.so:00007FDB6BDC6A30 var_18= qword ptr -18h
ld_2.17.so:00007FDB6BDC6A30 var_8= qword ptr -8
ld_2.17.so:00007FDB6BDC6A30 arg_0= qword ptr 8
ld_2.17.so:00007FDB6BDC6A30 arg_8= qword ptr 10h
ld_2.17.so:00007FDB6BDC6A30 arg_10= qword ptr 18h
ld_2.17.so:00007FDB6BDC6A30 arg_18= qword ptr 20h
ld_2.17.so:00007FDB6BDC6A30 arg_20= qword ptr 28h
ld_2.17.so:00007FDB6BDC6A30 arg_30= byte ptr 38h
ld_2.17.so:00007FDB6BDC6A30 arg_240= qword ptr 248h
ld_2.17.so:00007FDB6BDC6A30 arg_248= qword ptr 250h
ld_2.17.so:00007FDB6BDC6A30 arg_250= qword ptr 258h
ld_2.17.so:00007FDB6BDC6A30 arg_258= qword ptr 260h
ld_2.17.so:00007FDB6BDC6A30 arg_260= qword ptr 268h
ld_2.17.so:00007FDB6BDC6A30 arg_268= qword ptr 270h
ld_2.17.so:00007FDB6BDC6A30
ld_2.17.so:00007FDB6BDC6A30 push rbx // DATA XREF: .got.plt:off_601010↑o
ld_2.17.so:00007FDB6BDC6A31 mov rbx, rsp
ld_2.17.so:00007FDB6BDC6A34 and rsp, 0FFFFFFFFFFFFFFC0h
ld_2.17.so:00007FDB6BDC6A38 sub rsp, cs:qword_7FDB6BFD1CB0
ld_2.17.so:00007FDB6BDC6A3F mov [rsp+8+var_8], rax
ld_2.17.so:00007FDB6BDC6A43 mov [rsp+8], rcx
ld_2.17.so:00007FDB6BDC6A48 mov [rsp+8+arg_0], rdx
ld_2.17.so:00007FDB6BDC6A4D mov [rsp+8+arg_8], rsi
ld_2.17.so:00007FDB6BDC6A52 mov [rsp+8+arg_10], rdi
ld_2.17.so:00007FDB6BDC6A57 mov [rsp+8+arg_18], r8
ld_2.17.so:00007FDB6BDC6A5C mov [rsp+8+arg_20], r9
ld_2.17.so:00007FDB6BDC6A61 mov eax, 0EEh
ld_2.17.so:00007FDB6BDC6A66 xor edx, edx
ld_2.17.so:00007FDB6BDC6A68 mov [rsp+8+arg_240], rdx
ld_2.17.so:00007FDB6BDC6A70 mov [rsp+8+arg_248], rdx
ld_2.17.so:00007FDB6BDC6A78 mov [rsp+8+arg_250], rdx
ld_2.17.so:00007FDB6BDC6A80 mov [rsp+8+arg_258], rdx
ld_2.17.so:00007FDB6BDC6A88 mov [rsp+8+arg_260], rdx
ld_2.17.so:00007FDB6BDC6A90 mov [rsp+8+arg_268], rdx
ld_2.17.so:00007FDB6BDC6A98 xsavec [rsp+8+arg_30]
ld_2.17.so:00007FDB6BDC6A9D mov rsi, [rbx+10h] // 参数二,push 的索引值
ld_2.17.so:00007FDB6BDC6AA1 mov rdi, [rbx+8] // 参数一,.got.plt[1] == link_map
ld_2.17.so:00007FDB6BDC6AA5 call near ptr _dl_fixup // 调用 _dl_fixup 函数
ld_2.17.so:00007FDB6BDC6AAA mov r11, rax
ld_2.17.so:00007FDB6BDC6AAD mov eax, 0EEh
ld_2.17.so:00007FDB6BDC6AB2 xor edx, edx
ld_2.17.so:00007FDB6BDC6AB4 xrstor [rsp+8+arg_30]
ld_2.17.so:00007FDB6BDC6AB9 mov r9, [rsp+8+arg_20]
ld_2.17.so:00007FDB6BDC6ABE mov r8, [rsp+8+arg_18]
ld_2.17.so:00007FDB6BDC6AC3 mov rdi, [rsp+8+arg_10]
ld_2.17.so:00007FDB6BDC6AC8 mov rsi, [rsp+8+arg_8]
ld_2.17.so:00007FDB6BDC6ACD mov rdx, [rsp+8+arg_0]
ld_2.17.so:00007FDB6BDC6AD2 mov rcx, [rsp+8]
ld_2.17.so:00007FDB6BDC6AD7 mov rax, [rsp+8+var_8]
ld_2.17.so:00007FDB6BDC6ADB mov rsp, rbx
ld_2.17.so:00007FDB6BDC6ADE mov rbx, [rsp+18h+var_18]
ld_2.17.so:00007FDB6BDC6AE2 add rsp, 18h
ld_2.17.so:00007FDB6BDC6AE6 bnd jmp r11
ld_2.17.so:00007FDB6BDC6AE6 _dl_runtime_resolve_xsavec endp可以看到:
1、主要调用
_dl_fixup
函数,带有两个参数,即参数一为.got.plt[1] == link_map
,参数二为push
的索引值。2、堆栈恢复后,继续执行
bnd jmp r11
,r11
中的目标地址是函数_dl_fixup
的返回值。关于函数
_dl_fixup
分析,参考Linux pwn入门教程(10)——针对函数重定位流程的几种攻击。
参考计算机系统篇之链接(14):.plt、.plt.got、.got 和 .got.plt sections 之间的区别:
section | 所在 segment | section 属性 | 用途 |
---|---|---|---|
.plt | 代码段 | RE(可读,可执行) | .plt section 实际就是通常所说的过程链接表(Procedure Linkage Table, PLT) |
.plt.got | 代码段 | RE | .plt.got section 用于存放 __cxa_finalize 函数对应的 PLT 条目 |
.got | 数据段 | RW(可读,可写) | .got section 中可以用于存放全局变量的地址;.got section 中也可以用于存放不需要延迟绑定的函数的地址。 |
.got.plt | 数据段 | RW | .got.plt section 用于存放需要延迟绑定的函数的地址 |
参考:
5 模块加载
6 解析代码
1 |
|
输出结果:
1 | ************************************************************************ |