65.9K
CodeProject 正在变化。 阅读更多。
Home

通过共享对象注入到正在运行的进程中进行 PLT 重定向。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (8投票s)

2008年12月10日

BSD

30分钟阅读

viewsIcon

73924

downloadIcon

484

这是两部分文章的第一部分,将说明如何通过将共享对象注入进程的地址空间来重定向进程的 PLT。

目录

  1. 引言
  2. 必备组件
  3. ELF 格式简介
  4. ELF 加载
  5. 搭建我们的实验环境...
  6. PLT:一个实际的例子
  7. 结论
  8. 参考文献

引言

在过去的几周里,我出于各种原因一直在玩弄 Linux,所以我想到的第一件事就是将一些广为人知的 Win32 方法应用到这个操作系统上,其中大部分是从逆向工程的角度来看的。这是两部分文章的第一部分,将介绍 Linux 下的代码注入。这里介绍的技术类似于一个广为人知的 Win32 技术:将 DLL(在 Linux 和类 Unix 系统中是共享对象)注入到正在运行的进程中,从而能够钩取进程的某些导入函数。有两种方法可能导致代码注入。

  1. 使用 LD_PRELOAD 方法(这需要重新启动要注入共享对象的进程)。
  2. 将一个存根注入目标进程,该存根会加载所需的库。

当然,正如您可能已经猜到的,第二种方法要求目标进程的地址空间中存在“libdl”,但这是一个合理的假设,因为大多数现代 Linux 和 Unix 软件都相当复杂,而且它们中的大多数都会将 libdl 映射到它们的地址空间中。我们将使用第二种方法,因为我们的目标是将共享对象注入到正在运行的进程中。关于钩取技术,我们将使用 PLT 重定向,这是一种在 Linux/Unix 下钩取来自其他库的函数的方法,简单而优雅。

第一部分将向您介绍 ELF 格式(大多数 Linux 和类 Unix 操作系统的标准对象格式)的基础知识,并附带一些代码示例,因此它将是对下一部分内容的介绍,下一部分将展示实际技术,但这一部分是必读的,因为它将建立所需的概念。

必备组件

在继续深入阅读文章之前,最好先查阅 ELF 手册。它们是理解我所写内容的大部分内容的必读材料。我只会对 ELF 格式做简要介绍,但关于它的内容太多了,我们可能会忘记本文的目标(本文的目标不是关于 ELF 格式,而是关于共享对象注入)。在文本的各个部分,我都会引用手册,所以我们来看看“如何”阅读它们。您需要下载两本手册,第一本是通用手册,第二本是 x86 特定手册。如果您查看通用手册,会发现它缺少一些部分,这些部分会在特定手册中找到(每种支持 ELF 格式的平台都有额外的 ELF 手册)。缺少的部分以标记开头,因此您会注意到它们。

您应该对 C 编程语言有扎实的了解,并且应该知道如何使用 GCC 和 GDB。对 x86 汇编语言有基本了解即可。这是参考系统:

  • xubuntu 8.10,内核 2.6.27-generic
  • GCC 4.3.2
  • GNU GDB 6.8-debian
  • GNU readelf (Ubuntu 的 GNU Binutils) 2.18.93.20081009
  • GNU objdump (Ubuntu 的 GNU Binutils) 2.18.93.20081009

ELF 格式简介

在本章中,将从程序员的角度介绍 ELF 格式的主要方面。本章不会是 ELF 手册的生搬硬套,它只会向您介绍该格式的某些方面,因此务必阅读手册。本章将讨论 ELF 格式的各种结构以及如何读取它们。动态链接将在下一章中进行解释。

历史背景

ELF 格式(是 Executable and Linking Format 的缩写)是描述对象文件(包含编译代码的文件)从逻辑和/或物理角度来看的多种格式之一。与 ELF 类似的格式包括:PE 格式(Portable Executable,主要用于 Microsoft 平台)、Mach-O(用于 OSX)、COFF 和 a.out。从历史角度来看,COFF 和 a.out 对 PE(源自 COFF)和 ELF(具有 a.out 的某些方面)等更新格式的开发产生了深远影响。正如您从名称中可以猜到的,ELF 格式允许描述可执行文件和库。

ELF 格式被引入以取代 UNIX 系统上的 a.outCOFF 格式,并于 1999 年成为标准。如今,大多数非 Microsoft 平台都使用 ELF 格式,它非常适合适应大多数平台。此外,“ld”链接器可以通过使用自定义 ld 脚本来构建几乎满足您任何需求的自定义 ELF 文件。使用 ELF 格式的非 UNIX 平台示例包括 PSP、PS2、PS3、Wii、Dreamcast、BeOS、Haiku、AmigaOS、MorphOS 和 SymbianOS(实际上使用派生自 ELF 的格式)。这种灵活性源于大多数 ELF 的主要结构不受特定平台限制(例如,重定位结构的格式(字段及其大小)独立于所使用的平台,但其内容高度依赖于平台)。

ELF 结构

ELF 结构与其他映像(object file 的同义词)格式相似:它有一个头,后面跟着描述内存各个段内容的节。在接下来的段落中,我们将使用“readelf”实用程序对一个测试可执行文件,即“/bin/ls”(您的系统应该有这个...)进行操作。

ELF

处理文件格式时,首先想到的是文件头,它是文件最重要的部分。对于 ELF 格式,文件头包含文件是为哪个架构构建的、其字节序(most of cases is bound to the architecture - 有些例外:现代 ARM 和 PPC 可以设置为大端或小端)、文件中包含的节的数量、段的数量(一个段包含一个或多个节)等。使用 C 结构语法(与手册相同的方式),我们来看一下文件头:

#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;

其中这些是重要字段:

  • e_ident: 允许标识文件。它是一个 16 字节的数组,是文件中唯一不依赖于字节序的部分。前四个字节是 ELF 签名,它们的值如下:{0x7F, 'E', 'L', 'F'},由常量 EI_MAG0EI_MAG1EI_MAG2EI_MAG3 索引。接下来的字节允许识别 ELF 对象的尺寸(即,我们处理的是 32 位还是 64 位 ELF)、字节序以及其他我们不关心的参数。头文件 elf.h 定义了两个常量来索引字节序字节和尺寸字节:它们是 EI_CLASSEI_DATA。处理 x86 架构将导致 e_ident[EI_CLASS] = ELFCLASS32e_ident[EI_DATA] = ELFDATA2LSB,正如稍后展示的那样。
  • e_entry: 此字段包含作为映像入口点的地址;也就是说,一旦映像完全加载到内存中,系统要执行的第一条指令。对于可执行文件,此字段将包含一个虚拟地址(因为可执行文件始终在相同的基地址加载)。对于库(.so 文件或共享对象),它将包含一个相对虚拟地址(即,虚拟地址减去基地址)。
  • e_phoff: 文件开头的物理偏移量,指示程序头表的位置。因此,要读取程序头表,只需“lseek”到此偏移量。程序头描述一个段,而段是节(由段头描述)的集合。
  • e_shoff: 与上面相同,但此处包含节头表偏移量。
  • e_phentsize: 程序头表中一个条目的大小。
  • e_shentsize: 段头表中一个条目的大小。
  • e_phnum: 程序头的数量。
  • e_shnum: 段头的数量。
  • e_shstrndx: 段头表中的一个索引,它包含用于获取节名称的字符串表(字符串表将在稍后几段中介绍)。

您可能已经看到,头字段使用自定义数据类型定义,例如 Elf32_WordElf32_Half 等,这允许以独特的方式识别字段的大小。我们进一步阐明这一点:ELF 字(二进制字)的大小在 32 位和 64 位架构上始终为 32 位,因此 Elf32_Word 是一个无符号 32 位整数。Elf64_Word 也是如此。这对于数据字是成立的(对于 ELF 格式,数据字始终为 32 位),但对于地址不是(当然,在 32 位架构上是 32 位,在 64 位架构上是 64 位),因此有地址类型:Elf32_AddressElf32_Offset,它们是无符号 32 位整数;在 64 位架构上,当然会有 Elf64_AddressElf64_Offset,它们是无符号 64 位整数。Elf32_HalfElf64_Half 的大小始终是 ELF 字的一半,即无符号 16 位整数。以下是 typedef:

/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;

/* Types for signed and unsigned 32-bit quantities.  */
typedef uint32_t Elf32_Word;
typedef    int32_t  Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef    int32_t  Elf64_Sword;

/* Types for signed and unsigned 64-bit quantities.  */
typedef uint64_t Elf32_Xword;
typedef    int64_t  Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef    int64_t  Elf64_Sxword;

/* Type of addresses.  */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;

/* Type of file offsets.  */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;

/* Type for section indices, which are 16-bit quantities.  */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;

/* Type for version symbol information.  */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;

让我们看一下我们二进制文件的“readelf”输出:

quake2@quake2-desktop:~$ readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8049b20
  Start of program headers:          52 (bytes into file)
  Start of section headers:          95096 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         28
  Section header string table index: 27

从这个输出可以清楚地看出,“/bin/ls”是 x86 平台的 32 位可执行文件。

程序头和段

程序头是 ELF 格式的基础结构。程序头描述一个段,该段包含一个或多个节。它包含加载程序有关如何将文件中的段映射到内存的指令。有些程序头具有特殊含义,稍后将讨论。重要的是要注意,没有程序头的 ELF 映像无法由系统加载程序加载到内存中(中间对象文件(通常以 .o 扩展名)不需要程序头,因为它不会加载到内存中),因此有效的 ELF 映像必须至少有一个 PT_LOAD 类型的程序头。一旦 ELF 映像完全加载到内存中,程序头就是唯一包含可靠信息的结构;段头会失去其含义(它们可能存在于内存映像中,但您不应该依赖它们)。这是程序头的结构:

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;
  • p_type: 此字段包含段的类型。每种类型可以有多个段(但并非对所有类型都有效;例如,只能有一个 PT_DYNAMICPT_INTERP 类型的段)。有关可能类型的描述,请参阅 ELF 手册。当然,每种不同类型的程序头包含不同类型的信息。
  • p_offset: 段开始的文件偏移量。
  • p_vaddr: 段开始的内存虚拟地址。
  • p_paddr: 段开始的内存物理地址;这在大多数现代架构上没有意义;通常与 p_vaddr 的值相同。
  • p_filesz: 文件中段的大小(磁盘上的大小);这可能与内存大小不同(对于文本和数据段通常是如此)。
  • p_memsz: 段的内存大小。
  • p_flags: 一个位掩码,包含常用的内存保护标志(读取、写入、执行);请参阅手册。
  • p_align: 段对齐;如果此字段不等于 0 或 1,则 p_vaddr % p_align = 0

让我们注意到,在映像文件中,程序头包含重要数据,以允许加载程序加载映像;段头只是辅助项,可能根本不存在于映像文件中。让我们看一下“readelf”的输出:

quake2@quake2-desktop:~$ readelf -l /bin/ls

Elf file type is EXEC (Executable file)
Entry point 0x8049b20
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 0x16eb4 0x16eb4 R E 0x1000
  LOAD           0x016ef0 0x0805fef0 0x0805fef0 0x003a0 0x0081c RW  0x1000
  DYNAMIC        0x016f04 0x0805ff04 0x0805ff04 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00020 0x00020 R   0x4
  GNU_EH_FRAME   0x016dec 0x0805edec 0x0805edec 0x0002c 0x0002c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x016ef0 0x0805fef0 0x0805fef0 0x00110 0x00110 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr 
          .gnu.version .gnu.version_r .rel.dyn 
          .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag 
   06     .eh_frame_hdr 
   07     
   08     .ctors .dtors .jcr .dynamic .got

从这个输出可以清楚地看出,单个程序头描述一个段,该段包含一个或多个节(例如,程序头 2、3、8);此外,一个节可能出现在多个程序头中(例如,.dynamic 节出现在 8 和 4 中)。请注意,PT_LOAD 段(包含代码和数据节)按页面边界对齐。

段头

段头包含描述文件磁盘结构一部分的信息。一个节可能小到只有几个字节(例如,.interp 节),也可能像 .text 节一样大,它包含几乎所有可执行代码。节是逻辑上将 ELF 文件细分为较小部分的一种方式。如果您熟悉 Microsoft 的 PE,那么 ELF 节与 PE 节无关,但 PE 节与 ELF 段是相同的。这是结构:

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;
  • sh_name: 节的名称。这是字符串表中的一个索引。字符串表是由空字节分隔的以 null 结尾的字符串集合;例如,“\0name1\0name2\0name3\0\0”;下一段将介绍它们。
  • sh_type: 节的类型。同一类型的节可以有多个,但有一些例外(例如,只能有一个 SHT_SYMTAB 和一个 SHT_DYNSYM)。类型为 SHT_PROGBITS 的节包含实际的已初始化代码/数据,类型为 SHT_NOBITS 的节包含未初始化的数据(不需要文件存储,例如 .bss 节)。
  • sh_flags: 节标志。一个位掩码,包含常用的内存保护属性(读取、写入、执行)。
  • sh_addr: 节的虚拟地址。一旦映像被映射到内存中,它将是段内的虚拟地址。
  • sh_offset: 节在文件中的偏移量;此字段是物理偏移量。
  • sh_size: 节在文件中的物理大小(字节)。对于 NOBITS 节,这可能是所需的存储空间,或映射后的实际大小。
  • sh_link: 这是一个特殊字段。其内容取决于节类型。对于 SHT_SYMTABSHT_DYNSYM,此字段包含一个节表索引,该索引指向包含符号名称字符串表的节;有关其他类型,请参阅手册。
  • sh_info: 此字段的内容取决于节类型。
  • sh_entsize: 如果节中包含表,则此字段给出表中一个条目的大小(字节)。

那么,让我们看看“readelf”的常规输出:

quake2@quake2-desktop:~$ readelf -S /bin/ls
There are 28 section headers, starting at offset 0x17378:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048188 000188 000330 04   A  5   0  4
  [ 4] .gnu.hash         GNU_HASH        080484b8 0004b8 00005c 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          08048514 000514 000690 10   A  6   1  4
  [ 6] .dynstr           STRTAB          08048ba4 000ba4 0004af 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08049054 001054 0000d2 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08049128 001128 0000d0 00   A  6   3  4
  [ 9] .rel.dyn          REL             080491f8 0011f8 000028 08   A  5   0  4
  [10] .rel.plt          REL             08049220 001220 0002e8 08   A  5  12  4
  [11] .init             PROGBITS        08049508 001508 000030 00  AX  0   0  4
  [12] .plt              PROGBITS        08049538 001538 0005e0 04  AX  0   0  4
  [13] .text             PROGBITS        08049b20 001b20 01145c 00  AX  0   0 16
  [14] .fini             PROGBITS        0805af7c 012f7c 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        0805afa0 012fa0 003e4c 00   A  0   0 32
  [16] .eh_frame_hdr     PROGBITS        0805edec 016dec 00002c 00   A  0   0  4
  [17] .eh_frame         PROGBITS        0805ee18 016e18 00009c 00   A  0   0  4
  [18] .ctors            PROGBITS        0805fef0 016ef0 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        0805fef8 016ef8 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        0805ff00 016f00 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         0805ff04 016f04 0000e8 08  WA  6   0  4
  [22] .got              PROGBITS        0805ffec 016fec 000008 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0805fff4 016ff4 000180 04  WA  0   0  4
  [24] .data             PROGBITS        08060180 017180 000110 00  WA  0   0 32
  [25] .bss              NOBITS          080602a0 017290 00046c 00  WA  0   0 32
  [26] .gnu_debuglink    PROGBITS        00000000 017290 000008 00      0   0  1
  [27] .shstrtab         STRTAB          00000000 017298 0000df 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

如您从输出中看到的,节表以一个空条目开始。有些节的地址字段为 0:这些节不会被映射到内存中(查看前面的程序头表,您找不到它们)。从输出中可以看出,有两个字符串表。请注意它们的地址,一个非零,另一个为零。这意味着一个字符串表在映射文件到内存时将被丢弃;未被丢弃的字符串表将保存动态符号(.dynsym)的名称,动态链接器使用这些名称来解析运行时重定位。请注意最后一个节的索引;它与您在 ELF 头部的 e_shstrndx 字段中找到的索引相同:.shstrtab 节保存 ELF 节的名称。

字符串表

严格来说,字符串表不是一种结构,而是字符串的无序集合。关于字符串表没有什么可说的;只需记住,当您在 ELF 结构中看到名称字段时,它总是字符串表中的索引。是哪个字符串表?这取决于上下文;但是,无论何时遇到字符串表,总有一种方法可以知道。让我们看一下最后一个节的内存转储:

quake2@quake2-desktop:~$ readelf -x 27 /bin/ls

Hex dump of section '.shstrtab':
  0x00000000 002e7368 73747274 6162002e 696e7465 ..shstrtab..inte
  0x00000010 7270002e 6e6f7465 2e414249 2d746167 rp..note.ABI-tag
  0x00000020 002e676e 752e6861 7368002e 64796e73 ..gnu.hash..dyns
  0x00000030 796d002e 64796e73 7472002e 676e752e ym..dynstr..gnu.
  0x00000040 76657273 696f6e00 2e676e75 2e766572 version..gnu.ver
  0x00000050 73696f6e 5f72002e 72656c2e 64796e00 sion_r..rel.dyn.
  0x00000060 2e72656c 2e706c74 002e696e 6974002e .rel.plt..init..
  0x00000070 74657874 002e6669 6e69002e 726f6461 text..fini..roda
  0x00000080 7461002e 65685f66 72616d65 5f686472 ta..eh_frame_hdr
  0x00000090 002e6568 5f667261 6d65002e 63746f72 ..eh_frame..ctor
  0x000000a0 73002e64 746f7273 002e6a63 72002e64 s..dtors..jcr..d
  0x000000b0 796e616d 6963002e 676f7400 2e676f74 ynamic..got..got
  0x000000c0 2e706c74 002e6461 7461002e 62737300 .plt..data..bss.
  0x000000d0 2e676e75 5f646562 75676c69 6e6b00   .gnu_debuglink.

它只是字符串的集合(最左列的值是偏移量):简单明了。

写一些代码...

我们已经到了可以开始编写一些代码来显示 ELF 头并对其进行健全性检查的地步了。完整的源代码可以在附件中找到。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <elf.h>

int elf_is_valid(Elf32_Ehdr *elf_hdr)
{
    if( (elf_hdr->e_ident[EI_MAG0] != 0x7F) || 
        (elf_hdr->e_ident[EI_MAG1] != 'E') ||
        (elf_hdr->e_ident[EI_MAG2] != 'L') ||
        (elf_hdr->e_ident[EI_MAG3] != 'F') )
    {
         return 0;
    }

    if(elf_hdr->e_ident[EI_CLASS] != ELFCLASS32)
        return 0;

    if(elf_hdr->e_ident[EI_DATA] != ELFDATA2LSB)
        return 0;

    return 1;
}

static char *elf_types[] = {
    "ET_NONE",
    "ET_REL",
    "ET_EXEC",
    "ET_DYN",
    "ET_CORE",
    "ET_NUM"
};

char *get_elf_type(Elf32_Ehdr *elf_hdr)
{
    if(elf_hdr->e_type > 5)
        return NULL;

    return elf_types[elf_hdr->e_type];
}

int print_elf_header(Elf32_Ehdr *elf_hdr)
{
    char *sz_elf_type = NULL;

    if(!elf_hdr)
        return 0;

    printf("ELF header information\n");

    sz_elf_type = get_elf_type(elf_hdr);
    if(sz_elf_type)
        printf("- Type: %s\n", sz_elf_type);
    else
        printf("- Type: %04x\n", elf_hdr->e_type);

    printf("- Version: %d\n", elf_hdr->e_version);
    printf("- Entrypoint: 0x%08x\n", elf_hdr->e_entry);
    printf("- Program header table offset: 0x%08x\n", elf_hdr->e_phoff);
    printf("- Section header table offset: 0x%08x\n", elf_hdr->e_shoff);
    printf("- Flags: 0x%08x\n", elf_hdr->e_flags);
    printf("- ELF header size: %d\n", elf_hdr->e_ehsize);
    printf("- Program header size: %d\n", elf_hdr->e_phentsize);
    printf("- Program header entries: %d\n", elf_hdr->e_phnum);
    printf("- Section header size: %d\n", elf_hdr->e_shentsize);
    printf("- Section header entries: %d\n", elf_hdr->e_shnum);
    printf("- Section string table index: %d\n", elf_hdr->e_shstrndx);

    return 1;
}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;

    if(argc < 2)
    {
        printf("Usage: %s \n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
    
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
        print_elf_header(p_ehdr);
    else
        fprintf(stderr, "Invalid ELF file\n");

    free(p_base);
    return 0;
}

代码相当简单。正如您所看到的,ELF 头位于文件开头,因此我们只需声明一个指向已分配缓冲区(包含文件)开始处的指针。查看 elf_is_valid 函数,它执行健全性检查。

让我们看另一个源代码,这次它还将显示节头表和程序头表:

static char *ptypes[] = {
        "PT_NULL",
        "PT_LOAD",
        "PT_DYNAMIC",
        "PT_INTERP",
        "PT_NOTE",
        "PT_SHLIB",
        "PT_PHDR"
};

int print_program_header(Elf32_Phdr *phdr, uint index)
{
    if(!phdr)
        return 0;

    printf("Program header %d\n", index);
    if(phdr->p_type <= 6)
        printf("- Type: %s\n", ptypes[phdr->p_type]);
    else
        printf("- Type: %08x\n", phdr->p_type);

    printf("- Offset: %08x\n", phdr->p_offset);
    printf("- Virtual Address: %08x\n", phdr->p_vaddr);
    printf("- Physical Address: %08x\n", phdr->p_paddr);
    printf("- File size: %d\n", phdr->p_filesz);
    printf("- Memory size: %d\n", phdr->p_memsz);
    printf("- Flags: %08x\n", phdr->p_flags);
    printf("- Alignment: %08x\n", phdr->p_align);
}

static char *stypes[] = {
        "SHT_NULL",
        "SHT_PROGBITS",
        "SHT_SYMTAB",
        "SHT_STRTAB",
        "SHT_RELA",
        "SHT_HASH",
        "SHT_DYNAMIC",
        "SHT_NOTE",
        "SHT_NOBITS",
        "SHT_REL",
        "SHT_SHLIB",
        "SHT_DYNSYM"
};

int print_section_header(Elf32_Shdr *shdr, uint index, char *strtable)
{
    if(!shdr)
        return 0;

    printf("Section header: %d\n", index);
    printf("- Name index: %d\n", shdr->sh_name);
    
    //as you can see, we're using sh_name as an index into the string table
    printf("- Name: %s\n", strtable + shdr->sh_name);
    if(shdr->sh_type <= 11)
        printf("- Type: %s\n", stypes[shdr->sh_type]);
    else
        printf("- Type: %04x\n", shdr->sh_type);
    printf("- Flags: %08x\n", shdr->sh_flags);
    printf("- Address: %08x\n", shdr->sh_addr);
    printf("- Offset: %08x\n", shdr->sh_offset);
    printf("- Size: %08x\n", shdr->sh_size);
    printf("- Link %08x\n", shdr->sh_link);
    printf("- Info: %08x\n", shdr->sh_info);
    printf("- Address alignment: %08x\n", shdr->sh_addralign);
    printf("- Entry size: %08x\n", shdr->sh_entsize);

}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    char *p_strtable = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;
    Elf32_Phdr *p_phdr = NULL;
    Elf32_Shdr *p_shdr = NULL;
    int i;

    if(argc < 2)
    {
        printf("Usage: %s </path/to/file>\n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
    
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
    {
        print_elf_header(p_ehdr);

        printf("\n");
        
        //to reach the section header table and the program header table
        //we simply add the offset of these table to the base address
        p_phdr = (Elf32_Phdr *)(p_base + p_ehdr->e_phoff);
        p_shdr = (Elf32_Shdr *)(p_base + p_ehdr->e_shoff);
        
        //this is the first example of string table usage: the e_shstrndx field
        //holds an index into the section header table,
        //which is address by p_shdr. The section's
        //sh_offset field will hold the offset
        //of the string table, to get the actual pointer
        //we have just to sum it to the base address
        p_strtable = (char *)(p_base + p_shdr[p_ehdr->e_shstrndx].sh_offset);

        for(i = 0; i < p_ehdr->e_phnum; i++)
        {
            print_program_header(&p_phdr[i], i);
        }

        for(i = 0; i < p_ehdr->e_shnum; i++)
        {
            print_section_header(&p_shdr[i], i, p_strtable);
        }
    }
    else
        printf("Invalid ELF file\n");

    free(p_base);
    return 0;
}

有趣的部分已被注释。

符号表

符号表是 ELF 格式的基本结构。顾名思义,它是一个包含符号数组的表。ELF 手册为符号表提供了一个优雅的定义:

“对象文件的符号表保存定位和重定位程序符号定义和引用的信息。”

让我们举一个例子,预测动态链接过程:当一个可执行文件需要使用在其他地方(通常在共享对象内)定义的函数时,链接器将在动态符号表中创建一个条目,其值为 0(符号的值与符号名称不同),并且其名称等于可执行文件所需的外部函数的名称;与符号表中的条目一起,将在重定位表中创建附加条目,其中包含有关如何解析符号的附加信息(稍后将解释)。链接器将查看已加载的库,搜索一个库,该库具有与可执行文件使用的符号同名的符号。在已加载库之一中找到的符号的值将是实际函数的入口点。这样,动态链接器就会“解析”一个指向外部符号的符号。这是符号表结构:

typedef struct
{
  Elf32_Word    st_name;        /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;        /* Symbol value */
  Elf32_Word    st_size;        /* Symbol size */
  unsigned char    st_info;        /* Symbol type and binding */
  unsigned char    st_other;        /* Symbol visibility */
  Elf32_Section    st_shndx;        /* Section index */
} Elf32_Sym;
  • st_name: 符号的名称;当然,它是字符串表中的一个索引。使用的字符串表,照例,取决于上下文。
  • st_value: 符号的值;它可能是一个地址、偏移量、索引等。这取决于符号类型。
  • st_size: 符号的大小;仅对某些类型的符号有意义:例如,对于类型为 OBJECT 的符号,它将包含对象的字节大小。
  • st_info: 实际上,这是一个字节打包类型。它由两个四位字节组成:一个包含符号类型,另一个包含绑定(局部、全局、弱)。头文件 elf.h 定义了一些有用的宏来操作此字段:
    • ELF32_ST_BIND(i): 返回符号的绑定。
    • ELF32_ST_TYPE(i): 返回符号的类型。
    • ELF32_ST_INFO(b,t): 将 bt 打包成一个字节,b 是绑定类型,t 是符号类型。
  • st_other: 根据 ELF 规范,此字段的值是未定义的。在 Linux 系统中,它包含符号可见性(已定义值:DEFAULTHIDDEN)。
  • st_shndx: 包含此条目所描述的符号的节的索引。

符号表是一个相当复杂的结构,因此最好查阅 ELF 手册。让我们看一下“readelf”的输出:

quake2@quake2-desktop:~$ readelf -W -s /bin/ls

Symbol table '.dynsym' contains 105 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
[...]
    23: 00000000   198 FUNC    GLOBAL DEFAULT  UND strncpy@GLIBC_2.0 (2)
    24: 00000000    35 FUNC    GLOBAL DEFAULT  UND freecon
    25: 00000000    88 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.0 (2)
    26: 00000000   441 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (2)
    27: 00000000    68 FUNC    GLOBAL DEFAULT  UND mempcpy@GLIBC_2.1 (5)
    28: 00000000    80 FUNC    GLOBAL DEFAULT  UND __memcpy_chk@GLIBC_2.3.4 (6)
    29: 00000000   186 FUNC    GLOBAL DEFAULT  UND _obstack_begin@GLIBC_2.0 (2)
    30: 00000000    19 FUNC    GLOBAL DEFAULT  UND _exit@GLIBC_2.0 (2)
    31: 00000000   441 FUNC    GLOBAL DEFAULT  UND strrchr@GLIBC_2.0 (2)
    32: 00000000   336 FUNC    GLOBAL DEFAULT  UND __assert_fail@GLIBC_2.0 (2)
    33: 00000000    29 FUNC    GLOBAL DEFAULT  UND bindtextdomain@GLIBC_2.0 (2)
    34: 00000000   597 FUNC    GLOBAL DEFAULT  UND mbrtowc@GLIBC_2.0 (2)
    35: 00000000    62 FUNC    GLOBAL DEFAULT  UND gettimeofday@GLIBC_2.0 (2)
    36: 00000000    64 FUNC    GLOBAL DEFAULT  UND __ctype_toupper_loc@GLIBC_2.3 (7)
    37: 00000000    69 FUNC    GLOBAL DEFAULT  UND __lxstat64@GLIBC_2.2 (3)
    38: 00000000   446 FUNC    GLOBAL DEFAULT  UND _obstack_newchunk@GLIBC_2.0 (2)
    39: 00000000   102 FUNC    GLOBAL DEFAULT  UND __overflow@GLIBC_2.0 (2)
    40: 00000000    73 FUNC    GLOBAL DEFAULT  UND dcgettext@GLIBC_2.0 (2)
    41: 00000000   100 FUNC    GLOBAL DEFAULT  UND sigaction@GLIBC_2.0 (2)
    42: 00000000   351 FUNC    GLOBAL DEFAULT  UND strverscmp@GLIBC_2.1 (5)
    43: 00000000   152 FUNC    GLOBAL DEFAULT  UND opendir@GLIBC_2.0 (2)
    44: 00000000    71 FUNC    GLOBAL DEFAULT  UND getopt_long@GLIBC_2.0 (2)
    45: 00000000    64 FUNC    GLOBAL DEFAULT  UND ioctl@GLIBC_2.0 (2)
    46: 00000000    64 FUNC    GLOBAL DEFAULT  UND __ctype_b_loc@GLIBC_2.3 (7)
    47: 00000000   226 FUNC    GLOBAL DEFAULT  UND iswcntrl@GLIBC_2.0 (2)
    48: 00000000    50 FUNC    GLOBAL DEFAULT  UND isatty@GLIBC_2.0 (2)
    49: 00000000   539 FUNC    GLOBAL DEFAULT  UND fclose@GLIBC_2.1 (5)
    50: 00000000    25 FUNC    GLOBAL DEFAULT  UND mbsinit@GLIBC_2.0 (2)
    51: 00000000    54 FUNC    GLOBAL DEFAULT  UND _setjmp@GLIBC_2.0 (2)
    52: 00000000    56 FUNC    GLOBAL DEFAULT  UND tcgetpgrp@GLIBC_2.0 (2)
    53: 00000000    60 FUNC    GLOBAL DEFAULT  UND mktime@GLIBC_2.0 (2)
    54: 00000000   222 FUNC    GLOBAL DEFAULT  UND readdir64@GLIBC_2.2 (3)
    55: 00000000    70 FUNC    GLOBAL DEFAULT  UND memcpy@GLIBC_2.0 (2)
    56: 00000000    76 FUNC    GLOBAL DEFAULT  UND strtoul@GLIBC_2.0 (2)
    57: 00000000   175 FUNC    GLOBAL DEFAULT  UND strlen@GLIBC_2.0 (2)
    58: 00000000   299 FUNC    GLOBAL DEFAULT  UND getpwuid@GLIBC_2.0 (2)
    59: 00000000   186 FUNC    GLOBAL DEFAULT  UND acl_extended_file@ACL_1.0 (8)
    60: 00000000  1931 FUNC    GLOBAL DEFAULT  UND setlocale@GLIBC_2.0 (2)
    61: 00000000    37 FUNC    GLOBAL DEFAULT  UND strcpy@GLIBC_2.0 (2)
    62: 00000000   148 FUNC    GLOBAL DEFAULT  UND raise@GLIBC_2.0 (2)
    63: 00000000   178 FUNC    GLOBAL DEFAULT  UND fwrite_unlocked@GLIBC_2.1 (5)
    64: 00000000   293 FUNC    GLOBAL DEFAULT  UND clock_gettime@GLIBC_2.2 (9)
    65: 00000000   123 FUNC    GLOBAL DEFAULT  UND getfilecon
    66: 00000000    98 FUNC    GLOBAL DEFAULT  UND closedir@GLIBC_2.0 (2)
    67: 00000000   403 FUNC    GLOBAL DEFAULT  UND fwrite@GLIBC_2.0 (2)
    68: 00000000   174 FUNC    GLOBAL DEFAULT  UND sigprocmask@GLIBC_2.0 (2)
    69: 00000000    32 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (10)
    70: 00000000    42 FUNC    GLOBAL DEFAULT  UND __fpending@GLIBC_2.2 (3)
    71: 00000000   123 FUNC    GLOBAL DEFAULT  UND lgetfilecon
    72: 00000000   223 FUNC    GLOBAL DEFAULT  UND error@GLIBC_2.0 (2)
    73: 00000000   299 FUNC    GLOBAL DEFAULT  UND getgrgid@GLIBC_2.0 (2)
    74: 00000000    75 FUNC    GLOBAL DEFAULT  UND __strtoull_internal@GLIBC_2.0 (2)
    75: 00000000   115 FUNC    GLOBAL DEFAULT  UND sigaddset@GLIBC_2.0 (2)
[...]

如您所见,“/bin/ls”可执行文件只有动态符号,即由动态链接器解析的符号。您可能注意到某些符号的名称后面附加了“@GLIBC_2.2”或类似字符串:这些字符串不是名称的一部分,“readelf”将其附加,它实际上会解析 GNU 的版本信息。如果您想知道如何读取版本信息,请查看“readelf”的源代码,它可以在“binutils”包中找到。

重定位表

重定位表包含一个条目数组,每个条目描述如何重定位代码/数据节。让我们尝试更好地理解重定位过程:代码节中的各种指令实际上引用其他地方(代码节内或其他位置)的对象(变量和/或函数)。要访问这些对象,需要一个绝对地址:对于可执行文件来说,这可能不是问题,因为它们总是以相同的基地址加载,但库不是,所以我们需要一种方法通过“操作”引用它们的绝对地址来定位这些对象。这正是重定位的作用:它们指示动态链接器(或加载程序)如何操作它们引用的地址,因为重定位将始终引用一个需要以某种方式操作的地址。有各种类型的重定位,但对于本文,重要的是 x86 架构的重定位。重定位有两种大类:RELA 和 REL。在 x86 架构上,只有 REL 类型的重定位(而在 x64 上,只有 RELA 类型的重定位)。这是 REL 重定位结构:

typedef struct
{
  Elf32_Addr    r_offset;        /* Address */
  Elf32_Word    r_info;          /* Relocation type and symbol index */
} Elf32_Rel;
  • r_offset: 必须应用重定位的偏移量。
  • r_info: 一个字,包含重定位类型,以及重定位是否与符号关联;它将包含正在使用的符号表中的符号索引。

本文中感兴趣的重定位类型将是 R_386_JMP_SLOT,它将在最后一章的最后一节中描述。现在是时候举一个涉及重定位的例子了。使用的共享对象是“libhook.so”,它将在文章的下一部分构建,但这只是一个示例,所以如果您无法重现它,请不要担心(您可以尝试使用另一个库重现它,作为一项练习)。让我们看一下“libhook.so”的前几个重定位:

quake2@quake2-desktop:~/elfinj$ readelf -r libhook.so

Relocation section '.rel.dyn' at offset 0x3b4 contains 49 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000679  00000008 R_386_RELATIVE   
0000069d  00000008 R_386_RELATIVE   
000006b8  00000008 R_386_RELATIVE   
000006c5  00000008 R_386_RELATIVE   
000006ca  00000008 R_386_RELATIVE

从这个输出可以清楚地看出,正在使用的重定位是 R_386_RELATIVE,因此根据 ELF 手册:

R_386_RELATIVE: 链接编辑器为动态链接创建此重定位类型。其偏移量成员给出了共享对象内的一个位置,该位置包含一个代表相对地址的值。动态链接器通过将共享对象加载到的虚拟地址与相对地址相加来计算相应的虚拟地址。此类型的重定位条目必须为符号表索引指定 0。

因此,r_offset 字段包含必须应用重定位的地址。让我们选择偏移量为 0x000006b8 的重定位。这是该地址处的内容:

6b5:    c7 04 24 81 0a 00 00     movl   $0xa81,(%esp)
6bc:    e8 fc ff ff ff           call   6bd <inithooklib+0x2d>

重定位引用的偏移量是“movl”指令的实际参数,用 Intel 语法重写是“mov [esp], 0x00000A81”,因此重定位将应用于“mov”右侧的立即值,该值位于偏移量 0x6B8 处。根据描述,很明显引用的值必须加上映像基址(即,库映像在内存中驻留的地址)才能形成一个有效的绝对地址。这些类型的重定位没有与之关联的符号。重定位过程由链接器在准备 ELF 文件的内存映像时执行。请查阅 ELF 手册以获取 x86 架构的有效重定位类型列表。

动态节

将要描述的最后一个结构是动态节。此节包含动态链接器的信息,特别是关于在映像加载后如何检索动态符号、加载映像所需的库、动态重定位等。通常,动态节包含在自己类型为 SHT_DYNAMIC 的节中;一旦 ELF 加载到内存中,动态节就可以从类型为 PT_DYNAMIC 的程序头中检索。照例,动态节是由各种条目组成的表。表以 NULL 条目结束。让我们看结构:

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;
  • d_tag: 条目的类型;本文感兴趣的条目是 DT_PLTRELSZDT_SYMTABDT_STRTABDT_RELDT_JMPREL,因此请务必在 ELF 手册中查看它们并理解它们的工作原理。类型为 DT_SYMTAB 的条目给出包含动态符号的符号表,类型为 DT_STRTAB 的条目给出包含动态符号名称的字符串表。因此,如前所述,在使用哪个符号表/字符串表方面没有歧义。
  • d_ptr: 由符号标识的条目的虚拟地址。如果我们处理的是可执行文件,这将是虚拟地址;否则,它将是相对虚拟地址(需要加到映像基址上)。

这是“/bin/ls”的动态节:

quake2@quake2-desktop:~$ readelf -d /bin/ls

Dynamic section at offset 0x16f04 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [librt.so.1]
 0x00000001 (NEEDED)                     Shared library: [libselinux.so.1]
 0x00000001 (NEEDED)                     Shared library: [libacl.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8049508
 0x0000000d (FINI)                       0x805af7c
 0x00000004 (HASH)                       0x8048188
 0x6ffffef5 (GNU_HASH)                   0x80484b8
 0x00000005 (STRTAB)                     0x8048ba4
 0x00000006 (SYMTAB)                     0x8048514
 0x0000000a (STRSZ)                      1199 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x805fff4
 0x00000002 (PLTRELSZ)                   744 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8049220
 0x00000011 (REL)                        0x80491f8
 0x00000012 (RELSZ)                      40 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x8049128
 0x6fffffff (VERNEEDNUM)                 3
 0x6ffffff0 (VERSYM)                     0x8049054
 0x00000000 (NULL)                       0x0

当然,动态节不包含任何节索引,因为一旦映像加载到内存中,节就会失去其意义,所以只有(相对)虚拟地址。从“/bin/ls”的动态节可以看出,该可执行文件需要四个库才能运行,即:“librt.so.1”、“libselinux.so.1”、“libacl.so.1”和“libc.so.6”。

哈希表

如果您更深入地查看节列表,您会找到一个类型为 SHT_HASH 的节。这意味着该节包含一个哈希表。哈希表用于快速符号查找,但它们不在此文章中使用,因此没有什么需要关心的,除了一个方面:每个哈希表都有一个名为 nchains 的字段(请参阅通用手册第 94 页的图 5-11),它等于已哈希的符号名称的总数。因此,此字段给出符号的总数,它将在第二部分用于执行符号查找(以知道何时停止搜索特定符号)。

再写一些代码...

对 ELF 格式的简要描述已经结束,现在是时候看更多代码片段了。它们在此:

static char *btypes[] = {
        "STB_LOCAL",
        "STB_GLOBAL",
        "STB_WEAK"
};

static char *symtypes[] = {
        "STT_NOTYPE",
        "STT_OBJECT",
        "STT_FUNC",
        "STT_SECTION",
        "STT_FILE"
};

void print_bind_type(u_char info)
{
    u_char bind = ELF32_ST_BIND(info);
    if(bind <= 2)
        printf("- Bind type: %s\n", btypes[bind]);
    else
        printf("- Bind type: %d\n", bind);
}

void print_sym_type(u_char info)
{
    u_char type = ELF32_ST_TYPE(info);

    if(type <= 4)
        printf("- Symbol type: %s\n", symtypes[type]);
    else
        printf("- Symbol type: %d\n", type);
}

int print_sym_table(u_char *filebase, Elf32_Shdr *section, char *strtable)
{
    Elf32_Sym *symbols;
    size_t sym_size = section->sh_entsize;
    size_t cur_size = 0;

    if(section->sh_type == SHT_SYMTAB)
        printf("Symbol table\n");
    else
        printf("Dynamic symbol table\n");

    if(sym_size != sizeof(Elf32_Sym))
    {
        printf("There's something evil with symbol table...\n");
        return 0;
    }

    symbols = (Elf32_Sym *)(filebase + section->sh_offset);
    symbols++;
    cur_size += sym_size;
    do
    {
        printf("- Name index: %d\n", symbols->st_name);
        printf("- Name: %s\n", strtable + symbols->st_name);
        printf("- Value: 0x%08x\n", symbols->st_value);
        printf("- Size: 0x%08x\n", symbols->st_size);

        print_bind_type(symbols->st_info);
        print_sym_type(symbols->st_info);

        printf("- Section index: %d\n", symbols->st_shndx);
        cur_size += sym_size;
        symbols++;
    } while(cur_size < section->sh_size);

    return 1;
}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    char *p_strtable = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;
    Elf32_Phdr *p_phdr = NULL;
    Elf32_Shdr *p_shdr = NULL;
    int i;

    if(argc < 2)
    {
        printf("Usage: %s \n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
    
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
    {
        print_elf_header(p_ehdr);

        printf("\n");
        p_phdr = (Elf32_Phdr *)(p_base + p_ehdr->e_phoff);
        p_shdr = (Elf32_Shdr *)(p_base + p_ehdr->e_shoff);
        p_strtable = (char *)(p_base + p_shdr[p_ehdr->e_shstrndx].sh_offset);

        for(i = 0; i < p_ehdr->e_phnum; i++)
        {
            print_program_header(&p_phdr[i], i);
        }

        for(i = 0; i < p_ehdr->e_shnum; i++)
        {
            print_section_header(&p_shdr[i], i, p_strtable);
            if(p_shdr[i].sh_type == SHT_SYMTAB || p_shdr[i].sh_type == SHT_DYNSYM)
            {
                printf("This section holds a symbol table...\n");

                //being a symbol table, the field sh_link of the section header
                //will hold an index into the section table which gives the
                //section containing the string table
                print_sym_table(p_base, &p_shdr[i], 
                  (char *)(p_base + p_shdr[p_shdr[i].sh_link].sh_offset));
            }
        }
    }
    else
        printf("Invalid ELF file\n");

    free(p_base);
    return 0;
}

作为练习,您应该编写代码来打印出动态节(现在您应该知道如何做)。

ELF 加载

本节是对 ELF 映像加载过程的简要描述。主要内容将是 PLT,因此将更关注该主题。但它将提供加载过程的总体概述。有关详细信息,您应该阅读手册(它对加载过程提供了非常好的描述,并附有各种内存配置示例)。

程序头

程序头描述如何将文件的一个或多个节映射到内存(例如,PT_LOAD 类型),并且可以包含在加载过程结束后运行时可能有用信息。某些程序头具有特殊作用:

  • PT_INTERP: 此段描述一个内存区域,该区域包含一个字符串。此字符串是文件的绝对路径,该文件是与系统加载程序协作以实际加载 ELF 的程序/库。在为 Linux 构建的 ELF 中,此程序头包含:
  • quake2@quake2-desktop:~$ readelf -l /bin/ls
    [...]
      INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
          [Requesting program interpreter: /lib/ld-linux.so.2]
    [...]

    程序头要求 /lib/ld-linux.so.2 的协作,这是 Linux 中的 ELF 加载程序。

    quake2@quake2-desktop:~$ /lib/ld-linux.so.2
    Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
    You have invoked `ld.so', the helper program for shared library executables.
    This program usually lives in the file `/lib/ld.so', and special directives
    in executable files using ELF shared libraries tell the system's program
    loader to load the helper program from this file. This helper program loads
    the shared libraries needed by the program executable, prepares the program
    to run, and runs it. You may invoke this helper program directly from the
    command line to load and run an ELF executable file; this is like executing
    that file itself, but always uses this helper program from the file you
    specified, instead of the helper program file specified in the executable
    file you run. This is mostly of use for maintainers to test new versions
    of this helper program; chances are you did not intend to run this program.
    
      --list                list all dependencies and how they are resolved
      --verify              verify that given object really is a dynamically linked
                            object we can handle
      --library-path PATH   use given PATH instead of content of the environment
                            variable LD_LIBRARY_PATH
      --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                            in LIST
  • PT_LOAD: 此段类型包含有关如何映射通常包含数据和/或代码的节的信息。
  • PT_DYNAMIC: 此段包含链接器的信息,关于在哪里查找符号、字符串表、重定位等...通常,其内容与动态节相同。

动态节

动态节包含动态链接可执行文件/库所需的所有信息。让我们来谈谈动态链接:这是实际应用重定位到 ELF 映像的过程,并且,如果使用惰性绑定,它将解析任何尚未解析的外部符号。动态节还包含并非严格用于动态链接的信息,例如初始化/最终化函数的入口点。下一段将完全介绍 PLT,这是动态符号在运行时解析的过程。以下是重要动态类型列表:

  • DT_JMPREL: 此字段包含一个表,该表包含仅与 PLT 相关的重定位条目数组。这样,如果启用了惰性绑定,链接器将在加载期间忽略这些重定位。
  • DT_PLTREL: 此字段包含 PLT 重定位的类型。对于 x86 架构,只能有 REL 重定位。
  • DT_PLTRELSZ: 由 DT_JMPREL 寻址的表的大小(字节)。
  • DT_NEEDED: 一个字符串,包含加载映像所需的库的名称。照例,此字段是字符串表中的一个索引,该字符串表由 DT_STRTAB 动态条目寻址。
  • DT_INIT: 此字段包含要在初始化时调用的函数的地址。如果映像是可执行文件,它将在“main”之前执行;否则,如果映像是库,它将在控制权返回到主可执行文件之前执行。
  • DT_FINI: 此字段包含要在最终化时调用的函数的地址。最终化函数调用的顺序与初始化函数相反。

PLT

过程链接表 (Procedure Linkage Table) 是一个表,其中的各个条目由代码块组成。它是允许动态链接器解析外部函数的主要组件。例如:假设您编写了一个引用库中定义的函数的程序:

int main()
{
    int res = external_function(3,4);
    return 0;
}

当然,要调用该函数,您需要知道其绝对地址,因为它是外部的。绝对地址不能由链接器硬编码,因为库通常以不同的基地址加载,因此绝对地址对它们没有意义。这种情况通过使用 PLT 来克服,因此当您调用外部函数时,会生成以下代码:

push 0x04
push 0x03
call external_function@plt
add esp, 8

[,..]

; this is a PLT entry
external_function@plt (address 0xXXXXXX00):
  ; reloc_address is just a memory location
  external_function@plt+0x00: jmp dword ptr [reloc_address]
  ; reloc_offset is a byte offset (not an index) into the relocation table
  external_function@plt+0x06: push reloc_offset
  ; resolve_function is a function that will resolve the external symbol
  external_function@plt+0x0B: jmp resolve_function
  
; this is what you find at reloc_address, data is displayed using dwords
reloc_address: XXXXXX06 ........

这里发生的是,当程序执行到“call”时,它会将执行转移到一个 PLT 条目。然后执行的第一条指令是“jmp”,它会将执行转移到“reloc_address”位置中包含的值,正如您所看到的,这是 PLT 条目中第一个“jmp”之后的指令的地址。因此,回到 PLT,一个字节偏移量被“PUSH”到堆栈上,然后执行转移到一个函数,该函数通过从堆栈中取出最后推入的值来解析外部符号。到目前为止,您可能认为这个过程非常缓慢,充满了那些会杀死缓存的跳转。但是,让我们更进一步,看看在外部符号解析后会发生什么:

push 0x04
push 0x03
call external_function@plt
add esp, 8

[...]

external_function@plt:
    external_function@plt+0x00:  jmp dword ptr [reloc_address]
    external_function@plt+0x06:  push reloc_index
    external_function@plt+0x0B:  jmp  resolve_function
    
reloc_address: BFF31337 ........

有什么改变了吗?在外部符号解析后,“reloc_address”寻址的内存位置将不再包含第一个 jmp 指令后的指令的地址,而是包含外部函数的实际入口点,因此所有的“jmp”疯狂只会在第一次发生。如果您不理解任何内容,请仔细阅读手册。PLT 是这个两部分文章中最重要的事情,下一部分将更深入地讨论它,所以请确保您了解它的工作原理。无论如何,在本部分结束时,将有一个使用 GDB 的实际示例来说明 PLT 的工作原理。

搭建我们的实验环境...

在对 ELF 格式进行简要介绍后,现在是时候开始准备我们的“实验室”了,也就是说,构建一组我们将用于下一部分的示例 ELF。我们将构建三个 ELF:一个可执行文件,两个库,其中一个将是将被注入到可执行文件并钩取另一个库(可执行文件直接使用该库)中的函数的库。让我们开始构建我们将使用的第一个和第二个映像文件:可执行文件和它使用的库。这是库的源代码,只显示了 .c 文件:

#include "libdummy.h"

int dummy_add(int a, int b)
{
    return a+b;
}

编译并链接它

gcc -fPIC -c libdummy.c
ld -shared -soname libdummy.so.1 -o libdummy.so.1.0 -lc libdummy.o

现在,我们使用“ldconfig”更新缓存(如果您将库移动到 /usr/lib 或任何其他系统路径,则可能需要删除“-n .”参数)。

ldconfig -v -n .

并创建链接器所需的符号链接,以便我们可以使用“-ldummy”进行链接。

ln -sf libdummy.so.1 libdummy.so

这是使用刚构建的库的可执行文件:

#include <stdio.h>
#include "libdummy.h"

int main()
{
    int a,b;
    int res = 0;

    printf("Enter the first number: ");
    scanf("%d", &a);
    printf("Enter the second number: ");
    scanf("%d", &b);
    res = dummy_add(a,b);
    printf("Result is: %d\n", res);
    return 0;
}

编译并链接它

gcc -o dummyelf dummyelf.c -L. -ldummy

不要忘记尝试查看一切是否正常工作(如果您没有将 libdummy.so.1.0 移动到 /usr/lib,您应该设置 LD_LIBRARY_PATH:“export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH”)。

暂时就到这里;在下一章中,我们将探索 PLT。我们将在本文的下一部分构建第二个库。

PLT:一个实际的例子

PLT 已经详细讨论过,但没有什么比实际例子更好的了。所以,让我们来玩玩刚构建的可执行文件。首先,让我们用 GDB 调试可执行文件,在“dummy_add”调用上设置断点(请记住,您的地址可能不同)。

quake2@quake2-desktop:~/elfinj$ gdb dummyelf
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http:>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) disassemble main
Dump of assembler code for function main:
[...]
0x08048507 <main+99>:    call   0x80483cc <dummy_add@plt>
[...]
End of assembler dump.
(gdb) break *0x08048507
Breakpoint 1 at 0x8048507
(gdb)

启动程序并单步执行直到中断,然后单步执行直到进入调用。

(gdb) display/i $pc
(gdb) run
Starting program: /home/quake2/elfinj/dummyelf 
Enter the first number: 23
Enter the second number: 23

Breakpoint 1, 0x08048507 in main ()
1: x/i $pc
0x8048507 <main+99>:    call   0x80483cc <dummy_add@plt>
Current language:  auto; currently asm
(gdb) stepi
0x080483cc in dummy_add@plt ()
1: x/i $pc
0x80483cc <dummy_add@plt>:    jmp    *0x804a00c
(gdb)

所以,正如预期的那样,第一条指令是跳转到 0x804a00c 地址处的值;让我们看看那里有什么:

(gdb) print /x *0x804a00c
$1 = 0x80483d2
(gdb)

它包含 0x80483d2,这是第一个 jmp 指令之后的指令的地址;可以通过反汇编 eip 周围的指令来验证这一点。

(gdb) disassemble
Dump of assembler code for function dummy_add@plt:
0x080483cc <dummy_add@plt+0>:    jmp    *0x804a00c
0x080483d2 <dummy_add@plt+6>:    push   $0x18
0x080483d7 <dummy_add@plt+11>:    jmp    0x804838c <_init+48>
End of assembler dump.
(gdb)

我告诉过您,jmp 指令之后的指令会将一个偏移量推入堆栈,该偏移量是重定位表中一个字节的偏移量;让我们检查一下是否属实:获取重定位表的基地址(DT_JMPREL 动态条目的值)并加上偏移量:

(gdb) print /x *(0x8048334+0x18)
$4 = 0x804a00c
(gdb)

如果您检查 Elf32_Rel 结构,您会看到第一个字段的类型是 Elf32_Addr,这是一个 32 位无符号整数,并且包含重定位必须应用的地址。动态链接器将 0x804a00c 处的值替换为 dummy_add 函数的实际地址。

0x80483d7 <dummy_add@plt+11>:    jmp    0x804838c <_init+48>
(gdb) step
Single stepping until exit from function dummy_add@plt, 
which has no line number information.
0xb8095168 in dummy_add () from ./libdummy.so.1
1: x/i $pc
0xb8095168 <dummy_add>:    push   %ebp
(gdb) print /x *0x804a00c
$1 = 0xb8095168
(gdb)

内存位置 0x804a00c 现在包含 dummy_add 的虚拟地址,即 0xb8095168

结论

本教程应该向读者介绍了 ELF 格式的基础知识以及系统(在本例中是 Linux)如何加载它。本部分绝不旨在取代 ELF 手册,ELF 手册是下一部分(事情将变得复杂得多)的必读材料。我决定将文章分成两部分,所以如果您熟悉 ELF 格式,可以跳到第二部分。此外,由于有很多代码和其他占用空间的章节,制作一个大的单个文章会产生一个非常长的 HTML 文件,这很不方便。事情应该简单明了。

下一部分将介绍实际的注入和钩取。将涵盖的论点包括:ptrace 接口及其示例、代码注入(概览)、共享对象注入及其局限性,以及 PLT 钩取(将使用迄今学到的所有概念)。下一部分预计将在未来几周内发布(希望不超过 2 周),具体取决于我的时间安排(我是一名有全职工作的大学生,很难找到空闲时间 : ))。希望您喜欢这第一部分...下次再见!

参考文献

在这里,您将找到本文使用的所有工具/书籍。

历史

  • 2008/10/11:初稿。
© . All rights reserved.