文章目录
要反编译Linux 二进制文件,首先需要理解二进制格式本身。ELF 目前已经成为UNIX 和类UNIX操作系统的标准二进制格式。在Linux、BSD 变体以及其他操作系统中,ELF 格式可用于可执行文件、共享库、目标文件、coredump文件,甚至内核引导镜像文件。因此,对于那些想要更好地理解反编译、二进制攻破和程序执行的人来说,学习ELF 至关重要。 — 《Linux二进制分析》
一、ELF 文件类型
ELF(Executable and Linkable Format)即可执行的和可链接的格式。一个ELF 文件可以被标记为以下几种类型之一。
ET_NONE
:未知类型。这个标记表明文件类型不确定,或者还未定义。ET_REL
:重定位文件。ELF 类型标记为relocatable 意味着该文件被标记为了一段可重定位的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码(position independent code)。在编译完代码之后通常可以看到一个.o 格式的文件,这种文件包含了创建可执行文件所需要的代码和数据。ET_EXEC
:可执行文件。ELF 类型为executable,表明这个文件被标记为可执行文件。这种类型的文件也称为程序,是一个进程开始执行的入口。ET_DYN
:共享目标文件。ELF 类型为dynamic,意味着该文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中。ET_CORE
:核心文件。在程序崩溃或者进程传递了一个SIGSEGV 信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB 读取这类文件来辅助调试并查找程序崩溃的原因。
出于方便性和效率考虑,ELF文件提供了两种并行视图,即链接视图,执行视图。
二、ELF组成部分
2.1 ELF Header
文件头主要标记了ELF 类型、结构和程序开始执行的入口地址,并提供了其他ELF 头(节头和程序头)的偏移量。
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff; // 程序头入口地址
ElfN_Off e_shoff; // 节头入口地址
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize; // 节区每项字节数
uint16_t e_shnum; // 节区数目
uint16_t e_shstrndx;
}ElfN_Ehdr;
2.2 Program Headers
ELF 程序头是对二进制文件中段的描述,存在于可执行文件或共享库中,是一个结构体组成的数组。每个数组元素描述一个段(segment)或者其他程序执行时需要的信息,该结构用于执行阶段。
ELF程序头一般在ELF Header之后,如前面ElfN_Ehdr 结构中所示,可以通过引用原始ELF 头中名为e_phoff(程序头表偏移量)的偏移量来得到程序头表。
下面是Elf32_Phdr 结构体:
typedef struct {
uint32_t p_type; // 段类型
Elf32_Off p_offset; // 文件头到该段第一个字节的偏移
Elf32_Addr p_vaddr; // 该段第一个字节在内存中的虚拟地址
Elf32_Addr p_paddr; // 仅用于与物理地址相关的系统中
uint32_t p_filesz; // 该段在文件中所占字节
uint32_t p_memsz; // 该段在内存中所占字节
uint32_t p_flags; (segment flags, I.E execute|read|read)
uint32_t p_align; // 指出段在文件和内存中如何对齐,p_vaddr和p_offset对p_align取模后应该相等
} Elf32_Phdr;
2.2.1 PT_LOAD
一个可执行文件至少有一个PT_LOAD类型的段。这类程序头描述的是可装载的段,也就是说,这种类型的段将被装载或者映射到内存中。
例如,一个需要动态链接的ELF 可执行文件通常包含以下两个可装载的段(类型为PT_LOAD):
- 存放程序代码的text 段;
- 存放全局变量和动态链接信息的data 段。
通常将text 段(也称代码段)的权限设置为PF_X | PF_R(读和可执行)。通常将data 段的权限设置为PF_W | PF_R(读和写)。
感染了千面人病毒(polymorphic virus)文件的text 段或data 段的权限可能会被修改,如通过在程序头的段标记(p_flags)处增加PF_W 标记来修改text 段的权限。
2.2.2 PT_DYNAMIC——动态段的Phdr
动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针,包括但不限于以下内容:
- 运行时需要链接的共享库列表;
- 全局偏移表(GOT)的地址
- 重定位条目的相关信息。
动态段包含了一些结构体,在这些结构体中存放着与动态链接相关的信息。32 位ELF 文件的动态段结构体如下:
typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[];
有关动态节区的内容,会在动态链接一节中更深入的讨论。
2.2.3 PT_NOTE
PT_NOTE 类型的段可能保存了与特定供应商或者系统相关的附加信息。
下面是标准ELF 规范中对PT_NOTE 的定义:
有时供应商或者系统构建者需要在目标文件上标记特定的信息,以便于其他程序对一致性、兼容性等进行检查。SHT_NOTE 类型的节(section)和PT_NOTE类型的程序头元素就可以用于这一目的。节或者程序头元素中的备注信息可以有任意数量的条目,每个条目都是一个4 字节的目标处理器格式的数组。下面的标签可以解释备注信息的组织结构,不过这些标签并不是规范中的内容。
比较有意思的一点:事实上,这一段只保存了操作系统的规范信息,在可执行文件运行时是不需要这个段的(因为系统会假设一个可执行文件是本地的),这个段成了很容易被病毒感染的一个地方。由于篇幅限制,就不具体介绍了。
2.2.4 PT_INTERP
PT_INTERP 段只将位置和大小信息存放在一个以null 为终止符的字符串中,是对程序解释器位置的描述。例如,/lib/linux-ld.so.2 一般是指动态链接器的位置,也即程序解释器的位置。
2.2.5 PT_PHDR
PT_PHDR 段保存了程序头表本身的位置和大小。Phdr 表保存了所有的Phdr 对文件(以及内存镜像)中段的描述信息。
可以查阅ELF(5)手册或者ELF 规范文档来查看所有的Phdr 类型。我们已经介绍了一些最常用的Phdr 类型,其中一些对程序执行至关重要,有一些在反编译时会经常用到。
使用readelf –l 命令可以查看文件的Phdr 表:
[root@localhost tgt]# readelf -l /bin/ls
Elf 文件类型为 DYN (共享目标文件)
Entry point 0x5914
There are 9 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001b 0x000000000000001b R 0x1
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000020780 0x0000000000020780 R E 0x10000
LOAD 0x000000000002ef30 0x000000000003ef30 0x000000000003ef30
0x0000000000001380 0x0000000000002648 RW 0x10000
DYNAMIC 0x000000000002f948 0x000000000003f948 0x000000000003f948
0x0000000000000220 0x0000000000000220 RW 0x8
NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x000000000001c914 0x000000000001c914 0x000000000001c914
0x000000000000093c 0x000000000000093c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x000000000002ef30 0x000000000003ef30 0x000000000003ef30
0x00000000000010d0 0x00000000000010d0 R 0x1
Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
04 .dynamic
05 .note.gnu.build-id .note.ABI-tag
06 .eh_frame_hdr
07
08 .init_array .fini_array .data.rel.ro .dynamic .got
2.3 Section Headers
前面介绍了程序头相关的内容,接下来对节头(section header)相关的内容进行介绍。
首先前面我们了解到段是程序执行的必要组成部分,而在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。节头对于程序的执行来说不是必需的,没有节头表,程序仍可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局的描述是程序头表的任务。节头是对程序头的补充。
如果二进制文件中缺少节头,并不意味着节就不存在。只是没有办法通过节头来引用节,对于调试器或者反编译程序来说,只是可以参考的信息变少了而已。
每一个节都保存了某种类型的代码或者数据。数据可以是程序中的全局变量,也可以是链接器所需要的动态链接信息。正如前面提到的,每个ELF 目标文件都有节,但是不一定有节头,尤其是有人故意将节头从节头表中删除了之后。当然,默认是有节头的。
通常情况下,这是由于可执行文件被篡改导致的(如去掉节头来增加调试的难度)。GNU 的binutils 工具,像objcopy、objdump,还有gdb 等,都需要依赖节头定位到存储符号数据的节来获取符号信息。如果没有节头,gdb和objdump 这样的工具几乎无用武之地。
节头便于我们更细粒度地检查一个ELF 目标文件的某部分或者某节。事实上,有了节头,一些需要使用节头的工具,如objdump 等,就能为逆向工程带来很多便利。如果去掉了节头表,就无法获取像.dynsym 这样的节,而在.dynsym 节中包含了描述函数名和偏移量/地址的导入/导出符号。
即便从一个可执行文件中去掉了节头表,一个中级逆向工程师
也可以从特定的程序头中获取相关信息来重构节头表(甚至能
够重构部分符号表),因为一个程序或者共享库中一定是存在
程序头的。之前讲过动态段以及各种保存了符号表和重定位入
口信息的DT_TAG,可以利用这一部分来重构可执行文件的其
余部分。
下面是一个32 位ELF 节头的结构:
typedef struct {
uint32_t sh_name; // offset into shdr string table for shdr name
uint32_t sh_type; // shdr type I.E SHT_PROGBITS
uint32_t sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC
Elf32_Addr sh_addr; // address of where section begins
Elf32_Off sh_offset; // offset of shdr from beginning of file
uint32_t sh_size; // size that section takes up on disk
uint32_t sh_link; // points to another section
uint32_t sh_info; // interpretation depends on section type
uint32_t sh_addralign; // alignment for address of section
uint32_t sh_entsize; // size of each certain entries that may be in
section
} Elf32_Shdr;
下面我们可以看到,一个可执行文件是如何使用phdr 和shdr 来进行布局排列的。
text 段的布局如下。
- [.text]:程序代码。
- [.rodata]:只读数据。
- [.hash]:符号散列表。
- [.dynsym]:共享目标文件符号数据。
- [.dynstr]:共享目标文件符号名称。
- [.plt]:过程链接表。
- [.rel.got]:G.O.T 重定位数据。
data 段布局如下。
- [.data]:全局的初始化变量。
- [.dynamic]:动态链接结构和对象。
- [.got.plt]:全局偏移表。
- [.bss]:全局未初始化变量。
介于篇幅限制,这里就不对各个节的类型和作用进行详细展开了,建议查阅ELF(5)手册和ELF 官方规范文档,来查看更多节相关的信息。
使用readelf -S
可以看到ELF节头表,使用readelf -l
可以查看程序段和节之间的对应关系。
[root@localhost tgt]# readelf -S /bin/ls
There are 28 section headers, starting at offset 0x30410:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
...
[ 5] .dynsym DYNSYM 00000000000002d8 000002d8
0000000000000c90 0000000000000018 A 6 3 8
[ 6] .dynstr STRTAB 0000000000000f68 00000f68
00000000000005e4 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000000154c 0000154c
000000000000010c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000001658 00001658
0000000000000070 0000000000000000 A 6 3 8
[ 9] .rela.dyn RELA 00000000000016c8 000016c8
00000000000016f8 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000002dc0 00002dc0
0000000000000a98 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000003858 00003858
0000000000000014 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000003870 00003870
0000000000000730 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000003fa0 00003fa0
0000000000013fa8 0000000000000000 AX 0 0
...
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
2.3 SYMBOL
符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。例如,printf()
函数会在动态符号表.dynsym
中存有一个指向该函数的符号条目。
.dynsym
保存了引用来自外部文件符号的全局符号,如printf
这样的库函数,.dynsym
保存的符号是.symtab
所保存符号的子集,symtab
中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。因此,.symtab
保存了所有的符号,而.dynsym
只保存动态/全局符号。
下面是一个64 位ELF 文件符号项的结构:
typedef struct{
uint32_t st_name;
unsigned char st_info;
unsigned char st_other;
uint16_t st_shndx;
Elf64_Addr st_value;
Uint64_t st_size;
} Elf64_Sym;
2.3.1 st_name
st_name 保存了指向符号表中字符串表(位于.dynstr 或者.strtab)
的偏移地址,偏移地址存放着符号的名称,如printf。
2.3.2 st_value
st_value 存放符号的值(可能是地址或者位置偏移量)。
2.3.3 st_size
st_size 存放了一个符号的大小,如全局函数指针的大小,在一个32 位
系统中通常是4 字节。
2.3.4 st_other
st_other 变量定义了符号的可见性。
2.3.5 st_shndx
每个符号表条目的定义都与某些节对应。st_shndx 变量保存了相关节头表的
索引。
2.3.6 st_info
st_info 指定符号类型及绑定属性。可以查阅ELF(5)手册来查看完
整的类型以属性列表。符号类型以STT 开头,符号绑定以STB 开头,下面对几种常见的符号类型和符号绑定进行介绍。
1.符号类型
下面是几种符号类型。
- STT_NOTYPE:符号类型未定义。
- STT_FUNC:表示该符号与函数或者其他可执行代码关联。
- STT_OBJECT:表示该符号与数据目标文件关联。
2.符号绑定
下面是几种符号绑定。
- STB_LOCAL:本地符号在目标文件之外是不可见的,目标文件包含
了符号的定义,如一个声明为static 的函数。 - STB_GLOBAL:全局符号对于所有要合并的目标文件来说都是可见的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这个符号进行引用。
参考资料: