微信号:code_gg_home

介绍:分享技术干货,分享最新新闻动态,分享开心段子,让大家轻松愉快的成长.

深入Android源码系列(二) HOOK技术大作战

2017-07-01 10:17 陆晓明

   漫天的标题党的口水文打赏爆表,冷落了一群默默输出高质量文章的人群。真正的技术文章能否得到认可?

    本文讲解内容有

    hook技术原理探究

    hook本进程方法

    hook跨进程的系统调用,方法

    so注入

    GOT完成so方法hook

    ELF文件头信息

00

简单描述下原理,当我们想去监听func方法,如果我们按照代码去编写,则会是如下格式:

void hook()
{
    //这里加入我们特殊的处理
    //调用真正的func方法
    func();
    //这里加入我们特殊的处理
}
是这样子来实现。

而上面的描述,如果用汇编表示,则会变成:
将func方法的汇编代码的前几个字节,进行修改,修改成跳转到hook方法的位置,然后在hook方法里面处理,过程中调用func,最后退出来。这里理论便是如此,实际中需要考虑很多问题,类似地址修改,查找,堆栈平衡之类的内容。

引用网上的一张图来说明:(原文地址 http://www.tuicool.com/articles/ju2i2ia)

我们这里来讲,不会从零开始,我们直接拿到别人写好的代码,进行修改,我们去看下怎么实现,然后演示下如何使用它,里面用到的哪些技术函数。
下来我们开始准备工作:

所有测试代码,下载地址:链接:https://pan.baidu.com/s/1nu6gZKt 密码:rtbt

我们使用https://github.com/ele7enxxh/Android-Inline-Hook 这个开源库,来进行inline hook。

01

演示如何hook本进程的某个方法,具体的demo演示为:

这里用到的方法为(关于编译,直接给出我这边的项目,自己去下载编译吧,需要配置ndk即可)
registerInlineHook 参数为:需要hook的原始方法,需要替换的方法,以及存储下来hook的原始方法,这个是注册进来,为hook做准备。
inlineHook 这个是实行hook,真正的修改位置
inlineUnHook 这个是unhook,撤销动作
我们这里get_valget_val_hook代码为:

我们编译运行下此代码,看下效果

我们看到,这里的第二次执行get_val  的时候,打印的是hook getVal,于是我们的hook方法是有效的。
我们现在知道了,此库可以使用,代码没有什么问题,那么我们现在来研究下,这里的几个方法
registerInlineHook 参数为:需要hook的原始方法,需要替换的方法,以及存储下来hook的原始方法,这个是注册进来,为hook做准备。
inlineHook 这个是实行hook,真正的修改位置
inlineUnHook 这个是unhook,撤销动作
一个个来,先来看下registerInlineHook

这里主要完成的方法为:
isExecutableAddr 是否选择的hook方法,在可执行区域,因为我们要hook的是方法,代码段,是必须可执行的。
findInlineHookItem 这个是从当前记录的列表里面去找下,我们之前hook过没有,如果有,直接给结果即可。
addInlineHookItem 如果之前没hook过,加入列表里面,配置基础信息。
relocateInstruction 这个完成代码的重定位。这里有个关键的地方trampoline_instructions的mmap方法,这个就是要在我们进程的内存空间,映射一段内存,作为我们的代码段,来写入我们的修改跳转的逻辑代码。
我们看下isExecutableAddr

这个就是从当前进程的maps列表里面,去找下,我们给出的hook方法的地址,是否落在r-xp ,这里关键的x执行位,如果在,则返回true,否则false。
relocateInstruction 里面完成的内容,我们看下:

这里依据地址的对齐方式,来检查是Thumb 还是arm指令,对应的修改汇编指令是不一样的,我们这里来看arm的那条case。

这里简单讲下,就是我们在上面mmap的一段代码内存中,写入一堆指令,指令会在hook的原始方法最前面,将后面的执行,转移到我们的自定义的hook方法上来,然后执行完毕,直接退回到原始方法的返回位置,如此来跳过原始方法,达到我们的hook目的。
当然,这里的注册,是没有做真正的hook动作,只是完成了自己构造了一份代码,实现了我们的hook跳转代码.

具体实施,是在inlineHook方法里面。于是我们看下这个方法

这里的逻辑为:先去看下是否注册过,如果没注册,返回。
如果hook了,返回。
如果是注册过,没有hook,做真正的hook动作。这里具体为:
freeze
doInlineHook
unFreeze

那么我们一个个来看下:
freeze 主要核心的调用方法:
getAllTids 获取所有线程任务
ptrace 跟踪线程 撤销跟踪
processThreadPC 处理代码
PTRACE_GETREGS PTRACE_SETREGS 两个参数,来实现修改寄存器。

doProcessThreadPC 完成寄存器的实际修改,主要完成,当我们当前的pc位置在我们原始函数的运行边界上,我们即修改到目标的pc位置上。
doInlineHook的主要方法:

mprotect 完成将原始方法的代码段内存,改成可读可写。
然后修改此段内存,让跳转到真正的hook方法上去。
unFreeze 解除锁定,继续执行。

如上,我们讲解了整个的inline hook的执行过程,以及实现原理。

02

如果只到这里,那有什么意思。上面的内容,主要完成了本进程的hook方案,但是如果我们想hook一个新的进程,不属于我们自己的进程的话,该如何来做呢?
我们参照https://github.com/ManyFace/AndroidInjection项目来讲解。

第一个例子,hook systemcall

需要修改的是这里的printf方法:printf //printf() eventually call write(fd,str,size)

我们看下我们远程写入的代码:

拿到对应的进程pid //pid_t target_pid = atoi(argv[1]);
ptrace(PTRACE_ATTACH 跟踪进程
ptrace(PTRACE_SYSCALL 追踪直到发生了系统调用
intercept_syscall 实现修改syscall

这里具体为:
get_syscall_number 从寄存器里面拿到syscall num  具体是 ptrace(PTRACE_GETREGS 拿到寄存器值,我们的swi在pc上面的地址存储。
如果syscall_number == __NR_write ,拿到这里的寄存器信息,拿到对应的字串(peek_data),修改后(reverse),回写回去(poke_data)如此,则完成了hook syscall

04

第二个例子,hook 具体的某个方法

(这里它demo演示的比较随意,并且有些小问题,我们讲下原理即可)

我们向远程进程注入代码的时候,根本的实现方法是使用poke_data 向具体的远程进程写入一段代码,修改对方进程的某段地址汇编指令。相比hook本身的代码来说,hook远程进程麻烦点在于,如果需要调用某个方法的时候,是需要找到对方远程进程里面的对应方法的位置,然后向远程地址写入一段代码,让跳转到对应的远程hook方法。(实质就是要解决一个函数在两个进程里面的方法具体位置,我们要在远程进程运行一个方法,那这个方法的地址,不能是我们这个进程的某方法的地址,而需要的是对方远程进程里面 某方法的地址)
我们直接看这里的hook方式
inject_process

这里用了一个get_module_base 这个含义在于,我们要找远程地址,某个so库的加载地址
poke_data(pid, base_addr+0xcf8,inject_code, strlen(inject_code)); 这里的0xcf8是我们要hook的方法在so的偏移位置。
这里要说的就是,这里用了硬编码0xcf8. 其优雅的方案是怎么做呢? 我们使用dlopen 打开so,dlsym找到方法的地址,用找到的地址,减去加载的起始地址,便是偏移地址,这里便是0xcf8
这里我们看下
char inject_code[] = "\x02\x20\xF7\x46"; //mov r0,#2; mov pc,lr.
我们真正演示的时候,发现注入进去后,当前远程进程就退出了。而退出的原因就在这里的\xF7\x46  //mov pc,lr. 这句话将我们pc指向了lr,直接引向了退出循环了,进程则结束了。 我们可以修改下,将\xF7\x46 去掉即可。
演示如下

新开一个窗口,运行target


原来的窗口


这里我们可以看到我们的值改成了1

05

第三种方案,GOT hook
关于GOT,参考
http://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html
要注入的进程代码:

这里目标为:将libhook.so扔进目标进程,然后让目标进程调用libhook里面的hook_init,里面找到我们要hook的方法(GOT),然后hook此方法。
show_msg里面调用了strlen strcmp,我们就是要hook这两个方法。
我们看下注入的代码实现:

如此看来,我们要关注的就是inject_remote_process 这个方法了。

参数含义:
remote_pid 远程进程pid
library_path 需要注入远程的lib库
remote_hookinit_func 远端hook init 方法
hooking_remote_funcs_name 远端hook的方法列表
funcs_count 方法数目
我们先完整的看个代码,然后我们慢慢解释

ptrace_attach 跟踪远程进程
ptrace_getregs 获取对应进程的寄存器列表
mmap_remote_addr =get_remote_addr(xxx,mmap) 去查找mmap方法在远程进程的地址(具体方法,我们本进程的mmap对应方法的地址减去mmap对应so的加载地址,拿到相对地址,然后从远端进程找到mmap对应的so的加载地址,相加拿到mmap在远端进程的方法地址)
call_mmap_remote 调用mmap方法,拿到返回的值,传入regs
dlopen_remote_addr = get_remote_addr (xxx,dlopen)找到dlopen方法在远程进程的地址
ptrace_pokedata  向远程的mmap地址上面,写入一段代码,为调用远端的dlopen做准备
ptrace_call 调用远端方法,这里是dlopen,打开的是libhook.so
dlsym_remote_addr = get_remote_addr(xxx,dlsym) 去查找dlsym方法在远程进程的地址
找到"hook_init"方法的地址,后面紧跟着就是调用远端的hook_init 方法
hook_init完成查找需要替换的方法,然后加载libtraget.so,找到这个加载之后,里面的strlen地址,传递回来
ptrace_peekdata 读取返回值 ,拿到hook_strlen 的地址  和 strlen的地址
ptrace_pokedata 修改,将远程进程里面 strlen 的地址改到hook_strlen地址即可,完成本次修改。
ptrace_setregs 恢复现场
ptrace_detach 结束跟踪,释放远程进程,修改完成。
具体每一个方法,不再继续讲解了,我们可以自己下去看。核心的技术点在于,自己构造一段代码,作为跳转hook,  然后将之前的代码地址进行修改,改到hook的位置。相比较本进程的hook,跨进程主要解决的是我们每个方法的调用,都是需要找到远程进程的具体方法的地址,主要麻烦点就在这里,同时GOT是为了解决so里面的地址修正,对于导入的外部符号,系统默认是未赋值的,我们要做的就是找到未赋值的对应方法的加载位置在这里塞入我们的hook方法,替换掉系统默认的赋值,完成hook 远程so的方法

原理讲完了,发现测试上面这段代码的时候,出异常了。。why??

通过定位,最终找到代码查找GOT的方案,用的是dlopen的返回值,而android4.4之后,将dlopen的返回值,改成了handle,于是没法强转成soinfo,也就没法通过这个途径打开符号表了。于是我们要换成ELF解析工具,来处理这里的逻辑。此demo就此作废,不过原理已经掌握。

于是又找来一份代码https://github.com/shunix/AndroidGotHook这次先验证了,本身的原理性知识没有问题,在查看测试之后,发现仍然异常。。no,why???但我们坚信,此方案是可行的,于是阅读代码,调试,终于定位到出错原因,修改后,测试通过。


我们的目标为,修改远程进程里面,printf的GOT的数据,改成我们hook的新的my_printf方法(这个方法也在一个so里面)。
远程进程代码为:

my_printf  的so源码为:

注入的代码为:

我们当前的流程为:

使用GetPid 方法找到进程id
InjectLibrary 将我们的libhook.so扔到远程进程里面,加载起来
CallDlsym 从远端的进程里面,找到我们注入的so(libhook.so)里面的my_printf 地址
GetRemoteFuctionAddr 找到远程进程里面printf的地址
PatchRemoteGot 到远端进程里面,去找下当前传入的要注入的elf文件,原始要替换的方法(printf),从elf文件的符号地址里面,解析出来printf的GOT位置,在此处将hook的地址写入,完成hook
我们具体看下代码:
InjectLibrary

PtraceAttach我们就不讲解了
CallDlopen 在远程打开一个动态库,具体的为:
GetRemoteFuctionAddr 在远程进程上,找到dlopen的地址
CallMmap 在远程地址上mmap一段地址
PtraceWrite 在mmap的地址上,写入参数
CallRemoteFunction调用mmap方法,返回handle句柄
CallMunmap umap即可
InjectLibrary讲解完成,主要完成了查找dlopen,然后传入so的文件地址,使用dlopen将其加载起来,返回handle后面再找符号需要。
 CallDlsym(pid, so_handle, "my_printf");
查找my_printf 在远程进程的真实地址值。
参数pid 远程进程id
so_handle上面dlopen的句柄
my_printf要找的符号地址
完成找到my_printf在远程进程的方法地址
GetRemoteFuctionAddr(pid, LIBC_PATH, (long)printf);找到原始的printf的地址
 PatchRemoteGot(pid,
    target_library_path,
     original_function_addr,
     hook_fuction_addr);

pid 远程进程id
target_library_path 目标库
original_function_addr 原始方法地址
hook_fuction_addr hook方法地址

这里原理为:在pid的进程上面,从map表里面找到目标库,拿到基地址,然后使用ELF格式解析,将目标库的文件头拿到,解析里面的.got节,去匹配里面的每个地址(.got存储了所有导入的符号真实地址),如果跟我们找到的原始地址一致,就说明此处需要hook,替换掉原始的地址记录下来,然后我们直接修改即可。

看下演示效果:
将编译出来的libhook.so扔进system/lib下面
将target和got-hook放在system/bin下面
然后执行:

adb shell target

adb shell got-hook target /system/lib/libhook.so /system/bin/target

结果为:

运行变成了hello(我们my_printf方法里面内容)

这里参数的 含义为

got-hook运行的程序
target注入的程序名字
 /system/lib/libhook.so 向target注入的so
/system/bin/target 替换这里面的printf为my_printf
我们简单说下/system/bin/target的运行过程,系统将/system/bin/target加载进入内存,对于里面的printf方法(具体就是导入符号),会假定是从一个偏移地址去取printf的真实地址,而这个真实地址,就是我们系统加载/system/bin/target的时候,找到printf的地址,放置在这个偏移位置,而这块偏移地址区域,叫做GOT,于是我们把放置printf的真实地址的这个偏移地址上,放入我们hook的方法,那么/system/bin/target在运行的时候,找printf方法的时候,从GOT表拿出来的地址,就会是我们的hook方法地址,于是乎,我们就完成了hook

06

最后再来一个做个结束:
注入一个

so到远进程里面,然后hook住里面的printf方法.
我们看注入的方案:

InjectLibrary实现dlopen,将libinject-hook.so库在远端进程打开.我们看下libinject-hook.so的源码:

这里用到一个属性__attribute__((constructor)) ,这个标记的方法,会在so加载之后,立即调用。于是这里就执行了before_main方法,完成hook printf的动作。由于知识点前面都讲过,因此不再烦述,读者自行领悟。


本文结束。


更多精彩,敬请期待。


更多内容,关注微信公众号:code_gg_home

加微信 code_gg_boy  进入代码GG交流群



参考文档:
http://blog.csdn.net/earbao/article/details/51605612
http://bbs.pediy.com/thread-180918.htm
http://blog.sina.com.cn/s/blog_4ac74e9a0100n7w1.html
http://blog.csdn.net/zhangmiaoping23/article/details/17919611
https://mikecvet.wordpress.com/2010/08/14/ptrace-tutorial/
http://blog.sina.com.cn/s/blog_88b60ea001017bc9.html
http://blog.csdn.net/cos_sin_tan/article/details/7667582
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204g/Cjaghefc.html
http://blog.csdn.net/myarrow/article/details/9630377
http://www.kanxue.com/bbs/showthread.php?t=141355&highlight=%E5%8F%A4%E6%B2%B3
http://www.kanxue.com/bbs/showthread.php?t=207710


 
代码GG之家 更多文章 vlc for android 完全编译 android native 代码内存泄露 定位方案 深入Android源码系列(一) google 进入分屏后在横屏模式按home键界面错乱( 四) google 分屏 横屏模式 按home键界面错乱故障分析(三)
猜您喜欢 周二见|企鹅智酷年终重磅报告,今天先曝光封面! 程序员面试技巧总结 DaoCloud 邀您参加中国双态运维大会·乌镇峰会,共同围绕“微服务和 FinTech”进行探讨 Android应用框架最佳实践 青云QingCloud:竞速云计算2.0时代