上一篇 android 5 HOOK 技术研究之 ADBI 项目 02 分析了hijack.c, 这个文件编译为一个可执行程序 hijack, 该程序实现了向目标进程注入一个动态库的功能。这一篇继续研究 adbi 项目其他源码,解决真正替换目标进程函数的问题。

在开始之前,先看看 adbi 给出的一个例子,这个例子替换了目标进程epoll_wait函数的实现为自定义的实现:

首先,给出例子的epoll_wait自定义实现,共2个:

int my_epoll_wait_arm(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
return my_epoll_wait(epfd, events, maxevents, timeout);
}
int my_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
{
int (*orig_epoll_wait)(int epfd, struct epoll_event *events, int maxevents, int timeout);
orig_epoll_wait = (void*)eph.orig; hook_precall(&eph);
int res = orig_epoll_wait(epfd, events, maxevents, timeout);
if (counter) {
hook_postcall(&eph);
log("epoll_wait() called\n");
counter--;
if (!counter)
log("removing hook for epoll_wait()\n");
} return res;
}
hook(&eph, getpid(), "libc.", "epoll_wait", my_epoll_wait_arm, my_epoll_wait);
my_epoll_wait_arm 是 arm 格式指令下的 hook 函数,
my_epoll_wait 是 thumb 格式指令下的 hook 函数,
hook 函数将 libc 库的 epoll_wait 函数替换成上述函数中的一个,并将一些关键信息存放在 eph 结构里。
查看 my_epoll_wait 这个函数主要执行3步: hook_precall, orig_epoll_wait, hook_postcall。 
hook_precall 将 eph 结构保存的原始 epoll_wait 地址重新赋值回去,orig_epoll_wait 调用原始 epoll_wait, 最后hook_postcall再用自定义的my_epoll_wait 赋值为原始epoll_wait. 一般inline hook 都是这种套路,这样做可以在hook函数内部调用原始函数,以实现在原始函数之上做一些事情。
其中,eph 是 struct hook_t 结构,用于保存一次hook的相关信息,如下:
struct hook_t {
unsigned int jump[]; // 对应arm,12个字节,执行这部分指令可以调用 hook 函数
unsigned int store[]; // 对应arm, 12 个字节,执行这部分指令可以让目标函数恢复到原始状态
unsigned char jumpt[]; //对应 thumb,20个字节,执行这部分指令可以调用 hook 函数
unsigned char storet[];//对应 thumb, 20个字节,执行这部分指令可以让目标函数恢复到原始状态
unsigned int orig; // 目标函数地址,真正发挥作用的指令,它的值可能是上述4种其一
unsigned int patch; // hook函数地址
unsigned char thumb; // 取1 表示指令格式是 thumb, 取 0 表示是 arm
unsigned char name[];// 被hook的目标函数名
void *data;
};

下面逐步分析目标进程函数的劫持过程,首先第一个问题,需要找到目标函数在目标进程内的地址。

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
unsigned long int addr;
int i; if (find_name(pid, funcname, libname, &addr) < ) {
log("can't find: %s\n", funcname)
return ;
}
    。。。
}

这个问题,在  android 5 HOOK 技术研究之 ADBI 项目 02  分析向目标进程注入so的过程时已经有涉及(需要找到目标进程内的 dlopen 函数, mprotect 函数)。 在 hijack.c 的实现里,是通过解析本进程和目标进程的 maps 文件,得到本进程和目标进程 libdl.so 库加载的起始地址,然后用dlsym获取本进程内 dlopen 函数的地址,算出dlopen函数到 libdl.so 加载初始地址的 offset, 然后目标进程 libdl.so 的起始地址加上刚刚算出的 offset 即得到目标进程内部 dlopen的真实地址,这里,利用的是tracer进程和 tracee 进程加载的是同一个 libdl.so ,因而同一个符号在该动态库里的offset是固定的。

那么,如果要hook的目标函数不是在一个动态库里,而是tracee进程静态链接的一个函数,上述方式就失效了,这时候,如何获取目标函数地址呢?

这里,adbi项目的作者给出了另外一种方式,可以解决这个问题,find_name 函数仍然是获取动态库里的函数的地址,但这个实现改一下也可以用于获取静态链接的函数的地址:

int find_name(pid_t pid, char *name, char *libn, unsigned long *addr)
{
struct mm mm[];
unsigned long libcaddr;
int nmm;
char libc[];
symtab_t s; if ( > load_memmap(pid, mm, &nmm)) { // load_memap 函数加载 maps 文件并将内存块解析成一个数组
log("cannot read memory map\n")
return -;
}
if ( > find_libname(libn, libc, sizeof(libc), &libcaddr, mm, nmm)) { // 在上述数组里使用so名字寻找对应的起                                              // 始地址
log("cannot find lib: %s\n", libn)
return -;
}
//log("lib: >%s<\n", libc)
s = load_symtab(libc); // 加载so文件,解析后获取符号表
if (!s) {
log("cannot read symbol table\n");
return -;
}
if ( > lookup_func_sym(s, name, addr)) { // 获取目标函数在符号表里的地址值,这个值其实就是该函数相对于so起始地
                              // 址的offset
log("cannot find function: %s\n", name);
return -;
}
*addr += libcaddr; // so 在maps文件里的起始地址 + 函数在so符号表里的值 = 函数在目标进程的地址
return ;
}
load_symtab, lookup_func_sym 这两个函数在  android 5 HOOK 技术研究之 ADBI 项目 02  没有出现过, load_symtab 的作用是使用 elf 格式解析 so 文件, (linux平台下, 可执行程序,动态库,.o 文件,都是elf格式的文件, android 底层是linux), 获取该elf文件的符号表。 lookup_func_sym 函数使用函数名遍历符号表,找到该符号(函数)对应的地址,对与 so 文件来说,这时候找到的地址是一个以起始地址为0计算的地址,即本质是一个offset, 用这个值加上目标进程 maps 文件里 so 加载的起始地址,即得到了目标进程里函数的地址。

如果要hook的地址不是so的函数呢,则使用 load_symtab 加载目标进程的可执行程序并获取符号表, lookup_func_sym 得到的函数地址就是真实地址。

如果目标进程对应的可执行程序,或者加载的so,符号被 strip 了(这时候其elf文件里没有符号表),如果获取函数地址 ?

==========

找到要hook的函数的地址后,开始填充 hook_t 结构,并覆盖原始地址:

int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
{
    。。。
     log("hooking:   %s = 0x%x ", funcname, addr)
strncpy(h->name, funcname, sizeof(h->name)-); // 被hook的函数名 if (addr % == ) { // ARM 格式
log("ARM using 0x%x\n", hook_arm)
h->thumb = ;
h->patch = (unsigned int)hook_arm; // hook 函数先保存在 h->patch 字段
h->orig = addr; // 目标进程的被hook函数原始地址保存在 h->orig 字段
h->jump[] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
h->jump[] = h->patch; // 新的hook函数地址放在 hook指令的第4到12个字节
h->jump[] = h->patch;
for (i = ; i < ; i++)
h->store[i] = ((int*)h->orig)[i]; // 由于hook需要12个指令,这里先把原始12个字节的指令保存在 h-                                  //>store, unhook 时用到
for (i = ; i < ; i++)
((int*)h->orig)[i] = h->jump[i]; // 新的指令12个字节覆盖老的指令
}
else {
if ((unsigned long int)hook_thumb % == ) //thumb格式
log("warning hook is not thumb 0x%x\n", hook_thumb)
h->thumb = ;
log("THUMB using 0x%x\n", hook_thumb)
h->patch = (unsigned int)hook_thumb; // hook函数地址保存在 h->patch
h->orig = addr; // 原始地址保存在 h->orig
h->jumpt[] = 0xb4; // jumpt 保存实现 hook 的指令,20字节
h->jumpt[] = 0x30; // push {r4,r5}
h->jumpt[] = 0xa5;
h->jumpt[] = 0x03; // add r5, pc, #12
h->jumpt[] = 0x68;
h->jumpt[] = 0x2d; // ldr r5, [r5]
h->jumpt[] = 0xb0;
h->jumpt[] = 0x02; // add sp,sp,#8
h->jumpt[] = 0xb4;
h->jumpt[] = 0x20; // push {r5}
h->jumpt[] = 0xb0;
h->jumpt[] = 0x81; // sub sp,sp,#4
h->jumpt[] = 0xbd;
h->jumpt[] = 0x20; // pop {r5, pc}
h->jumpt[] = 0x46;
h->jumpt[] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
memcpy(&h->jumpt[], (unsigned char*)&h->patch, sizeof(unsigned int));// hook函数地址赋值给 jump                                    //t 指令的第 16字节开始的位置
unsigned int orig = addr - ; // sub 1 to get real address
for (i = ; i < ; i++) {
h->storet[i] = ((unsigned char*)orig)[i]; //同等数量(20字节)的原始指令保存在 h->storet 里
//log("%0.2x ", h->storet[i])
}
//log("\n")
for (i = ; i < ; i++) {
((unsigned char*)orig)[i] = h->jumpt[i]; // 新的指令覆盖老的指令
//log("%0.2x ", ((unsigned char*)orig)[i])
}
}
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));//刷新缓存
return ;
}

上述注释可以看到,hook函数需要分 arm 指令和 thumb 指令实现两个分支的hook, 其中 , arm 格式需要替换 12 个字节的指令,thumb需要替换 20个字节,将原始的指令存放在 hook_t结构的 store/storet 字段,将新的指令构造好后存放在 hook_t结构的 jump/jumpt字段,hook_t结构的 orig 字段存放原始函数的地址,当执行 hook 调用时,用 jump/jumpt 指令覆盖 orig地址对应长度的内存,这时候执行 orig 函数,就会执行到 jump/jumpt指令,也就是执行到hook函数,当执行 unhook 调用时,就用 store/storet 指令覆盖 orig 地址对应长度的内存,这时候的这块指令等于原始执行,相当于执行原始函数。

void unhook(struct hook_t *h)
{
log("unhooking %s = %x hook = %x ", h->name, h->orig, h->patch)
hook_precall(h);
}

unhook函数,直接调用 hook_precall, 前面分析 my_epoll_wait 函数时发现,hook函数里也调用了 hook_precall, hook_postcall ,以实现在hook函数内可以调用原始函数,下面是这两个函数的实现:

void hook_precall(struct hook_t *h)
{
int i; if (h->thumb) {
unsigned int orig = h->orig - ;
for (i = ; i < ; i++) {
((unsigned char*)orig)[i] = h->storet[i];
}
}
else {
for (i = ; i < ; i++)
((int*)h->orig)[i] = h->store[i];
}
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
} void hook_postcall(struct hook_t *h)
{
int i; if (h->thumb) {
unsigned int orig = h->orig - ;
for (i = ; i < ; i++)
((unsigned char*)orig)[i] = h->jumpt[i];
}
else {
for (i = ; i < ; i++)
((int*)h->orig)[i] = h->jump[i];
}
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
}

实现比较简单,参见上面的讲述,这里不再赘述。

上述hook方式本质上是一个内存的赋值操作(((unsigned char*)orig)[i] = h->jumpt[i];)赋值操作的左值orig是代码区的地址,但不意味着这个赋值操作完成后,对orig代码区的调用就马上会用到刚刚赋值的指令,这是因为,CPU执行指令时,是从缓存(cache)获取的指令,内存的指令同步到缓存需要时间,所以,adbi 在每次对目标地址的指令做内存赋值操作后,都添加了一个刷新缓存的操作,人为触发一次指令同步操作,

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"
);
}
  1. #define __ARM_NR_BASE (__NR_SYSCALL_BASE+0x0f0000)
  2. #define __ARM_NR_breakpoint (__ARM_NR_BASE+1)
  3. #define __ARM_NR_cacheflush (__ARM_NR_BASE+2)
  4. #define __ARM_NR_usr26 (__ARM_NR_BASE+3)
  5. #define __ARM_NR_usr32 (__ARM_NR_BASE+4)
  6. #define __ARM_NR_set_tls (__ARM_NR_BASE+5)

实现方式是会汇编调用一个系统调用  __ARM_NR_cacheflush (0xf0002 )

参考:http://blog.csdn.net/roland_sun/article/details/36049307

Arm格式的hook指令: 12个字节,

前面4个字节是:  LDR pc, [pc, #0],pc寄存器读出的值是当前值+8,所以这一句是把jump[2]对应的地址加载到 pc 寄存器,这一句指令执行后,程序计数器执行 jump[2], 即 h->patch ,即hook函数。  jump[1] 的值是用来填充空间的,可以任意
 h->jump[] = 0xe59ff000; // LDR pc, [pc, #0] // h->jump 填充hook指令
h->jump[] = h->patch; // 新的hook函数地址放在 hook指令的第4到12个字节
h->jump[] = h->patch;

Thumb格式的hook指令:20个字节,

首先,hook函数的地址被存放在 jumpt数组(char型)的第16,20这4个字节处。

                h->jumpt[] = 0xb4;
h->jumpt[] = 0x30; // push {r4,r5}
h->jumpt[] = 0xa5;
h->jumpt[] = 0x03; // add r5, pc, #12
h->jumpt[] = 0x68;
h->jumpt[] = 0x2d; // ldr r5, [r5]
h->jumpt[] = 0xb0;
h->jumpt[] = 0x02; // add sp,sp,#8
h->jumpt[] = 0xb4;
h->jumpt[] = 0x20; // push {r5}
h->jumpt[] = 0xb0;
h->jumpt[] = 0x81; // sub sp,sp,#4
h->jumpt[] = 0xbd;
h->jumpt[] = 0x20; // pop {r5, pc}
h->jumpt[] = 0x46;
h->jumpt[] = 0xaf; // mov pc, r5 ; just to pad to 4 byte boundary
memcpy(&h->jumpt[], (unsigned char*)&h->patch, sizeof(unsigned int));

android hook 框架 ADBI 如何实现so函数挂钩的更多相关文章

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

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

  2. Android Hook框架adbi的分析(2)--- inline Hook的实现

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74452308 一. Android Hook框架adbi源码中inline Hoo ...

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

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

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

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

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

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

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

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

  7. android hook 框架 ADBI 简介、编译、运行

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

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

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

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

    前面两篇 android hook 框架 libinject2 简介.编译.运行 android hook 框架 libinject2 如何实现so注入 实际运行并分析了 Android中的so注入( ...

随机推荐

  1. 机器学习实战 -- 决策树(ID3)

    机器学习实战 -- 决策树(ID3)   ID3是什么我也不知道,不急,知道他是干什么的就行   ID3是最经典最基础的一种决策树算法,他会将每一个特征都设为决策节点,有时候,一个数据集中,某些特征属 ...

  2. The Image Gallery Revisited

    Lets apply the Best practices to the Image Gallery . /*** index.html ***/ <!DOCTYPE html> < ...

  3. poj2001Shortest Prefixes(trie)

    Shortest Prefixes Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 18687   Accepted: 808 ...

  4. 《Cracking the Coding Interview》——第18章:难题——题目11

    2014-04-29 04:30 题目:给定一个由‘0’或者‘1’构成的二维数组,找出一个四条边全部由‘1’构成的正方形(矩形中间可以有‘0’),使得矩形面积最大. 解法:用动态规划思想,记录二维数组 ...

  5. Python urllib模块详解

    在Python 2中,有urllib和urllib2两个库来实现请求的发送.而在Python 3中,已经不存在urllib2这个库了,统一为urllib,其官方文档链接为:https://docs.p ...

  6. Python面试题之一:解密

    Python面试题之一: 说明:就是Python工程师面试题 一.字典转换与正则提取值 1:key与Value交换 a = {'a':1,'b':2} print({value:key for key ...

  7. 【志银】MySQL命令总结

    ===0-MySQL密码设置===0.1-登入和进入MySQL数据库: 0.1.1-登入MySQL数据库:C:\Users\Administrator>mysql -u用户名 -hMySQL服务 ...

  8. 附录:python and numpy

    http://cs231n.github.io/python-numpy-tutorial/ Python is a great general-purpose programming languag ...

  9. Linux cooked-mode capture 格式转换

    tcpdump抓包时,如果-i选项指定为一个网卡地址,那么抓取的数据包数据链路层是以太网头部:如果指定any,则以太网头部将被替换为linux cooked capture头部 # tcpdump - ...

  10. [错误解决]pandas DataFrame中经常出现SettingWithCopyWarning

    先从原dataframe取出一个子dataframe,然后再对其中的元素赋值,例如 s = d[d['col_1'] == 0] s.loc[:, 'col_2'] = 1 就会出现报错: Setti ...