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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章