ELF基本知识
讲解got hook之前,我们首先要对ELF的文件结构有一个基本的认识。ELF中的内容主要包括代码、数据,以及符号表,字符串等。这些信息以”节"(section)的形式存储。我习惯把节称为段。常见的段比如代码段.text,数据段.data,字符串表.strtab,符号表.symtab。
需要注意的是符号表和字符串表的关系,符号表中不会直接存储符号名,符号名等字符串会被放到字符串表中,符号表只会存储符号名在字符串表中的偏移信息。段名也同理,段头表中不会存储段名,段名会被放到段头字符串表中。
静态链接时还会用到.rel.*的段,存储重定位表。比如代码段.text如有要被重定位的地方,那么会有一个相对应叫“.rel.text”的段保存了代码段的重定位表。重定位表中记录需要重定位的符号在符号表中的下标,以及重定位的位置(即修正地址的位置)。
和静态链接不同,动态链接的时候不能重定位代码段。因为动态链接的目的就是为了共享代码,节约内存。倘若代码中的指令能被修改,就肯定没办法做到共享指令,因为不同进程的内存分布不同,它们重定位指令的结果肯定也是不同的。ELF通过重定位数据段中的全局偏移表(Global Offset Table, GOT)和PLT来实现动态链接。
代码段中并不直接跳转到函数,而是先跳转到”函数名@plt“,”函数名@plt“中的第一条指令就是跳转到GOT中相应项保存的地址。初始时GOT中相应项保存的地址是”函数名@plt“第二条指令的地址,这样程序又跳回了PLT,执行后面的解析代码。解析代码会找到函数真正的地址,并填写进GOT相应项中。这样下次发生调用的时候,程序就不用再跳到解析代码而直接跳到函数地址执行了。
动态链接有专门的段。其中.got和.got.plt就是GOT表,前者是变量的GOT表,后者是函数的GOT表。.rel.dyn是对数据引用的重定位表,它所修改的位置位于.got以及数据段;而.rel.plt是对函数引用的重定位表,它所修改的位置位于“.got.plt”。
got hook其实就是修改目标函数在got表中的表项,使其变成我们的hook函数的地址。这样每次发生目标函数调用的时候,实际调用的就是hook函数。
.rel.dyn和.rel.plt
既然要修改目标函数的表项,我们首先得找到该表项的位置。我们可以通过elf文件的.rel.dyn和.rel.plt段来找到函数的相关重定位信息。
“.rel.dyn”和“.rel.plt”是重定位表。“.rel.dyn”是对数据引用的修正,它所修改的位置位于“.got”以及数据段;而“.rel.plt”是对函数引用的修正,它所修改的位置位于“.got.plt”。在got hook中,其实我们只要找.rel.plt就可以了。
这两个段是Elf32_Rel结构的数组,Elf32_Rel定义如下:
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
)Elf32_Rel;
每个Elf32_Rel对应一个需要重定位的符号,可以是函数也可以是变量。为了方便讲解,下面统一将其视为函数。
在.rel.dyn和.rel.plt段的Elf32_Rel中,r_offset就是该函数的got表项的虚拟地址。注意这里指的是虚拟地址,而不是偏移。因为有些elf文件的起始地址并不是0,我们需要将虚拟地址减去起始地址才能得到got表项的偏移地址。再把偏移地址加上动态库被装载的地址,我们就能得到函数got表项的真实地址,也就是我们要写入hook函数地址的位置。
r_info的高24位表示函数名在符号表中的下标。符号表里有符号的相关信息,我们需要该信息来取得Elf32_Rel对应的函数名,和我们要找的函数的名字一一匹配,从而找到目标函数对应的Elf32_Rel。
既然我们要读取elf文件中的某个段,我们就要对elf文件进行解析。接下来我们看elf文件是怎么解析的。
读取ELF Header
解析ELF从读取文件头开始。ELF Header位于文件首部,是Elf32_Ehdr结构。
typedef struct{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_off e_phoff;
Elf32_off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
sh是section headers段头表的缩写,ph是program headers的缩写。从名字上看我们大概知道里面各个成员变量的意思。例如shoff就代表段表的偏移。
而shstrndx代表section header string table index,是段表字符串表在段头表中的下标。段表字符串表是存储段表名的段。我们可以根据该下标找到段表字符串表的段头,进而读取该段,有了该段之后,我们就能查到其它段的名字。
读取ELF Header的代码很简单,就是把ELF首部数据转成Elf32_Ehdr类型就行了:
const Elf32_Ehdr * ElfParser::read_elf_header() {
if (elf_header != NULL)
{
return elf_header;
}
//make sure that the file have been opened
if (elf_file_ == NULL)
{
LOGE("[-] read_elf_header: fail to open %s",elf_name_);
return NULL;
}
//read magic
fseek(elf_file_, 0, SEEK_SET);
char magic[4];
fread(magic, 1, 4, elf_file_);
if (strncmp(magic + 1, "ELF", 3) != 0)
{
LOGE("[-] read_elf_header: %s is not a elf file", elf_name_);
return NULL;
}
//read file header
elf_header = new Elf32_Ehdr;
fseek(elf_file_, 0, SEEK_SET);
fread(elf_header, sizeof(Elf32_Ehdr), 1, elf_file_);
return elf_header;
}
读取section header string table
前面提到了,section header string table(shstrtab)段中存储了段的名字字符串,如果我们要找.rel.plt,我们首先得先得到各个段的段名。
从文件头中,我们可以得到section header string table段头在段头表中的下标,根据这个和段头表的偏移,我们就能得到section header string table的段头。段头是Elf32_Shdr类型的结构体,如下:
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
其中sh_name是段名字符串在“.shstrtab"段中的偏移。sh_offset是段的偏移。sh_size是段的大小。我们读取段头,找到段,然后把shstrtab段的所有内容都存储起来。
const char* ElfParser::read_section_header_string_table() {
if (section_header_string_table != NULL)
{
return section_header_string_table;
}
read_elf_header();
if (elf_header == NULL) //currently the only situation that causes header become NULL is that the file is not elf
{
LOGE("[-] read_section_header_string_table: can not get section header string table because missing Elf header");
return NULL;
}
Elf32_Shdr SecStrHeader; //header of section headers string table
int Size; //size of section headers string table
auto Index = elf_header->e_shstrndx; //Index of section headers string table in section headers table
fseek(elf_file_, elf_header->e_shoff + Index * sizeof(Elf32_Shdr), SEEK_SET); //seek the poision of section header of section headers string table in section headers
fread(&SecStrHeader, sizeof(Elf32_Shdr), 1, elf_file_); //read the section header
Size = SecStrHeader.sh_size; //get the size of section
section_header_string_table = new char[Size]; //allocate space to back up section headers string table
fseek(elf_file_, SecStrHeader.sh_offset, SEEK_SET);//seek the offset of section headers string table
fread(section_header_string_table, Size, 1, elf_file_); //read the content of section and back it up
return section_header_string_table;
}
read_section_header根据名字读取对应的段头
读取了shstrtab之后,我们就可以指定名字来读取相应段的段头了。其实就是遍历段头表,根据段头里的名字偏移信息从shstrtab查看每个段的名字,如果名字匹配,则返回该段头。
int ElfParser::read_section_header(const char *SectionName, Elf32_Shdr *Section) {
read_section_header_string_table();
if (section_header_string_table_ == NULL) //make sure that the section headers string table have been found
{
LOGE("[-] read_section_header: fail to read section header %s because missing string table", SectionName);
return -1;
}
Elf32_Shdr TmpHeader;
char *TmpStr;
int SectionHeadersNum = elf_header_->e_shnum;
//we can assume that ElfHeader exists because the string table, which need ElfHeader to find, have been found
fseek(elf_file_, elf_header_->e_shoff, SEEK_SET);
for (int i = 0; i<SectionHeadersNum; i++)
{
fread(&TmpHeader, sizeof(Elf32_Shdr), 1, elf_file_);
TmpStr = section_header_string_table_ + TmpHeader.sh_name;
//LOGD("section name: %s", TmpStr);
if (strcmp(TmpStr, SectionName) == 0)
{
*Section = TmpHeader;
return 0;
}
}
LOGE("[-] read_section_header: %s does not exist in %s", SectionName, elf_name_);
return -1;
}
get_symbol_got_item_vaddr得到函数got项虚拟地址
接下来我们开始正式读取函数got表项的偏移。
void* ElfParser::get_symbol_got_item_vaddr(char *SymbolName) {
Elf32_Shdr dynsymHeader, dynstrHeader, reldynHeader, relpltHeader;
void* ReturnOffset = NULL;
long dynsymSymbolNum, reldynSymbolNum, relpltSymbolNum;
//read the relative headers
if (read_section_header(STR_DYNSYM, &dynsymHeader)<0 ||
read_section_header(STR_DYNSTR, &dynstrHeader)<0 ||
read_section_header(STR_RELDYN, &reldynHeader)<0 ||
read_section_header(STR_RELPLT, &relpltHeader)<0)
{
LOGE("[-] get_symbol_got_item_offset: fail to get symbol offset because missing some section");
return NULL;
}
//...
读取了4个段的段头。其中rel.dyn和rel.plt是重定位表,dynsym是符号表,dynstr是字符表。读取这4个段的目的是为了从重定位表中找到符号索引,根据索引找到符号表中对应符号,再根据符号信息在字符表中找到符号的名字。最后判断符号名字和我们要找的函数名字是否匹配。
//...
//read the section according to the information from section header
dynsymSymbolNum = dynsymHeader.sh_size / sizeof(Elf32_Sym);//number of symbol in .dynsym;
reldynSymbolNum = reldynHeader.sh_size / sizeof(Elf32_Rel);
relpltSymbolNum = relpltHeader.sh_size / sizeof(Elf32_Rel);
char *dynstr = new char[dynstrHeader.sh_size];
Elf32_Sym *dynsym = new Elf32_Sym[dynsymSymbolNum]; //memory leak
Elf32_Rel *reldyn = new Elf32_Rel[reldynSymbolNum];
Elf32_Rel *relplt = new Elf32_Rel[relpltSymbolNum];
fseek(elf_file_, dynstrHeader.sh_offset, SEEK_SET);
fread(dynstr, dynstrHeader.sh_size, 1, elf_file_);
fseek(elf_file_, dynsymHeader.sh_offset, SEEK_SET);
fread(dynsym, sizeof(Elf32_Sym), dynsymSymbolNum, elf_file_);
fseek(elf_file_, reldynHeader.sh_offset, SEEK_SET);
fread(reldyn, sizeof(Elf32_Rel), reldynSymbolNum, elf_file_);
fseek(elf_file_, relpltHeader.sh_offset, SEEK_SET);
fread(relplt, sizeof(Elf32_Rel), relpltSymbolNum, elf_file_);
//...
根据段头从elf中把段全部读了出来。接下来就可以从段中找寻符号信息了
for (int i = 0; i<relpltSymbolNum; i++) //traverse the .rel.plt
{
uint16_t SymIndex = ELF32_R_SYM(relplt[i].r_info); //get one .rel.plt symbol index in .dynsym
if (SymIndex > dynsymSymbolNum)
{
continue;
}
char *relpltSymName = dynsym[SymIndex].st_name + dynstr;
LOGD("[d] function name in .rel.plt: %s",relpltSymName);
if (strstr(relpltSymName, SymbolName) != 0)
{
ReturnOffset = (void*)relplt[i].r_offset;
LOGI("[+] find the item of %s in .rel.plt, the vaddr the item record is %p", relpltSymName, ReturnOffset);
goto RETURN_RESULT;
}
}
LOGE("[-] get_symbol_got_item_offset: fail to find %s", SymbolName);
RETURN_RESULT:
delete []dynstr;
delete []dynsym;
delete []reldyn;
delete []relplt;
return ReturnOffset;
}
get_loadsegment_offset得到起始地址
注意get_symbol_got_item_vaddr得到的只是函数got项的虚拟地址。把虚拟地址减去elf虚拟空间的起始地址,才能得到got项的偏移地址。根据ida的反编译结果,load段(这里是真正的段,前面所说的段其实指的是节)的起始地址就是该elf虚拟空间的起始地址。段头表和节头表一样,它的偏移同样可以在elf文件头中取得。在段头表中找到load段,返回其虚拟地址,这样就得到了elf起始地址。
//actually i don't understand why it work, maybe it's wrong
//return -1 if fail
long ElfParser::get_loadsegment_offset() {
read_elf_header();
if (elf_header_ == NULL)
{
LOGE("[-] get_loadsegment_offset: can not get load segment offset because missing elf header");
return -1;
}
Elf32_Phdr ProgramHeaderTab;
int ProgramHeaderNum = elf_header_->e_phnum;
fseek(elf_file_, elf_header_->e_phoff, SEEK_SET);
for (int i = 0; i < ProgramHeaderNum; i++)
{
fread(&ProgramHeaderTab, sizeof(Elf32_Phdr), 1, elf_file_);
if (ProgramHeaderTab.p_type == 1)
{
LOGI("[+] load segment vaddr of %s is %p", elf_name_,ProgramHeaderTab.p_vaddr);
return ProgramHeaderTab.p_vaddr;
}
}
LOGE("[-] get_loadsegment_offset: can not find load segment");
return -1;
}
get_targetfunc_rel_addr取得函数重定位地址
前面通过ElfParser,得到函数重定位地址(got项地址)相对于动态库起始地址的偏移地址。接下来只要将这个偏移地址加上动态库被装载的地址,就得到函数在进程中的重定位地址。
void* GotHooker::get_targetfunc_rel_addr() {
void* so_base_addr=NULL; //address where so file load
void* symbol_got_addr=NULL;
void* symbol_got_item_vaddr=NULL;
long loadsegment_offset=-1;
ElfParser Parser(library_name_);
so_base_addr=find_module_addr_by_name(-1,library_name_);//m_SoFileName;
if(so_base_addr==NULL)
{
LOGE("[-] get_targetfunc_rel_addr: fail to get %s address",library_name_);
return NULL;
}
symbol_got_item_vaddr=Parser.get_symbol_got_item_vaddr(targetfunc_name_);
loadsegment_offset=Parser.get_loadsegment_offset();
if(symbol_got_item_vaddr==NULL||loadsegment_offset==-1){
LOGE("[-] get_targetfunc_rel_addr: fail to get symbol_got_item_vaddr or load segment");
return NULL;
}
symbol_got_addr=(void*)((long)so_base_addr + (long)symbol_got_item_vaddr-loadsegment_offset);
LOGI("[+] get_targetfunc_rel_addr: Symbol relocating address is %p",symbol_got_addr);
return symbol_got_addr;
}
其实就是so_base_addr + symbol_got_item_vaddr-loadsegment_offset。so_base_addr可以在proc/-1/maps中得到。具体怎么找可以看另一篇文章Android注入要点记录。
do_hook完成hook工作
取得了重定位地址后,直接将重定位地址记录的函数地址改成我们自己的函数地址就完成got hook了。
void GotHooker::do_hook(){
targetfunc_relocation_addr_= get_targetfunc_rel_addr();
if(targetfunc_relocation_addr_==NULL){
LOGE("do_hook: fail because targetfunc_relocation_addr_ is NULL");
return;
}
targetfunc_original_addr_=(void*)*(long*)targetfunc_relocation_addr_;
LOGI("[+] do_hook: address recorded in .got table is %p",targetfunc_original_addr_);
change_addr_writtable((long) targetfunc_relocation_addr_, true);
if(hook_function_addr_!=NULL){
*(long*)targetfunc_relocation_addr_=(long)hook_function_addr_;
}
change_addr_writtable((long) targetfunc_relocation_addr_, false);
LOGI("[+] do_hook: after changing, address recorded in .got table is %p",*(long*)targetfunc_relocation_addr_);
}
注意在修改之前要更改重定位地址处的保护模式,改成可写:
bool change_addr_writtable(long address, bool writable)
{
long page_size=sysconf(_SC_PAGESIZE);
long page_start=(address)&(~(page_size-1));
if(writable){
return mprotect((void*)page_start,page_size,PROT_READ|PROT_WRITE|PROT_EXEC)!=-1;
} else{
return mprotect((void*)page_start,page_size,PROT_READ|PROT_EXEC)!=-1;
}
}
64位程序got hook实现
要实现64位程序的got hook,我们需要修改ElfParser。64位ELF文件的结构和32位文件的结构是一样的,它们都由文件头,段表等构成,因而ElfParser的代码逻辑不用改。而在elf.h头文件中我们可以看到,ELF32的结构体和ELF64的结构体的成员名字是一样的,只不过成员类型和结构体的名字有所差异。这说明ElfParser的代码部分完全不用动,只要将结构体名换一换就行了。另外,64位ELF中重定位表的名字变成了”.rela.dyn"和“.rela.plt",所以重定位表名字也要换掉,如下:
#ifdef __aarch64__
#define Elf32_Dyn Elf64_Dyn
#define Elf32_Rel Elf64_Rel
#define Elf32_Rela Elf64_Rela
#define Elf32_Sym Elf64_Sym
#define Elf32_Ehdr Elf64_Ehdr
#define Elf32_Phdr Elf64_Phdr
#define Elf32_Shdr Elf64_Shdr
#define Elf32_Nhdr Elf64_Nhdr
#define ELF32_R_SYM ELF64_R_SYM
#define ELF32_R_TYPE ELF64_R_TYPE
#define STR_RELDYN ".rela.dyn"
#define STR_RELPLT ".rela.plt"
#endif
其中#define ELF32_R_SYM ELF64_R_SYM和#define ELF32_R_TYPE ELF64_R_TYPE是将宏进行了重定义。这样重定义之后,在预处理的时候elf.h头文件中的ELF32_R_SYM失效,源代码中的这个符号会被换成ELF64_R_SYM,然后再被elf.h中的ELF64_R_SYM宏换成相应的计算代码。ELF64_R_TYPE同理。这里涉及到了预处理器的工作顺序。
这样就实现了64位程序的got hook。