当初写的时候,没想着发出来,所以有些网图的地址没有保存,等我找到了再补上。
这篇文章和我的另一篇关于ret2resolve的实验配合使用
ELF是linux下的文件格式,可分为三类
ELF文件分为两种试图,分别是编译时的“链接视图”和程序运行时的“执行试图”。
注意,图1中Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
链接视图以section为单位,执行视图以segment为单位。ELF文件之所以拥有链接视图和执行视图,是为了满足程序在不同阶段的需求,实现了更高的灵活性和效率。
如下,给出使用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查看头部信息如下
可以看到我这是64位的elf程序
unsigned char e_ident[EI_NIDENT]; // EI_NIDENT=16
Elf64_Half e_type //2个子节
,e_type的可取值如下
Elf64_Half e_machine //2个子节
其可取值如下
Elf64_Word e_version //4个子节
,其取值如下
Elf64_Addr e_entry //8个子节
。e_entry代表的是程序的入口地址,上图和使用readelf得到的结果是一样的
Elf64_Off e_phoff //8个子节
。该字段表示程序表头偏移,程序头表即Program Header Table,程序头表包含了关于可执行文件或共享目标文件中各个段(segment)的信息,如段的加载位置、权限、大小等。而e_phentsize
表示每个程序头表条目(Program Header Entry)的大小,程序头表条目包含了一个段的相关信息,比如段的类型、文件偏移量、虚拟地址、大小等。e_phnum
表示程序头表中的条目数量,所以程序头表打大小可以这么计算e_phentsize * e_phnum
。
Elf64_Off e_shoff //8个子节
。用于表示节头表(Section Header Table)在 ELF 文件中的偏移量。头表包含了关于各个节(sections)的信息,如节的名称、大小、偏移量等。其大小可以这么计算e_shentsize * e_shnum
,符号代表的含义和上边一样。
Elf64_Word e_flags //4个子节
。字段的具体含义取决于不同的架构和用途,它可能包含一些特定的标志位,用于标识特定的属性或行为。
Elf64_Half e_ehsize //2个子节
。用于表示 ELF 文件头(ELF Header)的大小。
Elf64_Half e_shstrndx //2个子节
,用于表示节名称字符串表(Section Name String Table)的索引。即包含了所有的节的名称。
之前提到的计算程序头表和节头表大小的字段展示如下:
根据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的内容如下:
Section Header Table就不放全了,太多了,有64*30=1920个字节,图19中14712即通过e_shoff中的0x3978h
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;
}
动态加载器,在第一次调用函数是由_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的回答如下:
ida解析出来的结果是rela,查看其中的一个结果,如下
可知r_offset为4018h, r_info为200000007h,r_offset指向的地址为.got.plt表中的函数偏移地址。
r_info这个参数要分为两部分,高32位是符号索引r_sym
,低32位是重定位类型r_type
,r_sym的值代表Elf64_Sym的索引。Elf64_Sym结构代表.syntab或.dynsym节的信息的,如图22中,r_sym就是2,如图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的内容如下:
总结起来就是
当程序导入函数时,动态链接器在.dynstr段中添加一个函数名称字符串; 在.dynsym段中添加一个指向函数名称字符串的Elf Sym结构体; 在.rel.plt段中添加一个指向Elf Sym的Elf Rel结构体; 最后Elf Rel的r_offse构成GOT表,保存在.got.plt段中。
_dl_runtime_resolve函数可以在plt表中看到。plt表中,两个条目是用于寻找函数地址的,其他条目中都有一个jmp指定,比如下图
红框里的是正常的plt表,在push后,有一个jmp,跳转到sub_1020去寻址。而黑框中cs:qword_4010就是存的_dl_runtime_resolve的绝对地址。
_dl_runtime_resolve(link_map,reloc_arg)具体做的事:
libc的源码中其实顺序和上边有点偏差,不过问题不大,方便理解。
来自于ctfhub
https://writeup.ctfhub.com/Skill/Web%E8%BF%9B%E9%98%B6/Linux/4YqbrWboUwvdxqXmPHW9EQ.html