二进制分析属于信息安全业界逆向工程中的一种技术,通过利用可执行的机器代码(二进制)来分析应用程序的控制结构和运行方式,它有助于信息安全从业人员更好地分析各种漏洞、病毒以及恶意软件,从而找到相应的解决方案。
要反编译Linux二进制文件,首先需要理解二进制格式本身。ELF目前已经成为UNIX和类UNIX操作系统的标准二进制格式。在Linux、BSD变体以及其他操作系统中,ELF格式可用于可执行文件、共享库、目标文件、coredump文件,甚至内核引导镜像文件。因此,对于那些想要更好地理解反编译、二进制攻破和程序执行的人来说,学习ELF至关重要。要想学习ELF这样的二进制格式,可不是一蹴而就的,需要随着对不同组件的学习来逐步掌握并加以实际应用。要达到熟练应用的效果,还需要实际的动手经验。ELF二进制格式比较复杂,也很枯燥,不过可以在进行反编译或者编程任务中应用ELF二进制格式相关的编程知识,通过这样的方式学习,倒是一种很有趣的尝试。ELF跟程序加载、动态链接、符号表查找和许多其他精心设计的组件一样,都是计算机科学非常重要的一部分。
在本文中,你将会更加深入地了解程序如何映射到磁盘并加载到内存中。程序执行的内部逻辑比较复杂,对于有抱负的二进制黑客、逆向工程师或者普通的程序员来说,对二进制格式的理解将会是非常宝贵的知识财富。在Linux中,程序就是以ELF二进制的格式执行的。
像许多Linux反编译工程师一样,我也是先了解ELF的说明规范,然后把学到的内容以一种创造性的方式进行应用,通过这样的方式来进行ELF的学习。
2.1 ELF文件类型
一个ELF文件可以被标记为以下几种类型之一。
-
ET_NONE
:未知类型。这个标记表明文件类型不确定,或者还未定义。 -
ET_REL
:重定位文件。ELF类型标记为relocatable意味着该文件被标记为了一段可重定位的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码(position independent code)。在编译完代码之后通常可以看到一个.o
格式的文件,这种文件包含了创建可执行文件所需要的代码和数据。 -
ET_EXEC
:可执行文件。ELF类型为executable,表明这个文件被标记为可执行文件。这种类型的文件也称为程序,是一个进程开始执行的入口。 -
ET_DYN
:共享目标文件。ELF类型为dynamic,意味着该文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中。 -
ET_CORE
:核心文件。在程序崩溃或者进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB读取这类文件来辅助调试并查找程序崩溃的原因。
使用readelf–h
命令查看ELF文件,可以看到原始的ELF文件头。ELF文件头从文件的0偏移量开始,是除了文件头之后剩余部分文件的一个映射。文件头主要标记了ELF类型、结构和程序开始执行的入口地址,并提供了其他ELF头(节头和程序头)的偏移量,稍后会细讲。一旦理解了节头和程序头的含义,就容易理解文件头了。通过查看Linux的ELF(5)手册,可以了解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;
在本文的后续内容中,我们会用一个简单的C程序来展示如何利用上面结构中的字段映射一个ELF文件。我们先继续介绍现存的其他类型的ELF头。
2.2 ELF程序头
ELF程序头是对二进制文件中段的描述,是程序装载必需的一部分。段(segment)是在内核装载时被解析的,描述了磁盘上可执行文件的内存布局以及如何映射到内存中。可以通过引用原始ELF头中名为e_phoff
(程序头表偏移量)的偏移量来得到程序头表,如前面ElfN_Ehdr
结构中所示。 下面讨论5种常见的程序头类型。程序头描述了可执行文件(包括共享库)中的段及其类型(为哪种类型的数据或代码而保留的段)。首先,我们来看一下Elf32_Phdr
的结构,它构成了32位ELF可执行文件程序头表的一个程序头条目。
在本书的后续内容中有时还会引用Phdr的程序头结构。
下面是Elf32_Phdr
结构体:
typedef struct {
uint32_t p_type; (segment type)
Elf32_Off p_offset; (segment offset)
Elf32_Addr p_vaddr; (segment virtual address)
Elf32_Addr p_paddr; (segment physical address)
uint32_t p_filesz; (size of segment in the file)
uint32_t p_memsz; (size of segment in memory)
uint32_t p_flags; (segment flags, I.E execute|read|read)
uint32_t p_align; (segment alignment in memory)
} Elf32_Phdr;
2.2.1 PT_LOAD
一个可执行文件至少有一个PT_LOAD
类型的段。这类程序头描述的是可装载的段,也就是说,这种类型的段将被装载或者映射到内存中。 例如,一个需要动态链接的ELF可执行文件通常包含以下两个可装载的段(类型为PT_LOAD
):
- 存放程序代码的text段;
- 存放全局变量和动态链接信息的data段。
上面的两个段将会被映射到内存中,并根据p_align
中存放的值在内存中对齐。建议读者阅读一下Linux的ELF手册,以便理解Phdr结构体中所有变量的含义,这些变量描述了段在文件和内存中的布局。
程序头主要描述了程序执行时在内存中的布局。本章稍后会使用Phdr来说明什么是程序头,以及如何在反编译软件中使用程序头。
通常将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)
的地址
——ELF动态链接部分(2.6节)会讨论; - 重定位条目的相关信息。
表2-1是完整的标记名列表。
表2-1
标 记 名 | 描 述 |
| 符号散列表的地址 |
| 字符串表的地址 |
| 符号表地址 |
| 相对地址重定位表的地址 |
| Rela表的字节大小 |
| Rela表条目的字节大小 |
| 字符串表的字节大小 |
| 符号表条目的字节大小 |
| 初始化函数的地址 |
| 终止函数的地址 |
| 共享目标文件名的字符串表偏移量 |
| 库搜索路径的字符串表偏移量 |
| 修改链接器,在可执行文件之前的共享目标文件中搜索符号 |
| Rel relocs表的地址 |
| Rel表的字节大小 |
| Rel表条目的字节大小 |
| PLT引用的reloc类型(Rela或Rel) |
| 还未进行定义,为调试保留 |
| 缺少此项表明重定位只能应用于可写段 |
| 仅用于PLT的重定位条目地址 |
| 指示动态链接器在将控制权交给可执行文件之前处理所有的重定位 |
| 库搜索路径的字符串表偏移量 |
动态段包含了一些结构体,在这些结构体中存放着与动态链接相关的信息。d_tag
成员变量控制着d_un
的含义。
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字节的目标处理器格式的数组。下面的标签可以解释备注信息的组织结构,不过这些标签并不是规范中的内容。 比较有意思的一点:事实上,这一段只保存了操作系统的规范信息,在可执行文件运行时是不需要这个段的(因为系统会假设一个可执行文件是本地的),这个段成了很容易被病毒感染的一个地方。由于篇幅限制,就不具体介绍了。更多NOTE段病毒感染相关的信息可以从http://vxheavens.com/ lib/vhe06.html
了解到。
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 <filename>
命令查看文件的Phdr表:
Elf file type is EXEC (Executable file) Entry point 0x8049a30 There are 9 program headers, starting at offset 52 Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x1622c 0x1622c R E 0x1000
LOAD 0x016ef8 0x0805fef8 0x0805fef8 0x003c8 0x00fe8 RW 0x1000
DYNAMIC 0x016f0c 0x0805ff0c 0x0805ff0c 0x000e0 0x000e0 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x016104 0x0805e104 0x0805e104 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x016ef8 0x0805fef8 0x0805fef8 0x00108 0x00108 R 0x1
从上面的片段中,可以看到可执行程序的入口点,还有刚刚讨论的不同段的类型。注意看中间部分的PT_LOAD
段,从最左边的偏移量到最右边的权限标识和对齐标识。 text段是可读可执行的,data段是可读可写的,这两个段都有0x1000
(4096)的对齐标识,刚好是32位可执行文件一页的大小,该标识用于在程序装载时对齐。
本文摘自于《Linux二进制分析》
本书是目前为止唯一一本剖析Linux ELF工作机制的图书,共分为9章,其内容涵盖了Linux环境和相关工具、ELF二进制格式、Linux进程追踪、ELF病毒技术、Linux二进制保护、Linux中的ELF二进制取证分析、进程内存取证分析、扩展核心文件快照技术、Linux/proc/kcore分析等。
本书适合具有一定的Linux操作知识,且了解C语言编程技巧的信息安全从业人员阅读。
目 录
第1章 Linux环境和相关工具1
1.1 Linux工具1
1.1.1 GDB2
1.1.2 GNU binutils中的objdump2
1.1.3 GNU binutils中的objcopy3
1.1.4 strace3
1.1.5 ltrace4
1.1.6 基本的ltrace命令4
1.1.7 ftrace4
1.1.8 readelf4
1.1.9 ERESI——ELF反编译系统接口5
1.2 有用的设备和文件6
1.2.1 /proc//maps6
1.2.2 /proc/kcore6
1.2.3 /boot/System.map6
1.2.4 /proc/kallsyms7
1.2.5 /proc/iomem7
1.2.6 ECFS7
1.3 链接器相关环境指针7
1.3.1 LD_PRELOAD环境变量8
1.3.2 LD_SHOW_AUXV环境变量8
1.3.3 链接器脚本9
1.4 总结10
第2章 ELF二进制格式11
2.1 ELF文件类型12
2.2 ELF程序头14
2.2.1 PT_LOAD14
2.2.2 PT_DYNAMIC——动态段的Phdr15
2.2.3 PT_NOTE17
2.2.4 PT_INTERP17
2.2.5 PT_PHDR17
2.3 ELF节头18
2.3.1 .text节20
2.3.2 .rodata节20
2.3.3 .plt节21
2.3.4 .data节21
2.3.5 .bss节21
2.3.6 .got.plt节21
2.3.7 .dynsym节21
2.3.8 .dynstr节22
2.3.9 .rel.*节22
2.3.10 .hash节22
2.3.11 .symtab节22
2.3.12 .strtab节23
2.3.13 .shstrtab节23
2.3.14 .ctors和.dtors节23
2.4 ELF符号27
2.4.1 st_name28
2.4.2 st_value28
2.4.3 st_size28
2.4.4 st_other28
2.4.5 st_shndx29
2.4.6 st_info29
2.5 ELF重定位34
2.6 ELF动态链接43
2.6.1 辅助向量44
2.6.2 了解PLT/GOT46
2.6.3 重温动态段49
2.7 编码一个ELF解析器52
2.8 总结55
第3章 Linux进程追踪57
3.1 ptrace的重要性57
3.2 ptrace请求58
3.3 进程寄存器状态和标记60
3.4 基于ptrace的调试器示例61
3.5 ptrace调试器67
3.6 高级函数追踪软件75
3.7 ptrace和取证分析75
3.8 进程镜像重建77
3.8.1 重建进程到可执行文件的挑战78
3.8.2 重建可执行文件的挑战78
3.8.3 添加节头表79
3.8.4 重建过程算法79
3.8.5 在32位测试环境中使用Quenya重建进程81
3.9 使用ptrace进行代码注入83
3.10 简单的例子演示复杂的过程91
3.11 code_inject工具演示92
3.12 ptrace反调试技巧92
3.13 总结94
第4章 ELF病毒技术——Linux/UNIX病毒95
4.1 ELF病毒技术96
4.2 设计ELF病毒面临的挑战97
4.2.1 寄生代码必须是独立的97
4.2.2 字符串存储的复杂度99
4.2.3 寻找存放寄生代码的合理空间100
4.2.4 将执行控制流传给寄生代码100
4.3 ELF病毒寄生代码感染方法101
4.3.1 Silvio填充感染101
4.3.2 逆向text感染106
4.3.3 data段感染108
4.4 PT_NOTE到PT_LOAD转换感染110
4.5 感染控制流112
4.5.1 直接PLT感染113
4.5.2 函数蹦床(function trampolines)113
4.5.3 重写.ctors/.dtors函数指针114
4.5.4 GOT感染或PLT/GOT重定向115
4.5.5 感染数据结构115
4.5.6 函数指针重写115
4.6 进程内存病毒和rootkits——远程代码注入技术115
4.6.1 共享库注入116
4.6.2 text段代码注入120
4.6.3 可执行文件注入120
4.6.4 重定位代码注入——ET_REL注入120
4.7 ELF反调试和封装技术121
4.7.1 PTRACE_TRACEME技术121
4.7.2 SIGTRAP处理技术122
4.7.3 /proc/self/status技术122
4.7.4 代码混淆技术123
4.7.5 字符串表转换技术124
4.8 ELF病毒检测和杀毒124
4.9 总结126
第5章 Linux二进制保护127
5.1 ELF二进制加壳器127
5.2 存根机制和用户层执行128
5.3 保护器存根的其他用途133
5.4 现存的ELF二进制保护器133
5.4.1 DacryFile——Grugq于2001年发布134
5.4.2 Burneye——Scut于2002年发布134
5.4.3 Shiva——Neil Mehta和Shawn Clowes于2003年发布135
5.4.4 May's Veil——Ryan O'Neill于2014年发布136
5.5 下载Maya保护的二进制文件142
5.6 二进制保护中的反调试142
5.7 防模拟技术143
5.7.1 通过系统调用检测模拟144
5.7.2 检测模拟的CPU不一致144
5.7.3 检测特定指令之间的时延144
5.8 混淆方法145
5.9 保护控制流完整性145
5.9.1 基于ptrace的攻击145
5.9.2 基于安全漏洞的攻击146
5.10 其他资源147
5.11 总结147
第6章 Linux下的ELF二进制取证分析149
6.1 检测入口点修改技术150
6.2 检测其他形式的控制流劫持154
6.2.1 修改.ctors/.init_array节154
6.2.2 检测PLT/GOT钩子155
6.2.3 检测函数蹦床158
6.3 识别寄生代码特征159
6.4 检查动态段是否被DLL注入161
6.5 识别逆向text填充感染164
6.6 识别text段填充感染166
6.7 识别被保护的二进制文件170
6.8 IDA Pro175
6.9 总结175
第7章 进程内存取证分析177
7.1 进程内存布局178
7.1.1 可执行文件内存映射179
7.1.2 程序堆179
7.1.3 共享库映射180
7.1.4 栈、VDSO和vsyscall180
7.2 进程内存感染181
7.2.1 进程感染工具181
7.2.2 进程感染技术182
7.3 检测ET_DYN注入184
7.3.1 Azazel:用户级rootkit检测184
7.3.2 映射出进程的地址空间184
7.3.3 查找栈中的LD_PRELOAD187
7.3.4 检测PLT/GOT钩子188
7.3.5 ET_DYN注入内部原理190
7.3.6 操纵VDSO194
7.3.7 共享目标文件加载195
7.3.8 检测.so注入的启发方法196
7.3.9 检测PLT/GOT钩子的工具197
7.4 Linux ELF核心文件198
7.5 总结204
第8章 ECFS——扩展核心文件快照技术205
8.1 历史205
8.2 ECFS原理206
8.3 ECFS入门206
8.3.1 将ECFS嵌入到核心处理器中207
8.3.2 在不终止进程的情况下使用ECFS快照208
8.4 libecfs——解析ECFS文件的库208
8.5 readecfs工具209
8.6 使用ECFS检测被感染的进程210
8.6.1 感染主机进程210
8.6.2 捕获并分析ECFS快照211
8.6.3 使用readecfs提取寄生代码215
8.6.4 Azazel用户级rootkit分析216
8.7 ECFS参考指南224
8.7.1 ECFS符号表重建225
8.7.2 ECFS节头226
8.7.3 使用ECFS文件作为常规的核心文件229
8.7.4 libecfs API的使用229
8.8 使用ECFS恢复中断的进程230
8.9 了解更多ECFS相关内容231
8.10 总结232
第9章 Linux/proc/kcore分析233
9.1 Linux内核取证分析和rootkit233
9.2 没有符号的备份vmlinux234
9.3 探索/proc/kcore和GDB236
9.4 直接修改sys_call_table237
9.4.1 检测sys_call_table修改238
9.4.2 内核函数蹦床238
9.4.3 函数蹦床示例239
9.4.4 检测函数蹦床241
9.4.5 检测中断处理器修复243
9.5 Kprobe rootkit243
9.6 调试寄存器rootkit——DRR244
9.7 VFS层rootkit244
9.8 其他内核感染技术245
9.9 vmlinux和.altinstructions修补245
9.9.1 .altinstructions和.altinstr_replace246
9.9.2 arch/x86/include/asm/alternative.h代码片段246
9.9.3 使用textify验证内核代码完整性247
9.9.4 使用textify检查sys_call_table247
9.10 使用taskverse查看隐藏进程248
9.11 感染的LKM——内核驱动249
9.11.1 方法一:感染LKM文件——符号劫持249
9.11.2 方法二:感染LKM文件——函数劫持249
9.11.3 检测被感染的LKM250
9.12 /dev/kmem和/dev/mem250
9.12.1 /dev/mem251
9.12.2 FreeBSD /dev/kmem251
9.13 K-ecfs ——内核ECFS251
9.14 内核黑客工具252
9.14.1 通用的逆向工程和调试253
9.14.2 高级内核劫持/调试接口253
9.14.3 本章提到的论文253
9.15 总结254