本文不是指南,也并不权威。起这个标题只是想模仿《Window PE 权威指南》这本在我 Windows 入门过程中起到十分重要作用的书。而现在我需要研究研究 linux 相关的技术,因此就起了对应的标题。另外,如果你想要入门 Windows 相关知识用于逆向、开发、武器化等方向,我同样推荐这本书。
文件头
同样是由 coff 格式发展而来的 elf 与 pe 一样,一切都是从文件头开始。
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;
相比于 pe 的复杂文件头,elf 的文件头简单许多。比较重要的几个项:
- e_entry 入口点地址
- e_phoff program header FOA
- e_shoff section header FOA
program header 描述了文件装载到内存后的布局,每一个 entry 描述一个 segment 或其他信息用于文件的执行;section header 则描述了文件中各个 section 的信息,在 elf 装在过程中,文件中的 section 会被载入到内存中可执行文件的各个 segment 中。
使用 readelf 可以看到 elf 中 section 与 segment 的对应关系
$ readelf -l a.out
Elf file type is DYN (Shared object file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000005f8 0x00000000000005f8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001f5 0x00000000000001f5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000160 0x0000000000000160 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002010 0x0000000000002010 0x0000000000002010
0x0000000000000044 0x0000000000000044 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
导入表
elf 其实没有导入表,相对的,它直接使用符号的概念来替代导入函数。elf 中有两个符号表,分别为.dynsym section 和.symtab sectio,.dynsym 只包含动态链接所需要的符号,.symtab 则包含程序中的所有符号,.dynsym 为.symtab 的子集。在 elf 装载的过程中,.dynsym 需要被装载到内存中,而.symtab 则无需装载到内存。对于程序的运行来说,.symtab 是不必要的,因此可以使用 strip 来删去 elf 中的.symtab。你可以把.dynsym 理解为 pe 中的导入导出表,而 symtab 则是程序编译出来所产生的 pdb 文件。
导入与导出:
在符号表的每项中字段 st_shndx 表示了符号的类型,如果符号类型为 SHN_UNDEF (0) 则代表这个符号在当前文件中没有定义,是需要导入的符号。同时符号具有可见性级别,在 st_other 字段的低 3 位有对于符号可见性的定义,分别是:
- STB_LOCAL 本地可见,只有当前文件可见的符号
- STB_GLOBAL 全局可见,设置此项意味着这个符号是导出的
- STB_WEAK 类似全局可见,但是具有低优先级
.got: elf 中的 iat 表
装载器在获取到程序需要的函数地址后,将地址写入到 got 表中。got 表中的第一项为.dynamic section 的偏移,在有 plt 的情况下,第二项为 link_map ,第三项为_dl_runtime_resolver,之后则是各个符号的地址。
填充 got 表:
值得注意的是,.dynsym 与 .got 并没有明确的对应关系,也就是说单单从这两张表无法得知 got 表中的某项是哪个符号的地址。而其对应关系存在 elf 的.rela section,是重定位相关的 section 。因此符号的地址的填充就被放在了重定位相关的过程中,这个放到下一段说。
重定位
在说重定位之前首先要说 linux 的 pic 技术,而在说 pic 技术之前还要先说 x86 的指令架构以及 aslr。x86 指令中对于内存数据的读写往往是通过绝对地址来寻址的。举一个例子
;833D BC69BB77 00
cmp dword ptr ds:[0x77BB69BC], 0x0;
这条指令访问了内存 0x77BB69BC ,我们可以看到其地址是直接写在字节码中的。而在 x64 中,这个地址则会被转换为相对于下一条指令地址的偏移。如果使用绝对地址寻址,那就代表这个程序在内存中加载的位置必须是固定的,如果改变了位置,那么就会找不到相应的数据。而 aslr 机制则会让程序在不同的地址上加载,这就使程序无法正常运行。windows 的解决方案是重定位表,即在程序在内存装载后,通过程序中的重定位表对程序进行修补让程序可以正常运行。在 linux 中,不光有重定位表,还有 pic 技术。
pic 由编译器实现,即通过生成地址无关代码来使程序可以在不同地址下运行。其中对数据的访问部分,编译器将需要绝对地址寻址的部分改为间接地址寻址。看一个例子
endbr32
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ebx
push ecx
call __x86_get_pc_thunk_ax; 获取eip
add eax, (offset _GLOBAL_OFFSET_TABLE_ - $) ;获取到got表
sub esp, 0Ch
lea edx, (str - 3FD8h)[eax] ; "adfafds" got表地址+got表到str字符串地址的偏移
push edx ; format
mov ebx, eax
call _printf
add esp, 10h
mov eax, 0
lea esp, [ebp-8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-4]
retn
尽管有 pic,但是仍有需要修正的数据,如全局变量中的函数指针就需要在运行时进行修正,因此 elf 中仍然有重定位表。elf 中的 .rel.plt 、.rel.dyn 就是其重定位表。其中.rel.dyn 是对代码段访问的修正,.rel.plt 是对代码段函数调用的修正。
重定位的过程中也包括了对导入符号的填充,因此每个重定位项中就包含了 got 表与.dynsym 中符号的对应关系。
// 重定位项结构体
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
r_info
- This member gives both the symbol table index with respect to which the relocation must be made, and the type of relocation to apply. For example, a call instruction’s relocation entry would hold the symbol table index of the function being called. If the index is
STN_UNDEF
, the undefined symbol index, the relocation uses 0 as the ``symbol value’’. Relocation types are processor-specific; descriptions of their behavior appear in the processor supplement. When the text below refers to a relocation entry’s relocation type or symbol table index, it means the result of applyingELF32_R_TYPE
(orELF64_R_TYPE
) orELF32_R_SYM
(orELF64_R_SYM
), respectively, to the entry’sr_info
member.
r_info 中给出了重定位目标的类型与重定位目标在符号表中的索引(如果有的话),与 r_offset 相结合形成了.dynsym 与 got 的对应关系。在重定位过程中,动态链接器根据符号索引找到程序所要导入的符号,再将符号地址写入到 got 表的相应位置(由 r_offset 计算得出)。
一些参考资料: