0x01 ELF文件简介
常用的一般为下面三种:
- 可重定位文件(.o):用于与其他目标文件连接生成可执行文件或共享目标文件(.so)的数据与代码;
- 可执行文件(.exe):包含一个可执行程序,并且此文件规定
exec()
进程如何创建一个程序的进程映像; - 共享目标文件(.so):包含可在两种上下文中链接的代码与数据。首先链接编辑器可以将它与可重定位文件和其他共享目标文件一起处理,生成一个新的目标文件。另外,动态链接器可以将它与可执行文件和其他共享目标文件一起处理,创建程序镜像;
目标文件都是程序的二进制表示,目的是在处理器上直接执行;
这里再简单说一下编译原理的一些东西
一般来说由.c
或.cpp
,直接生成的文件都应该是.o文件,如果在linux平台下的话,通过gcc编译来生成共享目标文件(动态链接库),和可执行文件,在linux平台下可执行文件是不带后缀的。
编译过程如下图:
关于编译过程推荐一下下面这一篇文章:
另外关于链接这一块的东西,这一篇文章要更详细一点,特别是关于静态链接中的重定位与符号表介绍比较详细;动态链接中的PIC(地址无关代码)
技术,GOT
,PLT
等知识有简单的介绍,对于动态链接的理解帮助很大;另外对静态链接可执行文件的生成与动态链接可执行文件的生成也介绍得很详细,对理解编译过程的链接阶段理解有很大帮助;
0x02 ELF文件格式
首先我们给一个ELF文件的格式图,对后面讲述的东西有个大致的模型,下面的每个部分以及段与节的联系区别我们都会在下面给出:
一、ELF文件头
使用readelf -h <filename>
命令可以查看ELF文件头,另外可以用readelf -h
查看其它命令。readelf对于分析ELF文件格式来说是很有用的一个命令。
下面给出ELF文件头的结构:
typedef struct elfhdr {
unsigned char e_ident[EI_NIDENT]; /* ELF Identification */
Elf32_Half e_type; /* object file type */
Elf32_Half e_machine; /* machine */
Elf32_Word e_version; /* object file version */
Elf32_Addr e_entry; /* virtual entry point */
Elf32_Off e_phoff; /* program header table offset */
Elf32_Off e_shoff; /* section header table offset */
Elf32_Word e_flags; /* processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size */
Elf32_Half e_phentsize; /* program header entry size */
Elf32_Half e_phnum; /* number of program header entries */
Elf32_Half e_shentsize; /* section header entry size */
Elf32_Half e_shnum; /* number of section header entries */
Elf32_Half e_shstrndx; /* section header table's "section
header string table" entry offset */
} Elf32_Ehdr;
结构体中每个变量的值注释都给得比较清楚,值得说一下的就是最后一个变量e_shstrndx
,它指的是存放每一个Section
名字的Section
的Section header
在Section header table
中的偏移量,如果对于Section
不是很熟悉的朋友可以先不必在这纠结,我们后面讲到Section
的时候,还会再说。另外可以通过查看ELF手册详细了解。
二、ELF程序头
ELF程序头是对二进制文件中段(Segment)的描述,是程序装载必须的部分。Segment是内核装载程序时被解析的,程序头描述了对应的段在内存中的布局以及如何映射到内存中。可以通过引用ELF头中的e_phoff(程序头表偏移量)
来得到程序头表。程序头表由多个程序头条目组成,每一个程序头条目描述了其对应类型的段,存储了段相关的信息。下面给出32位ELF文件的程序头的结构体,在程序头表中就是存放了多个程序头结构体:
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment offset in file */
Elf32_Addr p_vaddr; /* Segment virtual address in memory*/
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
结构体中每个变量的值注释都说得比较清楚,我们就不解释了。接下来我们说一下4种常见类型的程序头。
- PT_LOAD:一个可执行文件至少包含一个PT_LOAD段。这种类型的段将会被装载或映射到内存中。一个需要动态链接的可执行文件一般包括存放代码的
test
段与存放全局变量和动态链接信息的data
段。 - PT_DYNAMIC:这种类型的段是动态链接可执行文件特有的,包含了动态链接器所需要的一些信息。主要包括程序运行时所需要链接的共享库列表,全局偏移表(GOT)的地址和重定位条目的相关信息。这在后面讨论动态链接时会再说。
- PT_INTERP:这个段存一般存放了动态链接器的位置,也就是程序解释器的位置。如:
/lib/linux-ld.so.2
。 - PT_PHDR:这个段保存了程序头表本身的位置和大小。程序头表保存了所有程序头对文件中段的描述。
可以通过查看ELF手册来详细了解段的相关信息。
我们可以通过readelf -l <filename>
命令查看文件的程序头表,如下所示:
其中可以看见我们上面所介绍的几种常用类型的程序头,其中类型为LOAD,偏移量为0x0的条目即为text段的程序头,另一个类型为LOAD的条目即为data段的程序头。
三、ELF节头
这里我们先说一下节(Section)与段(Segment)的联系与区别,很多人将其混为一谈。段是每个程序执行的必要组成部分,在每个段中的代码或者数据,会划分为不同的节。也就是节相对于段来说,是对可执行文件在内存中更为详细的划分。后面我们介绍节的类型的时候,可以充分体现这一点。
每个节也和段一样,有自己的头,我们叫做节头(Section Header Table),许多节头构成了节头表。如果可执行文件存在节头表,可以通过引用ELF文件头中的e_shoff
来得到其在文件中的偏移量。我们为什么要说如果存在呢,是因为节头表对于程序的执行不是必须的,没有节头表程序仍可以正常运行,节头的作用是描述相应的节的位置和大小,主要用于链接和调试。所以我们从内存中dump出来的.so
文件,是可能没有节头的。
虽然二进制文件可能会缺少节头,但并不意味着节不存在。只是无法通过节头来引用节,对于调试器和反编译程序来说,少了许多参考信息。默认情况下,可执行程序是自带节头的,只是某些时候,为了程序不被黑客反编译,利用IDA,objdump等工具得到相关信息,厂商会自己将节头从中删除,或者填充垃圾信息。
下面我们给出一个32位ELF节头的结构:
typedef struct {
Elf32_Word sh_name; /* name - index into section heade string table section */
Elf32_Word sh_type; /* type */
Elf32_Word sh_flags; /* flags */
Elf32_Addr sh_addr; /* address */
Elf32_Off sh_offset; /* file offset */
Elf32_Word sh_size; /* section size */
Elf32_Word sh_link; /* section header table index link */
Elf32_Word sh_info; /* extra information */
Elf32_Word sh_addralign; /* address alignment */
Elf32_Word sh_entsize; /* section entry size */
} Elf32_Shdr;
我们可以利用readelf -l <filename>
命令来得到每个段与节的关系,接上图,如下:
其中02对应那一行即为text段包含的不同类型的节,下面我们给出几种常见类型的节:
- .text: .text节保存了程序代码指令,存在于text段,对应的类型为
SHT_PROGBITS
- .rodata: .rodata节保存了只读数据,因为它是只读的,所以只能放在只读段,存在于text段。比如c语言中
printf("hello world\n")
这条指令就是放在rodata节,对应的类型为SHT_PROGBITS
- .plt: .plt节包含了动态链接器调入从共享库导入的函数所必须的相关代码。存在于text段。对应的类型为
SHT_PROGBITS
- .data: .data节保存了初始化的全局变量等数据。存在于data段,对应的类型为
SHT_PROGBITS
- .bss: .bss节保存了未初始化的全局变量等数据。存在于data段,对应的类型为
SHT_NOBITS
- .got.plt: .got节保存了全局偏移表。.got节与.plt节一起提供了对从共享库中导入的函数的访问入口,由动态链接器在运行时修改.got节。存在于data,对应的类型为
SHT_PROGBITS
- .dynsym: .dynsym节保存了从共享库导入的动态符号信息。存在于text段,对应的类型为
SHT_DYNSYM
- .dynstr: .dynstr节保存了动态符号字符串表。存在于text段
- .rel*: 其保存了重定位的相关信息,这些信息描述了如何在链接或者运行时,对ELF目标文件的某一部分内容或者镜像进行补充或者修改。存在于text段,对应的类型为
SHT_REL
- .symbol: 其保存了二进制文件的所有符号,也就是我们所说的符号表。存在于text段,对应的类型为
SHT_SYMTAB
- .strtab: 其保存了二进制文件所以的字符串,也就是我们所说的字符串表。表中的内容会被符号表所引用。存在于text段,对应的类型为
SHT_STRTAB
- .shstrtab: 这个节就是保存了所有节头的名字字符串,在Elf文件头中的
e_shstrndx
这个变量就是指的这个节的节头在节头表中的偏移量,我们可以通过获取这个节的位置来得到每个节的名字字符串。存在于text段,对应的类型为SHT_STRTAB
一些主要类型的节我们就介绍,如果想了解更多,可以查看ELF手册
四、ELF符号
符号是对某些代码或者数据的符号引用,多个符号组成了一个二进制文件的符号表。比如,我们的代码中存在printf("hello world\n")
这条语句,那么在符号就保存了它的名字字符串在字符表中的偏移,它的地址或者偏移量,大小之类的信息。
对于大多数共享库和动态链接可执行文件来说都存在两个符号表,.dynsym和.symtab。它们的区别在于.dynsym保存的是引用外部文件符号的全局/动态符号,如printf这样的库函数。而.symtab则是保存了程序所有的符号,除了.dynsym中的符号除外,还有本地函数之类的符号,即.dynsym是.symtab的子集。但为什么有.symtab符号表还要有.dynsym呢?是因为.dynsym在程序运行时会被装载进内存中,是程序运行所必须的,而.symtab不会。,dynsym中的符号只有在程序运行时被解析,是运行时动态链接器所需要唯一的符号。.symtab符号表只是用来链接和调试的,有时候为了节省空间,会将.symtab符号表从二进制文件中删除。
下面给出32位ELF文件符号项的结构,其构成了符号表:
typedef struct Elf32_Sym
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value (addr or offset)*/
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
}Elf32_Sym;
ELF的文件格式到这里我们就说差不多了,讲得比较少,但本身ELF文件不是一两篇文章就可以说完的。需要花更多时间去理解,可以多多看一下ELF手册,这个讲得非常的细。最后给出一个ELF文件的解析器的代码,自己也可以实现以下,对理解很有帮助:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdint.h>
#include <elf.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
int fd,i;
uint8_t *mem;
struct stat st;
char *stringSectionName,*interp;
Elf32_Ehdr *ehdr;
Elf32_Shdr *shdr;
Elf32_Phdr *phdr;
/* code */
if (argc < 2)
{
/* code */
printf("Usage: %s <excutable file>\n", argv[0]);
exit(0);
}
if ((fd = open(argv[1],O_RDONLY)) < 0)
{
/* code */
perror("open error");
exit(-1);
}
if (fstat(fd,&st) < 0)
{
/* code */
perror("fstat error");
exit(-1);
}
mem = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,fd,0);
if (mem == MAP_FAILED)
{
/* code */
perror("mmap error");
exit(-1);
}
ehdr = (Elf32_Ehdr *) mem;
phdr = (Elf32_Phdr *) &mem[ehdr->e_phoff];
shdr = (Elf32_Shdr *) &mem[ehdr->e_shoff];
if (mem[0] != 0x7f && strcmp((char *)&mem[1], "ELF"))
{
/* code */
fprintf(stderr, "%s is not ELF file\n", argv[1]);
exit(-1);
}
if (ehdr->e_type != ET_EXEC)
{
/* code */
fprintf(stderr, "%s is not excutable file\n", argv[1]);
exit(-1);
}
printf("Entry Addr : 0x%x\n",ehdr->e_entry);
stringSectionName = (char *)&mem[shdr[ehdr->e_shstrndx].sh_offset];
printf("Section Header list :\n");
printf(" SectionName\t\tVirAddr\n\n");
for (i = 1; i < ehdr->e_shnum; ++i)
{
/* code */
printf(" %s\t\t0x%x\t\t%d\n", &stringSectionName[shdr[i].sh_name],shdr[i].sh_addr,shdr[i].sh_type);
}
printf("Segment Header list :\n");
printf(" SegmentName\t\tVirAddr\n\n");
for (int i = 0; i < ehdr->e_phnum; ++i)
{
/* code */
switch(phdr[i].p_type){
case PT_LOAD:
if (phdr[i].p_offset == 0)
{
/* code */
printf(" Text Segment\t\t0x%x\n", phdr[i].p_vaddr);
}else{
printf(" Data Segment\t\t0x%x\n", phdr[i].p_vaddr);
}
break;
case PT_NOTE:
printf(" NOTE Segment\t\t0x%x\n",phdr[i].p_vaddr );
break;
case PT_INTERP:
interp = strdup(&mem[phdr[i].p_offset]);
printf(" (Interpreter Path = %s)\n", interp);
break;
case PT_DYNAMIC:
printf(" Dynamic Segment\t\t0x%x\n", phdr[i].p_vaddr);
break;
case PT_PHDR:
printf(" Phdr Segment\t\t0x%x\n",phdr[i].p_vaddr);
break;
default:
printf(" %s\t\t0x%x\n", "Other Segment",phdr[i].p_vaddr);
}
}
return 0;
}