GOT表hook

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct elf32_hdr {
unsigned char e_ident[EI_NIDENT];
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;

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
2
3
4
5
6
7
8
9
10
11
12
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:节区名称在字符串表中的偏移
sh_type:节区类型

通过判断类型值和节区名称来确定GOT表节区

最后获取到模块加载地址后作为基址,加上偏移即是GOT表地址,GOT表就是一个地址表,通过遍历挨个地址匹配,找到后修改即可,注意修改内存页权限。(不知道为什么GOT在内存中是没有可写权限的)

在地址0xb6f2af54处

代码地址:https://github.com/Minxin/gothooklocal

结果

  在测试中发现一些问题,程序在调用函数时,特别在多次调用时,会将函数指针存放在寄存器中,供下一次调用使用,这个时候无论修改何处,寄存器中的值是不变的,每次调用形式为:BLX R4.所以这样hook是没有任何效果的。
  写在其他函数中,调用就会正常,间接跳转的形式,GOT修改后生效。
result-w500

可以看到GOT修改之后,just 2just 3是通过寄存器跳转的,没有效果;for test是通过间接跳转的,生效。

远程hook

相比较的话,多了一步注入操作。

注入程序参照:https://blog.csdn.net/qq1084283172/article/details/46859931
注入流程:
injectflow-c

注入方案:

我们将上面实现的本地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。

结果

注入程序:
inject-w500

被注入程序:
tobehook-w500

被注入程序首先在被hook前调用puts,进入sleep,启动注入进程,完成注入和hook后,输出的信息和之前本地hook的输出差不多,最后继续执行,再次调用puts时,已经被myputs代替,输入信息前有前缀”FAKE:”。

代码地址:https://github.com/Minxin/gothookremote