引言
仔细看了一遍第二章,发现第二章好像和Linux下漏洞挖掘没有什么太大的关联。第二章主要讲了程序从源代码阶段如何一步一步变成可执行程序的,但是在正常漏洞挖掘的时候一般都是挖掘已经编译好的可执行程序,很少关心编译过程中的流程,所以第二章就不写了。
今天来讲讲目标文件里面的事,比如在挖掘中很重要的ELF文件构造,开整!
一、骨骼惊奇的目标文件
目标文件从结构上讲,它是已经编译后的可执行文件格式,但是还没有经过链接的过程,和真正的可执行文件在结构上稍有不同。但目标文件一般和可执行文件格式一起采用一种格式存储,从广义上看,可以把目标文件与可执行文件看成是一种类型的文件。
在Linux平台流行的可执行文件格式(Executable)主要是ELF(Executable Linkable Format),在Linux下通常将目标文件和可执行文件统称为ELF文件。不光是可执行文件(Linux下的ELF文件)
- 动态链接库(DLL,Dynamic Linking Library)(Linux的.so)
- 静态链接库(Static Linking Library)(Linux的.a)
两个链接库都按照可执行文件格式(ELF)存储。ELF文件标准里把系统采用ELF格式的文件归为4类:
ELF文件类型 | 说明 | 示例 |
---|---|---|
可重定位文件(Relocatable) | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库可可以归为这一类 | Linux的.o |
可执行文件(Excutable File) | 这类文件包含了可直接执行的程序,比如ELF可执行文件,一般都没有扩展名 | 比如/bin/bash文件 |
共享目标文件(Shared Object File) | 这类文件包含了代码和数据,可以再以下两种情况使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享文件目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux的.so,如/lib/glibc-2.5.so |
核心转储文件(Core Dump File) | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | Linux下的core dump |
二、来人,把目标文件给我拆喽
下面用一个表格来展示目标文件的主要构造:
ELF文件内构造 | 存放的内容 | 名称 |
---|---|---|
文件头 | 文件属性:描述是否可执行、标明静态链接还是动态链接、入口地址(可执行文件)、目标硬件、目标操作系统 段表:描述了文件中各个段在文件中的偏移位置及段属性 |
File Header |
代码段 | 程序源代码编译后的机器指令 | .code /.text段 |
数据段 | 已初始化的全局变量和局部静态变量 | .data段 |
BSS段 | 未初始化的全局变量和局部变量 | .bss段 |
程序源代码被编译后主要分成两种段:程序指令和程序数据。代码段属于程序指令,二数据段和.bss段属于程序数据。
三、挖掘SimpleSection.o
上面提到的是ELF主要的构造,但是还有一些细节的地方没有提到,接下来通过调试一个例子,能够更好地查看真正环境下的文件结构:
/* SimpleSction.c
*Linux :
编译命令:gcc -c SimpleSection.c -m32
*/
int printf( const char* format , ...);
int global_init_var = 84;
int global_uninit_var2;
void func1( int i){
printf( "%d\n", i );
}
int main( void ){
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1( static_var + static_var2 + a + b );
return a;
}
通过objdump这个工具查看一下编译后的.o文件
hollk@ubuntu:~/Desktop/text$ objdump -h SimleSection.o
SimleSection.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text 0000007f 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000008 00000000 00000000 000000bc 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000004 00000000 00000000 000000c4 2**2
ALLOC
4 .rodata 00000004 00000000 00000000 000000c4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .text.__x86.get_pc_thunk.ax 00000004 00000000 00000000 000000c8 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .comment 0000002a 00000000 00000000 000000cc 2**0
CONTENTS, READONLY
7 .note.GNU-stack 00000000 00000000 00000000 000000f6 2**0
CONTENTS, READONLY
8 .eh_frame 0000007c 00000000 00000000 000000f8 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
可以看到除了在前面提到的text段、data段、bss段以外还有几个的段,分别是只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)、gcc处理异常产生的段(.eh_frame),还有一个group段。几个重要的段的属性:
属性名 | 作用 |
---|---|
size | 段的长度 |
File Offset | 段所在位置 |
CONTENTS | 表示改段在文件中存在 |
通过objdump可以看到bss段标注的是ALLOC,说明bss段不是在文件中的,堆栈提示段的size大小为0,也认为他不在ELF文件中。
其他段
常用的段名 | 说明 |
---|---|
.rodata | Read only Data,这种段里中存放的是只读数据,比如字符串常量、全局const变量 |
.comment | 存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0” |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的型号表,即源代码行号与编译后指令的对应表 |
.note | 额外的编译器信息,比如程序的公司名、发布版本号等 |
.strab | String Table字符串标,用于存储ELF文件中用到的各种字符串 |
.symtab | Symbol Table符号表 |
.shstrrab | Section String Table段名表 |
.plt、.got | 动态链接的跳转表和全局入口表 |
.init、.fini | 程序初始化与终结代码段 |
四、福尔马林中的ELF文件
下图描述的是ELF目标文件的总体结构,省去了ELF一些繁琐的结构,把最重要的结构提取出来形成了ELF文件基本机构图:
ELF目标文件格式的最前部是ELF文件头(ELF Header),其中ELF文件中与段有关的重要结构是段表(Section Header Table),该表描述了ELF文件包含的所有段信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。接下来单独去讲这几部分。
1、文件头
可以使用readelf工具来查看一下ELF文件:
hollk@ubuntu:~/Desktop/text$ readelf -h SimleSection.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1068 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 15
Section header string table index: 14
从输出结果可以看到ELF的文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序入口和长度、段表的位置和长度及段的数量等。
ELF文件头结构及相关常数被定义在”/usr/include/elf.h“中。32位版本与64位版本的ELF文件的文件头内容一样,但是成员不一样。”elf.h“使用typedef定义了一套自己的变量体系:
自定义类型 | 描述 | 原始类型 | 长度(字节) |
---|---|---|---|
Elf32_Addr | 32位版本程序地址 | uint32_t | 4 |
Elf32_Half | 32位版本的无符号短整形 | uint16_t | 2 |
Elf32_Off | 32位版本的偏移地址 | uint32_t | 4 |
Elf32_Sword | 32位版本有符号整形 | uint32_t | 4 |
Elf32_word | 32位版本无符号整形 | Int32_t | 4 |
Elf64_Addr | 64位版本程序地址 | uint64_t | 8 |
Elf64_Half | 64位版本的无符号短整形 | uint16_t | 2 |
Elf64_Off | 64位版本的偏移地址 | uint64_t | 8 |
Elf64_Sword | 64位版本有符号整形 | uint32_t | 4 |
Elf64_word | 64位版本无符号整形 | int32_t | 4 |
上面的表格就是结构体内定义成员的自定义类型,如果你不理解结构体是什么,可以把结构体想象成函数,自定义类型就像int啊char啊这种东西,成员就是函数内定义的变量(别人好像都拿结构体去理解函数,我特么拿函数理解结构体,nice)
拿32位的文件头结构ELF32_Ehdr作为例子:
typedef struct{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
2、段表
前面讲了ELF文件中有很多的段,但是如果没有人管他们就会很乱套,这个时候这些段的头头段表(Section Header Table)出现了,段表就是确保这些段的基本属性的结构。段表是ELF文件除头文件意外最重要的结构,描述了ELF的各个段的信息,比如每个段的段名、长度、在文件中的偏移、读写权限等。编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。前面使用objdump列出了SimpleSction.o的主要段,其实是不全的,可以使用readelf工具查看完整的段结构:
hollk@ubuntu:~/Desktop/text$ readelf -S SimleSection.o
There are 15 section headers, starting at offset 0x42c:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .group GROUP 00000000 000034 000008 04 12 16 4
[ 2] .text PROGBITS 00000000 00003c 00007f 00 AX 0 0 1
[ 3] .rel.text REL 00000000 000348 000048 08 I 12 2 4
[ 4] .data PROGBITS 00000000 0000bc 000008 00 WA 0 0 4
[ 5] .bss NOBITS 00000000 0000c4 000004 00 WA 0 0 4
[ 6] .rodata PROGBITS 00000000 0000c4 000004 00 A 0 0 1
[ 7] .text.__x86.get_p PROGBITS 00000000 0000c8 000004 00 AXG 0 0 1
[ 8] .comment PROGBITS 00000000 0000cc 00002a 01 MS 0 0 1
[ 9] .note.GNU-stack PROGBITS 00000000 0000f6 000000 00 0 0 1
[10] .eh_frame PROGBITS 00000000 0000f8 00007c 00 A 0 0 4
[11] .rel.eh_frame REL 00000000 000390 000018 08 I 12 10 4
[12] .symtab SYMTAB 00000000 000174 000140 10 13 13 4
[13] .strtab STRTAB 00000000 0002b4 000091 00 0 0 1
[14] .shstrtab STRTAB 00000000 0003a8 000082 00 0 0 1
readelf输出的结果就是ELF文件段表的内容,段表是一个以”Elf32_Shdr“结构体为元素的数组。数组元素的个数等于段的个数,每个”Elf32_Shdr“结构体对应一个段。”Elf32_Shdr“又被称为段描述符(Section Descriptor),”Elf32_Shdr“的各个成员的含义如下图:
段的类型(sh_type)。段名只是在链接和编译过程中有意义,但它并不能真正表示段的类型。就比如说在HTML中一个标签的name属性可以作为传值的参数,但是name属性并不是一个标签的唯一标识,id属性才是。而且段名是可以通过代码更改的,对于编译器和链接器来说,主要决定短的属性的是段的类型(sh_type)和标志位(sh_flag),段的类型相关常量以SHT_开头:
常量 | 值 | 含义 |
---|---|---|
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_SNYSYM | 11 | 动态链接的符号表 |
段的标志位(sh_flag)短的标志位表示该段在进程虚拟地址中间中的属性,比如是否可写、是否可执行等,相关常量以SHF_开头:
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 2 | 表示该段在进程空间中须要分配空间,有些包含提示或控制信息的段不须要在进程空间中被分配空间,它们一般不会有这个标志,像代码段、数据段、bss段都会有这个标志位 |
SHF_EXECINSTR | 4 | 表示该段在进程空间中可以被执行,一般指代码段 |
对于系统保留段,同样列举了它们的属性:
名称(Name) | 类型(sh_type) | 标志(sh_flag) |
---|---|---|
.bss | SHT_NOBITS | STF_ALLOC + SHF_WRITE |
.comment | SHT_PROGBITS | none |
.data | SHT_PROGBITS | STF_ALLOC + SHF_WRITE |
.data l | SHT_PROGBITS | STF_ALLOC + SHF_WRITE |
.debug | SHT_PROGBITS | none |
.dynamic | SHT_DYNAMIC | STF_ALLOC + SHF_WRITE 在有些系统下.dynamic段可能是只读的,所以没有SHF_WRITE标志位 |
.hash | SHT_HASH | SHF_ALLOC |
.line | SHT_PROGBITS | none |
.note | SHT_NOTE | none |
.rodata | SHT_PROGBITS | SHF_ALLOC |
.rodata l | SHT_PROGBITS | SHF_ALLOC |
.shstrab | SHT_STRTAB | none |
.strtab | SHT_STRTAB | 如果该ELF文件中有可装载的段需要用到该字符串表,那么该字符串表也将被装载到进程空间,则有SHF_ALLOC标志位 |
.symtab | SHT_SYMTAB | 同字符串表 |
.text | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR |
段的链接信息(sh_link、sh_info)如果段的类型是链接相关的(不论是动态链接还是静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如下,对于其他类型的段,这两个成员没有意义:
类型(sh_type) | 链接信息(sh_link) | 链接信息(sh_info) |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_HASH | 该段所使用的的符号表在段表的下标 | 0 |
SHT_REL | 该段所使用的的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_RELA | 该段所使用的的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB | 操作系统相关的 | 操作系统相关的 |
SHT_DYNAYM | 操作系统相关的 | 操作系统相关的 |
other | SHN_UNDEF | 0 |
3、重定位表
在通过前面使用readelf查看时,发现有一个叫做.rel.text的段,类型是”SHT_REL“,是一个重定位表(Relocation Table)。链接器在处理目标文件时,需要对目标文件中的的位置进行重定位,即代码段和数据段中那些绝对地址的引用位置。这些重定位信息都记录在ELF文件的重定位表中。每个段都有一个重定位表,例如”.rel.text“是”text“段的重定位表。
一个重定位表是ELF中的一个段,这个段的类型是”SHT_REL“类型,”sh_link“表示符号表的下标,它的”sh_info“表示它作用于哪个段。比如”.rel.text“作用于”.text“段,而”.text“段的下标为”1“,那么”.rel.text“的”sh_info“为”1“。
4、字符串表
ELF文件中用到很多字符串,如段名、变量名、函数名等。但是字符串长度不一,所以将这些字符串存放到一个表中,然后使用字符串在表中的偏移来引用:
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 | h | e | l | l | o | w | o | r | l |
+10 | d | \0 | M | y | v | a | r | i | a | b |
+20 | l | e | \0 |
对应偏移的字符串为:
偏移 | 字符串 |
---|---|
0 | 空字符串 |
1 | helloworld |
6 | world |
12 | Myvariable |
这样在ELF文件中,引用字符串只需给出一个数字下标就行,单个字符串都以\0结尾,所以不需要考虑长度问题。ELF中两个表为:
- 字符串表(String Table):常见段名为”.strtab“
- 段表字符串表(Section Header String Table):常见段名为”.shstrtab“
通过分析ELF文件头,可以得到段表和段表字符串表的位置。
五、链接的接口——符号
链接过程本质是把多个不同的文件通过函数和变量引用的方式链接起来,就像贼贵的 乐高积木一样,通过凹凸部分互相扣住,最后将多个块拼接成超级马里奥 一整个可执行文件。
举个栗子:目标文件B需要用到目标文件A中的函数”func“,那么成目标文件A定义(define)了函数”func“,成目标文件B引用(reference)了目标文件A中的函数”func“。这两个概念同样适用于变量。在连接中,将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)
在链接过程中可以将符号看做粘合剂,所以符号需要统一管理,每个目标文件中都会有一个相应的符号表(Symbol Table),表中记录了目标文件所用到的所有符号,每个定义的符号有一个对应的值,叫做符号值(Symbol Value),符号是就是变量和函数的地址,下面是符号表中所有符号的分类:
- 定义在奔目标文件的全局符号,可以被其他目标引用,例如SimpleSection.o中的”func1“、”main“和”global_init_var“。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做外部符号(External Symbol),例如:SimpleSection.o中的”printf“
- 段名,这种符号往往由编译器产生,它的值就是该段的起始地址,例如SimpleSection.o中的”.text“、”.data“等
- 局部符号,这类符号只在编译单元内部可见。例如:SimpleSection.o里面的”static_var“和”static_var2“
- 行号信息,即目标文件指令与源代码中代码行对应关系,可选
值得关注的是前两个,在链接过程中只关心全局符号的链接,局部符号、段名、行号都是次要的。
1、ELF符号表结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫”.symtab“。表结构是一个Elf32_Sym结构的数组,每个Elf32_Sym结构对应一个符号。Elf32_Sym结构定义如下:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char sy_other;
Elf32_Half st_shndx;
} Elf32_Sym;
成员定义如下:
成员名 | 解释 |
---|---|
st_name | 符号名。这个成员包含了该符号名在字符串表中的下标 |
st_value | 符号相对应的值。这个值和符号有关,可能是一个绝对值,也可能是一个地址等,不同的符号对应的值含义不同 |
st_size | 符号大小,对于包含数据的符号,这个值是该数据类型的大小,比如一个double类型的符号占用8个字节,如果该值为0,则表示该符号大小为0或位置 |
st_info | 符号类型和绑定信息 |
st_other | 该成员目前为0,没用 |
st_shndx | 符号所在段 |