BLOG

Record, summarize, and improve.

ELF

ELF文件作用:

第一个是告诉内核将磁盘上的 ELF 文件中的内容放置在内存中的位置,以及提供调用动态加载器函数的方法,甚至可能帮助提供一些调试信息。

ELF Header and Identification fields(ELF 标头和标识字段)

typedef struct {
  uint8_t  e_ident[16];
  uint16_t e_type;
  uint16_t e_machine;
  uint32_t e_version;
  uint64_t e_entry;
  uint64_t e_phoff;
  uint64_t 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;
} Elf64_Ehdr;

e_ident文件之后的下一个文件是e_type 。在上面的示例中,我声称该类型是EXEC之一(因为它读取0x02 0x00 ) - 根据 ELF 标准,这意味着它要被执行(检查标准将确认这一点)。

e_entry字段列出了文件中程序应开始执行的偏移量。通常它指向您的_start方法(当然,如果您使用通常的东西编译它)。您可以将e_entry指向您喜欢的任何位置,

e_flags字段包含许多特定于体系结构的值,有时还指示有关 ABI 的信息。

e_shstrndx字段保存节头表中.shstrtab的索引。该节只是节名称的数组(也由 readelf 使用),提供一些解释语义。

Program Headers

typedef struct {
  uint32_t p_type;
  uint32_t p_flags;
  uint64_t p_offset;
  uint64_t p_vaddr;
  uint64_t p_paddr;
  uint64_t p_filesz;
  uint64_t p_memsz;
  uint64_t p_align;
} Elf64_Phdr;

程序头(以及其他功能)通常用于告诉内存加载器将东西放在哪里。

程序头有多种类型。他们每个人都有不同的目的:

  • 0x00000006 PHDR - 指示程序头表本身的开始。根据文档,本节需要一个可加载段条目,但在这里我们看到它是通过PT_INTERP进行的,这意味着这不是真的!
  • 0x00000003 PT_INTERP - 此部分指示将作为 ELF 解释器调用的程序路径名(如果它是可执行文件)。
  • 0x00000001 PT_LOAD - 最重要的程序头类型。定义必须将文件的一部分放置在内存中的方式。

Section Headers

typedef struct {
  uint32_t sh_name;
  uint32_t sh_type;
  uint64_t sh_flags;
  uint64_t sh_addr;
  uint64_t sh_offset;
  uint64_t sh_size;
  uint32_t sh_link;
  uint32_t sh_info;
  uint64_t sh_addralign;
  uint64_t sh_entsize;
} Elf64_Shdr;

sh_name - 包含节名称的.strtab的索引

sh_type - 节类型( SHT_NULL、SHT_DYN 、...)

sh_flags - 执行期间此部分的内存属性( SHF_WRITE、SHF_ALLOC 、...)

sh_addr - 文件中此部分开始的地址

sh_link - 将一个节与此节关联,字段值可以取决于sh_type

.symtab 主符号表

.interp 解释器的路径名

.dynsym 动态符号表

Image in a image block

.shstrtab实际上是用来引用各部分的名称的。

sh_type 的字段

  • SHT_NULL - 纯粹用于存储空字节,文档将其称为直接将节标记为未使用,并且很可能会被大多数语义驱动的 ELF 实用程序跳过。
  • SHT_PROGBITS - 这只是一个部分的标记,表示它可以包含任何内容,其格式通常由正在执行的程序决定。 PROGBIT几乎适用于程序特定的行为 - 可以是任何东西 - 字面上任何东西,甚至图灵完成任何东西!这些通常用于标记包含要执行的实际代码、数据部分、初始化/终结过程的部分(或者甚至可能是特定于 ABI 或生成可执行代码部分和伴奏的编译器的更狂野的概念 - 同样,此部分类型不确实施加了很多格式控制
  • SHT_SYMTAB -这提供了一个指向应该具有符号表格式的部分的指针
  • SHT_STRTAB - 保存以 null 结尾的字符串列表的部分。
  • SHT_HASH -此部分用于保存哈希表,通常是为了加快查找符号的速度。事实上,文档表明,如果可执行文件参与动态链接,它必须具有这些部分之一。
  • SHT_NOTE类型部分是简单的整数列表,为供应商提供版本控制和类型。

看看十六进制转储中典型的SHT_STRTAB是什么样的:

Image in a image block

正如您所看到的,字符串很好,并且由空字节整齐地分隔,在 C 中读取字符串时非常容易不会弄乱它:)))。.shstrtab包含节名称,

通过移动shstrtab部分的开头,我们看到字符串对于每个条目向下跳转 8 个字节。更准确地说,我们可以说它们都是从 8 个字节开始,但因为它们是字符串,所以readelf会读取字节直到遇到空字节。例如,我们可以看到第一个节名称而不是原来位于0x1910的.interp现在指向0x1917 。 .interp部分通常是第一个有效部分,现在称为.note.ABI-tag。接下来的节名称(从 8 个字节开始)是I-tag (因为它从0x191F开始),然后读取直到它到达0x1924处的空字节。其余的节遵循相同的模式

C Start Up

_start方法本质上需要做的是对__libc_start_main执行函数调用,这意味着我们需要一个指向 main 函数的指针作为__libc_start_main 的参数

LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
   int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
   ElfW(auxv_t) *auxvec,
#endif
   __typeof (main) init,
   void (*fini) (void),
   void (*rtld_fini) (void), void *stack_end)
  • int (*main) 这是指向二进制文件中 main 方法的指针
    • int argc 从命令行传递给二进制文件的参数数量,包括二进制文件的名称
    • char **argv 保存实际字符串的数组,记住这里的一些术语很重要,argv 本质上是通过堆栈指针传递给_start函数的。
  • __typeof (main) init 指向处理调用初始化程序或构造函数的函数 ( __libc_csu_init ) 的指针。我将其称为构造函数“调用处理程序”
  • void (*fini) (void) 这是一个类似的函数指针,指向处理调用析构函数的函数指针。
  • void (*rtld_fini) 动态链接器的析构函数调用处理程序,该值通过edx从加载器传递给_start
  • void *stack_end 堆栈结束标记

__libc_start_main在此之后所做的事情还有一些其他有趣的细节,其中一些涉及深层精灵魔法,例如读取 argv 的值来查找 envp。互联网上有关于这方面的精彩文章,并且__libc_start_main的代码也可用。我想你们会喜欢确认它按描述工作的练习。

总结__libc_start_main ,并将.init_array和.fini_array引入上下文。本质上,start_main 所做的事情是这样的:

其他一些很酷的东西,当然最终会调用(*init) ,它在start_main的上下文中意味着__libc_csu_init。正如我们在脚注中看到的,这个函数实际上调用了我们定义的 init 函数。

C启动过程

Symbol Table and Relocations

typedef struct {
  uint32_t st_name;
  uint8_t  st_info;
  uint8_t  st_other;
  uint16_t st_shndx;
  uint64_t st_value;
  uint64_t st_size;
} Elf64_Sym;

有两个符号表,即主符号表(节标题中的.symtab )和动态符号表.dynsym ,它只是主符号表中条目的较小子集。这是一个较小的副本,仅与动态链接器相关。

  • st_name - .strtab中的索引,保存符号的空终止名称中的第一个字节。并非所有符号都有名称,如果没有名称,则此部分的值为0x0000 。
  • st_info - 确定符号的一些属性的位字段。即 C 程序中的“范围”和符号类型,旨在帮助重定位。它将指示它是函数、变量还是其他东西。其工作方式与真正的 C 风格中的每个位字段非常相似,它通过宏传递。该宏应用位掩码、移位来隔离专用于某些属性的位字段中的偏移量。
  • st_other - 这是一个用于确定符号可见性的位字段。控制如何允许代码在特定上下文中引用变量的属性。这是 glibc 用于提取可见性值的宏:
    617 /* How to extract and insert information held in the st_other field.  */
    618 
    619 #define ELF32_ST_VISIBILITY(o)   ((o) & 0x03)
    620 
    621 /* For ELF64 the definitions are the same.  */
    622 #define ELF64_ST_VISIBILITY(o)   ELF32_ST_VISIBILITY (o)

符号的可见性类型包括(也可从上图中获得):

  • STV_DEFAULT 0x00 - 这意味着这是默认的可见性规则
  • STV_INTERNAL 0x01 - 处理器特定的隐藏类
  • STV_HIDDEN 0x02 - 表示该符号不可用于其他模块中的参考
  • STV_PROTECTED 0x03 - 文档将其称为受保护符号。我相信这个符号和普通的STV_DEFAULT符号之间唯一的区别是,当从它自己的共享库中引用它时,它不允许被覆盖。

  • st_shndx字段指示与该符号关联的节索引。符号以这种方式与节相关联,因为定义为符号的所有内容都可能有一个关联的节 -例如变量值将在哪里定义?可能是.data *-esq 部分不是?有几个特殊的节号,表示与符号相关的节的一些信息,这些节号可以采用几个值,请查看glibc/elf.h:414+以了解这些值的范围。
  • st_value符号的值,根据符号类型有不同的解释:
    • 在可执行文件和共享对象中,该文件保存符号定义的虚拟地址。
    • 对于可重定位文件,该值在很大程度上指示定义符号的位置的偏移量。
    • 对于st_shndx是SHN_COMMON的符号, st_value将在重新定位时保留对齐约束
  • st_size符号的大小,指示该符号所代表的内容将占用多少字节,这又取决于符号类型 -大多数情况下是变量数据字段的大小或函数代码的大小。