GOT表hook
ELF文件中,GOT表和PLT表,不同映像间的函数和数据引用都是通过它们实现的。GOT(全局偏移表)给出了映像中所有被引用符号(函数或变量)的值。每个普通PLT表项相当于一个函数的桩函数(stub),支持懒绑定的情况下,当发生对外部函数的调用时,程序会通过PLT表将控制交给动态连接器,后者解析出函数的绝对地址,修改GOT中相应的值,之后的调用将不再需要连接器的绑定。由于linker是不支持懒绑定的,所以在进程初始化时,动态链接器首先解析出外部过程引用的绝对地址,一次性的修改所有相应的GOT表项。对共享对象来说,由于GOT,PLT节以及代码段和数据段之间的相对位置是固定的,所有引用都是基于一个固定地址(GOT)的偏移量,所以实现了PIC代码,重定位时只需要修改可写段中的GOT表。而可执行程序在连接过程中则可能发生对不可写段的修改。如果只读段和可写段不是以固定的相对位置加载的,那么在重定位是还需要修改所有指向GOT的指针。
本地hook
ELF文件结构
我们需要了解一下ELF文件的结构,因为我们得到GOT表地址是通过ELF文件格式中的字段来一步步索引的。
索引过程为:从ELFHeader里找到字符串表,因为要找节表是通过名称来搜索的,所以首先要字符串表的地址。
ELFHeader
1 | typedef struct elf32_hdr { |
Elf32_Ehdr.e_shoff:节区头表偏移
Elf32_Ehdr.e_shstrndx:字符串表在节区头表中的索引
Elf32_Ehdr.e_shentsize:每个节区头大小
字符串表偏移:Elf32_Ehdr.e_shoff+Elf32_Ehdr.e_shstrndx*Elf32_Ehdr.e_shentsize
节区头表名称来源有里之后,就可以遍历节区头表了。
节区头表偏移:Elf32_Ehdr.e_shoff
ELFSectionHeader
1 | typedef struct elf32_shdr { |
sh_name:节区名称在字符串表中的偏移
sh_type:节区类型
通过判断类型值和节区名称来确定GOT表节区
最后获取到模块加载地址后作为基址,加上偏移即是GOT表地址,GOT表就是一个地址表,通过遍历挨个地址匹配,找到后修改即可,注意修改内存页权限。(不知道为什么GOT在内存中是没有可写权限的)
在地址0xb6f2af54处
代码地址:https://github.com/Minxin/gothooklocal
结果
在测试中发现一些问题,程序在调用函数时,特别在多次调用时,会将函数指针存放在寄存器中,供下一次调用使用,这个时候无论修改何处,寄存器中的值是不变的,每次调用形式为:BLX R4
.所以这样hook是没有任何效果的。
写在其他函数中,调用就会正常,间接跳转的形式,GOT修改后生效。
可以看到GOT修改之后,just 2
和just 3
是通过寄存器跳转的,没有效果;for test
是通过间接跳转的,生效。
远程hook
相比较的话,多了一步注入操作。
注入程序参照:https://blog.csdn.net/qq1084283172/article/details/46859931
注入流程:
注入方案:
我们将上面实现的本地hook代码编译成动态库,把这个库注入到目标进程,然后调用hook函数,那么需要目标程序调用dlopen函数来装载库,调用dlsym来获取到hook函数地址,最后调用。并且这些函数需要的参数我们也需要写入目标进程的地址空间中,所以还需要mmap来开出一块地址空间开写入参数数据。
获取函数地址,我们计算一下偏移即可:remote_func_addr=local_func_addr+(remote_base-local_base)
获取本模块内存基址和目标模块的内存基址,差值即为某函数在本地地址和目标地址的偏移。
基址获取:
在/proc/[pid]/maps
中查找模块名,对应行的起始地址就是模块的基址。
开辟空间:
调用mmap得到一块地址空间,mmap函数定义void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
调用前需要构造参数。
参数设置:
arm中参数存放在r0~r3及栈中,超出四个参数,就需要用栈存放。
利用ptrace设置布置好的参数列表,跳转到函数的话,直接设置pc值即可。
mmap调用后需要获得返回值,返回值是开辟出的地址空间的地址,返回值是存放在r0中的,PTRACE_GETREGS获取寄存器,取出r0即可。
装载hook库:
调用dlopen将指定的库装载进目标进程中void * dlopen( const char * pathname, int mode);
这里由于参数是指针,是一个地址,进程中没有需要的字符串,所以需要将字符串写入目标进程中,之前mmap申请的空间派上用场了,使用PTRACE_POKETEXT/PTRACE_POKEDATA写入指定地址即可,注意字节数量。
dlopen调用后返回装载的库的句柄,也需要获取。
获取函数地址:
调用dlsym获得hook函数的地址void* dlsym(void* handle,const char* symbol)
这里的符号参数也需要写入目标内存中,用ida打开so,可以看它的导出符号。
dlsym的返回值就是hook函数的地址,也需要获取。
最后再调用hook函数即可。
其实hook函数的获取可以使用计算偏移的方式,注入程序自己也装载动态库,获取hook函数地址后,加上基址差值,也可以得到hook函数在目标进程中的地址。可以不用让目标程序调用dlsym。
结果
注入程序:
被注入程序:
被注入程序首先在被hook前调用puts,进入sleep,启动注入进程,完成注入和hook后,输出的信息和之前本地hook的输出差不多,最后继续执行,再次调用puts时,已经被myputs代替,输入信息前有前缀”FAKE:”。