Elf文件格式学习

背(che)景(dan)

又到了每年的中秋节了,话说从大学开始,对于节日的回忆基本都停留在了学校中,尤其是大三开始,每逢节日都会窝在宿舍中学习一些东西,大三的清明节了解了下缓冲区溢出,大四的清明节,在准备面试,也在学一些东西,貌似是Windows内核啥的。ok,时间转到了大学最后一个学期,清明节忙着写毕设,Wow,终于毕业了,今年中秋搞点啥呢…Elf文件格式。

为什么学(了)习(解)这个呢?主要是渐渐觉得要去搞移动端安全相关了,所以该有些准备知识了,话说我java也不是菜鸡(虽然N久都没看,没用了),但那又怎样,所以路线大致是Elf->Arm汇编->Dex(smail)->Apk->java(native)->android(hook、anti&prot)->android(PWN!),这个路线有些扯淡,但目前就这么定了,后面再做的调整,2333…

正(yan)文(su)

先说下文件对象:

  1. 可重定位的对象文件(Relocatable file)
    类似于编译器生产的中间文件,在链接时使用。
  2. 可执行的对象文件(Executable file)
    可以运行的文件,比如linux下的ls、vi编辑器等
  3. 可被共享的对象文件(Shared object file)
    应用程序可以将公共函数放在一个文件中(.so),这样在运行时动态导入,节省了很多磁盘空间

Elf文件格式提供了两种视图,一种是链接(文件)视图,一种是运行(内存)视图

上图左侧为链接视图,右侧为运行视图,其中主要区别在section(segment)上,以Android NDK中的helloWord jni程序编译的.so文件为例,使用readelf命令解析一下:
使用readelf -S 解析sections

可以看到文件有19个section(段或者节),其中.shstrtab是文件的字符串段,后面出现的关于字符串的字段的值都是在这个字段中的下标值。
使用readelf --segments解析运行视图


现在段变成了6个,可以观察到这些段都是前面section中的项,也就是说elf文件在运行时有些相同属性的段被放在同一个段中。

为什么要这么做呢?

原因是目前的os大多都是以页为单位管理内存的,在linux中,典型的页大小的为4096b=4kb,所以即使一个大小不够4kb的数据,都必须分配一个4kb大小的内存,这样对于超多section的视图来说就是在浪费空间,所以linux将相同flg值以及相关属性的section放在一个segment中,方便权限管理,这样内存的浪费也就减小下来,所以从这里可以看出,segment是section的一个子集。

Elf组织结构

Elf 文件的大体结构

  1. Elf header 描述整个文件的组织
  2. Elf program_header_table 描述文件的各种segment,用来告诉系/统如何创建进程映像(一个段 包含 1个或多个section)
  3. Elf section_header_table 包含文件的各个section的信息

类型

typedef DWORD Elf32_Addr;
typedef WORD Elf32_Half;
typedef DWORD Elf32_Off;
typedef DWORD Elf32_Sword;
typedef DWORD Elf32_Word;

Elf header

typedef struct
{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
// 该成员确定该object的类型。
// Name Value Meaning
// ==== ===== =======
// ET_NONE 0 No file type
// ET_REL 1 Relocatable file
// ET_EXEC 2 Executable file
// ET_DYN 3 Shared object file
// ET_CORE 4 Core file
// ET_LOPROC 0xff00 Processor-specific
// ET_HIPROC 0xffff Processor-specific
Elf32_Half e_machine;
// 该成员变量指出了运行该程序需要的体系结构。
// Name Value Meaning
// ==== ===== =======
// EM_NONE 0 No machine
// EM_M32 1 AT&T WE 32100
// EM_SPARC 2 SPARC
// EM_386 3 Intel 80386
// EM_68K 4 Motorola 68000
// EM_88K 5 Motorola 88000
// EM_860 7 Intel 80860
// EM_MIPS 8 MIPS RS3000
Elf32_Word e_version;
// 这个成员确定object文件的版本。
//
// Name Value Meaning
// ==== ===== =======
// EV_NONE 0 Invalid version
// EV_CURRENT 1 Current version
Elf32_Addr e_entry;
// OEP;
// 假如文件没有如何关联的入口点,该成员就保持为 0。
Elf32_Off e_phoff;
// 该成员保持着程序头表(program header table)在文件中的偏移量(以字节计数)。 same as NT HEADER
// 假如该文件没有程序头表的的话,该成员就保持为 0。
Elf32_Off e_shoff;
// 该成员保持着程序头表(section header table)在文件中的偏移量(以字节计数)。 same as NT HEADER
// 假如该文件没有程序头表的的话,该成员就保持为 0。
Elf32_Word e_flags;
// 该成员保存着相关文件的特定处理器标志。
// flag的名字来自于EF_<machine>_<flag>。看下机器信息“Machine Information”部分的flag的定义。
Elf32_Half e_ehsize;
//该成员保存着ELF头大小(以字节计数)。
Elf32_Half e_phentsize;
// 该成员保存着在文件的程序头表(program header table)中一个入口的大小
// (以字节计数)。所有的入口都是同样的大小。
Elf32_Half e_e_phnum;
// 该成员保存着在程序头表中入口的个数。
// 因此,e_phentsize和e_phnum的乘机就是表的大小(以字节计数).
// 假如没有程序头表(program header table),e_phnum变量为0。
Elf32_Half e_shentsize;
// 该成员保存着section头的大小(以字节计数)。
// 一个section头是在section头表(section header table)的一个入口;
// 所有的入口都是同样的大小。
Elf32_Half e_shnum;
// 该成员保存着在section header table中的入口数目。
// 因此,e_shentsize和e_shnum的乘积就是section头表的大小(以字节计数)。
// 假如文件没有section头表,e_shnum值为0。
Elf32_Half e_shstrndx;
// 该成员保存着跟section名字字符表相关入口的section头表(section header table)索引。
// 假如文件中没有section名字字符表,该变量值为SHN_UNDEF。
// 更详细的信息 看下面“Sections”和字符串表(“String Table”) 。
}Elf32_Ehdr;


program header

typedef struct
{
Elf32_Word p_type;
// Name Value
// ==== =====
// PT_NULL 0
// PT_LOAD 1
// PT_DYNAMIC 2
// PT_INTERP 3
// PT_NOTE 4
// PT_SHLIB 5
// PT_PHDR 6
// PT_LOPROC 0x70000000
// PT_HIPROC 0x7fffffff
Elf32_Off p_offset;
// 该成员给出了该段的驻留位置相对于文件开始处的偏移。 offset file
Elf32_Off p_vadrr;
// 该成员给出了该段在内存中的首字节地址。 rva
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
// 文件映像中该段的字节数;它可能是 0 。
Elf32_Word p_memsz;
// 内存映像中该段的字节数;它可能是 0 。
Elf32_Word p_flags;
Elf32_Word p_align;
// 该成员给出了该段在内存和文件中排列值。
// 0 和 1 表示不需要排列。否则, p_align 必须为正的 2 的幂,
// 并且 p_vaddr 应当等于 p_offset 模 p_align 。
}Elf32_Phdr;

section header

typedef struct
{
Elf32_Word sh_name;
// 该成员指定了这个section的名字。
// 它的值是section报头字符表section的索引。[看以下的“String Table”], 以NULL空字符结束。
Elf32_Word sh_type;
// Section Types, sh_type
// ---------------------------
// Name Value Description
// ==== ===== ===========
// SHT_NULL 0 该值表明该section头是无效的;它没有相关的section。
// SHT_PROGBITS 1 该section保存被程序定义了的一些信息,它的格式和意义取决于程序本身。
// SHT_SYMTAB 2 该sections保存着一个符号表(symbol table)。
// SHT_STRTAB 3 该section保存着一个字符串表。
// SHT_RELA 4 该section保存着具有明确加数的重定位入口。
// SHT_HASH 5 该标号保存着一个标号的哈希(hash)表。
// SHT_DYNAMIC 6 该section保存着动态连接的信息。
// SHT_NOTE 7 该section保存着其他的一些标志文件的信息。
// SHT_NOBITS 8 该类型的section在文件中不占空间,但是类似SHT_PROGBITS。
// SHT_REL 9 该section保存着重定位的入口。
// SHT_SHLIB 10 该section类型保留但语意没有指明。包含这个类型的section的程序是不符合ABI的。
// SHT_DYNSYM 11 该sections保存着一个符号表(symbol table)。
// SHT_LOPROC 0x70000000 在这范围之间的值为特定处理器语意保留的。
// SHT_HIPROC 0x7fffffff 在这范围之间的值为特定处理器语意保留的。
// SHT_LOUSER 0x80000000 该变量为应用程序保留的索引范围的最小边界。
// SHT_HIUSER 0xffffffff 该变量为应用程序保留的索引范围的最大边界。
Elf32_Word sh_flags;
// Section Attribute Flags, sh_flags
// -----------------------------------
// Name Value Description
// ==== ===== ===========
// SHF_WRITE 0x1 该section包含了在进程执行过程中可被写的数据。
// SHF_ALLOC 0x2 该section在进程执行过程中占据着内存。
// SHF_EXECINSTR 0x4 该section包含了可执行的机器指令。
// SHF_MASKPROC 0xf0000000 所有的包括在这掩码中的位为特定处理语意保留的。
Elf32_Addr sh_addr;
// 假如该section将出现在进程的内存映象空间里,该成员给出了一个该section在内存中的位置。否则,该变量为0。
Elf32_Off sh_offset;
// 该成员变量给出了该section的字节偏移量(从文件开始计数)。
Elf32_Word sh_size;
// 该成员给你了section的字节大小。
Elf32_Word sh_link;
// 该成员保存了一个section报头表的索引连接,它的解释依靠该section的类型。
// 更多信息参见表"sh_link and sh_info Interpretation"
Elf32_Word sh_info;
// 该成员保存着额外的信息,它的解释依靠该section的类型。
// sh_link and sh_info Interpretation
// -------------------------------------------------------------------------------
// sh_type sh_link sh_info
// ======= ======= =======
// SHT_DYNAMIC The section header index of 0
// the string table used by
// entries in the section.
// -------------------------------------------------------------------------------
// SHT_HASH The section header index of 0
// the symbol table to which the
// hash table applies.
// -------------------------------------------------------------------------------
// SHT_REL The section header index of The section header index of
// SHT_RELA the associated symbol table. the section to which the
// relocation applies.
// -------------------------------------------------------------------------------
// SHT_SYMTAB The section header index of One greater than the symbol
// -------------------------------------------------------------------------------
// SHT_DYNSYM the associated string table. table index of the last local
// symbol (binding STB_LOCAL).
// -------------------------------------------------------------------------------
// other SHN_UNDEF 0
// -------------------------------------------------------------------------------
Elf32_Word sh_addralign;
// 一些sections有地址对齐的约束。
Elf32_Word sh_entsize;
// 一些sections保存着一张固定大小入口的表,就象符号表。
// 对于这样一个section来说,该成员给出了每个入口的字节大小。
// 如果该section没有保存着一张固定大小入口的表,该成员就为0。
}Elf32_Shdr;

###关键段(符号表、重定位表、GOT表)

####符号表(.dynsym)
描述了用来定位、重定位程序中所有的符号定义以及引用的信息,符号指的是经过修饰了的函数名或者变量名,修饰方法由编译器制定。
010编辑器中Elf解析模板

符号表的组织结构:

typedef struct
{
Elf32_Word st_name; //符号表项名称。如果该值非0,则表示符号名的字
//符串表索引(offset),否则符号表项没有名称。
Elf32_Addr st_value; //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
Elf32_Word st_size; //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
unsigned char st_info; //符号的类型和绑定属性。
unsigned char st_other; //未定义。
Elf32_Half st_shndx; //每个符号表项都以和其他节区的关系的方式给出定义。
             //此成员给出相关的节区头部表索引。
} Elf32_sym;

####字符串(.dynstr)
段描述

内容


其组织形式与符号表相同

###重定位表
程序经过编辑器->编译器->链接器步骤之后,并不能直接去运行,因为很多情况之下编译器是将程序从0地址开始做为基址的,当加载到内存的基地址发生变化后,原来静态计算的变量、函数地址都发生了变化,导致程序不能继续执行,或者.so文件在被加载到一个被占用的基地址时,需要重新加载到其他空闲地址,这样也要涉及到重定位,简单来说,重定位就是将程序运行所需要的函数、变量地址都关联到实际内存地址。
重定位表的格式

typedef struct
{
Elf32_Addr r_offset; //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
Elf32_Word r_info; //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
//其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
} Elf32_Rel;

重定位段:

未完待续……

Ref:

  1. http://blog.csdn.net/feglass/article/details/51469511