Android got hook实现

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。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值