PE文件的组成
头部
DOS头(兼容老版本系统而保留的)
NT头
文件头
扩展头
数据目录表
导出表 : 用于保存dll中的导出符号
导入表 : 用于保存本模块所使用的其它模块的符号
在程序中调用导出函数的时候,形成汇编句式:
call [IAT]
重定位表 : 支持随机加载基址
区段头
.text : 用于保存可执行的代码
.data : 用于全局变量/对象的初始数据
.rdata : 用于保存常量数据
区段数据
ELF文件组成
ELF头
文件头 : 用于记录一个ELF文件的信息(多少位? 能够运行的CPU平台是什么?,程序入口点在哪里?等等)
程序头表 : 程序运行起来的时候, 需要用到哪些区段的数据? 例如,程序的可执行代码存在哪个区段? 常量数据存在哪个区段? ELF文件运行起来的时候, 必须存在程序头,否则ELF文件无法加载并运行.
区段头表 : 用于记录ELF文件的主要的数据.
.text 记录代码
.data 记录数据
.rdata 记录常量数据
.symtab 记录符号表(相当于PE文件的导出表)的数据
.rel.plt 记录某个区段的重定位内容(相当于PE文件的导入表)
命令readelf -h 文件名可以文件头信息
命令readelf -l 文件名可以查看程序头信息
命令readelf -s 文件名可以段信息
区段头
区段名
sh_link字段
如本区段的数据是一张表, 表中的数据要引用其它区段的数据时, link字段就会保存被引用的区段的区段头下标.
sh_info字段
每个不同类型的区段, info的意义是不一样的
例如,区段是符号表时, info的值是一个符号在符号表中的下标. 这个符号符号表中第一个非
LOCAL
类型的符号.
ELF的原理
怎么支持随机加载基址
什么样的汇编代码会产生需要重定位的代码
printf("hello world ");
; 对应的汇编:
push 0x43000 ; "hello world "字符串首地址
call printf
add esp,4
MessageBox(0,0,0,0);
; 对应的汇编:
push 0
push 0
push 0
push 0
call [0x403008]; MessageBox所在的IAT的地址在PE文件中, 使用重定位表来保存需要重定位的代码的位置, 在加载的时候, 如果发生随机基址, 那么就遍历重定位表来修改每个地址.
在ELF文件中, 没有这样的重定位表. 但是ELF文件是支持随机加载基址的. 在ELF文件中,使用的是
getpc + 偏移
的技术来支持随机加载基址.#include <stdio.h>
int g_nNum = 0x12345678;
int main()
{
printf("hello %d",g_nNum);
}汇编:
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ebx
push ecx
call __x86_get_pc_thunk_ax
add eax, 1AA7h
; 根据全局变量的偏移来访问全局变量的值.
; 这样一来是不会产生重定位的.
mov edx, (g_nNum - 1FD8h)[eax] ; mov edx,[eax + g_nNum - 0x1FD8] ; MOV EDX,DWORD PTR DS:[EAX+0x30]
sub esp, 8
push edx
lea edx, (aHelloD - 1FD8h)[eax] ; "hello %d"
push edx ; format
mov ebx, eax
call _printf
add esp, 10h
mov eax, 0
lea esp, [ebp-8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-4]
retngetpc
之后再加上一个偏移值, 得出的值是一个.got
段的首地址一般,只读数据段
.rodata
在.got
的上面, 数据段.data
在.got
的下面.
怎么实现导出表的功能
在PE文件中,导出一个函数需要使用关键字声明或使用def文件定义.才能导出.
PE文件使用导出表来记录导出函数的信息:
导出函数的名字 (导出名称表)
导出函数的序号 (导出表的序号基数+导出地址表的下标)
导出函数在文件中的rva (导出地址表)
在ELF文件中, 导出一个函数不需要任何的额外手续, 在文件中定义的函数都被默认导出. 只有被加了
static
的函数或全局变量才不会被导出.使用符号表来记录导出函数的信息
符号表由符号结构体来组成
符号结构体记录的是:
符号的名字
符号的类型
符号的绑定类型(决定了符号是否被导出)
符号数据在文件中的位置(如果符号是一个函数,那么这个位置就是导出函数的地址.)
符号数据在文件中的大小(如果符号是一个函数, 就是函数机器码占的字节数)
ELF文件的符号表保存在区段表中. 只要区段类型是
SHT_SYMTAB
的时候, 说明该区段保存的就是符号表.符号表使用的结构体:
typedef struct
{
Elf64_Word st_name;//符号名
unsigned char st_info;//符号类型和绑定类型
unsigned char st_other;// 符号的可见性
Elf64_Section st_shndx;//符号所在的区段的下标
Elf64_Addr st_value;//符号数据在文件的偏移
Elf64_Xword st_size;//符号数据所占的字节数
} Elf64_Sym;
怎么实现导入表的功能
在PE文件,使用导入表的时候, 是通过
call [IAT]
来调用导入函数在PE文件中, 使用了导入结构体数组来记录导出的dll和函数
DllName的字段记录要导入的dll的名字
有导入名称表记录了从dll中导入进来的函数名或函数的序号
在加载器加载PE文件的时候, 会根据导入名称表修复导入地址表(IAT表)
在ELF文件中, 使用导入表的时候, 所有的导入函数的地址都保存在
.got
段中, 保存的时候,是以一个4字节的数组来保存的, 这些导入函数的地址在运行前是没有的, 在运行的时候,加载器会找到所有导入函数的地址,并依次填充进去.在调用导入函数的时候, 步骤有两步
; 1. 先调用.plt段中.
call .plt + 偏移 ; 通过偏移调用到.plt段
; 2. 在plt段中有一个句代码.直接通过.got的首地址
; 索引到一个导入函数的地址, 最终完成调用.
.plt jmp [ ebx + 偏移 ] ;ebx保存的是.got的首地址
.got 导入函数地址1
.got 导入函数地址2
.got 导入函数地址3导入函数的名字以及导入函数应该填充到.got表的哪个位置?