Linux逆向之ELF文件格式

1、ELF 文件格式

1.1 ELF 文件类型

ELF文件是一种用于二进制文件、可执行文件、目标代码、共享库和core转存格式文件。是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。ELF有四种类型:

  • 可重定位文件:即.o文件。包含适合于其他目标文件链接起来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件:.out文件,包含社和与执行的一个程序,此文件规定了exec()如何创建一个程序的进程映像。
  • 共享目标文件:.so文件,包含可在两种上下文中链接的代码和数据,链接编辑器可以将它和可重定位文件、目标共享文件一起处理生成另外一个目标文件;动态链接器可能将它和某个可执行文件以及共享目标一起组合,创建进程映像。
  • 内核转储文件:存放当前进程执行的上下文,用于dump信号触发

1.2 ELF文件结构

ELF文件由4部分组成,分别是ELF头(ELF header程序头表(Program header table节(Section节头表(Section header table)。

注意:一个文件中不一定包含全部内容,而且他们的位置也是不固定的。只有ELF文件头是固定的。其余各部分的信息都由文件头来决定。

ELF文件头: 它的主要目的是定位文件的其他部分

linux中关于EF文件头格式的定义在:/usr/include/elf.h中。Elf32_Ehdr是32位ELF文件结构体, Elf64_Ehdr是64位ELF文件结构体。

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
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf64_Half	e_type;			/* Object file type */
  Elf64_Half	e_machine;		/* Architecture */
  Elf64_Word	e_version;		/* Object file version */
  Elf64_Addr	e_entry;		/* Entry point virtual address */
  Elf64_Off	e_phoff;		/* Program header table file offset */
  Elf64_Off	e_shoff;		/* Section header table file offset */
  Elf64_Word	e_flags;		/* Processor-specific flags */
  Elf64_Half	e_ehsize;		/* ELF header size in bytes */
  Elf64_Half	e_phentsize;		/* Program header table entry size */
  Elf64_Half	e_phnum;		/* Program header table entry count */
  Elf64_Half	e_shentsize;		/* Section header table entry size */
  Elf64_Half	e_shnum;		/* Section header table entry count */
  Elf64_Half	e_shstrndx;		/* Section header string table index */
} Elf64_Ehdr;

程序头表: 列举了所有有效的段(segments)和他们的属性, 程序头是一个结构的数组,每一个结构都表示一个段(segments)。在可执行文件或者共享链接库中所有的节(sections)都被分为不同的几个段(segments)。

/* Program segment header.  */

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
{
  Elf64_Word	p_type;			/* Segment type */
  Elf64_Word	p_flags;		/* Segment flags */
  Elf64_Off	p_offset;		    /* Segment file offset */
  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;

节头表: 包含对节(sections)的描述。每个section描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。

节区名存储在.shstrtab字符串表中,sh_name是表中偏移。

/* Section header.  */

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
{
  Elf64_Word	sh_name;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf64_Off	sh_offset;		    /* Section file offset */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;		/* Section alignment */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf64_Shdr;

查看各个节表信息

sh_name sh_type description 查看方式
.text SHT_PROGBITS 代码段,包含程序的可执行指令 objdump -s -d initenv
.data SHT_PROGBITS 包含初始化了的数据,将出现在程序的内存映像中 objdump -x -s -d initenv
.bss SHT_NOBITS 未初始化数据,因为只有符号所以
.rodata SHT_PROGBITS 包含只读数据
.comment SHT_PROGBITS 包含版本控制信息
.eh_frame SHT_PROGBITS 它生成描述如何unwind 堆栈的表
.debug SHT_PROGBITS 此节区包含用于符号调试的信息
.dynsym SHT_DYNSYM 此节区包含了动态链接符号表
.shstrtab SHT_STRTAB 存放section名,字符串表。Section Header String Table
.strtab SHT_STRTAB 字符串表 readelf -S initenv
.symtab SHT_SYMTAB 符号表 readelf -s initenv
.got SHT_PROGBITS 全局偏移表
.plt SHT_PROGBITS 过程链接表
.relname SHT_REL 包含了重定位信息,例如 .text 节区的重定位节区名字将是:.rel.text readelf -r initenv

1.3 ELF文件视图

在解释elf文件试图之前先明确两个概念:Segment和Section。

Segment: 从运行额角度描述EFL文件,对应ELF文件的执行视图。

  • 用于告诉内核,在在执行ELF文件时应该如何映射内存
  • 每个Segmnet主要包含加载地址、文件中的范围、内存权限、对齐方式等
  • 是运行时必须提供的信息

Section: 从链接角度来描述ELF文件,对应ELF文件的链接视图。

  • 用于告诉链接器。ELF文件中每个部分是什么,哪里是代码、那里是制度数据、哪里是重定位数据
  • 每个Section主要包含Section类型、文件中的位置、大小等
  • 链接器依赖Section信息将不同的对象文件中的代码、数据信息合并,并修复互相引用

Segment和Section的关系:

  • 相同权限的Section会放到同一个Segment中
  • 一个Segment包含许多个Section

2、ELF文件格式解析

2.1 文件格式解析工具

用于查看elf文件的工具很多,如010Editor、readelf、objdump等。 这里以readelf为例进行演示说明(测试程序: initenv)。

readelf具体用法可以通过:readefl --help查看。

查看ELF文件头: readelf -h initenv

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:                              EXEC (Executable file)   //elf文件类型
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x45e4c0    //程序入口点
  Start of program headers:          64 (bytes into file)   //程序头的起始位置
  Start of section headers:          456 (bytes into file)  //段表开始位置
  Flags:                             0x0
  Size of this header:               64 (bytes)  //文件头的大小
  Size of program headers:           56 (bytes)  //程序头的大小
  Number of program headers:         7           //程序头中的项数
  Size of section headers:           64 (bytes)  //段表长度
  Number of section headers:         23          //段表中的项数
  Section header string table index: 3

查看程序头表: readelf -l initenv

Elf file type is EXEC (Executable file)
Entry point 0x45e4c0
There are 7 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000188 0x0000000000000188  R      0x1000
  NOTE           0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
                 0x0000000000000064 0x0000000000000064  R      0x4
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000812dd 0x00000000000812dd  R E    0x1000
  LOAD           0x0000000000082000 0x0000000000482000 0x0000000000482000
                 0x000000000008e988 0x000000000008e988  R      0x1000
  LOAD           0x0000000000111000 0x0000000000511000 0x0000000000511000
                 0x0000000000017f40 0x000000000004bae8  RW     0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x8
  LOOS+0x5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         0x8

 Section to Segment mapping:
  Segment Sections...
   00
   01     .note.go.buildid
   02     .text .note.go.buildid
   03     .rodata .typelink .itablink .gosymtab .gopclntab
   04     .go.buildinfo .noptrdata .data .bss .noptrbss
   05
   06

这里可以看到:该程序共有7个Segmnet,将23个Section分别映射到了01、02、03、04四个segment中。映射到同一Segmnet中的Section有着相同的属性。需要注意的是Section .note.go.buildid映射到了两个Segment中。

查看节头表:readelf -S initenv ,信息与Elf32_Shdr定义对应。

There are 23 section headers, starting at offset 0x1c8:

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         0000000000401000  00001000
       00000000000802dd  0000000000000000  AX       0     0     32
  [ 2] .rodata           PROGBITS         0000000000482000  00082000
       0000000000037667  0000000000000000   A       0     0     32
  [ 3] .shstrtab         STRTAB           0000000000000000  000b9680
       000000000000016e  0000000000000000           0     0     1
  [ 4] .typelink         PROGBITS         00000000004b9800  000b9800
       00000000000004dc  0000000000000000   A       0     0     32
  [ 5] .itablink         PROGBITS         00000000004b9ce0  000b9ce0
       0000000000000058  0000000000000000   A       0     0     32
  [ 6] .gosymtab         PROGBITS         00000000004b9d38  000b9d38
       0000000000000000  0000000000000000   A       0     0     1
  [ 7] .gopclntab        PROGBITS         00000000004b9d40  000b9d40
       0000000000056c48  0000000000000000   A       0     0     32
  [ 8] .go.buildinfo     PROGBITS         0000000000511000  00111000
       0000000000000110  0000000000000000  WA       0     0     16
  [ 9] .noptrdata        PROGBITS         0000000000511120  00111120
       00000000000105e0  0000000000000000  WA       0     0     32
  [10] .data             PROGBITS         0000000000521700  00121700
       0000000000007830  0000000000000000  WA       0     0     32
  [11] .bss              NOBITS           0000000000528f40  00128f40
       000000000002f020  0000000000000000  WA       0     0     32
  [12] .noptrbss         NOBITS           0000000000557f60  00157f60
       0000000000004b88  0000000000000000  WA       0     0     32
  [13] .debug_abbrev     PROGBITS         0000000000000000  00129000
       0000000000000133  0000000000000000   C       0     0     1
  [14] .debug_line       PROGBITS         0000000000000000  00129133
       000000000001c216  0000000000000000   C       0     0     1
  [15] .debug_frame      PROGBITS         0000000000000000  00145349
       0000000000005724  0000000000000000   C       0     0     1
  [16] .debug_gdb_s[...] PROGBITS         0000000000000000  0014aa6d
       000000000000002e  0000000000000000           0     0     1
  [17] .debug_info       PROGBITS         0000000000000000  0014aa9b
       0000000000034e48  0000000000000000   C       0     0     1
  [18] .debug_loc        PROGBITS         0000000000000000  0017f8e3
       000000000001bdf1  0000000000000000   C       0     0     1
  [19] .debug_ranges     PROGBITS         0000000000000000  0019b6d4
       0000000000009187  0000000000000000   C       0     0     1
  [20] .note.go.buildid  NOTE             0000000000400f9c  00000f9c
       0000000000000064  0000000000000000   A       0     0     4
  [21] .symtab           SYMTAB           0000000000000000  001a4860
       000000000000c5e8  0000000000000018          22    98     8
  [22] .strtab           STRTAB           0000000000000000  001b0e48
       000000000000b2f3  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)

2.2 实现文件解析器

Golang标准库debug/elf包实现了对ELF文件的访问。可以用来实现解析ELF文件。

....
// Indexes into the Header.Ident array.
const (
	EI_CLASS      = 4  /* Class of machine. */
	EI_DATA       = 5  /* Data format. */
	EI_VERSION    = 6  /* ELF format version. */
	EI_OSABI      = 7  /* Operating system / ABI identification */
	EI_ABIVERSION = 8  /* ABI version */
	EI_PAD        = 9  /* Start of padding (per SVR4 ABI). */
	EI_NIDENT     = 16 /* Size of e_ident array. */
)

const (
	ET_NONE   Type = 0      /* Unknown type. */
	ET_REL    Type = 1      /* Relocatable. */
	ET_EXEC   Type = 2      /* Executable. */
	ET_DYN    Type = 3      /* Shared object. */
	ET_CORE   Type = 4      /* Core file. */
	ET_LOOS   Type = 0xfe00 /* First operating system specific. */
	ET_HIOS   Type = 0xfeff /* Last operating system-specific. */
	ET_LOPROC Type = 0xff00 /* First processor-specific. */
	ET_HIPROC Type = 0xffff /* Last processor-specific. */
)
...

具体实现逻辑:读取ELF文件到内存,按照ELF文件格式去解析数据即可,没有什么难度。以下是读取ELF文件头的部分代码:

func readHeader(filedata []byte) {
	// analysis elf file

	fmt.Println("ELF Header:")
	fmt.Print("  Magic:		")
	for i := 0; i < int(elf.DT_INIT); i++ {
		fmt.Printf("%02x ", filedata[i])
	}
	//1、get  magic
	var elfHeader elf.FileHeader
	elfHeader.Class = elf.Class(filedata[4])
	fmt.Print("\n  Class:		")
	switch filedata[4] {
	case 0:
		fmt.Println("Invailed Class")
	case 1:
		fmt.Println("ELF32")
	case 2:
		fmt.Println("ELF64")
	case 3:
		fmt.Println("Error")
	}

	//2. get data
	fmt.Print("  Data:			")
	switch filedata[5] {
	case 0:
		fmt.Println("Invalid encoding data")
	case 1:
		fmt.Println("2's complement, little endian")
	case 2:
		fmt.Println("2's complement, little endian")
	default:
		fmt.Println("Error")
	}

	//3. get Version  OS/ABI  API Version
	fmt.Print("  Version:		1(current)\n")
	fmt.Print("  OS/ABI:		UNIX - Sytem V\n")
	fmt.Print("  API Version:		0\n")

	//4.get type
	fmt.Print("  Type:			")
	switch filedata[elf.EI_NIDENT] {
	case 0:
		fmt.Println("no file type")
	case 1:
		fmt.Println("relocate file")
	case 2:
		fmt.Println("EXEC (executable file) ")
	case 3:
		fmt.Println("Shared objective file")
	case 4:
		fmt.Println("Core file")
	default:
		fmt.Println("Error")
	}

	//5.machine
	fmt.Print("  Machine:		")
	switch filedata[elf.EI_NIDENT+2] {
	case byte(elf.EM_386):
		fmt.Println("Inter 83086")
	case byte(elf.EM_ARM):
		fmt.Println("ARM")
	case byte(elf.EM_X86_64):
		fmt.Println("AMD X86-64 arrchitecture")
	default:
		fmt.Printf("Error")
	}
....
}

3、ELF文件加载过程

关于ELF加载过程可以查看源码:https://elixir.bootlin.com/linux/v6.0.11/source/fs/binfmt_elf.c

ELF程序执行时,代码和数据必须在内存,因此同windows下Exe文件的加载执行过程类似,需要先申请一定的内存空间、映射elf文件到内存、修改CPU的指令指针寄存器为程序的入口开始执行代码。不同点在于进行内存映射、创建进程的具体实现上存在差异,但是整个逻辑上相似。

Linux 提供三种方式来加载可执行程序:

  • load_binary: 通过读存放在可执行文件中的信息为当前进程建立一个新的执行环境
  • load_shlib: 用于动态的把一个共享库捆绑到一个已经在运行的进程, 这是由uselib()系统调用激活的
  • core_dump: 在名为core的文件中, 存放当前进程的执行上下文. 这个文件通常是在进程接收到一个缺省操作为”dump”的信号时被创建的, 其格式取决于被执行程序的可执行类型

3.1 Shell执行命令的完整周期

1、当Shell执行一个程序时,父Shell进程生成一个子shell进程(父进程的复制),子shell进程通过系统调用execve启动加器;/linux/v4.20.17/source/fs/exec.c

SYSCALL_DEFINE3(execve,
		const char __user *, filename,
		const char __user *const __user *, argv,
		const char __user *const __user *, envp)
{
	return do_execve(getname(filename), argv, envp);
}

2、在内核中真正执行execv()/execve()的是do_execve(),该函数首先打开映像文件,并从目标文件头部读取若干字节(即ELF头部大小),然后调用search_binary_handler。这里需要注意的是函数参数bprm, 其类型为linux_binprm结构体。linux内核对所支持的每种可执行的程序类型都有个struct linux_binfmt来记录程序信息。这是一种可执行文件类型的注册机制:所有注册的的linux_binfmt对象都处于一个链表中(全局链表变量 formats),通过registre_fmt()注册一种linux_binfmt新类型。系统初始化时为每个编译进内核的可执行格式都执行registre_fmt()函数。当我们执行一个可执行程序的时候, 内核会list_for_each_entry遍历所有注册的linux_binfmt对象, 对其调用load_binrary方法来尝试加载, 直到加载成功为止(前面介绍的search_binary_handler() 函数)。

ELF文件格式注册的linux_binfmt结构对象elf_format。

int search_binary_handler(struct linux_binprm *bprm)
{
	bool need_retry = IS_ENABLED(CONFIG_MODULES);
	struct linux_binfmt *fmt;
	int retval;

	/* This allows 4 levels of binfmt rewrites before failing hard. */
	if (bprm->recursion_depth > 5)
		return -ELOOP;

	retval = security_bprm_check(bprm);
	if (retval)
		return retval;

	retval = -ENOENT;
 retry:
	read_lock(&binfmt_lock);
	list_for_each_entry(fmt, &formats, lh) {
		if (!try_module_get(fmt->module))
			continue;
		read_unlock(&binfmt_lock);
		bprm->recursion_depth++;
		retval = fmt->load_binary(bprm);
		read_lock(&binfmt_lock);
		put_binfmt(fmt);
		bprm->recursion_depth--;
		if (retval < 0 && !bprm->mm) {
			/* we got to flush_old_exec() and failed after it */
			read_unlock(&binfmt_lock);
			force_sigsegv(SIGSEGV, current);
			return retval;
		}
		if (retval != -ENOEXEC || !bprm->file) {
			read_unlock(&binfmt_lock);
			return retval;
		}
	}
	read_unlock(&binfmt_lock);

	if (need_retry) {
		if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
		    printable(bprm->buf[2]) && printable(bprm->buf[3]))
			return retval;
		if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
			return retval;
		need_retry = false;
		goto retry;
	}

	return retval;
}

3、search_binary_handler函数遍历format格式,对于linux下的elf格式的可执行文件而言,会找到elf_format,调用其load_binary函数,对elf类型而言,则调用load_elf_binary。

static struct linux_binfmt elf_format = {
	.module		= THIS_MODULE,
	.load_binary	= load_elf_binary,    //处理函数
	.load_shlib	= load_elf_library,
	.core_dump	= elf_core_dump,
	.min_coredump	= ELF_EXEC_PAGESIZE,
};

4、load_elf_binary函数加载和启动ELF程序。具体代码位于文件:/source/fs/binfmt_elf.c。该函数主要完成以下工作来加载elf文件:

  • 填充并检查目标程序ELF头部
  • 调用 load_elf_phdrs加载目标程序的程序头部
  • 如果需要动态链接,则寻找和处理解释器段
  • 装入程序的segment
  • 填写程序入口
  • 调用create_elf_tables填写目标文件的参数环境变量等信息
  • start_thread进入新程序的入口

参考资料:
https://blog.csdn.net/gatieme/article/details/51628257

https://elixir.bootlin.com/linux/v4.20.17/source/fs/binfmt_elf.c#L690

https://blog.csdn.net/conansonic/article/details/53740670

https://zhuanlan.zhihu.com/p/147322084

https://www.bookstack.cn/read/gctt-godoc/Source-debug-elf-elf.md

https://bbs.pediy.com/thread-259901.htm

posted @ 2022-12-07 14:50  丘山996  阅读(70)  评论(0)    收藏  举报