ELF文件格式

当初写的时候,没想着发出来,所以有些网图的地址没有保存,等我找到了再补上。
这篇文章和我的另一篇关于ret2resolve的实验配合使用

ELF文件

ELF是linux下的文件格式,可分为三类

  • 可执行文件(.out):包含代码和数据,是可以直接运行的程序。其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行。
  • 可重定位文件(.o):包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。
  • 共享目标文件(.so):也称动态库文件,包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用。

文件视图

ELF文件分为两种试图,分别是编译时的“链接视图”和程序运行时的“执行试图”。
ELF文件格式_第1张图片注意,图1中Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
ELF文件格式_第2张图片
链接视图以section为单位,执行视图以segment为单位。ELF文件之所以拥有链接视图和执行视图,是为了满足程序在不同阶段的需求,实现了更高的灵活性和效率。

  • 链接视图:在链接阶段,编译器和链接器使用链接视图来处理ELF文件。链接视图关注的是代码和数据的逻辑组织,例如函数、变量的定义和声明,代码的逻辑结构等。这个视图有助于将不同的源文件和库合并成一个完整的可执行文件或共享库。在链接视图中,各个节(sections)扮演了关键角色,它们是代码和数据的逻辑组织单位。例如,代码部分会放在.text节,只读数据在.rodata节,可读写数据在.data节等。这种分节的方式使得程序的各个部分可以独立地被处理和链接。
  • 执行视图:在程序运行时,操作系统和加载器使用执行视图来处理ELF文件。执行视图关注的是代码和数据在内存中的实际布局,以及加载时的内存映射和权限设置。执行视图对节进行分组,形成了段(segments),每个段代表着一块连续的内存区域,包含了执行时所需的相关节的数据。这种分段的方式有助于提高内存加载效率和运行时的性能。

如下,给出使用readelf -l命令查看的一个链接后的elf可执行文件和section到segment的映射关系:

Elf 文件类型为 DYN (共享目标文件)
Entry point 0x1070
There are 13 program headers, starting at offset 64

程序头:
  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
                 0x0000000000000668 0x0000000000000668  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x000000000000020d 0x000000000000020d  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000198 0x0000000000000198  R      0x1000
  LOAD           0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000258 0x0000000000000260  RW     0x1000
  DYNAMIC        0x0000000000002df8 0x0000000000003df8 0x0000000000003df8
                 0x00000000000001e0 0x00000000000001e0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000358 0x0000000000000358 0x0000000000000358
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x0000000000002028 0x0000000000002028 0x0000000000002028
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000218 0x0000000000000218  R      0x1

 Section to Segment mapping:
  段节...
   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 .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .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 

(可以看到每个segment都是section的组合)

文件格式

头部结构
typedef struct elf32_hdr
{
	  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 {
        unsigned char   e_ident[EI_NIDENT];
        Elf64_Half      e_type;
        Elf64_Half      e_machine;
        Elf64_Word      e_version;
        Elf64_Addr      e_entry;
        Elf64_Off       e_phoff;
        Elf64_Off       e_shoff;     // 重要: 我们下一步的解析目标就从这里开始,这个段表的文件内偏移
        Elf64_Word      e_flags;
        Elf64_Half      e_ehsize;
        Elf64_Half      e_phentsize;
        Elf64_Half      e_phnum;
        Elf64_Half      e_shentsize;
        Elf64_Half      e_shnum;     // 重要: 这里给出了段表中的表项的个数
        Elf64_Half      e_shstrndx;  // 重要: 这里给出了段表中引用的用于表示段名的字符串表的段表项索引
} Elf64_Ehdr;
typedef unsigned          char uint8_t;//给无符号char,取别名为uint8_t
typedef unsigned short     int uint16_t;//给无符号短整型short int,取别名为uint16_t
typedef unsigned           int uint32_t;//给无符号整型short int,取别名为uint32_t
typedef unsigned       __INT64 uint64_t;

/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half; // Elf64_Half也是这个定义
/* Types for signed and unsigned 32-bit quantities.  */
typedef uint32_t Elf32_Word; // Elf64_Word同此
/* 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;

使用hexdump查看16进制内容,并套用上述的结构体。使用readelf -h查看头部信息如下
ELF文件格式_第3张图片可以看到我这是64位的elf程序

头部各字段信息

unsigned char e_ident[EI_NIDENT]; // EI_NIDENT=16
图4
Elf64_Half e_type //2个子节
图5,e_type的可取值如下
ELF文件格式_第4张图片
Elf64_Half e_machine //2个子节
图7其可取值如下
ELF文件格式_第5张图片
Elf64_Word e_version //4个子节
图9,其取值如下
ELF文件格式_第6张图片
Elf64_Addr e_entry //8个子节。e_entry代表的是程序的入口地址,上图和使用readelf得到的结果是一样的
图11

Elf64_Off e_phoff //8个子节。该字段表示程序表头偏移,程序头表即Program Header Table,程序头表包含了关于可执行文件或共享目标文件中各个段(segment)的信息,如段的加载位置、权限、大小等。而e_phentsize表示每个程序头表条目(Program Header Entry)的大小,程序头表条目包含了一个段的相关信息,比如段的类型、文件偏移量、虚拟地址、大小等。e_phnum表示程序头表中的条目数量,所以程序头表打大小可以这么计算e_phentsize * e_phnum
图12
Elf64_Off e_shoff //8个子节。用于表示节头表(Section Header Table)在 ELF 文件中的偏移量。头表包含了关于各个节(sections)的信息,如节的名称、大小、偏移量等。其大小可以这么计算e_shentsize * e_shnum,符号代表的含义和上边一样。
图13
Elf64_Word e_flags //4个子节。字段的具体含义取决于不同的架构和用途,它可能包含一些特定的标志位,用于标识特定的属性或行为。
图14
Elf64_Half e_ehsize //2个子节。用于表示 ELF 文件头(ELF Header)的大小。
图15
Elf64_Half e_shstrndx //2个子节,用于表示节名称字符串表(Section Name String Table)的索引。即包含了所有的节的名称。
图16
之前提到的计算程序头表和节头表大小的字段展示如下:
图17
根据readelf得到的header的长度为64字节,即0x40h,所以header到0x40停止。根据图12,可以知道,从0x40h开始,是Program Header Table的内容,并且长度为0x0038h * 0x000bh = 0x0268h=616个字节,这与图3中“Size of Program headers * Number of program headers”的结果是一致的。所以,Program Header的内容如下:
ELF文件格式_第7张图片
Section Header Table就不放全了,太多了,有64*30=1920个字节,图19中14712即通过e_shoff中的0x3978h
ELF文件格式_第8张图片

Program Header和Section Header
typedef struct {
    Elf64_Word p_type;    // 段类型 (Program Header Type)
    Elf64_Word p_flags;   // 段标志 (Segment Flags)
    Elf64_Off p_offset;   // 段在文件中的偏移量 (Segment Offset in File)
    Elf64_Addr p_vaddr;   // 段在内存中的虚拟地址 (Segment Virtual Address)
    Elf64_Addr p_paddr;   // 段在内存中的物理地址 (Segment Physical Address)
    Elf64_Xword p_filesz; // 段在文件中的大小 (Segment Size in File)
    Elf64_Xword p_memsz;  // 段在内存中的大小 (Segment Size in Memory)
    Elf64_Xword p_align;  // 段的对齐方式 (Segment Alignment)
} Elf64_Phdr;
typedef struct {
    Elf64_Word  sh_name;      // 节的名称在节名称字符串表中的索引 (Section Name Index)
    Elf64_Word  sh_type;      // 节的类型 (Section Type)
    Elf64_Xword sh_flags;     // 节的标志 (Section Flags)
    Elf64_Addr  sh_addr;      // 节的虚拟地址 (Section Virtual Address)
    Elf64_Off   sh_offset;    // 节在文件中的偏移量 (Section Offset in File)
    Elf64_Xword sh_size;      // 节的大小 (Section Size)
    Elf64_Word  sh_link;      // 节头表中链接的节的索引 (Link to another section)
    Elf64_Word  sh_info;      // 额外信息 (Extra Information)
    Elf64_Xword sh_addralign; // 节的对齐方式 (Section Alignment)
    Elf64_Xword sh_entsize;   // 每个条目的大小 (Entry Size)
} Elf64_Shdr;

不一一看了。
现在来看一看e_shstrndx的作用:e_shstrndx肯定是小于等于e_shnum的,*(e_shstrndx)其实就是指向的某一个条目的开头,每个条目的结构体如Elf64_Shdr所示。然后再获取这个指定结构体中sh_offset指向的地址,就可以得到节名称字符串表的偏移,这个过程可以用下边的C代码表示:

#include 
#include 

// ELF文件的基地址就不写了,以偏移为主,加上ELF基址的部分注释了
int main() {
    uint64_t e_shoff = 0x3978 /*下边的值都以以上已经提到的值做为例子*/;
    uint64_t e_shentsize = 0x40;
    uint16_t e_shstrndx = 0x1e;

    // 假设 ELF 文件的基地址为 base_addr
    // uint64_t base_addr = /* 这里填入 ELF 文件的基地址 */;

    // 计算节头表的起始地址
    // uint64_t section_header_start = base_addr + e_shoff;
	uint64_t section_header_start = e_shoff;
    // 计算 e_shstrndx 对应的节头表条目地址
    uint64_t shstrtab_entry_addr = section_header_start + e_shstrndx * e_shentsize;
	// 上述计算得到 0x40b8
    
    // 假设 shstrtab_entry_addr 对应的节头表条目为 Elf64_Shdr 结构体
    // 可以根据 Elf64_Shdr 结构体的定义获取 sh_offset 字段的值,即节名称字符串表的偏移量

    // 假设节名称字符串表的偏移量为 shstrtab_offset
    // 64位中,这个值在结构体中的偏移是0x18,在32位中,偏移是0x10
    uint64_t shstrtab_offset = 0x18;

    // 计算 e_shstrndx 对应的节区地址
    uint64_t e_shstrndx_addr = base_addr + shstrtab_offset;
    // 得到40d0
	
    printf("e_shstrndx 对应的节区地址为: 0x%llx\n", e_shstrndx_addr);
	// 40d0指向的地址为0x3870,其内容如图20所示
    return 0;
}

ELF文件格式_第9张图片

符号解析过程以及结构体定义

动态加载器,在第一次调用函数是由_dl_runtime_resolve函数来完成的,在将二进制文件加载到内存,该过程中包含了对导入符号的解析。
对符号的解析除了plt和got表之外,还有.ret.plt, .dynsym, .dynstr。下面将分析这几个表之间的关系。
与.ret.plt对应的结构体为Elf64_Rela,如下

typedef struct {
    uint64_t r_offset; // 重定位的偏移量
    uint64_t r_info;   // 重定位类型和符号索引的组合信息
    int64_t r_addend;  // 重定位的加数
} Elf64_Rela;	

还有一个结构体很类似,如下

typedef struct {
    uint64_t r_offset; // 重定位的偏移量
    uint64_t r_info;   // 重定位类型和符号索引的组合信息
} Elf64_Rela;

chatgpt的回答如下:
ELF文件格式_第10张图片
ida解析出来的结果是rela,查看其中的一个结果,如下
图22
可知r_offset为4018h, r_info为200000007h,r_offset指向的地址为.got.plt表中的函数偏移地址。
图23
r_info这个参数要分为两部分,高32位是符号索引r_sym,低32位是重定位类型r_type,r_sym的值代表Elf64_Sym的索引。Elf64_Sym结构代表.syntab或.dynsym节的信息的,如图22中,r_sym就是2,如图24
图24

typedef struct {
    Elf64_Word st_name;         // 符号名称在字符串表中的偏移量
    unsigned char st_info;      // 符号类型和绑定信息
    unsigned char st_other;     // 保留,未使用
    Elf64_Half st_shndx;        // 符号关联的节索引
    Elf64_Addr st_value;        // 符号的值或地址
    Elf64_Xword st_size;        // 符号的大小
} Elf64_Sym;

而st_name就是.dynstr的偏移量,.dynstr的内容如下:
ELF文件格式_第11张图片
总结起来就是

当程序导入函数时,动态链接器在.dynstr段中添加一个函数名称字符串; 在.dynsym段中添加一个指向函数名称字符串的Elf Sym结构体; 在.rel.plt段中添加一个指向Elf Sym的Elf Rel结构体; 最后Elf Rel的r_offse构成GOT表,保存在.got.plt段中。

_dl_runtime_resolve函数可以在plt表中看到。plt表中,两个条目是用于寻找函数地址的,其他条目中都有一个jmp指定,比如下图
ELF文件格式_第12张图片
红框里的是正常的plt表,在push后,有一个jmp,跳转到sub_1020去寻址。而黑框中cs:qword_4010就是存的_dl_runtime_resolve的绝对地址。
_dl_runtime_resolve(link_map,reloc_arg)具体做的事:

  1. 访问.dynamic,取出.dysym、.dystr、.rel.plt指针
  2. .rel.plt+reloc_arg,求出当前函数的重定位表项Elf32_Rel的指针,记作rel。
  3. 做rel->r_info >> 8运算作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym指针,记作sym。
  4. dynstr + sym->st_name得出符号字符串指针
  5. 在动态链接库查找该函数地址,并将地址赋值给*rel->r_offset,即GOT表。
  6. 调用该函数

libc的源码中其实顺序和上边有点偏差,不过问题不大,方便理解。

ELF文件格式_第13张图片

ELF总结

综上,从网上找了张图,如图21所示:
ELF文件格式_第14张图片

拓展

来自于ctfhub

https://writeup.ctfhub.com/Skill/Web%E8%BF%9B%E9%98%B6/Linux/4YqbrWboUwvdxqXmPHW9EQ.html

你可能感兴趣的:(linux,安全)