目录
理解Linux二进制文件,才能反编译。ELF目前成为Unix和类Unix操作系统上二进制格式的标准。在Linux和BSD变种以及其它操作系统中,也都使用ELF二进制格式。可用于可执行文件、共享库、目标文件、coredump文件、内核引导镜像文件等。
学习ELF二进制格式的规范,将加深理解程序如何映射到磁盘并加载到内存,对于二进制黑客、逆向工程师或者普通程序员来说,对二进制的理解是非常重要的知识。
本章节首先介绍ELF二进制格式规范,然后进行实践实验,后续章节将继续了解ELF中的跟病毒、进程内存取证、二进制包含、rootkit相关联的知识。
1. ELF文件类型
ELF有以下文件类型:
-
ET_NONE
未知类型,标记表明文件类型不明确,或未定义。确,或未定义。
-
ET_REL
可重定位文件。relocatable文件,意味着该文件被标记了一段可重定位代码,有时也称作目标文件。
可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码(PIC, position independent code)。 编译完成后,通常是.o格式的文件。该文件包含了创建可执行文件所需要的代码和数据。
-
ET_EXEC
可执行文件。executable文件,该文件也称为程序,可以被加载到内存执行,包含进程创建、开始执行的入口。
-
ET_DYN
共享目标文件。dynamic文件,该文件是一个动态的、可链接的目标文件,也称为共享库。该文件在程序运行时被装载并链接到程序的进程内存镜像中。
-
ET_CORE
核心文件。core文件,在程序崩溃或进程传递一个SIGSEGV信号时,会在核心文件中记录整个进程的内存镜像。可以使用GDB读取该类文件来辅助调试并查找程序崩溃的原因。
使用readelf -h
可以查看ELF文件,原始的ELF文件头,ELF文件头从文件的0偏移处开始,是文件剩余部分的描述元数据。标记了ELF类型、结构和程序开始执行的入口地址;提供了其他ELF头(节头和程序头)的偏移量。 示例如下,ls命令的文件头:
其文件头结构体如下:
// /usr/include/elf.h
#define EI_NIDENT (16)
// 32位
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
// 64位
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
hexdump验证结果如下:
2. ELF程序头(program header)
ELF程序头对二进制文件中的段(segment)进行了描述,程序向内存load(装载)时会用到。
段(segment)是由内核装载并解析的,其描述了可执行文件的内存布局,以及如何映射到内存中。可以通过原始ELF头中名为e_phoff的偏移量来得到程序头表(ElfN_Ehdr结构体中的e_phoff字段)。
有5种场景的程序头类型:
2.1 PT_LOAD
可在内存中装载的段。 一个可执行文件至少包含一个PT_LOAD段,该类型的程序头描述了可被装载或映射到内存的段。例如:
一个需要动态链接的ELF可执行文件,至少包含两个可装载的段(程序头表中至少有两个PT_LOAD类型的表项):
-
存放程序代码的text段(通常权限为PF_X|PF_R,可读可执行)
-
存放全局变量和动态链接信息的data段(通常权限设置为PF_W|PF_R,可读可写)
著名的千人面病毒(polymorphic virus)感染的文件的text段或data段的权限可能会被改写,如通过在程序头的段标记(p_flags)处增加PF_W标记来修改text段的权限。
2.2 PT_DYNAMIC
动态链接可执行文件特有的段。 包含了动态链接所必需的一些信息,至少如下:
-
运行时需要链接的共享库列表
-
全局偏移表(GOT)的地址
-
重定位条目的相关信息
动态段中包含一些标记和指针,完整的标记名和指针列表如下:
标记名 | 描述 |
DT_HASH | 符号散列表的地址 |
DT_STRTAB | 字符串表的地址 |
DT_SYMTAB | 符号表地址 |
DT_RELA | 相对地址重定位表的地址 |
DT_RELASZ | Rela表的字节大小 |
DT_RELAENT | Rela表条目的字节大小 |
DT_STRSZ | 字符串表的字节大小 |
DT_SYMENT | 符号表条目的字节大小 |
DT_INIT | 初始化函数的地址 |
DT_FINI | 终止函数的地址 |
DT_SONAME | 共享目标文件名的字符串表偏移量 |
DT_RPATH | 库搜索路径的字符串表偏移量 |
DT_SYMBOLIC | 修改链接器,在可执行文件之前的共享目标文件中搜索符号 |
DT_REL | Rel relocs表的地址 |
DT_RELSZ | Rel表的字节大小 |
DT_RELENT | Rel表条目的字节大小 |
DT_PLTREL | PLT引用的reloc类型(Rela或Rel) |
DT_DEBUG | 还未进行定义,为调试保留 |
DT_TEXTREL | 缺少此项表明重定位只能应用于可写段 |
DT_JMPREL | 仅用于PLT的重定位条目地址 |
DT_BIND_NOW | 指示动态链接器在将控制权交给可执行文件之前处理所有的重定位 |
DT_RUNPATH | 库运行路径的字符串表偏移量? |
动态段中包含一些结构体,存放着与动态链接相关的信息,如下:
// /usr/include/elf.h
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
d_tag成员控制着d_un成员的含义。
2.3 PT_NOTE
PT_NOTE类型的段可能保存了与特定供应商或者系统相关的附件信息。ELF规范中的定义如下:
供应商或者系统构建者可以在目标文件中标记特定的信息,以便于对其它程序对一致性、兼容性等的检查。
PT_NOTE类型的程序头和SHT_NOTE类型的节元素可以用于实现此目的。程序头或节元素中的备注信息可以有任意数量的项数,每个条目都是一个4自己的目标处理器格式的数组。
而在实际上,目前这一段只保存了操作系统的规范信息,在可执行文件运行时是不需要该段的。 因为今天的系统是始终假设一个可执行文件是本地的,有非常多的病毒会感染NOTE段。
2.4 PT_INTERP
该段将位置和大小信息放在一个以null终止符的字符串中,是对程序解释器位置的描述。例如,/lib/linux-ld.so.2一般是指该文件使用的动态链接器的位置,也即程序解释器的位置。
2.5 PT_PHDR
该段保存了程序头表本身的位置和大小。而Phdr(程序头表)表则保存了所有的Phdr对文件(以及内存镜像)中段的描述信息。
上述是一些至关重要的程序头表(Phdr)类型,更详细的可以查阅ELF规范。
使用readelf -l <filename>命令可以查看文件的Phdr表,如下:
# readelf -l /bin/ls
Elf file type is DYN (Shared object file)
Entry point 0x5850
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R E 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000001e6e8 0x000000000001e6e8 R E 0x200000
LOAD 0x000000000001eff0 0x000000000021eff0 0x000000000021eff0
0x0000000000001278 0x0000000000002570 RW 0x200000
DYNAMIC 0x000000000001fa38 0x000000000021fa38 0x000000000021fa38
0x0000000000000200 0x0000000000000200 RW 0x8
NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x000000000001b1a0 0x000000000001b1a0 0x000000000001b1a0
0x0000000000000884 0x0000000000000884 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x000000000001eff0 0x000000000021eff0 0x000000000021eff0
0x0000000000001010 0x0000000000001010 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .data.rel.ro .dynamic .got
上述结果,可以看到:1)可执行程序的入口点(0x5850);2)共有9个程序头(00-08),起始与文件的64字节处;3)大多数节(Sections)包含在PT_LOAD(LOAD)段中;4)data段是可读可写的(第2个LOAD),text段是可读可执行的(第1个LOAD)
关于作者:
犇叔,浙江大学计算机科学与技术专业,研究生毕业,而立有余。先后在华为、阿里巴巴和字节跳动,从事技术研发工作,资深研发专家。主要研究领域包括虚拟化、分布式技术和存储系统(包括CPU与计算、GPU异构计算、分布式块存储、分布式数据库等领域)、高性能RDMA网络协议和数据中心应用、Linux内核等方向。
专业方向爱好:数学、科学技术应用
关注犇叔,期望为您带来更多科研领域的知识和产业应用。
内容坚持原创,坚持干货有料。坚持长期创作,关注犇叔不迷路