PWN二进制安全修仙秘籍【第二章#二进制文件篇02】ELF文件详解

发布于:2024-10-08 ⋅ 阅读:(12) ⋅ 点赞:(0)

什么是ELF文件?

英文全称就是Executable Linkable Format,即可执行可链接格式,Linux系统上所运行的就是ELF格式的文件,相关定义在“/usr/include/elf.h”文件里。

 1. 编写示例代码

这里我们编写下面的示例代码,用来编译生成ELF文件

#include <stdio.h>

int global_init_var = 10;
int global_uninit_var;
void func(int sum) {
        printf("%d\n",sum);
}
void main(void) {
        static int local_static_init_var = 20;
        static int local_static_uninit_var;

        int local_init_val = 30;
        int local_uninit_var;

        func(global_init_var + local_init_val + local_static_init_var);
}

稍微解释一下上述代码,就是分别在全局创建可变变量,main函数内部创建静态变量和可变变量,然后在main函数内对变量进行相加并输出。

 接下来分别使用以下命令对示例代码进行编译:

gcc elfDemo.c -o elfDemo.exec    //生成动态可执行文件

gcc -static elfDemo.c -o elfDemo_static.exec    //生成静态可执行文件

gcc -c elfDemo.c -o elfDemo.rel         //生成可重定位文件

gcc -c -fPIC elfDemo.c -o elfDemo_pic.rel && gcc -shared elfDemo_pic.rel -o elfDemo.dyn   //生成动态链接库文件(共享目标文件)

分别解释一下上述gcc参数的作用:

-static:生成静态可执行文件,使用静态链接;

-c:编译、汇编指定的源文件,但是不进行链接;

-fPIC:产生位置无关码(position independent code) ;

-shared:生成共享库(动态链接库);

让我们使用file命令来看看生成的究竟是什么妖孽

➜  file elfDemo.exec
elfDemo.exec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f9dc99d717e41298f465ac3be2db2775aede666, for GNU/Linux 3.2.0, not stripped

dynamically linked说明第一个文件属于动态可链接文件

➜  file elfDemo_static.exec
elfDemo_static.exec: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=af7a8c76ceca3b151e669aabb38f753002d4a394, for GNU/Linux 3.2.0, not stripped

statically linked说明第二个文件属于静态可链接文件

➜  file elfDemo.rel
elfDemo.rel: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

relocaltable说明第三个文件属于可重定位文件

➜  file elfDemo.dyn
elfDemo.dyn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d43a76d4eeb0d10e1d7fe26e929ae8e669f87346, not stripped

shared object说明第四个文件属于可共享文件

通过上述案例我们可以发现ELF文件可以分为三种类型:可执行文件(.exec)可重定位文件(.rel)共享目标文件(.dyn)

  • 可执行文件:经过链接的、可执行的目标文件,通常也被称为程序;
  • 可重定位文件:由源文件编译而成且尚未链接的目标文件,通常以“.o”作为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置独立的代码;
  • 共享目标文件:动态链接库文件。用于在链接过程中与其他动态链接库或可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。 

 2. ELF文件结构

我们讨论ELF文件结构通常有两个不同的视角:链接视角,即用户视角,通过节(Section)来进行划分ELF文件;另一种是运行视角,即操作系统的视角,通过段(Segment)来进行划分。相信大家在学习计算机组成原理的时候也有了解过这两个概念。

在网上找来了一张图,可以更直观的看出来两种视角的结构

2.1 ELF文件总体结构

在进行PWN时,我们通常是以用户的视角来找到二进制漏洞的,因此我们先学习链接视角下ELF的文件结构👇

在链接视角下,通常目标文件都会包含以下三个部分:

  1. 代码节(.text):用于保存可执行的机器指令;
  2. 数据节(.data):用于保存已初始化的全局变量和局部静态变量;
  3. BSS节(.bss):用于保存未初始化的全局变量和局部静态变量。

为什么要将目标文件分成一个个节呢❓❓❓

从安全的角度来说,将程序指令程序数据分开进行存储,由于数据区域对于进程而言是可读写的,而指令区域对于进程而言是只读的,两块区域权限分别为可读写只读,这样可以有效地防止程序的指令被改写和利用。

在这部分,我们最后说明一下节和段的关系👇

相同权限的节会放入同一个段中,例如.text.rodata节;一个段包含许多节,一个节可以属于多个段。(.rodata节是一个只读的数据节)

2.2 ELF文件头

在一个ELF文件中,除了具有上述三个节外,还应包含一个文件头(LEF header)。 

ELF文件头位于目标文件最开始的位置,包含描述整个文件的一些基本信息,如ELF文件类型、版本/ABI版本、目标机器、程序入口、段表和节表的位置和长度等。

在文件头部存在魔术字符(7f 45 4c 46),即字符串“\177ELF”,当文件被映射到内存时,可以通过搜索该字符串确定映射地址,这个方法通常用于dump内存。

下面将展示一下这个方法👇

➜  readelf -h elfDemo.rel
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0

从上面可以看见,Magic前四个字节内容为7f 45 4c 46

2.3 节头表 (链接视角)

 一个目标文件中包含许多节,这些节的信息保存在节头表中。

表的每一项都记录了节的名称、长度、偏移、读写权限等信息。

节头表的位置记录在文件头的e_shoff域中。

节头表对于程序运行而言并不是必须的,因为它与程序内存布局无关,所以常有程序去除节头表,以增加反编译器的分析难度

下面将展示一下程序的节头表长什么样👇

➜  readelf -S elfDemo.rel
There are 14 section headers, starting at offset 0x400:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000005b  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002e0
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000009c
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a4
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a4
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a8
       000000000000002c  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000d4
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  000000d8
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000f8
       0000000000000058  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000358
       0000000000000030  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  00000150
       0000000000000120  0000000000000018          12     7     8
  [12] .strtab           STRTAB           0000000000000000  00000270
       0000000000000070  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000388
       0000000000000074  0000000000000000           0     0     1
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),
  D (mbind), l (large), p (processor specific)

我们不难发现,上述程序具有.text.rela.text.data.bss.rodata.comment等节。

2.3.1 .text节详解

接下来仔细分析一下.text节,也就是代码节👇

➜  objdump -x -s -d elfDemo.rel

Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000005b  0000000000000000  0000000000000000  00000040  2**0

Contents of section .text:
 0000 f30f1efa 554889e5 4883ec10 897dfc8b  ....UH..H....}..
 0010 45fc89c6 488d0500 00000048 89c7b800  E...H......H....
 0020 000000e8 00000000 90c9c3f3 0f1efa55  ...............U
 0030 4889e548 83ec10c7 45fc1e00 00008b15  H..H....E.......
 0040 00000000 8b45fc01 c28b0500 00000001  .....E..........
 0050 d089c7e8 00000000 90c9c3

Disassembly of section .text:

0000000000000000 <func>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 83 ec 10             sub    $0x10,%rsp
   c:   89 7d fc                mov    %edi,-0x4(%rbp)
   f:   8b 45 fc                mov    -0x4(%rbp),%eax
  12:   89 c6                   mov    %eax,%esi
  14:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 1b <func+0x1b>
                        17: R_X86_64_PC32       .rodata-0x4
  1b:   48 89 c7                mov    %rax,%rdi
  1e:   b8 00 00 00 00          mov    $0x0,%eax
  23:   e8 00 00 00 00          call   28 <func+0x28>

可以看到,Contents of section .text部分是.text数据的十六进制形式,最左边一列是偏移量,中间四列是内容,最右边一列是ASCII码形式;Disassembly of section .text部分则是反汇编的结果。

2.3.2 .data节和.rodata节详解

接下来仔细分析一下.text.rodata节,也就是数据节和只读数据节👇

➜  objdump -x -s -d elfDemo.rel

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  1 .data         00000008  0000000000000000  0000000000000000  0000009c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

Contents of section .data:
 0000 0a000000 14000000                    ........
Contents of section .rodata:
 0000 25640a00                             %d..

 容易看到.data节保存已经初始化的全局变量和局部静态变量。在源代码中共有两个这样的变量:global_init_var(0a000000)local_static_init_var(14000000),每个变量4个字节,一共8个字节。

.rodata节保存只读数据,包括只读变量和字符串常量。源代码中调用printf()函数时,用到了一个字符串“%d\n”,它是一种只读数据,因此保存在.rodata节中,可以看到字符串常量的ASCII形式,以“\0”结尾。

2.3.3 .bss节详解

最后分析一下.bss节,也就是BSS节👇

➜  objdump -x -s -d elfDemo.rel

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  2 .bss          00000008  0000000000000000  0000000000000000  000000a4  2**2
                  ALLOC

BSS节是用于保存未初始化的全局变量和局部静态变量。BSS节没有CONTENTS属性,表示该节在文件中实际上不存在,只是为变量预留了位置而已,因此该节的sh_offset域也没有意义。

2.3.4 其他常见的节

除了上述节外,还有一些常用的节,下面进行简单的列举,以后用到再细嗦

节名 说明
.got 全局偏移量表(global offset table),用于保存全局变量引用的地址
.got.plt 全局偏移量表,用于保存函数引用的地址
.plt 过程链接表(procedure linkage table),用于延迟绑定

2.4 可执行文件的装载(运行视角)

在上述内容都是以链接视角对目标文件进行解读,接下来我们以运行视角进行审视目标文件。

可执行文件是如何装载到内存中的❓❓❓

首先运行一个可执行文件

然后将该文件和动态链接库装载到进程空间中

最后形成一个进程镜像,每个进程都拥有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头决定的。ELF文件头的e_phoff域给出了段头表的位置

下面将展示一个可执行文件的程序头👇

➜  readelf -l elfDemo.exec

Elf file type is DYN (Position-Independent Executable 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
                 0x0000000000000628 0x0000000000000628  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001b1 0x00000000000001b1  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000114 0x0000000000000114  R      0x1000
  LOAD           0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000260 0x0000000000000270  RW     0x1000
  DYNAMIC        0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
                 0x00000000000001f0 0x00000000000001f0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x0000000000002008 0x0000000000002008 0x0000000000002008
                 0x000000000000003c 0x000000000000003c  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

容易看到一个段内包含了一个节或者多个节,即段就是对这些节进行分组。

分段的目的👇

随着节的数量增多,在进行内存映射时就产生空间和资源的浪费。实际上系统并不关心每个节的内容,而是关心这些节的权限(读、写、执行),通过将不同权限的节分组,即可同时装载多个节,从而节省资源。

2.4.1 常见段详解

PT_LOAD段——每个可执行文件至少有一个PT_LOAD段,用于描述可装载的节,而动态链接的可执行文件则包含两个,将.data和.text分开存放。

动态段PT_DYNAMIC——包含一些动态链接器所必须的信息,如共享库列表、GOT表、重定位表等。

PT_NONE段——保存了系统相关的附加信息,但并不是程序运行所需要的。

PT_INTERP段——将位置和大小信息存放在一个字符串中,是对程序解释器位置的描述。

PT_PHDR段——保存了程序头表本身的位置和大小。