首页
论坛
专栏
课程

[调试逆向] [系统底层] [原创]Linux可执行文件——ELF文件分析学习笔记及C语言实现ELF文件解析

2019-3-6 13:12 883

[调试逆向] [系统底层] [原创]Linux可执行文件——ELF文件分析学习笔记及C语言实现ELF文件解析

2019-3-6 13:12
883

1、ELF文件类型:

一个ELF文件可以被标记为以下几种类型之一。

ET_NONE:未知类型。这个标记表明文件类型不确定,或者还未定义。

ET_REL(relocatable):重定位文件,该文件(整个文件)被当做(标记为)一段可执行的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接可执行程序的一段位置独立的代码(position independent code)。在编译完代码之后通常可以看到一个.o格式的文件 ,文件中包含了创建可执行文件所需要的代码和数据。如下图所示,使用gcc -o helloworld.o helloworld.c命令后,产生了名为helloworld.o的文件。


ET_EXEC(executable):可执行文件,即程序,是一个进程开始执行的入口。
ET_DYN:共享目标文件。ELF类型为dynamic,即动态可链接的目标文件,也称为共享库。

ET_CORE:核心文件,在程序崩溃或者进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB读取这类文件来辅助调试并查找程序崩溃的原因。


使用readelf -h命令查看ELF文件,可以看到原始的ELF文件头。

ELF文件从文件的0偏移量开始,是除了文件头之后剩余部分文件的一个映射。文件头主要标记了ELF类型,结构和程序开始执行的入口地址,并提供了其他ELF头(节头和程序头)的偏移量。在/usr/include目录下的elf.h文件(或Linux的ELF手册)中可以了解到ELF的头部结构:

/* The ELF file header.  This appears at the start of every ELF file.  */

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

2、ELF程序头

ELF程序头是二进制文件中的段,是程序装载必需的一部分。段(segment)是在内核装载时被解析的,描述了磁盘上可执行文件的内部布局以及如何映射到内存中。可以通过引用原始ELF头中名为e_phoff(程序头表偏移量)的偏移量来得到程序头表。

在Elf32_Phdr结构中包含了ELF文件可执行文件程序头表的一个程序头条目。

/* 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 , I.E execute|read|write */
	Elf32_Word	p_align;		/* Segment alignment */
} Elf32_Phdr;

5种常见的程序头:

2.1、PT_LOAD

一个可执行文件至少有一个PT_LOAD类型的段。这类程序头描述的是可装载的段,也就是说,这种类型的段将被装载或映射到内存中。

例如,一个需要动态链接的ELF可执行文件通常包含以下两个可装载的段(类型为PT_LOAD):

存放程序代码的text段;

存放全局变量和动态链接信息的data段。

上面的两个段将会被映射到内存中,并根据p_align中存放的值在内存中对齐。

Text段(代码段)的权限设置为PF_X|PF_R时,可读可执行。

Data段的权限设置为PF_W|PF_R,可读可写。


2.2、PT_DYNAMIC——动态段的Phdr

动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针,包含但不限于以下内容:

  • 运行时需要链接的共享库列表;
  • 全局偏移表(GOT)的地址——ELF动态链接库部分;
  • 重定位条目的相关信息。

动态段包含了一些结构体,在这些结构体中存放着与动态链接相关的信息。d_tag成员变量控制着d_un的含义。

ELF文件的动态段结构体如下:

/* Dynamic section entry.  */

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;

typedef struct
{
	Elf64_Sxword	d_tag;			/* Dynamic entry type */
	union
	{
		Elf64_Xword d_val;		/* Integer value */
		Elf64_Addr d_ptr;			/* Address value */
	} d_un;
} Elf64_Dyn;

d_tag(标记名)合法取值定义如下:

/* Legal values for d_tag (dynamic entry type).  */

#define DT_NULL		0		/* Marks end of dynamic section */
#define DT_NEEDED	1		/* Name of needed library */
#define DT_PLTRELSZ	2		/* Size in bytes of PLT relocs */
#define DT_PLTGOT	3		/* Processor defined value */
#define DT_HASH		4		/* Address of symbol hash table */
#define DT_STRTAB	5		/* Address of string table */
#define DT_SYMTAB	6		/* Address of symbol table */
#define DT_RELA		7		/* Address of Rela relocs */
#define DT_RELASZ	8		/* Total size of Rela relocs */
#define DT_RELAENT	9		/* Size of one Rela reloc */
#define DT_STRSZ	10		/* Size of string table */
#define DT_SYMENT	11		/* Size of one symbol table entry */
#define DT_INIT		12		/* Address of init function */
#define DT_FINI		13		/* Address of termination function */
#define DT_SONAME	14		/* Name of shared object */
#define DT_RPATH	15		/* Library search path (deprecated) */
#define DT_SYMBOLIC	16		/* Start symbol search here */
#define DT_REL		17		/* Address of Rel relocs */
#define DT_RELSZ	18		/* Total size of Rel relocs */
#define DT_RELENT	19		/* Size of one Rel reloc */
#define DT_PLTREL	20		/* Type of reloc in PLT */
#define DT_DEBUG	21		/* For debugging; unspecified */
#define DT_TEXTREL	22		/* Reloc might modify .text */
#define DT_JMPREL	23		/* Address of PLT relocs */
#define	DT_BIND_NOW	24		/* Process relocations of object */
#define	DT_INIT_ARRAY	25		/* Array with addresses of init fct */
#define	DT_FINI_ARRAY	26		/* Array with addresses of fini fct */
#define	DT_INIT_ARRAYSZ	27		/* Size in bytes of DT_INIT_ARRAY */
#define	DT_FINI_ARRAYSZ	28		/* Size in bytes of DT_FINI_ARRAY */
#define DT_RUNPATH	29		/* Library search path */
#define DT_FLAGS	30		/* Flags for the object being loaded */
#define DT_ENCODING	32		/* Start of encoded range */
#define DT_PREINIT_ARRAY 32		/* Array with addresses of preinit fct*/
#define DT_PREINIT_ARRAYSZ 33		/* size in bytes of DT_PREINIT_ARRAY */
#define DT_SYMTAB_SHNDX	34		/* Address of SYMTAB_SHNDX section */
#define	DT_NUM		35		/* Number used */
#define DT_LOOS		0x6000000d	/* Start of OS-specific */
#define DT_HIOS		0x6ffff000	/* End of OS-specific */
#define DT_LOPROC	0x70000000	/* Start of processor-specific */
#define DT_HIPROC	0x7fffffff	/* End of processor-specific */
#define	DT_PROCNUM	DT_MIPS_NUM	/* Most used by any processor */

2.3、PT_NOTE

PT_NOTE类型的段可能保存了与特定供应商或者系统相关的附加信息。

有时供应商或系统构建者需要在目标文件上标记特定的信息,以便于其他程序对一致性、兼容性等进行检查。SHT_NOTE类型的节(section)和PT_NOTE类型的程序头元素就可以用于这一目的。节或者程序头元素中的备注信息可以有任意数量的条目,每个条目都是一个4字节的目标处理器格式的数组。下面的标签可以解释备注信息的组织结构,不过这些标签并不是规范中的内容。

事实上,这一个段中只是保存了操作系统的规范信息,在可执行文件运行时是不需要该段的,因此这个段极易被病毒感染。


2.4、PT_INTERP

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


2.5、PT_PHDR

PT_HDR段保存了程序头表本身的位置和大小。Phdr表保存了所有的Phdr对文件(以及内存镜像)中段的描述信息。

可以使用readelf -l <filename>命令来查看文件的Phdr表。如:


在图中,可以看到程序的执行入口点,PT_LOAD段的权限和对齐等信息,以及其他各种重要信息。



3、ELF节头

在程序中,段(segment)并不等于节(section)。段是程序执行的必要组成部分,在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小描述,主要用于链接和调试。节头表对于程序的执行来说不是必需的,没有节头表,程序仍然可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局描述是程序头表的任务。

如果二进制文件中缺少节头,并不意味着节就不存在。只是没有办法通过节头来引用节,对于调试器或者反编译程序来说,只是可以参考的信息变少了而已。

每一个节都保存了某种类型的代码或者数据。数据可以是程序中的全局变量,也可以是链接器所需要的动态链接信息。正如前面所提到的,每个ELF目标文件都有节,但是不一定有节头,尤其是有人故意将节头从节头表中删除了之后。当然,默认是有节头的。

ELF文件节头结构如下:

/* 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;

3.1、.text节

.text节是保留了程序代码指令的代码节。一段可执行程序,如果存在Phdr,.text节就会存放在text段中。由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS。


3.2、.rodata节

.rodata节保存了只读数据,因此只能存放于一个可执行文件的只读段中。也因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,因此节类型为SHT_PROGBITS。


3.3、.plt节

过程链接表(Procedure Linkage Table,PLT),.plt节中包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于其存在于text段中,同样保存了代码,因此节类型为SHT_PROGBITS。


3.4、.data节

.data节存在于data段中,保存了初始化的全局变量等数据。由于其保存了程序的变量数据,因此节类型被标记为SHT_PROGBITS。


3.5、.bss节

.bss节保存了未进行初始化的全局数据,是data段的一部分,占用空间不超过4字节,仅表示这个节本身的空间。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,因此节类型为SHT_PROGBITS。


3.6、.got.plt节

.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。如果攻击者获得了堆或者.bss漏洞的一个指针大小的写原语,就可以对该节任意进行修改。.got.plt节跟程序执行有关,因此节类型被标记为SHT_PROGBITS。


3.7、.dynsym节

.dynsym节保存了从共享库导入的动态符号信息,该节保存在text段中,节类型被标记为SHT_PROGBITS。


3.8、.dynstr节

.dynstr节保存了动态符号字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,以空字符作为终止符。


3.9、.rel.*节

重定位节保存了重定位相关的信息,这些信息描述;了在链接或者运行时,对ELF目标文件的某部分内容或者进程镜像进行补充或者修改。重定位节保存了重定位相关的数据,因此节类型被标记为SHT_REL。


3.10、.hash节

.hash节有时也称为.gnu.hahs,保存了一个用于查找符号的散列表。


3.11、.symtab节

.symtab节保存了ElfN_Sym类型的符号信息,因此节类型被标记为SHT_SYMTAB。


3.12、.strtab节

.strtab节保存的是符号字符串表,表中的内容会被.symtab的ElfN_Sym结构中的st_name条目引用。由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。


3.13、.shstrtab节

.shstrtab节保存节头字符串表,该表是一个以空字符终止的字符串的集合,字符串保存了每个节的节名,如.text、.data等。有一个名为e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。


3.14、.ctors和.dtors节

.ctors(构造器)和.dtors(析构器)这两个节保存了指向析构函数和析构函数的指针,构造函数是在main函数执行之前需要执行的代码,析构函数是在main函数之后需要执行的代码。


在一个经典的helloworld ELF文件中,节头如下:


4、ELF符号

符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。在readelf -S命令的输出内容中,可以看到有两个节:.dynsym和.symtab。

.dynsym保存了引用来自外部文件符号的全局符号,如printf这样的库函数,.dynsym保存的符号是.symtab所保存符号的子集,.symtab中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。因此,.symtab保存了所有的符号,而.dynsym只保存动态/全局符号。

ELF文件符号项结构如下:

/* Symbol table entry.  */

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;

typedef struct
{
	Elf64_Word	st_name;		/* Symbol name (string tbl index) */
	unsigned char	st_info;		/* Symbol type and binding */
	unsigned char st_other;		/* Symbol visibility */
	Elf64_Section	st_shndx;		/* Section index */
	Elf64_Addr	st_value;		/* Symbol value */
	Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

5、C语言实现ELF文件解析

//
// Created by root on 19-3-4.
//

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <elf.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc, char **argv)
{
    int fd, i;
    uint8_t *mem;
    struct stat st;
    char *StringTable, *interp;

    Elf32_Ehdr *ehdr;
    Elf32_Phdr *phdr;
    Elf32_Shdr *shdr;

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

    if ((fd = open(argv[0], O_RDONLY)) < 0)
    {
        perror("open");
        exit(-1);
    }

    if (fstat(fd, &st) < 0)
    {
        perror("fstat");
        exit(-1);
    }

    /* Map the executable into memory */
     mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mem == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    /*
     *  The initial ELF Header starts at offset 0  of our mapped memory.
     */
    ehdr = (Elf32_Ehdr *)mem;

    /*
     *  The shdr table and phdr tabke offsets are given by e_shoff and e_phoff members of the Elf32_Ehdr
     */
    phdr = (Elf32_Phdr *)&mem[ehdr->e_phoff];
    shdr = (Elf32_Shdr *)&mem[ehdr->e_shoff];

    /*
     * Check to see if the ELF magic (The first 4 bytes) match up as 0x7f E L F
     */
    if (mem[0] != 0x7f && strcmp(&mem[1], "ELF"))
    {
        fprintf(stderr, "%s is not an ELF file\n", argv[1]);
        exit(-1);
    }

    /*
     *  We are only parsing executables with this code. so ET_EXEC marks an executable.
     
    if (ehdr->e_type != ET_EXEC)
    {
        fprintf(stderr, "%s is not an executable\n", argv[1]);
		 printf("ehdr->e_type = %x, ET_EXEC = %x\n",ehdr->e_type, ET_EXEC);
        //exit(-1);
    }*/

    printf("Program Entry point: 0x%x\n",ehdr->e_entry);

    /*
     *  We find the string table for the section header names with e_shstrndx which gives the index of which section holds the string table.
     */
    StringTable = &mem[shdr[ehdr->e_shstrndx].sh_offset];

    /*
     *  Print each section header name and address.
     *  Notice we get the index into the string table that contains each section header name with the shdr.sh_name member.
     */
    printf("Section header list:\n\n");
    for (i = 1; i < ehdr->e_shnum; i++) {
        printf("%s: 0x%x\n",&StringTable[shdr[i].sh_name], shdr[i].sh_addr);
    }

    /*
     *  Print out each segment name, and address.
     *  Except for PT_INTERP we print the path to the dynamic linker (Interpreter).
     */
    printf("\n Program header list\n\n");
    for (i  = 0; i < ehdr->e_phnum; i++) {
        switch (phdr[i].p_type)
        {
            case PT_LOAD:
                /*
                 * We know that text segment starts at offset 0. And only one other possible loadable segment exists which is the data segment.
                 */
                if (phdr[i].p_offset == 0)
                {
                    printf("Text segment: 0x%x\n", phdr[i].p_vaddr);
                }
                else
                {
                    printf("Data segment: 0x%x\n",phdr[i].p_vaddr);
                }
                break;
            case PT_INTERP:
                interp = strdup((char *)&mem[phdr[i].p_offset]);
                printf("Interpreter: %s\n", interp);
                break;
            case PT_NOTE:
                printf("Note segment:0x%x\n", phdr[i].p_vaddr);
                break;
            case PT_DYNAMIC:
                printf("Dynamic segment: 0x%x\n", phdr[i].p_vaddr);
                break;
            case PT_PHDR:
                printf("Phdr segment:0x%x\n",phdr[i].p_vaddr);
                break;
        }
    }

    exit(0);


    return 0;
}

参考:

《Linux二进制分析》

《ELF手册》

《elf.h》

关于ELF文件分析较好的两篇文章:

https://www.cnblogs.com/dengxiaojun/p/4279407.html

https://blog.csdn.net/muaxi8/article/details/79627859



[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

上一主题 下一主题
最新回复 (0)
游客
登录 | 注册 方可回帖
返回