简介

之前在 ELF中.got,.plt section的效果、lazy binding的完成及大局符号介入的影响 一文中说到:关于position independent code ,非static的函数调用是直接进行的,会先调用方针办法对应的一个 plt 跳板函数,然后跳板函数再跳转到对应 got 表项中实在方针函数的地址,完成方针办法的调用的。

补白:

  1. 关于调用当前库自己的非 static 函数,假如其 visibility 是 hidden 或许 protected 的话,是不会有上述直接流程的
  2. 上面没有提lazy binding的流程,由于 plt hook 跟他关系不大,在后面提 invoke original method 完成时会提一下

从上面的直接调用流程可知:假如咱们把对应 got 表项中方针函数地址修正为咱们的署理函数地址,那么后续的调用就会进入咱们的署理函数,然后完成了hook。PLT-GOT hook 也就这么做的,下面咱们看下详细完成。

ELF 加载地址

上面说到要hook某个库对某个函数的调用,就要修正它相应的got表项,修正got表项的条件是咱们能找到它在内存中的地址,从而咱们要先找到该库(ELF)在内存中的加载地址。

常见的查找 elf 文件加载地址的办法有:

  1. 借助 dl_iterate_phdr
  2. 解析 maps

经过dl_iterate_phdr查找elf加载地址

简略示例代码:

static int dl_callback(dl_phdr_info* info, size_t size, void* data) {
    auto arg = (std::pair<const char*, uint64_t>*)data;  
    string libName = arg->first;  
    string dlname = info->dlpi_name;  
    if (!std::equal(libName.rbegin(), libName.rend(), dlname.rbegin())) {  
        return 0;  
    }  
    if (dlname.size() > libName.size() && *(dlname.rbegin() + libName.size()) != '/') {  
        return 0;  
    }  
    arg->second = info->dlpi_addr;  
    return 0;  
}  
static uint64_t getLibLoadAddrWithDlIteratePhdr(const char* libName) {  
    std::pair<const char*, uint64_t> arg = {libName, 0};  
    dl_iterate_phdr(dl_callback, (void*)&arg);  
    return arg.second;  
}

解析maps获取elf加载地址

elf 文件是经过 mmap 映射到内存中的,而 /proc/${pid}/maps 中包含了一切的内存映射信息,因而经过读取并解析 maps 就能够获取 elf 的加载地址。

先来看下 maps 的格局:

address               perms offset  dev   inode  pathname
...
35b1800000-35b1820000 r-xp 00000000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a20000-35b1a21000 rw-p 00020000 08:02 135522 /usr/lib64/ld-2.15.so
35b1c00000-35b1dac000 r-xp 00000000 08:02 135870 /usr/lib64/libc-2.15.so
35b1dac000-35b1fac000 ---p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870 /usr/lib64/libc-2.15.so
...
  1. 最终一列是被映射到内存的库的路径(假如是文件映射的话),经过字符串匹配能够筛选出要查找库的相关条目
  2. 每个库可能会有多个映射段,寻觅到 offset 为 0 的那一段
  3. 榜首列是该映射区段的开始-结束地址,开始地址即是该库的加载地址

补白:
上面说到”寻觅 offset 为 0 的那一段“,一般是能找到的,不过并不能确保。
一般 elf file header 会放到 榜首个 loadable segment 中,因而一般 榜首个 loadable segment 的file offset为0,那么mmap映射的时分,榜首个map region的offset便是0,由此它的start address处便是elf file header在内存中的映射方位,便利咱们解析。可是经过linker script咱们能够将elf file header扫除在 loadable segment之外,还能够对segment做更多调整,因而offset可能不为0。

解析ELF

上面咱们拿到elf文件的加载地址后,就能够解析 elf file header 来找到 program header table的方位。然后遍历 program header table:

  1. 找到榜首个 loadable (PT_LOAD)的segment,依据它的p_vaddr 来计算出 elf base virtual address:load_bias,后续 elf 中各个部分在内存中的虚拟地址就需求经过 load_bias 加上它们的 p_vaddr取得
  2. 找到 dynamic (PT_DYNAMIC)segment,后续解析 dynamic segment 就能够知道动态符号表(.dynsym),动态字符串表(.dynstr),哈希表(.hash,.gnu.hash),重定向表等等重要信息

ELF base virtual address的计算

elf虚拟基地址的计算办法能够参阅Android bionic linker加载segment的完成逻辑:ElfReader::ReserveAddressSpace & ElfReader::LoadSegments

不过更简略直接的方式是看它的注释:

/**
However, in practice, segments do _not_ start at page boundaries. Since we
can only memory-map at page boundaries, this means that the bias is
computed as:
     load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)
(NOTE: The value must be used as a 32-bit unsigned integer, to deal with
     possible wrap around UINT32_MAX for possible large p_vaddr values).
And that the phdr0_load_address must start at a page boundary, with
the segment's real content starting at:
     phdr0_load_address + PAGE_OFFSET(phdr0->p_vaddr)
Note that ELF requires the following condition to make the mmap()-ing work:
     PAGE_OFFSET(phdr0->p_vaddr) == PAGE_OFFSET(phdr0->p_offset)
The load_bias must be added to any p_vaddr value read from the ELF file to
determine the corresponding memory address.
**/

这里明确给出了 load_bias 的计算办法:

load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)

并且也指出后续 elf 中各个部分在内存中的虚拟地址需求经过 load_bias 加上它们的 p_vaddr取得

解析dynamic segment

为了完成函数hook,咱们就需求依据函数名找到对应的符号,因而就需求哈希表,动态字符串表,动态符号表。

为了能修正函数对应got项中的地址,就需求先找到其got项的方位,因而需求重定位表的信息。

而这些信息在 dynamic segment 中都能获取到,解析后能够得到类似如下的结构:

struct Dynamic {
	// plt relocation section info (eg: .rela.plt)
	ElfW(Addr) pltRelSecAddr;// DT_JMPREL
	ElfW(Xword) pltRelSecSize;// DT_PLTRELSZ
	// dynamic 其他部分的重定位信息 (eg: .rela.dyn)
	ElfW(Addr) relSecAddr;// DT_RELA or DT_RELA,重定位表的地址
	ElfW(Xword) relSecSize;// DT_RELASZ or DT_RELSZ,重定位表的巨细
	ElfW(Xword) relSecEntrySize;// DT_RELAENT or DT_RELENT,重定位表 表项巨细
	// plt 对应的 got section info (.got.plt)
	ElfW(Addr) gotPltSecAddr;// DT_PLTGOT
	// 动态字符串表信息 (.dynstr)
	ElfW(Addr) dynstrSecAddr;// DT_STRTAB,动态字符串表地址
	ElfW(Xword) dynstrSecSize;// DT_STRSZ,动态字符串表巨细
	// 动态符号表信息 (.dynsym)
	ElfW(Addr) dynsymSecAddr;// DT_SYMTAB,动态符号表地址
	ElfW(Xword) dynsymEntrySize;// DT_SYMENT,动态符号表 表项巨细
	// hash section addr
	ElfW(Addr) hashSecAddr;
	ElfW(Addr) gnuHashSecAddr;
	bool isRela;
	bool bindNow;
};

补白:此处疏忽了 relr 类型的重定位表

查找函数符号

依据函数名,哈希表(.hash,.gnu.hash),动态符号表(.dynsym),动态字符串表(.dynstr)就能够查找到方针符号,详细查找办法能够参阅:ELF 经过 Sysv Hash & Gnu Hash 查找符号的完成及对比

查找方针got项的方位

ELF中.got,.plt section的效果、lazy binding的完成及大局符号介入的影响 一文中说到:关于非 static 函数的调用,由于符号地址在编译时不知道(外部库的符号地址不知道很好了解,同一个库的非static符号地址不知道是由于大局符号介入的影响),运行时由动态链接器将符号地址填入对应的got项中,这样函数调用的时分就能从got项中找到方针函数的地址以跳入履行。

那么动态链接器是怎么知道某个符号对应的got项在哪儿呢?由于有重定位项信息,咱们来看个详细比如:

Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     ...
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND malloc@GLIBC_2.17 (2)
     ...
Relocation section '.rela.plt' at offset 0x440 contains 3 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name + Addend
...
0000000000020008  0000000500000402 R_AARCH64_JUMP_SLOT    0000000000000000 malloc@GLIBC_2.17 + 0
...

关于aarch64架构,plt 重定位项的类型一定是:R_AARCH64_JUMP_SLOT,其info中有对应符号的索引,从上面的比如来看 symbol index = ELF64_R_SYM(0x0000000500000402) 正好是 5,跟上面动态符号表中的共同,而其 offset + load_bios 正是方针 got 项的方位。

一个简略完成比如:

ElfW(Addr) Elf::findSymbolRelAddr(uint32_t symbolIdx) const {
    auto start = (const ElfW(Rela)*)dynamic_.pltRelSecAddr;// rela for example 
    auto end = start + dynamic_.pltRelSecSize;  
    for (; start < end; ++start) {  
        if (ELF64_R_SYM(start->r_info) == symbolIdx) {  
            assert(ELF64_R_TYPE(start->r_info) == R_AARCH64_JUMP_SLOT);  
            return loadBias_ + start->r_offset;  
        }  
    }  
    return 0;  
}

完成hook

写权限

上面现已找到方针 got 项的方位,因而咱们向其间写入署理函数的地址,hook就完成了。

关于推迟绑定的情况来说,一般 .got.plt地点内存确实是有写权限的,由于跟着代码的履行,可能不断有函数榜首次被调用,需求动态链接器去查找并写入其地址。可是假如是加载时当即绑定的情况(Android arm架构便对错推迟绑定的情况),链接器在加载动态库时就会完成一切符号的绑定,因而一般会将 .got.plt 设置为只读模式。

因而在写入署理函数地址之前,需求先判别下是否有写权限,这个能够经过读 maps 来得知。假如没有写权限的话,能够经过 mprotect 先赋予写权限:

if (mprotect((void*)PAGE_START(symbolRelAddr), PAGE_SIZE, m->perms() | PROT_WRITE) != 0) {
	// todo handle error
}

原函数地址

一般在署理函数里面需求调用原函数,hook 办法一般需求回来原函数的地址。那么如何获取原函数的地址呢?

  1. 假如是当即绑定的case,那么对应got项中的值便是原函数的地址,在写入署理函数地址前先将其读出即可
  2. 假如是推迟绑定的case,hook前原函数可能还没有调用过,此刻got项中保存的是plt中的跳板函数地址,以跳入动态链接器的符号查找函数,这种情况能够经过dlsym来查找原函数地址

那么如何判别是当即绑定仍是推迟绑定呢?(Android arm架构其实不必判别,都是当即绑定的)

  1. 假如动态库.dynamicsection中存在DT_BIND_NOW项,那么会当即绑定
  2. 假如动态库.dynamicsection中DT_FLAGSvalue设置了DT_BIND_NOW或许DT_FLAGS_1value设置了DF_1_NOW的话,会当即绑定
  3. 假如运行时环境变量包含LD_BIND_NOW,会当即绑定

调用原函数

有了原函数地址后,调用原函数就简略了:将其cast成原函数类型直接调用就行。也能够界说一个简略的宏来简化运用,比如:

#define INVOKE_ORIGINAL(func, addr, ...) (((decltype(func)*)addr)(__VA_ARGS__))
void* my_malloc(size_t size) {  
    LOGI("my_malloc invoked with size: %zu", size);  
    void* ptr = INVOKE_ORIGINAL(my_malloc, malloc_fun_addr, size); 
    LOGI("malloc res: %p", ptr);  
    return ptr;  
}

铲除指令缓存

CPU有指令缓存,咱们修正got项后还需求铲除一下,使得CPU从头取指:

__builtin___clear_cache((char*) PAGE_START(symbolRelAddr), (char*) PAGE_END(symbolRelAddr));

Hook 一切库中某个函数的调用

有些情况下需求hook一切库中对某个函数的调用,这个时分有个费事的当地:有些库是在 hook 办法调用之后才被加载的,那怎么能hook到它呢?一种办法是咱们在内部hook一切库的 dlopen 办法,这样后续加载库的时分咱们hook框架能立马感知到,然后对新加载的库进行hook。