0x0 零
Hook是我们常用的一种技术手段,在开发中,也经常看到Self-Hook等操作,而在逆向分析中,Hook也是用的不少。民间也流传着很多Hook框架,比如Android上的Xposed,IOS上的Tweak,以及Substrate、Frida等等。那么作为Hook框架,他们之间有啥异同?
0x01 壹
说到Hook,不得不提一下注入,想要Hook其他进程的内容就得先注入别的进程。在Android上,进程注入一般有两种方式,一是ELF文件感染,通过添加各种信息(比如新增section信息、新增依赖库等),当被感染的ELF文件加载、执行时,便可达到进程注入的目的;第二种方式比较常用,就是老生常谈的ptrace注入,具体怎么注入的,见这个小demo。
0x02 贰
Android上常见的Hook Native方式根据原理不同大致分为三种:异常Hook、导入表Hook、inline Hook。
Inline Hook
三种Hook方式各有各的优缺点,那么先说说最常见的inline hook,这种方式是最暴力、最直接的方式。简单说,inline hook 就是进程注入后通过修改你要hook的那行指令为跳转指令,并且目的地址是你自己的代码区域,以此来获取到程序的控制权。
那么在跳转到我们的代码领空之后,我们要做的第一件事就是保存当前的寄存器环境,以免等会跳转回去执行LDR R3,[R2,R3](以及之后的指令)时报错。接下来,就可以执行我们的代码,执行结束后,第一步当然是要恢复刚才的寄存器环境,除此之外,还有一件重要的事情,就是执行被改成跳转指令的那条无辜的指令,对应上图中的指令是Mov R0,R2。完事后,跳转回刚才被修改过的指令的下一条指令地址,对应上图中的就是LDR R3,[R2,R3]指令的地址。
这样一来,我们就悄无声息的把注入代码给执行了,并且也把控制权归还给了原始代码流程。了解了inline hook的大致流程,我们该怎么去防止这种hook方式呢?讲道理,其实是防不住的,最多也就算给逆向工作者添加一些难度系数。我们常常看到有检测注入模块的方式去防止hook和注入,也有根据匹配某些特征去防的。但是他们都比较容易误报,我们这次来一点高精度的防护!想想看,一般hook都是针对function来的,也就是说,它们通常会去修改一个function的第一条指令为跳转指令(对于一个正常的function来说,第一条指令基本不可能会是跳转指令)。那么我们就从这里下手,去检测某一个关键函数的头几个字节是不是跳转指令(好捉急的方法%>_<%)。
我们这里针对frida来做inline hook的防御。先自己写几个脚本去hook某个function,同时对这个function的头几个字节做个内存快照,最后同步到IDA里,我们会发现frida使用的跳转指令在ARM和ARM64下由B系列或LDR构成(下图为我自己手动构造,请只关注指令,不需在意后面的目的地址)。
再看看frida的源码(这里是arm的,这里是thumb的,这里是arm64的)
frida通过B[..] XXXXXX或者LDR[..] PC, XXXXXX跳转到我们的功能函数里。看到这里,两个问题:1.为啥要用两种指令?2.既然有两种方式跳转,为啥不加一个MOV PC,XXXXX这种跳转?
第一个问题,先看看B系列指令的字节码构成:
offset是一个有符号的24bit数据,所以大小在-8Mb到8Mb之间,在实际跳转过程中,offset会左移2位(乘4,和ARM指令长度有关),并且B系列是一种相对跳转指令,所以算下来,跳转范围在(PC-32Mb,PC+32Mb)之间。而LDR跳转就不存在范围的问题了,后面的立即数是多少就跳多远,所以当范围过大的时候(通常在ARM64下)会选择使用LDR PC,XXXX来跳转。
第二个问题,MOV指令,它后面的立即数必须满足一个条件,能由8bit连续有效位通过偶数次移位能得到。比如0x1100、0xAF000000等,如果目的地址是0xEE0014这种地址,MOV PC,XXXXX就无法跳转过去。
好了,话说回来,根据上述情况,针对某些函数防inline hook,就可以通过一个宏去检测函数头部字节是否匹配跳转指令(opcode+异常的目的地址)。
绕过方式嘛....太多了
导入表Hook
我们知道在一个SO文件的.GOT表会在内存中存储导入函数的地址,那么导入表Hook原理就是去替换.GOT表里的那些地址,比如你把strcmp函数的地址替换成你的helloWorld函数地址,那么程序每次调用strcmp时,你都可以在控制台看到“Hello World”输出。
原理很简单,操作时第一步就是定位.GOT表的地址,通过遍历ELF文件的SectionHeader,然后获取其sh_name的值,这个值就是.shstrtab内容中的偏移值,取[.shstrtab_addr + Elf32_SectionHeader.sh_name]的字符串,如果是“.got”,就恭黑勒啦;第二步就是在.GOT表中找到strcmp的地址,然后替换成你helloWorld的地址就ok了(很好找,4字节对齐)。
防这种hook很容易,把编译好的SO文件的section信息抹去就行了,反正SO文件执行的时候不需要这些信息。绕过呢,看这里吧。
在IOS上有一种Hook方式和这种导入表方式有那么点点相似,都是替换的地址,叫做Method Swizzling。在编写Tweak插件的时候,对于OC的函数的Hook,就是通过替换函数的IMP实现的。那么IOS上怎么防这种Hook方式呢?不可能把Mach-O文件的dyld_info_command结构体给抹了吧。我是这样考虑的,既然地址被替换了,那么该函数与同模块的其他函数的相对偏移也就改变了,而且变化后的偏移肯定大的离谱。那么给个阈值,稍微检测一下,应该就可以了吧?绕过呢。。。
异常Hook
异常Hook,顾名思义,就是弄个异常,然后在异常处理的时候执行我们的功能函数。程序在运行时,如果遇到异常,系统会使用程序已注册的异常处理函数去处理抛出来的异常。那么各指令集对应的异常指令如下:
具体实施,就是注入后,把你要Hook的地址的指令给改成异常指令,然后注册你的异常处理函数,在异常处理时,修复异常指令,执行自己的功能函数。这里有个问题不知道大家发现没,就是异常指令被修复后,下次执行到这里的时候,就没办法触发我们的异常处理函数了,导致我们的Hook挂上之后执行一次就掉了。怎么确保每次执行到呢?我们需要在异常处理函数里将接下来要执行的那行指令也替换成异常指令,并且在第二处异常触发的异常处理函数里将第一处被修复的异常指令再次替换成异常指令,并且再修复第二处异常指令就ok了(好拗口)。
问题来了,怎么防?检测异常代码?异常处理函数白名单? 自定义异常处理函数陷阱?
0x3 叁
Android上最常见的Hook方式应该属于Xposed,它是针对java方法的一个Hook方式。简单说,Xposed的原理就是将一个java方法给修改成Native方法,方法对应Method结构体里的insns指针就指向的是你在Xposed里注册的功能函数,如果还要执行原始方法,Xposed会在insns里的代码执行结束后再去反射调用原始的方法。
怎么防?检测方法的属性,是否该为Java方法的变成了Native的;反射获取Xposed的一些属性,检测注册了关于你的函数的钩子;more。
慢慢更~