Linux二进制分析-ELF格式分析及符号重定位
一、ELF文件类型
- ET_NONE:未知类型。
- ET_REL:重定位文件。通常是还未被链接到可执行程序中的一段位置独立的代码,例如linux中的.o格式的文件。
- ET_EXEC:可执行文件。
- ET_DYN:共享目标文件。ELF类型为dynamic,意味着该文件被标记为了一个动态的可连接的目标文件,也称为共享库,这类共享库会在程序运行时被装载并链接到程序的进程镜像中。
- ET_CORE:核心文件,在程序崩溃或进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。
二、ELF文件结构
通过上图可以看到,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
- 《程序员的自我修养》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!