作者:GToad
作者博客:GToad Blog
本文章所对应项目长期维护与更新,因为在我自己的几台测试机上用得还挺顺手的。本项目作为作者本人的一个学习项目将会长期更新以修复当前可能存在的Bug以及跟进以后Android NDK可能出现的主流汇编模式。
前言
在目前的安卓APP测试中对于Native Hook的需求越来越大,越来越多的APP开始逐渐使用NDK来开发核心或者敏感代码逻辑。个人认为原因如下:
安全的考虑。各大APP越来越注重安全性,NDK所编译出来的so库逆向难度明显高于java代码产生的dex文件。越是敏感的加密算法与数据就越是需要用NDK进行开发。
性能的追求。NDK对于一些高性能的功能需求是java层无法比拟的。
手游的兴起。虚幻4,Unity等引擎开发的手游中都有大量包含游戏逻辑的so库。
因此,本人调查了一下Android Native Hook工具目前的现状。尽管Java层的Hook工具多种多样,但是Native Hook的工具却非常少并且在安卓5.0以上的适配工具更是寥寥无几。(文末说明1)而目前Native Hook主要有两大技术路线:
PLT Hook
Inline Hook
这两种技术路线本人都实践了一下,关于它们的对比,我在《Android Native Hook技术路线概述》中有介绍,所以这里就不多说了。最终,我用了Inline Hook来做这个项目。
本文篇幅已经较长,因此写了一些独立的学习笔记来对其中的细节问题进行解释:
目标效果
根据本人自身的使用需求提出了如下几点目标:
工具运行原理中不能涉及调试目标APP,否则本工具在遇到反调试措施的APP时会失效。尽管可以先去逆向调试patch掉反调试功能,但是对于大多数情况下只是想看看参数和返回值的Hook需求而言,这样的前期处理实在过于麻烦。
依靠现有的各大Java Hook工具就能运行本工具,换句话说就是最好能用类似这些工具的插件的形式加载起本工具从而获得Native Hook的能力。由于Java Hook工具如Xposed、YAHFA等对于各个版本的Android都做了不错的适配,因此利用这些已有的工具即可向目标APP的Native层中注入我们的Hook功能将会方便很多小伙伴的使用。
既然要能够让各种Java Hook工具都能用本工具得到Native Hook的能力,那就这个工具就要有被加载起来以后自动执行自身功能逻辑的能力!而不是针对各个Java Hook工具找调用起来的方式。
要适配Android NDK下的armv7和thumb-2指令集。由于现在默认编译为thumb-2模式,所以对于thumb16和thumb32的Native Hook支持是重中之重。
修复Inline Hook后的原本指令。
Hook目标的最小单位至少是函数,最好可以是某行汇编代码。
最终方案
最后完成项目的方案是:本工具是一个so库。用Java Hook工具在APP的入口Activity运行一开始的onCreate方法处Hook,然后加载本so。
加载后,自动开始执行Hook逻辑。
为了方便叙述,接下来的Java Hook工具我就使用目前这类工具里最流行的Xposed,本项目的生成文件名为libautohook.so。
自动执行
我们只是用Xposed加载了这个libautohook.so,那其中的函数该怎么自动执行呢?
目前想到两个方法:
利用JniOnload来自动执行。该函数是NDK中用户可以选择性自定义实现的函数。如果用户不实现,则系统默认使用NDK的版本为1.1。但是如果用户有定义这个函数,那Android VM就会在System.loadLibrary()加载so库时自动先执行这个函数来获得其返回的版本号。尽管该函数最终要返回的是NDK的版本号,但是其函数可以加入任意其它逻辑的代码,从而实现加载so时的自动执行。这样就能优先于所有其它被APP NDK调用的功能函数被调用,从而进行Hook。目前许多APP加固工具和APP初始化工作都会用此方法。
本文采用的是第二种方法。该方法网络资料中使用较少。它是利用了__attribute__((constructor))属性。使用这个constructor属性编译的普通ELF文件被加载入内存后,最先执行的不是main函数,而是具有该属性的函数。同样,本项目中利用此属性编译出来的so文件被加载后,尽管so里没有main函数,但是依然能优先执行,且其执行甚至在JniOnload之前。于是逆向分析了一下编译出来的so库文件。发现具有constructor属性的函数会被登记在.init_array中。(相对应的destructor属性会在ELF卸载时被自动调用,这些函数会被登记入.fini_array)

值得一提的是,constructor属性的函数是可以有多个的,对其执行顺序有要求的同学可以通过在代码中对这些函数声明进行排序从而改变其在.init_array中的顺序,二者是按顺序对应的。而执行时,会从.init_array中自上而下地执行这些函数。所以图中的自动优先执行顺序为:main5->main3->main1->main2->main4。并且后面会说到,从+1可以看出这些函数是thumb模式编译的。
方案设计
先说一下使用的工具:
使用keystone查找指定架构下汇编指令的机器码
使用MS VISIO制作了下面的设计图
调试工具用的是IDA pro
Arm32方案
现在我们的代码可以在一开始就执行了,那该如何设计这套Inline Hook方案呢?目标是thumb-2和arm指令集下是两套相似的方案。我参考了腾讯游戏安全实验室的一篇教程,其中给出了一个初步的armv7指令集下的Native Hook方案,整理后如下图:

Arm 第1步
根据/proc/self/map中目标so库的内存加载地址与目标Hook地址的偏移计算出实际需要Hook的内存地址。将目标地址处的2条ARM32汇编代码(8 Bytes)进行备份,然后用一条LDR PC指令和一个地址(共计8 Bytes)替换它们。这样就能(以arm模式)将PC指向图中第二部分stub代码所在的位置。由于使用的是LDR而不是BLX,所以lr寄存器不受影响。关键代码如下:
//LDR PC, [PC, #-4]对应的机器码为:0xE51FF004
BYTE szLdrPCOpcodes[8] = {0x04, 0xF0, 0x1F, 0xE5};
//将目的地址拷贝到跳转指令下方的4 Bytes中
memcpy(szLdrPCOpcodes + 4, &pJumpAddress, 4);
Arm 第2步
构造stub代码。构造思路是先保存当前全部的寄存器状态到栈中。然后用BLX命令(以arm模式)跳转去执行用户自定义的Hook后的函数。执行完成后,从栈恢复所有的寄存器状态。最后(以arm模式)跳转至第三部分备份代码处。关键代码如下:
_shellcode_start_s:
push {r0, r1, r2, r3}
mrs r0, cpsr
str r0, [sp, #0xC]
str r14, [sp, #8]
add r14, sp, #0x10
str r14, [sp, #4]
pop {r0}
push {r0-r12}
mov r0, sp
ldr r3, _hookstub_function_addr_s
blx r3
ldr r0, [sp, #0x3C]
msr cpsr, r0
ldmfd sp!, {r0-r12}
ldr r14, [sp, #4]
ldr sp, [r13]
ldr pc, _old_function_addr_s
Arm 第3步
构造备份代码。构造思路是先执行之前备份的2条a

本文介绍了Android Native Hook的现状和两种主要技术路线:PLT Hook和Inline Hook,并详细阐述了作者采用Inline Hook实现的一个Android Native Hook工具。文章通过分析目标需求,制定了工具设计原则,包括不依赖调试目标APP、支持Java Hook工具加载和自动执行等功能。作者通过实例详细解释了在arm和thumb-2指令集下的Hook方案,包括备份、恢复、指令修复等步骤,并给出了使用该工具的说明。
最低0.47元/天 解锁文章
1127

被折叠的 条评论
为什么被折叠?



