本文博客地址: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操作和函数还原,具体的结构定义如下:

struct hook_t {

    // arm指令模式的12字节Hook
unsigned int jump[3]; /* 要修改的hook指令(Arm) */
unsigned int store[3]; /* 被修改的原指令(Arm) */ // thumb指令模式的20字节Hook
unsigned char jumpt[20]; /* 要修改的hook指令(Thumb) */
unsigned char storet[20]; /* 被修改的源指令(Thumb) */ unsigned int orig; /* 被hook的目标函数地址 */
unsigned int patch; /* hook的自定义函数地址 */ unsigned char thumb; /* 表明被hook函数使用的指令集,1为Thumb,0为Arm */
unsigned char name[128]; /* 被hook的函数名 */ // 用于存放其他的数据(未使用)
void *data;
};

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

// 对目标pid进程的指定函数进行Hook处理

// h为记录Hook信息的静态变量的指针,pid为被Hook的目标进程的pid,libname为被Hook函数所在的so库文件名称,
// funcname为被Hook的目标函数,hook_arm为被Hook的函数的arm指令模式的替换函数,hook_thumb为被Hook的函数的thumb指令模式的替换函数
int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
unsigned long int addr;
int i; // 在指定pid进程的指定so库中查找将被Hook的目标函数funcname的调用地址VA即addr
if (find_name(pid, funcname, libname, &addr) < 0) { log("can't find funcname: %s\n", funcname)
return 0;
} log("hooking: %s = 0x%lx ", funcname, addr)
// 保存被Hook的目标函数的名称
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的研究(下),特地摘抄过来帮助分析和理解。

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

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函数处。


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

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

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

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的系统调用。

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




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

// 进行thumb或者arm模式被Hook目标函数指令的恢复即实现函数Hook的恢复
void hook_precall(struct hook_t *h)
{
int i; // thumb指令模式被Hook目标函数的指令的恢复
if (h->thumb) { // 获取被Hook目标函数的真实调用地址
unsigned int orig = h->orig - 1;
// 进行thumb指令模式被Hook指令的恢复
for (i = 0; i < 20; i++) { ((unsigned char*)orig)[i] = h->storet[i];
} } else { // 进行arm指令模式被Hook指令的恢复
for (i = 0; i < 3; i++){ ((int*)h->orig)[i] = h->store[i];
}
} // 刷新指令缓存
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}
// 进行thumb或者arm指令模式Hook目标函数的指令覆盖即实现函数的Hook
void hook_postcall(struct hook_t *h)
{
int i; if (h->thumb) { // 获取thumb指令模式函数真实的调用地址
unsigned int orig = h->orig - 1;
// 进行thumb指令模式Hook目标函数指令的覆盖
for (i = 0; i < 20; i++)
((unsigned char*)orig)[i] = h->jumpt[i]; } else { // 进行arm指令模式Hook目标函数指令的覆盖
for (i = 0; i < 3; i++)
((int*)h->orig)[i] = h->jump[i];
} // 刷新指令缓存
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}

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

Android Hook框架adbi的分析(2)--- inline Hook的实现的更多相关文章

  1. Android Hook框架adbi的分析(3)---编译和inline Hook实践

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/75200800 一.序言 在前面的博客中,已经分析过了Android Hook框架a ...

  2. Android Hook框架adbi的分析(1)---注入工具hijack

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74055505 一.Android Hook框架adbi的基本介绍 adbi是And ...

  3. Android平台dalvik模式下java Hook框架ddi的分析(2)--dex文件的注入和调用

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/77942585 前面的博客<Android平台dalvik模式下java Ho ...

  4. Android平台dalvik模式下java Hook框架ddi的分析(1)

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/75710411 一.前 言 在前面的博客中已经学习了作者crmulliner编写的, ...

  5. android hook 框架 ADBI 如何实现dalvik函数挂钩

    Android so注入-libinject2 简介.编译.运行 Android so注入-libinject2  如何实现so注入 Android so注入-Libinject 如何实现so注入 A ...

  6. Android Hook框架adbi源码浅析(一)

    adbi(The Android Dynamic Binary Instrumentation Toolkit)是一个Android平台通用hook框架,基于动态库注入与inline hook技术实现 ...

  7. Android Hook框架adbi源码浅析(二)

    二.libbase 其实上面加载完SO库后,hook的功能我们完全可以自己在动态库中实现.而adbi作者为了方便我们使用,编写了一个通用的hook框架工具即libbase库.libbase依然在解决两 ...

  8. android hook 框架 ADBI 如何实现so函数挂钩

    上一篇 android 5 HOOK 技术研究之 ADBI 项目 02 分析了hijack.c, 这个文件编译为一个可执行程序 hijack, 该程序实现了向目标进程注入一个动态库的功能.这一篇继续研 ...

  9. android hook 框架 ADBI 如何实现so注入

    Android so注入-libinject2 简介.编译.运行 Android so注入-libinject2  如何实现so注入 Android so注入-Libinject 如何实现so注入 A ...

随机推荐

  1. PHP配置 4. 虚拟主机配置open_basedir

    将/usr/local/php/etc/php.ini中open_basedir注释掉,编辑虚拟主机配置open_basedir #vim /usr/local/apache2 .4/conf/ext ...

  2. Apache配置 11. 访问控制-user_agent

    (1)介绍 user_agent是指用户浏览器端的信息.比如你是用IE的还是Firefox浏览器的.有些网站会根据这个来调整打开网站的类型,如是手机的就打开wap,显示非手机的就打开PC常规页面. ( ...

  3. 2019 GDUT Rating Contest II : Problem C. Rest Stops

    题面: C. Rest Stops Input file: standard input Output file: standard output Time limit: 1 second Memory ...

  4. java 各种类型转换

    public class TypeConversion { public static void main(String[] args) throws ParseException { // 1.将字 ...

  5. DataTable.SELECT日期类型筛选处理

    初始化: public DataTable1() { InitializeComponent(); Init(); } private void Init() { dt = new DataTable ...

  6. Android Studio 如何在TextView中设置图标并按需调整图标大小

    •任务 相信大家对这张图片都不陌生,没错,就是 QQ动态 向我们展示的界面. 如何实现呢? •添加文字并放入图标 新建一个 Activity,取名为 QQ,Android Studio 自动为我们生成 ...

  7. Java中的面向切面编程(AOP)

    一.什么是AOP? Aspect Oriented Programming ,即面向切面编程. AOP是对面向对象编程的一个补充. 它的目的是将复杂的需求分解为不同的切面,将散布在系统中的公共功能集中 ...

  8. PAT (Advanced Level) Practice 1002 A+B for Polynomials (25 分) 凌宸1642

    PAT (Advanced Level) Practice 1002 A+B for Polynomials (25 分) 凌宸1642 题目描述: This time, you are suppos ...

  9. 用优先队列构造Huffman Tree及判断是否为最优编码的应用

    前言 我们知道,要构造Huffman Tree,每次都要从堆中弹出最小的两个权重的节点,然后把这两个权重的值相加存放到新的节点中,同时让这两个节点分别成为新节点的左右儿子,再把新节点插入到堆中.假设节 ...

  10. 2018年block3学习计划

    还是要给自己制定一个block的计划,希望自己的技术能有更进一步的提升 1. 算法/数学 随着学习的进一步深入,愈发认识到了算法的重要性,这个block给自己定下的计划就是 1)把introducti ...