Android Hook框架adbi的分析(2)

本文博客地址: http://blog.csdn.net/qq1084283172/article/details/74452308

一、 Android Hook框架adbi源码中inline Hook实现部分的代码结构

Android Hook框架adbi源码中inline Hook部分的实现代码结构示意图如下所示,hijack代码部分是前面的博客中提到的root下Android跨进程注入so的注入工具,instruments\base代码部分为inline Hook的操作实现,instruments\example代码部分则为Android Hook框架adbi实现Hook系统调用函数epoll_wait的使用例子。

这里写图片描述

二、 adbi源码中inline Hook实现的详细步骤分析

1 .inline Hook函数被调用的时机

在so库文件加载的时候,会首先执行.init段的构造函数,因此在编写注入到Android目标进程中的so库文件时要定义该构造函数并实现在此处调用inline Hook。inline Hook实现就是在so库文件注入到Android进程中被加载调用该构造函数时被执行的。Android Hook框架adbi基于模块化的设计思想,该构造函数的编写是放在自定义Hook函数的接口中来实现的,在这里就是在Hook函数代码示例instruments\example\epoll.c中定义和实现的。

这里写图片描述

这里写图片描述

2 .inline Hook操作的Hook函数实现

inline Hook操作的Hook函数是在adbi\instruments\base\hook.c中实现的,在Hook目标pid进程的目标函数时,定义了一个全局的静态变量,保存被Hook目标函数相关的信息,用以对目标函数的Hook操作和函数还原,具体的结构定义如下:

1struct hook_t { 2 3 // arm指令模式的12字节Hook 4 unsigned int jump[3]; /* 要修改的hook指令(Arm) */ 5 unsigned int store[3]; /* 被修改的原指令(Arm) */ 6 7 // thumb指令模式的20字节Hook 8 unsigned char jumpt[20]; /* 要修改的hook指令(Thumb) */ 9 unsigned char storet[20]; /* 被修改的源指令(Thumb) */ 10 11 unsigned int orig; /* 被hook的目标函数地址 */ 12 unsigned int patch; /* hook的自定义函数地址 */ 13 14 unsigned char thumb; /* 表明被hook函数使用的指令集,1为Thumb,0为Arm */ 15 unsigned char name[128]; /* 被hook的函数名 */ 16 17 // 用于存放其他的数据(未使用) 18 void *data; 19};

在对目标进程的目标函数进行Hook之前,使用hijack注入工具中查找mprotect函数调用地址的方法,获取被Hook目标函数的调用地址,具体就是通过解析目标函数所在的so库文件中的“.symtab”或者“.dynsym”节,获取到库中所有的符号信息,查找得到目标函数的调用地址的RVA,加上目标函数所在so库文件的加载基地址就是目标函数的调用地址VA了。

1// 对目标pid进程的指定函数进行Hook处理 2 3// h为记录Hook信息的静态变量的指针,pid为被Hook的目标进程的pid,libname为被Hook函数所在的so库文件名称, 4// funcname为被Hook的目标函数,hook_arm为被Hook的函数的arm指令模式的替换函数,hook_thumb为被Hook的函数的thumb指令模式的替换函数 5int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb) 6{ 7 unsigned long int addr; 8 int i; 9 10 // 在指定pid进程的指定so库中查找将被Hook的目标函数funcname的调用地址VA即addr 11 if (find_name(pid, funcname, libname, &addr) < 0) { 12 13 log("can't find funcname: %s\n", funcname) 14 return 0; 15 } 16 17 log("hooking: %s = 0x%lx ", funcname, addr) 18 // 保存被Hook的目标函数的名称 19 strncpy(h->name, funcname, sizeof(h->name)-1);

Arm处理器支持两种指令集,一是基本的Arm指令集,二是Thumb指令集。因此,为了正确的Hook目标函数,不至于导致被Hook的Android进程崩溃,在Hook目标进程的目标函数之前还需要判断进程当前所处的arm指令模式。判断的方法是看函数跳转地址的最后两位是不是全0,如果是,那就是Arm模式的指令,如果最后两位不全为0,那就是Thumb模式的指令。由于Hook目标函数时的跳转指令需要4字节对齐,所以对目标函数调用地址进行4字节取模来判断执行的指令集。


Arm与Thumb之间的状态切换是通过专用的跳转交换指令BX来实现。BX指令以通用寄存器(R0~R15)为操作数,通过拷贝Rn到PC实现绝对跳转。BX利用Rn寄存器中目的地址值的最后一位判断跳转后的状态,如果为“1”表示跳转到Thumb指令集的函数中,如果为“0”表示跳转到Arm指令集的函数中。而Arm指令集的每条指令是32位,即4个字节,也就是说Arm指令的地址肯定是4的倍数,最后两位必定为“00”。所以,直接就可以将从符号表中获得的调用地址模4,看是否为0来判断要修改的函数是用Arm指令集还是Thumb指令集。


上面这段解释说明引用自博主Roland_Sun的博文Android平台下hook框架adbi的研究(下) ,特地摘抄过来帮助分析和理解。

1 // 通过判断函数跳转地址的最后两位是不是全0,来判断指令的运行模式, 2 // 如果后两位全是的0,那就一定是用Arm指令,如果后两位不全为0,那一定是用Thumb指令集 3 4 if (addr % 4 == 0) 5 { 6 // Arm指令模式的HooK目标函数的处理 7 ······ 8 } 9 else 10 { 11 // Thumb指令模式的Hook目标函数的处理 12 ······ 13 }

Arm指令模式HooK目标函数的处理是通过12字节指令覆盖来完成的,简单的来说就是将目标函数调用地址处的前12字节的指令先保存起来,然后使用12字节的Hook跳转指令进行覆盖。


Arm指令模式下Hook目标函数的处理,先将自定义hook函数和要被hook目标函数的地址保存起来。然后生成hook的代码指令,只有3个4字节就是12个字节,第一个dword字节是代码指令“LDR pc, [pc, #0]”,由于pc寄存器读出的值实际上是当前指令地址加8,所以这里是把jump[2]的值加载进pc寄存器中,而jump[2]处保存的是自定义hook函数的地址。因此,jump[0~3]实际上保存的是跳转到自定义hook函数的代码指令。再下面,将被hook函数的前3个4字节保存下来,方便后面函数的恢复。最后,将跳转指令写到被hook目标函数的前12字节。这样以后,当要调用被hook函数的时候,实际执行的指令就是跳转到自定义hook函数处。


1 // Arm指令模式的HooK目标函数的处理 2 if (addr % 4 == 0) { 3 4 log("ARM using 0x%lx\n", (unsigned long)hook_arm) 5 6 // arm指令模式 7 h->thumb = 0; 8 // 自己实现的Hook函数地址 9 h->patch = (unsigned int)hook_arm; 10 // 被Hook目标函数的原函数地址 11 h->orig = addr; 12 13 // 用于Hook目标函数的调用地址为新地址hook_arm 14 h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0] 15 h->jump[1] = h->patch; 16 // pc寄存器读出的值实际上是当前指令地址加8 17 // 把jump[2]的值加载进pc寄存器 18 h->jump[2] = h->patch; 19 20 // 保存原目标函数的12字节指令,用于函数的恢复 21 for (i = 0; i < 3; i++) 22 h->store[i] = ((int*)h->orig)[i]; 23 24 // 覆盖目标函数的12字节指令为Hook函数指令,实现对目标函数的Hook 25 for (i = 0; i < 3; i++) 26 ((int*)h->orig)[i] = h->jump[i]; 27 }

Thumb指令模式下Hook目标函数的处理方式和arm模式下的Hook处理一样,但是基于thumb指令的长度不同,在对目标函数代码指令的覆盖上有所不同,Thumb指令模式下Hook目标函数需要20字节的Hook指令,Hook目标函数的操作是先保存目标函数的前20字节的指令,然后使用20个字节的Hook指令对目标函数进行覆盖处理。

1 // Thumb指令模式的Hook目标函数的处理 2 else { 3 4 // 对自定义Hook函数的调用地址进行指令模式的判断 5 if ((unsigned long int)hook_thumb % 4 == 0) 6 log("warning hook is not thumb 0x%lx\n", (unsigned long)hook_thumb) 7 8 // thumb指令模式 9 h->thumb = 1; 10 log("THUMB using 0x%lx\n", (unsigned long)hook_thumb) 11 12 // 保存用于Hook目标函数的调用地址为新地址hook_thumb 13 h->patch = (unsigned int)hook_thumb; 14 // 保存被Hook目标函数的原函数地址 15 h->orig = addr; 16 17 // 保存寄存器r5,r6的值用于恢复环境(r6在高地址,r5在地址) 18 h->jumpt[1] = 0xb4; 19 h->jumpt[0] = 0x60; // push {r5,r6} 20// 将PC寄存器的值加上12赋值给r5。加上的立即数必须是4的倍数,而加上8又不够,只能加12。 21// 这样的话,读出的PC寄存器的值是当前指令地址加上4,再加上12的话,那么可以算出来r5寄存器的值实际指向的是jumpt[18],而不是jumpt[16]了。 22// 这里还有一点需要注意,对于Thumb的“Add Rd, Rp, #expr”指令来说,如果Rp是PC寄存器的话,那么PC寄存器读出的值应该是(当前指令地址+4)& 0xFFFFFFFC, 23// 也就是去掉最后两位,算下来正好可以减去2。但这里也有个假设,就是被hook函数的起始地址必须是4字节对齐的,哪怕被hook函数是使用Thumb指令集编写的。 24 h->jumpt[3] = 0xa5; 25 h->jumpt[2] = 0x03; // add r5, pc, #12 (比较难理解) 26 // 将保存在jumpt[16]处的hook函数地址加载到r5寄存器中 27 h->jumpt[5] = 0x68; 28 h->jumpt[4] = 0x2d; // ldr r5, [r5] 29 // 降低栈顶,恢复到初始的状态,释放内存空间 30 h->jumpt[7] = 0xb0; 31 h->jumpt[6] = 0x02; // add sp,sp,#8 32 // 用保存的自定义hook函数地址覆盖原来压入的r6的值,r5的值暂时不受影响 33 h->jumpt[9] = 0xb4; 34 h->jumpt[8] = 0x20; // push {r5} 35 // 抬高栈顶,r5的值被保护 36 h->jumpt[11] = 0xb0; 37 h->jumpt[10] = 0x81; // sub sp,sp,#4 38 // 进行出栈操作,pc寄存器得到自定义的Hook函数的地址,r5的值还是原来的 39 h->jumpt[13] = 0xbd; 40 h->jumpt[12] = 0x20; // pop {r5, pc} 41 // 仅仅用于4字节对齐的填充,只是因为前面的add指令只能加4的倍数 42 h->jumpt[15] = 0x46; 43 h->jumpt[14] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary 44 45 // 用于存放自定义Hook函数的调用地址(4字节) 46 memcpy(&h->jumpt[16], (unsigned char*)&h->patch, sizeof(unsigned int)); 47 // sub 1 to get real address,获取到thumb指令模式下函数的真实调用地址 48 unsigned int orig = addr - 1; 49 // 保存被Hook目标函数的原始thumb指令 50 for (i = 0; i < 20; i++) { 51 52 h->storet[i] = ((unsigned char*)orig)[i]; 53 //log("%0.2x ", h->storet[i]) 54 } 55 //log("\n") 56 57 // 覆盖被Hook目标函数的指令为自定义的Hook函数指令 58 for (i = 0; i < 20; i++) { 59 60 ((unsigned char*)orig)[i] = h->jumpt[i]; 61 //log("%0.2x ", ((unsigned char*)orig)[i]) 62 } 63 64 }

Thumb指令模式下Hook目标函数的Hook指令比较难理解,当初也是思考了好久才想明白了一些,主要参考的也是博主Roland_Sun的解释和分析。知道自己很多地方说不清楚,因此有关Thumb指令模式下Hook指令的理解就借用博主Roland_Sun的理解,在此分析基础上进行修改帮助理解。


和对Arm指令集的处理非常相似,只不过跳转指令换成了Thumb。和Arm的处理不同,这里是通过pop指令来修改PC寄存器的值实现函数的Hook跳转操作。
1.首先,入栈r6和r5寄存器的值,并在arm指令操作中寄存器编号大在栈的高地址编号小在栈的低地址,将r5压栈是因为后面的指令执行修改了r5寄存器的值,压栈后方便以后恢复,而将r6寄存器压栈纯粹是为了要保留一个位置。
2.接着,将PC寄存器的值加上12赋值给r5,加上的立即数必须是4的倍数,而加上8又不够,只能加12。这样的话,读出的PC寄存器的值是当前指令地址加上4,再加上12的话,那么可以算出来r5寄存器的值实际指向的是jumpt[18],而不是jumpt[16]了。
3.这里还有一点需要注意,对于Thumb模式下的“Add Rd, Rp, #expr”指令来说,如果Rp是PC寄存器的话,那么PC寄存器读出的值应该是(当前指令地址+4)& 0xFFFFFFFC,也就是去掉最后两位,算下来正好可以减去2。但这里也有个假设,就是被hook函数的起始地址必须是4字节对齐的,哪怕被hook函数使用Thumb指令集编写的。
4.再下面的指令目的就是将保存在jumpt[16]处的自定义hook函数地址覆盖r6寄存器在栈中的值,栈中r5寄存器的值不受影响,仅仅用于后面寄存器环境的恢复。所以,下面的“pop {r5, pc}”指令刚好可以完成恢复r5寄存器并且修改PC寄存器的值,从而实现跳转到自定义hook函数地址处执行。
5.接下来的指令(从jumpt[14])完全是多余的了,完全不会执行到,只是因为前面的add指令只能加4字节的倍数。最后,还有一点不同的是,因为被hook函数是Thumb指令集,所以其真正的内存映射地址是其符号地址减去1。


Hook操作覆盖目标函数的代码指令以后还需要刷新指令缓存。现代的处理器都有指令缓存,用来提高代码指令的执行效率,ARM处理器也一样也有指令缓存机制。虽然目标进程内存中被Hook目标函数的代码指令已经改变,但是cache中的代码指令可能仍为原有的代码指令,再进行代码指令执行时还是优先执行缓存中的代码指令,使得被Hook目标函数修改的指令得不到执行,所以需要手动刷新cache中的代码指令,解决的方法是触发Android系统隐藏刷新cache的系统调用。

1// 调用Android系统的私有系统调用__ARM_NR_cacheflush实现缓存指令的刷新 2void inline hook_cacheflush(unsigned int begin, unsigned int end) 3{ 4 const int syscall = 0xf0002; 5 6 // 禁止编译器对汇编指令进行指令优化 7 __asm __volatile ( 8 "mov r0, %0\n" 9 "mov r1, %1\n" 10 "mov r7, %2\n" 11 "mov r2, #0x0\n" 12 "svc 0x00000000\n" 13 : 14 : "r" (begin), "r" (end), "r" (syscall) // 输入列表 15 : "r0", "r1", "r7" // 修改寄存器列表 16 ); 17}

这里写图片描述

对目标函数进行Hook操作的时候还需要考虑对目标函数Hook的恢复还原和再次对目标函数进行Hook操作的处理。adbi的源码文件adbi\instruments\base\hook.c中,hook_precall函数就是对目标函数进行Hook后的恢复还原,hook_postcall函数就是对目标函数进行恢复还原之后的再次Hook操作。

1// 进行thumb或者arm模式被Hook目标函数指令的恢复即实现函数Hook的恢复 2void hook_precall(struct hook_t *h) 3{ 4 int i; 5 6 // thumb指令模式被Hook目标函数的指令的恢复 7 if (h->thumb) { 8 9 // 获取被Hook目标函数的真实调用地址 10 unsigned int orig = h->orig - 1; 11 // 进行thumb指令模式被Hook指令的恢复 12 for (i = 0; i < 20; i++) { 13 14 ((unsigned char*)orig)[i] = h->storet[i]; 15 } 16 17 } else { 18 19 // 进行arm指令模式被Hook指令的恢复 20 for (i = 0; i < 3; i++){ 21 22 ((int*)h->orig)[i] = h->store[i]; 23 } 24 } 25 26 // 刷新指令缓存 27 hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt)); 28} 29 30// 进行thumb或者arm指令模式Hook目标函数的指令覆盖即实现函数的Hook 31void hook_postcall(struct hook_t *h) 32{ 33 int i; 34 35 if (h->thumb) { 36 37 // 获取thumb指令模式函数真实的调用地址 38 unsigned int orig = h->orig - 1; 39 // 进行thumb指令模式Hook目标函数指令的覆盖 40 for (i = 0; i < 20; i++) 41 ((unsigned char*)orig)[i] = h->jumpt[i]; 42 43 } else { 44 45 // 进行arm指令模式Hook目标函数指令的覆盖 46 for (i = 0; i < 3; i++) 47 ((int*)h->orig)[i] = h->jump[i]; 48 } 49 50 // 刷新指令缓存 51 hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt)); 52}

3 .自定义Hook函数Thumb模式和Arm模式的实现

很显然,在上面的分析中提到的Hook目标函数实现操作中需要提供Thumb模式和Arm模式的自定义Hook函数的实现。在我们进行Hook目标函数的操作中并不知道要被Hook的目标函数是那种模式的指令集,只能通过被Hook目标函数的调用地址来判断,因此需要提供Thumb模式和Arm模式的自定义Hook函数的实现。那么,如何控制将代码编译成Arm指令集还是是Thumb指令集呢?


Android NDK默认情况下将C代码编译成Thumb指令,如果想将C代码编译成Arm指令集,有两种方法:
1.在Android.mk文件中添加上“LOCAL_ARM_MODE := arm”,这样会默认将所有的C代码编译成Arm指令集。
2.前面的方法只能将所有代码全部编译成Arm指令集,如果想一部分代码编译成Arm,一部分编译成Thumb就力不从心了。想要达到这个目的,可以将那些你想编译成Arm指令集的C代码文件名字后面加上一个“.arm”后缀。而其它的没有加上“.arm”后缀的C文件将使用“LOCAL_ARM_MODE”指定的指令集编译,默认情况下是Thumb。注意,这里只是在“LOCAL_SRC_FILES”里列出的C文件名后加上“.arm”后缀就可以了,不要真的去改那个要编译的C文件名。


adbi\instruments\example目录下的实例是用第二种方法指定“epoll.c”编译成Thumb指令,而“epoll_arm.c”编译成Arm指令集,同时连接通过base编译出的静态库。

这里写图片描述

三、 adbi源码中inline Hook实现的流程总结

  1. 在so库文件加载注入到Android目标进程中调用so库文件的构造函数时,调用inline Hook操作Hook目标进程的目标函数;
  2. 通过遍历目标进程的内存布局信息,获取到被Hook目标函数所在的so库文件的内存加载基地址以及解析该so库文件的“.symtab”或者“.dynsym”节获取被Hook目标函数的RVA,进而获取到被Hook目标函数的调用地址;
  3. 通过判断被Hook目标函数调用地址的最后两位是不是全0,来判断被Hook目标函数的指令运行模式是Thumb模式还是Arm模式;
  4. 如果是Arm指令集模式,先保存被Hook目标函数的前12个字节的代码指令,然后使用12字节的Hook代码指令覆盖被Hook目标函数的前12个字节;
  5. 如果是Thumb指令集模式,先保存被Hook目标函数的前20个字节的代码指令,然后使用20字节的Hook代码指令覆盖被Hook目标函数的前20个字节;
  6. 被Hook目标函数的代码指令被Hook修改以后,调用Android系统的隐藏系统调用cacheflush刷新指令缓存,使inline Hook操作生效,待到下一次被Hook目标函数被调用就是调用的我们自定义的Hook函数。

本篇博文中使用到带有注释分析的Android Hook框架adbi的源码下载地址:http://download.csdn.net/detail/qq1084283172/9893002

参考链接:
Android平台下hook框架adbi的研究(下)
Android Arm Inline Hook

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

PhoneGap设置Icon

参考:http://cordova.apache.org/docs/en/latest/config\_ref/images.html通过config.xml中的<icon标签来设置Icon<iconsrc"res/ios/icon.png"platform"ios"width"57"height"57"densi