Linux二进制分析-ELF格式分析及符号重定位

一、ELF文件类型

  • ET_NONE:未知类型。
  • ET_REL:重定位文件。通常是还未被链接到可执行程序中的一段位置独立的代码,例如linux中的.o格式的文件。
  • ET_EXEC:可执行文件。
  • ET_DYN:共享目标文件。ELF类型为dynamic,意味着该文件被标记为了一个动态的可连接的目标文件,也称为共享库,这类共享库会在程序运行时被装载并链接到程序的进程镜像中。
  • ET_CORE:核心文件,在程序崩溃或进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。

二、ELF文件结构

6E2PX9.png
通过上图可以看到,ELF文件的开始是ELF文件头,接着是程序头表(program header table),以及节头表(section header table)。

1. 段和节的区别

段是程序执行的必要组成部分,在每一个段中,会有着代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。节头对于程序的执行来说不是必需的,没有节头表,程序仍可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局的描述是程序头表的任务。节头是对程序头的补充。
如果二进制文件中缺少节头,并不意味着节不存在,只是没有办法通过节头来引用节,对于调试器或者反编译器程序来说,只是可以参考的信息变少了而已。

2. ELF文件头内容

typedef struct
{
	unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
	Elf32_Half	e_type;			/* 文件类型 */
	Elf32_Half	e_machine;		/* CPU平台属性 */
	Elf32_Word	e_version;		/* ELF版本号,一般为常数1 */
	Elf32_Addr	e_entry;		/* ELF程序的入口虚拟地址,可重定位文件一般没有入口地址,为0 */
	Elf32_Off	e_phoff;		/* 程序头表在文件中的偏移 */
	Elf32_Off	e_shoff;		/* 节头表在文件中偏移 */
	Elf32_Word	e_flags;		/* ELF标志位 */
	Elf32_Half	e_ehsize;		/* ELF文件头本身的大小 */
	Elf32_Half	e_phentsize;	/* 程序头描述符的大小 */
	Elf32_Half	e_phnum;		/* 程序头描述符的数量,多少个段 */
	Elf32_Half	e_shentsize;	/* 节表描述符的大小 */
	Elf32_Half	e_shnum;		/* 节表描述符的数量,多少个节 */
	Elf32_Half	e_shstrndx;		/* 节表字符串表所在节的下标,即存储节名字符串的字符串表所在节在节表中的下标 */
} Elf32_Ehdr;

3. 段头

typedef struct
{
	Elf32_Word	p_type;			/* 段的类型,我们基本上只关注PT_LOAD类型的,当然还有其他类型例如DYNAMIC等 */
	Elf32_Off	p_offset;		/* 段在文件中的偏移 */
	Elf32_Addr	p_vaddr;		/* 段在进程虚拟地址空间的起始位置 */
	Elf32_Addr	p_paddr;		/* 段的物理装载地址,一般情况下与p_vaddr相同 */
	Elf32_Word	p_filesz;		/* 段在文件中所占空间的长度 */
	Elf32_Word	p_memsz;		/* 段在进程虚拟地址空间中所占空间的长度 */
	Elf32_Word	p_flags;		/* 段的权限属性,例如RWX */
	Elf32_Word	p_align;		/* 段的对齐属性 */
} Elf32_Phdr;

段的类型

  • PT_LOAD

即可加载类型,一个可执行文件至少要有一个PT_LOAD类型的段。这种类型的段将会被装载或者映射到内存中。
例如一个需要动态链接的ELF可执行文件通常需要包含以下两个可装载的段

  • 存放程序代码的 text 段
  • 存放全局变量和动态链接信息的data段

通常将 text 段(也称代码段)的权限设置为 PF_X | PF_R(可读可执行)。
通常将 data 段的权限设置为 PF_W | PF_R (可读可写)

  • PT_DYNAMIC

动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。其中包含但不限于:

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

4. 节头

typedef struct
{
	Elf32_Word	sh_name;		/* 真实的节名字符串在一个叫做`.shstrtab`字符串表中,sh_name是其下标 */
	Elf32_Word	sh_type;		/* 节的类型 */
	Elf32_Word	sh_flags;		/* 节的标志位 */
	Elf32_Addr	sh_addr;		/* 节的虚拟地址,若不可被加载则为0 */
	Elf32_Off	sh_offset;		/* 节在文件中的偏移,但对于.bss节来说就无意义,因为没内容 */
	Elf32_Word	sh_size;		/* 节的长度 */
	Elf32_Word	sh_link;		/* Link to another section */
	Elf32_Word	sh_info;		/* Additional section information */
	Elf32_Word	sh_addralign;	/* 节地址对齐 */
	Elf32_Word	sh_entsize;		/* 若节中为固定大小的项,则为项的长度,否则为0 */
} Elf32_Shdr;

一些重要的节

一个节存于哪个段,主要看其权限

  • .text 节

.text 节保存了程序代码指令的代码节,一般该节会存在于 text 段中

  • .rodata 节

.rodata节只保存了只读的数据,如一行C语言代码中的字符串。

printf("hello world");

例如上述的 “hello world” 则存在于 .rodata 节
其一般也存在于 text 段,因为它是只读的

  • .plt 节

.plt 节包含了动态连接器调用从共享库导入的函数所必需的相关代码。存于 text 段中

  • .data 节

.data 节存在于data段中,其保存了初始化的全局变量等数据。

  • .bss节

.bss 节保存了未进行初始化的全局数据,是 data 段的一部分,占用空间不超过4字节。程序加载时数据被初始化为0,在程序执行期间可以进行赋值,由于其并没保存实际的数据,所以其在文件中是不存在的。

  • .got.plt节

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

  • .dynsym节

.dynsym 节保存了从共享库导入的动态符号信息,该节保存在 text 段中。

  • .rel.*节

重定位节保存了重定位相关的信息,这些信息描述了如何在链接或者运行过程中,对ELF目标文件的某部分内容或进程镜像进行补充和修改。
若为重定位表的话,那么sh_link则表示该段所使用的相应符号表所在节的下标,sh_info则表示该重定位表所作用的节的下标。

  • .symtab 节

.symtab 节保存了所有的符号信息,包括动态链接符号信息

  • .strtab 节

.strtab 节保存的是符号字符串表。符号的st_name即符号名在该表中的下标

  • .shstrtab 节

.shstrtab 节保存了节头字符串表。

  • .ctors 和 .dtors节

.ctors(构造器)和.dtors(析构器)这两个节保存了指向构造函数和析构函数的指针。

5. 符号

符号是对某些类型的数据或代码的符号引用,例如 printf() 函数会在动态符号表 .dynsym 中存在一个指向该函数的符号条目。在大多数共享库或者动态链接可执行文件中,都存在两个符号表 .symtab.dynsym

其中 .dynsym 保存了引用来自外部文件符号的全局符号,例如 printf 这样的库函数,.dynsym 保存的符号是 .symtab 所保存的符号的子集, .symtab 中还保存了可执行文件的本地富豪,例如全局变量,以及代码中定义的本地函数等。

那么既然 .symtab 中已经保存了 .dynsym 中所有的符号,那为什么还需要 .dynsym 呢?

.dynsym 被标记了 ALLOC 属性,而 .symtab 没有,该属性表示有该标记的节在运行时分配并装载进入内存,而 .symtab 不是在运行时必需的,因此不会被装载到内存。.dynsym 中所保存的符号只有在运行时才可以被解析,因此是运行时动态链接器所需要的唯一符号,其对动态链接可执行文件的执行来说是必需的,而 .symtab 符号表只是用来调试和链接的,有时为了节省空间,会将 .symtab 符号表从生产二进制文件中删掉。

符号表结构

typedef struct {
	Elf32_Word st_name;		/* 该符号名在字符串 */
	Elf32_Addr st_value;	/* 符号对应的值,具体与符号有关 */
	Elf32_Word st_size;		/* 符号的大小 */
	unsigned char st_info;	/* 符号类型和绑定信息 */
	unsigned char st_other;	
	Elf32_Half st_shndx;	/* 符号所在的节的下标 */
} Elf32_Sym;
  • st_value
  • 在目标文件中,若是符号的定义且该符号不是"COMMON块"类型的(即 st_shndx 不为 SHN_COMMON),则 st_value表示该符号在节中的偏移,即符号所对应的函数或变量位于由 st_shndx 所指定的节,偏移 st_value的位置。
  • 在目标文件中,若符号是"COMMON块"类型的,则 st_value 表示该符号的对齐属性。
  • 在可执行文件中, st_value 表示符号的虚拟地址。

三、ELF重定位

1. 重定位表

上面已经提到过重定位表 .rel.*,那么具体的重定位表项是怎样的呢?

typedef struct {
	Elf32_Addr r_offset;	/* 重定位入口偏移量,即该符号所作用的节加上该偏移量即为待重定位的地址 */
	Elf32_Word r_info;		/* 指定必须对其进行重定位的符号表索引(高24位)以及要应用的重定位类型(低8位) */
} Elf32_Rel;

重定位类型

  • R_386_PC32
    这种重定位类型对应着指令的相对寻址,叫做相对寻址修正,计算方式为 S + A - P
  • R_386_32
    这种重定位类型对应着指令的绝对寻址,叫做绝对寻址修正,计算方式为 S + A
  • A : 保存在被修正位置的值,即待修正位置保存的值,绝对寻址修正时为0
  • P :被修正的位置(相对于节开始的偏移量或虚拟地址),可以通过 r_offset 计算得到
  • S :符号的实际地址,由 r_info 的高24位指定的符号的实际地址

按照经验来看,在绝对寻址修正中,一般待修正位置保存的值为0,即 A = 0
在相对寻址修正中,A = 4,因为调用指令操作数的长度,也就是一个地址的长度为4个字节。
因为在相对寻址时,调用指令时,也是将当前的 PC + 偏移量,而当前的 PC 为调用的指令的下一条指令。

ELF重定位机制实现的部分代码

switch (obj.shdr[i].sh_type) {
    /* 重定位表 */
    case SHT_REL:
        /* rel为重定位表的起始位置,也为第一个重定位表项的首地址 */
        rel = (Elf32_Rel *)(obj.mem + obj.shdr[i].sh_offset);
        
        /* 遍历所有的重定位表项 */
        for (j = 0; j < obj.shdr[i].sh_size / sizeof(Elf32_Rel); j++, rel++) {
            /* sh_link表示该重定位表所用到的符号表的在节表中的下标 */
            symtab = (Elf32_Sym *)obj.section[obj.shdr[i].sh_link];

            /* r_info 的高24位表示重定位符号所在符号表的下标索引 */
            symbol = &symtab[ELF32_R_SYM(rel->r_info)];
            
            /* 重定位表中,sh_info表示该重定位表所作用的节的下标,
             * TargetSection 表示待修正的节 */
            TargetSection = &obj.shdr[obj.shdr[i].sh_info];
            TargetIndex = obj.shdr[i].sh_info;

            /* 待修正位置 = 节的首地址 + 偏移量 */
            TargetAddr = TargetSection->sh_addr + rel->r_offset;

            /* 这也是待修正的位置,*RelocPtr则为其保存的值 */
            RelocPtr = (Elf32_Addr *)(obj.section[TargetIndex] + rel->r_offset);

            /* 此时 st_value 表示符号所代表的变量在节中的偏移 */
            RelVal = symbol->st_value;
            /* st_shndx 表示符号所代表的变量所在的节的下标,sh_addr表示该节的首地址 */
            RelVal += obj.section[symbol->st_shndx].sh_addr;

            /* r_info 的低8位为重定位类型 */
            switch (Elf32_R_TYPE(rel->r_info)) {
                /* R_386_PC32 : S + A - P */
                case R_386_PC32:
                    *RelocPtr += RelVal;
                    *RelocPtr -= TargetAddr;
                    break;
                /* R_386_32 : S + A */
                case R_386_32:
                    *RelocPtr += RelVal;
                    break;
            }
        }
}

参考来源:

  • 《Linux二进制分析》 - Ryan O'Neill
  • 《程序员的自我修养》
posted @ 2021-03-04 17:11  bunner  阅读(930)  评论(0编辑  收藏  举报