首页
社区
课程
招聘
[原创]深入浅出ELF
发表于: 2020-8-9 17:02 21686

[原创]深入浅出ELF

2020-8-9 17:02
21686

本文介绍了ELF的基本结构和内存加载的原理,并用具体案例来分析如何通过ELF特性实现HIDS bypass、加固/脱壳以及辅助进行binary fuzzing。

作为一个安全研究人员,ELF可以说是一个必须了解的格式,因为这关系到程序的编译、链接、封装、加载、动态执行等方方面面。有人就说了,这不就是一种文件格式而已嘛,最多按照SPEC实现一遍也就会了,难道还能复杂过FLV/MP4?曾经我也是这么认为的,直到我在日常工作时遇到了下面的错误:

作为一个开源爱好者,我的radare2经常是用master分支编译的,经过在github中搜索,发现radare对于ELF的处理还有不少同类的问题,比如issue#17300以及issue#17379,这还只是近一个月内的两个open issue,历史问题更是数不胜数。

总不能说radare的开发者不了解ELF吧?事实上他们都是软件开发和逆向工程界的专家。不止radare,其实IDA和其他反编译工具也曾出现过各类ELF相关的bug

说了那么多,只是为了引出一个观点: ELF既简单也复杂,值得我们去深入了解。网上已经有了很多介绍ELF的文章,因此本文不会花太多篇幅在SPEC的复制粘贴上,而是结合实际案例和应用场景去进行说明。

ELF的全称是Executable and Linking Format,这个名字相当关键,包含了ELF所需要支持的两个功能——执行和链接。不管是ELF,还是Windows的PE,抑或是MacOS的Mach-O,其根本目的都是为了能让处理器正确执行我们所编写的代码。

在上古时期,给CPU运行代码也不用那么复杂,什么代码段数据段,直接把编译好的机器码一把梭烧到中断内存空间,PC直接跳过来就执行了。但随着时代变化,大家总不能一直写汇编了,即便编译器很给力,也会涉及到多人协作、资源复用等问题。这时候就需要一种可拓展(Portable)的文件标准,一方面让开发者(编译器/链接器)能够高效协作,另一方面也需要系统能够正确、安全地将文件加载到对应内存中去执行,这就是ELF的使命。

view.png

从大局上看,ELF文件主要分为3个部分:

其中,ELF Header是文件头,包含了固定长度的文件信息;Section Header Table则包含了链接时所需要用到的信息;Program Header Table中包含了运行时加载程序所需要的信息,后面会进行分别介绍。

ELF头部的定义在elf/elf.h中(以glibc-2.27为例),使用POD结构体表示,内存可使用结构体的字段一一映射,头部表示如下:

注释都很清楚了,挑一些比较重要的来说。其中e_type表示ELF文件的类型,有以下几种:

e_entry是程序的入口虚拟地址,注意不是main函数的地址,而是.text段的首地址_start。当然这也要求程序本身非PIE(-no-pie)编译的且ASLR关闭的情况下,对于非ET_EXEC类型通常并不是实际的虚拟地址值。

其他的字段大多数是指定Section Header(e_sh)和Program Header(e_ph)的信息。Section/Program Header Table本身可以看做是数组结构,ELF头中的信息指定对应Table数组的位置、长度、元素大小信息。最后一个e_shstrndx表示的是section table中的第e_shstrndx项元素,保存了所有section table名称的字符串信息。

上节说了section header table是一个数组结构,这个数组的位置在e_shoff处,共有e_shnum个元素(即section),每个元素的大小为e_shentsize字节。每个元素的结构如下:

其中sh_name是该section的名称,用一个word表示其在字符表中的偏移,字符串表(.shstrtab)就是上面说到的第e_shstrndx个元素。ELF文件中经常使用这种偏移表示方式,可以方便组织不同区段之间的引用。

sh_type表示本section的类型,SPEC中定义了几十个类型,列举其中一些如下:

虽然每个section header的大小一样(e_shentsize字节),但不同类型的section有不同的内容,内容部分由这几个字段表示:

与运行时信息相关的字段为:

另外两个字段sh_linksh_info的含义根据section类型的不同而不同,如下表所示:

type.png

至于不同类型的section,有的是保存符号表,有的是保存字符串,这也是ELF表现出拓展性和复杂性的地方,因此需要在遇到具体问题的时候查看文档去进行具体分析。

program header table用来保存程序加载到内存中所需要的信息,使用段(segment)来表示。与section header table类似,同样是数组结构。数组的位置在偏移e_phoff处,每个元素(segment header)的大小为e_phentsize,共有e_phnum个元素。单个segment header的结构如下:

既然program header的作用是提供用于初始化程序进程的段信息,那么下面这些字段就是很直观的:

剩下的p_type字段,表示该program segment的类型,主要有以下几种:

在不同的操作系统中还可能有一些拓展的类型,比如PT_GNU_STACKPT_GNU_RELRO等,不一而足。

至此,ELF文件中相关的字段已经介绍完毕,主要组成也就是Section Header Table和Program Header Table两部分,整体框架相当简洁。而ELF中体现拓展性的地方则是在Section和Segment的类型上(s_type和p_type),这两个字段的类型都是ElfN_Word,在32位系统下大小为4字节,也就是说最多可以支持高达2^32 - 1种不同的类型!除了上面介绍的常见类型,不同操作系统或者厂商还能定义自己的类型去实现更多复杂的功能。

在新版的ELF标准文档中,将ELF的介绍分成了三部分,第一部分介绍ELF文件本身的结构,第二部分是处理器相关的内容,第三部分是操作系统相关的内容。ELF的加载实际上是与操作系统相关的,不过大部分情况下我们都是在GNU/Linux环境中运行,因此就以此为例介绍程序的加载流程。

Linux中分为用户态和内核态,执行ELF文件在用户态的表现就是执行execve系统调用,随后陷入内核进行处理。

内核空间对execve的处理其实可以单独用一篇文章去介绍,其中涉及到进程的创建、文件资源的处理以及进程权限的设置等等。我们这里主要关注其中ELF处理相关的部分即可,实际上内核可以识别多种类型的可执行文件,ELF的处理代码主要在fs/binfmt_elf.c中的load_elf_binary函数中。

对于ELF而言,Linux内核所关心的只有Program Header部分,甚至大部分情况下只关心三种类型的Header,即PT_LOADPT_INTERPPT_GNU_STACK。以3.18内核为例,load_elf_binary主要有下面操作:

从内核的处理流程上来看,如果是静态链接的程序,实际上内核返回用户空间执行的就是该程序的入口地址代码;如果是动态链接的程序,内核返回用户空间执行的则是interpreter的代码,并由其加载实际的ELF程序去执行。

为什么要这么做呢?如果把动态链接相关的代码也放到内核中,就会导致内核执行功能过多,内核的理念一直是能不在内核中执行的就不在内核中处理,以避免出现问题时难以更新而且影响系统整体的稳定性。事实上内核中对ELF文件结构的支持是相当有限的,只能读取并理解部分的字段。

内核返回用户空间后,对于静态链接的程序是直接执行,没什么好说的。而对于动态链接的程序,实际是执行interpreter的代码。ELF的interpreter作为一个段,自然是编译链接的时候加进去的,因此和编译使用的工具链有关。对于Linux系统而言,使用的一般是GCC工具链,而interpreter的实现,代码就在glibc的elf/rtld.c中。

interpreter又称为dynamic linker,以glibc2.27为例,它的大致功能如下:

其中参与动态加载和重定向所需要的重要部分就是Program Header Table中PT_DYNAMIC类型的Segment。前面我们提到在Section Header中也有一部分参与动态链接的section,即.dynamic。我在自己解析动态链接文件的时候发现,实际上 .dynamic section中的数据,和PT_DYNAMIC中的数据指向的是文件中的同一个地方,即这两个entry的s_offset和p_offset是相同。每个元素的类型如下:

d_tag表示实际类型,并且d_un和d_tag相关,可能说是很有拓展性了:) 同样的,标准中定义了几十个d_tag类型,比较常用的几个如下:

其中有部分的类型可以和Section中的SHT_xxx类型进行类比,完整的列表可以参考ELF标准中的Book III: Operating System Specific一节。

在interpreter根据DT_NEEDED加载完所有需要的动态库后,就实现了完整进程虚拟内存映像的布局。在寻找某个动态符号时,interpreter会使用广度优先的方式去进行搜索,即先在当前ELF符号表中找,然后再从当前ELF的DT_NEEDED动态库中找,再然后从动态库中的DT_NEEDED里查找。

因为动态库本身是位置无关的(PIE),支持被加载到内存中的随机位置,因此为了程序中用到的符号可以被正确引用,需要对其进行重定向操作,指向对应符号的真实地址。这部分我在之前写的关于GOT,PLT和动态链接的文章中已经详细介绍过了,因此不再赘述,感兴趣的朋友可以参考该文章。

有人也许会问,我看你bibi了这么多,有什么实际意义吗?呵呵,本节就来分享几个我认为比较有用的应用场景。

在渗透测试中,红队小伙伴们经常能拿到目标的后台shell权限,但是遇到一些部署了HIDS的大企业,很可能在执行恶意程序的时候被拦截,或者甚至触发监测异常直接被蓝队拔网线。这里不考虑具体的HIDS产品,假设现在面对两种场景:

不管什么样的环境,我相信老红队都有办法去绕过,这里我们运用上面学到的ELF知识,其实有一种更为简单的解法,即利用interpreter。示例如下:

/lib64/ld-linux-x86-64.so.2本身应该是内核调用执行的,但我们这里可以直接进行调用。这样一方面可以在没有执行权限的情况下执行任意代码,另一方面也可以在一定程度上避免内核对execve的异常监控。

利用(滥用)interpreter我们还可以做其他有趣的事情,比如通过修改指定ELF文件的interpreter为我们自己的可执行文件,可让内核在处理目标ELF时将控制器交给我们的interpreter,这可以通过直接修改字符串表或者使用一些工具如patchelf来轻松实现。

对于恶意软件分析的场景,很多安全研究人员看到ELF就喜欢用ldd去看看有什么依赖库,一般ldd脚本实际上是调用系统默认的ld.so并通过环境变量来打印信息,不过对于某些glibc实现(如glibc2.27之前的ld.so),会调用ELF指定的interpreter运行,从而存在非预期命令执行的风险。

当然还有更多其他的思路可以进行拓展,这就需要大家发挥脑洞了。

与逆向分析比较相关的就是符号表,一个有符号的程序在逆向时基本上和读源码差不多。因此对于想保护应用程序的开发者而言,最简单的防护方法就是去除符号表,一个简单的strip命令就可实现。strip删除的主要是Section中的信息,因为这不影响程序的执行。去除前后进行diff对比可看到删除的section主要有下面这些:

其中.symtab是符号表,.strtab是符号表中用到的字符串。

仅仅去掉符号感觉还不够,熟悉汇编的人放到反编译工具中还是可以慢慢还原程序逻辑。通过前面的分析我们知道,ELF执行需要的只是Program Header中的几个段,Section Header实际上是不需要的,只不过在运行时动态链接过程会引用到部分关联的区域。大部分反编译工具,如IDA、Ghidra等,处理ELF是需要某些section信息来构建程序视图的,所以我们可以通过构造一个损坏Section Table或者ELF Header令这些反编译工具出错,从而干扰逆向人员。

当然,这个方法并不总是奏效,逆向人员可以通过动态调试把程序dump出来并对运行视图进行还原。一个典型的例子是Android中的JNI动态库,有的安全人员对这些so文件进行了加密处理,并且在.init/.initarray这些动态库初始化函数中进行动态解密。破解这种加固方法的策略就是将其从内存中复制出来并进行重建,重建的过程可根据segment对section进行还原,因为segment和section之间共享了许多内存空间,例如:

Section to Segment mapping中可以看到这些段的内容是跟对应section的内容重叠的,虽然一个segment可能对应多个section,但是可以根据内存的读写属性、内存特征以及对应段的一般顺序进行区分。

如果程序中有比较详细的日志函数,我们还可以通过反编译工具的脚本拓展去修改.symtab/.strtab段来批量还原ELF文件的符号,从而高效地辅助动态调试。

考虑这么一种场景,我们在分析某个IoT设备时发现了一个定制的ELF网络程序,类似于httpd,其中有个静态函数负责处理输入数据。现在想要单独对这个函数进行fuzz应该怎么做?直接从网络请求中进行变异是一种方法,但是网络请求的效率太低,而且触达该函数的程序逻辑也可能太长。

既然我们已经了解了ELF,那就可以有更好的办法将该函数抽取出来进行独立调用。在介绍ELF类型的时候其实有提到,可执行文件可以有两种类型,即可执行类型(ET_EXEC)和共享对象(ET_DYN),一个动态链接的可执行程序默认是共享对象类型的:

而动态库(.so)本身也是共享对象类型,他们之间的本质区别在于前者链接了libc并且定义了main函数。对于动态库,我们可以通过dlopen/dlsym获取对应的符号进行调用,因此对于上面的场景,一个解决方式就是修改目标ELF文件,并且将对应的静态函数导出添加到dynamic section中,并修复对应的ELF头。

这个思想其实很早就已经有人实现了,比如lief的bin2lib。通过该方法,我们就能将目标程序任意的函数抽取出来执行,比如hugsy就用这个方式复现了Exim中的溢出漏洞(CVE-2018-6789),详见Fuzzing arbitrary functions in ELF binaries(中文翻译)。

本文主要介绍了32位环境下ELF文件的格式和布局,然后从内核空间和用户空间两个方向分析了ELF程序的加载过程,最后列举了几个依赖于ELF文件特性的案例进行具体分析,包括dynamic linker的滥用、程序加固和反加固以及在二进制fuzzing中的应用。

ELF文件本身并不复杂,只有三个关键部分,只不过在section和segment的类型上保留了极大的拓展性。操作系统可以根据自己的需求在不同字段上实现和拓展自己的功能,比如Linux中通过dymamic类型实现动态加载。但这不是必须的,例如在Android中就通过ELF格式封装了特有的.odex.oat文件来保存优化后的dex。另外对于64位环境,大部分字段含义都是类似的,只是字段大小稍有变化(Elf32->Elf64),并不影响文中的结论。

博客地址: https://evilpan.com/ 排版带侧边目标,PC阅读体验更佳。
图片描述

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$ r2 a.out
Segmentation fault
#define EI_NIDENT (16)

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;
typedef struct
{
  Elf32_Word    sh_name;        /* Section name (string tbl index) */
  Elf32_Word    sh_type;        /* Section type */
  Elf32_Word    sh_flags;        /* Section flags */
  Elf32_Addr    sh_addr;        /* Section virtual addr at execution */
  Elf32_Off    sh_offset;        /* Section file offset */
  Elf32_Word    sh_size;        /* Section size in bytes */
  Elf32_Word    sh_link;        /* Link to another section */
  Elf32_Word    sh_info;        /* Additional section information */
  Elf32_Word    sh_addralign;        /* Section alignment */
  Elf32_Word    sh_entsize;        /* Entry size if section holds table */
} Elf32_Shdr;
typedef struct
{
  Elf32_Word    p_type;            /* Segment type */
  Elf32_Off    p_offset;          /* Segment file offset */
  Elf32_Addr    p_vaddr;        /* Segment virtual address */
  Elf32_Addr    p_paddr;        /* Segment physical address */
  Elf32_Word    p_filesz;        /* Segment size in file */
  Elf32_Word    p_memsz;        /* Segment size in memory */
  Elf32_Word    p_flags;        /* Segment flags */
  Elf32_Word    p_align;        /* Segment alignment */
} Elf32_Phdr;
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;
$ cat hello.c
#include <stdio.h>
int main() {
    return puts("hello!");
}
$ gcc hello.c -o hello
$ ./hello
hello!

$ chmod -x hello
$ ./hello
bash: ./hello: Permission denied
$ /lib64/ld-linux-x86-64.so.2 ./hello
hello!
$ strace /lib64/ld-linux-x86-64.so.2 ./hello 2>&1 | grep exec
execve("/lib64/ld-linux-x86-64.so.2", ["/lib64/ld-linux-x86-64.so.2", "./hello"], 0x7fff1206f208 /* 9 vars */) = 0
$ diff 0 1
1c1
< There are 35 section headers, starting at offset 0x1fdc:
---
> There are 28 section headers, starting at offset 0x1144:
32,39c32
<   [27] .debug_aranges    PROGBITS        00000000 00104d 000020 00      0   0  1
<   [28] .debug_info       PROGBITS        00000000 00106d 000350 00      0   0  1
<   [29] .debug_abbrev     PROGBITS        00000000 0013bd 000100 00      0   0  1
<   [30] .debug_line       PROGBITS        00000000 0014bd 0000cd 00      0   0  1
<   [31] .debug_str        PROGBITS        00000000 00158a 000293 01  MS  0   0  1
<   [32] .symtab           SYMTAB          00000000 001820 000480 10     33  49  4
<   [33] .strtab           STRTAB          00000000 001ca0 0001f4 00      0   0  1
<   [34] .shstrtab         STRTAB          00000000 001e94 000145 00      0   0  1
---
>   [27] .shstrtab         STRTAB          00000000 00104d 0000f5 00      0   0  1
$ readelf -l main1
...
 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 .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .dynamic .got
$ gcc hello.c -o hello
$ readelf -h hello | grep Type
  Type:  DYN (Shared object file)

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 15
支持
分享
打赏 + 5.00雪花
打赏次数 1 雪花 + 5.00
 
赞赏  orz1ruo   +5.00 2020/08/10 原创内容~
最新回复 (13)
雪    币: 8715
活跃值: (8619)
能力值: ( LV13,RANK:570 )
在线值:
发帖
回帖
粉丝
2
写的太好了
2020-8-9 19:35
0
雪    币: 4934
活跃值: (1057)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
写得太好了,mark 一下
2020-8-10 07:29
0
雪    币: 3496
活跃值: (749)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
谢谢分享
2020-8-11 09:53
0
雪    币: 238
活跃值: (551)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
mark 
2020-8-11 19:44
0
雪    币: 415
活跃值: (2633)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark
2020-8-20 08:23
0
雪    币: 120
活跃值: (1597)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
mark,感谢分享
2020-9-1 15:53
0
雪    币: 1282
活跃值: (1342)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
8
mark 后面的实战案例都可以学习到很多!
2020-10-10 00:20
0
雪    币: 211
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
写的挺好
2020-12-4 15:29
0
雪    币: 599
活跃值: (2001)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
m
2020-12-7 12:28
0
雪    币: 372
活跃值: (4087)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
11
牛牛牛,学习到了
2022-2-25 15:48
0
雪    币: 2605
活跃值: (5058)
能力值: ( LV9,RANK:225 )
在线值:
发帖
回帖
粉丝
12
tql,膜膜膜
2022-8-5 09:49
0
雪    币: 129
活跃值: (1095)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
很不错,学习了,谢谢
2022-9-2 05:04
0
雪    币: 142
活跃值: (36)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
学习到很多!
2022-9-2 11:29
0
游客
登录 | 注册 方可回帖
返回
//