分类 android破解 下的文章

前言

--

在目前的安卓APP测试中对于Native Hook的需求越来越大,越来越多的APP开始逐渐使用NDK来开发核心或者敏感代码逻辑。 个人认为原因如下:

安全的考虑。各大APP越来越注重安全性,NDK所编译出来的so库逆向难度明显高于java代码产生的dex文件。越是敏感的加密算法与数据就越是需要用NDK进行开发。
性能的追求。NDK对于一些高性能的功能需求是java层无法比拟的。
手游的兴起。虚幻4,Unity等引擎开发的手游中都有大量包含游戏逻辑的so库。

因此,本人调查了一下Android Native Hook工具目前的现状:尽管Java层的Hook工具多种多样,但是Native Hook的工具缺寥寥无几。(文末说明1) 主要有两大路线:
PLT Hook
Inline Hook
这两种技术路线本人都实践了一下,下面来对比总结。
PLT Hook
先来介绍一下Android PLT Hook的基本原理。Linux在执行动态链接的ELF的时候,为了优化性能使用了一个叫延时绑定的策略。相关资料有很多,这边简述一下:这个策略是为了解决原本静态编译时要把各种系统API的具体实现代码都编译进当前ELF文件里导致文件巨大臃肿的问题。所以当在动态链接的ELF程序里调用共享库的函数时,第一次调用时先去查找PLT表中相应的项目,而PLT表中再跳跃到GOT表中希望得到该函数的实际地址,但这时GOT表中指向的是PLT中那条跳跃指令下面的代码,最终会执行_dl_runtime_resolve()并执行目标函数。第二次调用时也是PLT跳转到GOT表,但是GOT中对应项目已经在第一次_dl_runtime_resolve()中被修改为函数实际地址,因此第二次及以后的调用直接就去执行目标函数,不用再去执行_dl_runtime_resolve()了。因此,PLT Hook通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。
了解PLT Hook的原理后,可以进一步分析出这种技术的特点:

由于修改的是GOT表中的数据,因此修改后,所有对该函数进行调用的地方就都会被Hook到。这个效果的影响范围是该PLT和GOT所处的整个so库。因此,当目标so库中多行被执行代码都调用了该PLT项所对应的函数,那它们都会去执行Hook功能。

PLT与GOT表中仅仅包含本ELF需要调用的共享库函数项目,因此不在PLT表中的函数无法Hook到。
那么这些特点会导致什么呢?

可以大量Hook那些系统API,但是难以精准Hook住某次函数调用。这比较适用于开发者对于自家APP性能监控的需求。比如Hook住malloc使其输出参数,这样就能大量统计评估该APP对于内存的需求。但是对于一些对Hook对象有一定精准度要求的需求来说很不利,比如说是安全测试或者逆向分析的工作需求,这些工作中往往需要对于目标so中的某些关键点有准确的观察。

对于一些so内部自定义的函数无法Hook到。因为这些函数不在PLT表和GOT表里。这个缺点对于不少软件分析者来说可能是无法忍受的。因为许多关键或核心的代码逻辑往往都是自定义的。例如NDK中实现的一些加密工作,即使使用了共享库中的加密函数,但秘钥的保存管理等依然需要进一步分析,而这些工作对于自定义函数甚至是某行汇编代码的监控能力要求是远远超出PLT Hook所能提供的范围。
在回调原函数方面,PLT Hook在hook目标函数时,如果需要回调原来的函数,那就在Hook后的功能函数中直接调用目标函数即可。可能有点绕,详细解释一下:假设对目标函数malloc()的调用在1.so中,用户用PLT Hook技术开发的HookMalloc()功能函数在 2.so中。(因为通常情况下目标函数与用户的自定义Hook功能函数不在一个ELF文件里)当1.so中调用malloc()时会去1.so的PLT表中查询,结果是执行流程进入了2.so中的HookMalloc()中。如果这时候HookMalloc中希望调用原目标函数malloc(),那就直接调用malloc()就好了。因为这里的malloc会去2.so中的PLT表中查询,不受1.so中那个被修改过的PLT表的影响。
典型的PLT Hook工具推荐
本技术路线的典型代表是爱奇艺开源的xHook工具库。xhook 是一个针对 Android 平台 ELF (可执行文件和动态库) 的 PLT (Procedure Linkage Table) hook 库。从维护频率和项目标志设计来看这是一款产品级的开源工具。
通过学习其源码与使用后可以发现,这个工具库主要是用于开发者开发时把该项目集成进自己的APP,然后使用这个工具库来帮助开发者监控APK运行时那些他们关心的性能数据。比如通过hook malloc来监控内存分配等。由于这个库是被开发者集成进了APP中,所以它对于这个app的监控是不需要Root权限的。
我个人认为这个工具对于PLT Hook技术的解读与定位非常好!在我上文分析的三点中不难看出,PLT Hook技术应用的方向就应该是对自家开发的APP的性能监控。用该技术进行Hook的最大优势就在于其对于目标API可以进行批量Hook,使得开发者可以节省下大量的原本需要在APP中各个功能点上插入相关日志输出的工作。PLT Hook技术是偏向于开发者的利器的定位非常明确。对于非官方的软件分析者似乎并不适合。
开源库:https://github.com/iqiyi/xHook
Inline Hook
本技术路线的基本原理是在代码段中插入跳转指令,从而把程序执行流程引向用户需要的功能代码中去,以此达到Hook的效果,如下图所示:
20200307172433500.png
这张图是一张arm下最基本的hook流程,从上图中可以看出主要有如下几个步骤:

在想要Hook的目标代码处备份下面的几条指令,然后插入跳转指令,把程序流程转移到一个stub段上去。
在stub代码段上先把所有寄存器的状态保存好,并调用用户自定义的Hook功能函数,然后把所有寄存器的状态恢复并跳转到备份代码处。
在备份代码处把当初备份的那几条指令都执行一下,然后跳转到当初备份代码位置的下面接着执行程序。

由此可以看出使用Inline Hook有如下的Hook效果特点:

完全不受函数是否在PLT表中的限制,直接在目标so中的任意代码位置都可进行Hook。这个Hook精准度是汇编指令级的。这对于逆向分析人员和安全测试人员来说是个非常好的特性!
可以介入任意函数的操作。由于汇编指令级的Hook精度,以及不受PLT表的限制,Inline Hook技术可以去函数执行中的任意代码行间进行Hook功能操作,从而读取或修改任意寄存器,使得函数的操作流程完全可以被控制。
对Hook功能函数的限制较小。由于在第二步调用Hook功能函数前已经把所有之前的寄存器状态都进行保存了,因此此时的Hook功能函数几乎就是个独立的函数,它无需受限于原本目标函数的参数形式,完全都由自己说了算。并且执行完后也完全是一个正常的函数退出形式释放栈空间。
对于PLT Hook的强制批量Hook的特性,Native Hook要灵活许多。当想要进行批量Hook一些系统API时也可以直接去找内存里对应的如libc.so这些库,对它们中的API进行Hook,这样的话,所有对这个API的调用也就都被批量Hook了。

开源库:https://github.com/ele7enxxh/Android-Inline-Hook

技术对比

根据以上的分析,我们发现这两种技术在原理和适用场景上的差别是相当大的。因此有必要进行一下对比,给那些有Native Hook需求的童鞋一些参考。

Name PLT Hook Native Hook
精准度 中 函数级 高 汇编级
范围 小 出现在PLT表中的动态链接函数 大 目标so内全部可执行代码
灵活性 差 只能批量 好 单次或批量都可以
技术难度 中 涉及内存地址计算和修改等 高 涉及寄存器计算、手写汇编、指令修复等

总结

从上面的分析中不难看出,这两种技术各有特点。PLT Hook技术就好比自行车,容易得到,操作简便,但是功能极为有限;Inline Hook技术就像汽车,造价昂贵,操作复杂,但是几乎可以应对各种需求。 因此对于正在寻找Native Hook工具的同学们需要仔细预估一下自己的Native Hook需求,如果只对于系统调用有参数或者性能上的监控需求,那可以考虑采用PLT Hook技术路线。一般适合APP的官方员工。 而如果是希望应对各种各样APP自己独有的NDK函数或者代码段的话,目前只能选择Inline Hook。适合APP逆向人员,软件分析人员,CTF Android逆向解题等。

https://gtoad.github.io/2018/07/06/Android-Native-Hook-Practice/

然后用010Editor工具打开so文件,找到这个地址:
请输入图片描述
怎么修改成NOP指令呢?有一个牛逼的网站在线转换arm为hex值:http://armconverter.com
请输入图片描述
这里看到转换BLX指令的HEX正好和上面看到的HEX值对应上了,这里修改NOP指令:
请输入图片描述
看到NOP指令对应的HEX值是C046,那就修改吧:
请输入图片描述
这里注意需要把那两条指令的所有HEX全部改成NOP指令,保存再用IDA打开查看:
请输入图片描述
修改成功,这两个函数就等于没调用了,在运行调用so还是崩溃,这时候需要想到的是有签名校验,而巧合的是在搜索JNI的时候无意发现了这个函数:
请输入图片描述
当然如果大家想知道so中有没有签名校验,可以直接Shift+F12查找字符串内容”signatures”:
请输入图片描述
一般有这类字符串信息都有签名校验功能了,我们继续看上面那个签名校验函数:
请输入图片描述
果然这里会获取签名信息,然后比对返回1表示正确的签名信息,这里我们不要直接修改返回值和那个v5变量值,因为我们知道strcmp函数执行的结果是-1,0,1;这里明显是需要让返回值是0才可以,那不如直接修改v3的初始值为1即可,修改方法和上面的指令修改类似:
请输入图片描述
记住这个地址,然后去010Editor工具中查看:
请输入图片描述
然后把赋值修改成1:
请输入图片描述
然后去010Editor修改即可:
请输入图片描述
修改之后保存,用IDA打开so:
请输入图片描述
看到已经修改成功了,然后在F5查看伪代码:
请输入图片描述
这里不管签名对不对,都直接返回1了,修改了之后我们在运行发现还是报错,这个需要再去看JNI_OnLoad函数了:
请输入图片描述
这里需要获取一个Java层的类,所以我们在工程中新建这个类即可,这个类可以没有任何方法:
请输入图片描述
然后运行成功,看看解密之后的内容是啥:
请输入图片描述
看到解密之后的内容是个字符串version内容,到此我们就成功的过掉了so中的一些检测调用so解密出来内容了,那么在这个过程中我们依然可以学到很多东西:

第一、修改指令,如果不想让一个函数执行,只需要把跳转指令修改成NOP空指令即可,前提是这个函数的执行结果和后面的逻辑没有半毛钱的关系,如果有那么就需要修改函数的返回值,一般需要修改跳转指令之后的MOVS指令的寄存器值,如果简单点可以直接修改变量的初始化值,比如这里的过掉签名校验。

第二、如果快速的知道so中是否有签名校验功能,可以直接在字符串列表中搜索”signatures”即可,现在也有很多应用会在so中调用Java层的类信息,所以需要去看JNI_OnLoad中arm指令,或者直接搜索字符串列表,因为一般Java层类信息,都是xxx/yyy/zzz/MMM这样的字符串格式,通过肉眼排查也是可以的。

一、样本静态分析
最近有位同学发了一个样本给我,主要是有一个解密方法,把字符串加密了,加解密方法都放在so中,所以之前也没怎么去给大家介绍arm指令和解密算法等知识,正好借助这个样本给大家介绍一些so加密方法的破解,首先我们直接在Java层看到加密信息,这个是这位同学直接告诉我这个类,我没怎么去搜了:
02.png
这个应用不知道干嘛的,但是他的防护做的还挺厉害的,之前我们介绍过小黄车应用内部也用了这种中文混淆变量和方法等操作,这里就不多解释了,这里主要看那个加密算法:
01.png
看到这里有一个加解密方法,传入字符串字节,返回加解密之后的字节数据,我们直接用IDA打开这个libwechat.so文件:
04.png
这里可惜没有收到Java_xxx这样的函数,说明他可能用了动态注册,所以就去搜JNI_OnLoad函数,所以这里注意大家以后如果打开so之后发现没有Java_xxx这样的函数开头一般都是在JNI_OnLoad中采用了动态注册方式,所以只需要找到JNI_OnLoad函数,然后找到RegisterNatives函数即可,不过在这个过程中我们需要转换JNIEnv指针信息:
09.png
这里大家如果看到类似于vXX+YY这样的,选中vXX变量,然后按Y按键,然后替换成JNIEnv*即可,我们如果手动注册过Native方法,都知道RegisterNatives函数的三个参数含义:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

第一个参数:需要注册native函数的上层Java类

第二个参数:注册的方法结构体信息

第三个参数:需要注册的方法个数

这里当然是重点看第二个参数,这里当然也需要知道方法结构体信息:

typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;

结构体包含三部分分别是:方法名、方法的签名、对应的native函数地址;那么这里我们肯定重点看第三部分,因为要找到具体的解密函数,这时候我们需要去对RegisterNatives函数查看他的实参值:
03.png
这里选中RegisterNatives函数名,然后右键选择Force call type即可:
10.png
这时候就看到了RegisterNatives的三个参数值,其实这里看到是四个,这个主要是调用方式的区别,因为我们还会看到有这种调用方式:(*JNIEnv)->RegisterNatives(JNIEnv env…),所以第一个参数其实是JNIEnv变量,这里就看第三个参数的地址就是需要注册方法的结构体信息,点击进入查看:
05.png
这里看到了方法名,方法签名以及对应的具体函数,这里主要看解密函数,找到decryptData即可,然后点击进入查看:
06.png
按下F5查看C语言代码:
07.png
继续点击进入查看:
08.png
这里就是实际的解密算法的地方了,大致看一下其实还是很简单的,就是有一个AES_CBC_128算法加解密的,我们用过这个算法都知道需要key和iv值,因为是128位的,所以这两个值肯定是16(128/8)个字节,这个是基础知识也是非常关键的知识,知道是16个字节对于后面分析破解非常关键。然后需要从解密之后的字节数组的最后一位获取实际字节的长度,最后构建byte数组返回给Java层即可。所以这里我们看到最重要的是如何获取aes解密的key和iv值。这里有很多种方式可以动态调试,可以hook。但是我们先不介绍这两种解密方式,我们先来看看另外一个问题。

二、调用so功能函数(修改指令)
我们在之前是不是有时候解密一个so算法,其实没不要真的知道他的解密算法,而是可以调用他的so然后直接解密出来数据即可,所以我们本文也来尝试做一下,为什么这么做因为在这个过程中我想给大家介绍一些知识点比如修改arm指令等,我们把这个应用的so拷贝到项目中,然后构建一个native类和方法,最后调用解密方法,发现调用直接出现崩溃信息:这时候我们发现在进入JNI_OnLoad挂了,说明JNI_OnLoad中做了一些东西检测:
11.png
看到JNI_OnLoad函数中有这两个函数调用,第一个我们都知道为了防止自己的进程被人恶意附加,就自己先占坑,这样别人就附加失败了,第二个看似也是类似功能,不过不用关心内部实现,我们为了后面动态调试成功,这里还是先把这两个函数干掉吧,这里干掉简单直接改成NOP空指令就可以了,就相当于没调用了。因为这两个函数的执行逻辑和返回结果和后面的逻辑是没任何关系的,所以可以这么做,如果有关系那只能修改返回值了。修改指令之前其实介绍过了,很简单先找到指令对应的偏移地址:
12.png

一.环境安装配置

因为网上的确有介绍了,而且官网也有文档说明:https://www.frida.re/docs/javascript-api,但是最重要的是片段化就是东一处西一处,没有归纳性的总结,而且很多常用的功能都没介绍,所以本文就把常用的hook工具详细介绍一下,主要从以下几个方面来介绍:
第一、如何修改Java层的函数参数和返回值
第二、如何打印Java层的方法堆栈信息
第三、如何拦截native层的函数参数和返回值
对于Java层会注重介绍,因为我们用过Xposed工具之后都知道,比如参数是自定义类型怎么Hook等。不多说了直接用一个案例作为样本进行操作,为了能够覆盖所有的操作可能性案例需要写的复杂点:
20180519104549417.jpg
参数和返回值有基本类型,也有自定义类型,接下来我们就开始我们的Frida之旅吧。
这个网上都已经有教程了,因为Frida大致原理是手机端安装一个server程序,然后把手机端的端口转到PC端,PC端写python脚本进行通信,而python脚本中需要hook的代码采用javascript语言。所以这么看来我们首先需要安装PC端的python环境,这个没难度直接安装python即可,然后开始安装frida了,直接运行命令:pip install frida
20180519104911904.jpg
前提是你需要配置好python环境变量,不然提示pip命令找不到。安装完成之后,我们再去官网下载对应版本的手机端程序frida-server:https://github.com/frida/frida/releases 注意这里一定要把frida-server版本和上面PC端安装的frida版本一致,不然运行报错的。其实这里看到真的实现hook功能的是手机端的frida-server,这个也是开源的大家可以研究他的原理。我们也看到这个工具和IDA是不是很类似,也是把手机端的端口转发到PC端进行通信而已。有了frida-server之后就好办了,直接push到手机目录下,然后修改一下文件的属性即可:
adb push /data/local/tmp frida-server
root# chmod 777 /data/local/tmp/frida-server
然后直接运行这个程序:
/data/local/tmp# ./frida-server
20180519105628236.jpg
然后把端口转发到PC端:
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
20180519105710906.jpg
到这里我们就把通信的手机端工作做完了,是不是感觉和Xposed相比非常方便,兼容性非常好,不需要安装Xposed等工具考虑系统手机等适配问题了。接下来就开始在PC端开始编写hook程序进行操作了:
20180519111514168.jpg
这里代码也非常简单,因为安装好了frida模块,直接导入模块,然后调用api获取设备的session然后hook程序包名,接着就可以执行js脚本代码进行hook操作,然后打印消息:
20180519111620503.jpg
这里用了python的print函数打印,其实如果想要打印可以在上面的js脚本中使用console.log也是可以的,看自己的习惯了。所以这里我们看到脚本的大致流程就是最外面用python引用frida库进行和设备通信,然后编写js脚本执行hook操作。所以这里最主要的还是js脚本也就是需要理解js语法了。不过这个没啥难度的。好了以上的准备条件都弄完了,下面就开始分部拆解操作看看如何涵盖我们平常使用的hook案例。

二、Java层Hook操作案例分析

第一个案例:hook类的构造方法
我们有时候想hook一个类的构造方法,在Xposed中直接用findConstructor方法就可以了,因为构造方法可能有多种重载形式,所以需要用参数作为区分,这里我们hook我们案例的CoinMoney类的构造方法:
20180519112328373.jpg
首先脚本中使用Java.use方法通过类名获取类类型,然后构造方法是固定写法:$init;这个要记住,然后因为需要重载所以用overload(......)形式即可,参数和参数之间用逗号隔开即可。后面就是拦截之后的操作了,这里方法参数可以自定义变量名,因为js是弱语言,不对类型做强检查,当然这里还有其他获取参数的方法后面会介绍。这里CoinMoney类的构造方法:
20180519112626889.jpg
然后我们这里使用send来发送打印消息即可,当然也可以用console.log形式打印日志,代码编写完了,下面就开始运行看效果,运行也很简单,直接python frida.py:
20180519112829135.jpg
在这之前一定要先打开hook的应用,不然会报错提示找不到这个程序进程
20180519112928212.jpg
这时候在运行看到了就成功了,我们把构造方法的参数打印出来了,那么这里hook就成功了。所以可以看到这个操作是不是比Xposed工具更方便呢。但是他也有弊端后面会总结的。

第二、hook类的普通方法
这里的普通方法包括了静态方法,私有方法和公开方法等,这个操作和上面的构造方法其实很类似,代码如下:
20180519113231644.jpg
这个就是把构造方法的固定写法$init改成了需要hook的方法名即可。如果方法有重载形式还是用overload进行区分即可,比如这里我们hook了Uitls.getPwd(String pwd)方法:
20180519113343267.jpg
然后这里我们看到可以用一个隐含的变量arguments获取参数,这个是保存了方法的参数信息是系统自带的。所以我们有两种方式获取方法的参数信息。运行看一下效果:
20180519113517805.jpg
看到打印消息,hook成功了。所以这里就把hook方法获取参数的案例都介绍完了,总结一下很简单,构造方法使用固定写法$init,其他方法全部用方法名即可。如果方法有重载形式需要用overload形式操作参数用逗号分隔。获取参数可以自定义参数名或者用系统隐含的arguments变量获取。当然在这之前都需要用Java.use通过类名获取类型。

第三、修改方法的参数和返回值
我们在使用Xposed进行hook的时候最常用的可能就是修改参数和返回值来实现插件和外挂功能了,在Frida中其实也可以做到但是和Xposed不一样,我们从上面的代码可以看到,没有像Xposed的before方法和after方法,而Frida直接是你可以在function中调用原来的方法这样来进行参数修改,比如这里我要修改上面的方法参数和返回值:
20180519131144700.jpg
第三、修改方法的参数和返回值
我们在使用Xposed进行hook的时候最常用的可能就是修改参数和返回值来实现插件和外挂功能了,在Frida中其实也可以做到但是和Xposed不一样,我们从上面的代码可以看到,没有像Xposed的before方法和after方法,而Frida直接是你可以在function中调用原来的方法这样来进行参数修改,比如这里我要修改上面的方法参数和返回值:
20180519131331636.jpg
其实这么做比before和after形式更为方便,而且可以在原始方法调用前做一些事情和后面做一些事情。

第四、构造和修改自定义类型对象和属性
我们在Xposed写外挂的时候也会遇到这种比较常见的问题,就是方法的参数不是基本类型是自定义类型,然后也想修改他的属性值或者调用他的一个方法我们会使用反射来进行操作,而在返回值的时候,想构造一个自定义类型的对象也是直接用反射实例化一个对象进行操作的。其实在这里因为js中也是支持反射操作的,所以就很简单了:
20180519132859763.jpg
这里构造一个对象其实很简单直接固定写法$new即可,然后有了对象也可以直接调用其对应的方法即可,然后就是如何修改一个对象类型的字段值呢?这个就要用反射了:
20180519133016709.jpg
这里我们拦截了getCoinMoney方法,参数是CoinMoney类型,我们想修改他的money字段值,这时候我们直接调用他的方法没什么问题,但是如果直接调用字段值或者修改就会出现失败了,所以只能通过反射去修改字段值,不过要先获取这个对象对应的class类型,用Java.cast接口就可以,然后获取反射字段直接修改即可,这里要注意不管字段是private还是public的写法都是一样的,都是这段代码大家要注意把这段代码记住即可。我们看看hook之后的结果:
20180519133343585.jpg
如果没有用反射去操作直接获取字段值打印就是object了。

第五、打印方法的堆栈信息
我们在破解过程中有时候通过抛出异常来打印堆栈信息跟踪代码效率会更高,Xposed中操作很方便直接Java代码用Log.xxx方法打印堆栈信息即可,但是在Frida中有点麻烦了,因为他是js代码不好操作,第一次想到的办法就是自己写一个打印堆栈信息的类然后弄成一个dex之后,把这个dex注入到程序中,因为Frida支持把一个dex文件注入到原始程序中运行的,注入之后在需要打印堆栈信息的方法中调用这个dex中的那个方法就可以了。具体怎么注入本文不多介绍了。当时觉得这种方案太麻烦了,那么还有其他方案吗?其实还是有的,因为我们既然可以构造一个对象那么为什么不直接构造一个Exception对象呢?其实操作很简单,首先我们用Java.use方法获取类型变量:var Exception = Java.use("java.lang.Exception");然后是js中支持throw语法的,直接在需要打印堆栈信息的方法中调用即可:
20180519134311511.jpg
不过这个是真得抛出异常了,没有捕获住,所以程序崩溃,我们在开发Android应用的时候如果程序崩溃了最快的查看异常信息的方法就是用日志过滤方式:adb logcat -s AndroidRuntime
20180519134421651.jpg
这样我们就把堆栈信息打印出来了,其实这里可以看到这个是真的一个崩溃异常了,因为没有catch所以直接用系统崩溃日志就可以查看了。这种方式最简单粗暴了。对于跟踪代码非常有用的。
到这里我们就把所有可能遇到的情形Java层hook操作都介绍完了,主要包括以下几种常见情形:

第一、Hook类的构造方法和普通方法,注意构造方法是固定写法$init即可,获取参数可以通过自定义参数名也可以直接用系统隐含的arguments变量获取即可。
第二、修改方法的参数和返回值,直接调用原始方法传入需要修改的参数值和直接修改返回值即可。
第三、构造对象使用固定写法$new即可。
第四、如果需要修改对象的字段值需要用反射去进行操作。
第五、堆栈信息打印直接调用Java的Exception类即可,通过adb logcat -s AndroidRuntime来过滤日志信息查看崩溃堆栈信息。
总结:记得用Java.use方法获取类的类型,如果遇到重载的方法用overload实现即可.

三、Native层Hook操作案例分析

下面继续来看Frida更强大的地方就是hook native代码,说的强大不是因为功能,而是便捷程度,我们之前hook native可能用Cydia比较多,但是都知道Cydia和Xposed一样都有兼容问题,环境安装配置太麻烦了,而Frida还是只需要几行js代码即可搞定,这里hook native还是用两个案例介绍:一个是hook导出的函数,一个是hook未导出的函数,通过获取参数和修改返回值来演示,这里我们不自己写native代码了,直接用之前破解快手的数据请求的so文件,他有一个函数在底层获取字符串信息,还有一个是最近正在研究的资讯类app的加密算法so,我们修改他的函数返回值。
第一、hook未导出函数功能
未导出的函数我们需要手动的计算出函数地址,然后将其转化成一个NativePointer的对象然后进行hook操作,那么如何计算一个函数地址呢?这个很简单只要得到so的内存基地址加上函数的相对地址就可以了。基地址获取直接查看程序对应的maps文件即可:
20180519143005279.jpg
相对地址直接用IDA打开so文件就可以查看,比如这里我们通过静态分析之后想hook这个sub_5070函数:
20180519143045572.jpg
然后我们F5查看函数对应的C语言代码查看参数信息:
20180519143504782.jpg
这里看到是三个参数,那么计算了后的实际地址就是0x7816A000+5070=0x7816F070,不过这个地址不是最后的地址,因为thumb和arm指令的区分,地址最后一位的奇偶性来进行标志,所以这里还需加1也就是最终的0x7816F071,这一点很重要不管使用Cydia还是Frida都要注意最后计算的绝对地址要+1,不然会报错的:
20180519144349822.jpg
这里hook之后有两个回调方法一个是进入函数之前,一个是执行完之后,这个和Xposed非常类似了,我们打印参数,不过这个和之前Hook Java层就不一样了,因为在C中大部分都是和地址指针相关,特别是常见的字符串信息,我们如果要正确的打印字符串值就需要借助Memory系统类来通过指针获取字符串信息了,这个类非常重要,在后面修改返回值也是用它写内存值的。我们先看看这个函数原始返回值是什么:
20180519144704631.jpg
这个是加密之后的值了,然后我们获取到参数了,而通过IDA分析之后发现这个函数最终的结果不是通过return来返回的,而是通过第三个指针参数返回的,因为C中有一个参数传值功能,就是直接操作指针就可以传回结果,这个在C中经常用到,因为一个函数返回值只有一处要是一个函数有多个返回值就没办法了,所以可以通过参数指针来传递。所以如果我们想修改函数的最终结果,需要修改参数指针的内存段数据,我们先把那个内存段数据获取到打印出来,这里因为通过静态分析知道最终的结果是16个字节数据,所以这里不能在用读取内存字符串方法了,而是读取纯的字节数据:
20180519145034891.jpg
然后在把返回值修改了,返回值修改也很简单,直接重写那段内存值就可以了,比如这里修改成1111:
20180519145120923.jpg
所以看到了C语言中很多地方都在直接操作内存也就是地址,特别需要借助Memory类,他有很多方法,包括内存拷贝等。具体用到的可以去官网查询:https://www.frida.re/docs/javascript-api/#memory;然后我们看hook结果:
20180519145355814.jpg
我们hook到了他的参数信息,第一个参数是需要加密的字符串信息我们是通过Memory方法获取字符串的,因为本身这个参数是一个字符串指针,第二个参数应该是字符串长度,第三个参数是操作结果值的指针,然后看到我们获取到的结果值就是原始加密的信息。说明我们获取成功了,然后再看看我们修改之后的1111值,通过日志查看:
20180519145537864.jpg
看到了在Java成通过native访问得到的签名信息已经被修改成了1111了,说明我们成功了。到这里我们就成功的,在hook native的时候一定要注意函数的绝对地址要计算对,最后一定要记住+1,函数的返回值有可能不是通过return而是参数指针传递的,操作内存的时候用Memory类即可。

第二、hook导出函数功能
这部分内容很简单了,比上面的简单是因为不需要手动的计算函数地址,因为是导出的,所以直接可以得到导出的函数名即可,因为C语言中没有重载的形式,而C++中有,所以有时候发现导出的函数名和正常的函数名前面加上了一串数据作为区分那应该是C++代码写的。有了so文件和导出的函数名就不需要构造NativePoniter了:
20180519150820857.jpg
这个看到比上面自己手动找函数地址方便多了吧,打印参数都一样的代码了。这里通过函数名可以知道就是一个native函数了,那么他第一个参数肯定是JNIEnv指针,第二个参数是jclass类型,这个是标准的如果是静态方法第二个参数没啥用,后面的参数就是真的传递到native层的值了,比如这里Java层的方法:
20180519151045868.jpg
那么按照上面的说明native层的函数就是4个参数了:
20180519151128351.jpg
的确是这样的,后面两个参数才是我们想要的值,我们通过IDA查看这个函数:
20180519151211635.jpg
然后我们用F5查看伪代码他的返回值:
20180519151233245.jpg
用env指针调用了NewStringUTF返回一个jstring对象了,好了到这里我们先不说返回值修改的问题,先看看hook参数信息:
20180519151412612.jpg
但是我们看到我们打印的返回值是个空也就是空指针,而如果这里我们想hook他的返回值怎么办呢?如果是一个正常的返回字符串信息,我们可以直接用Memory的方法构造出来Memory.allocUtf8String("XXXXX")一个内存字符串信息,然后直接返回一个指针地址即可,但是现在这里是返回一个jstring对象,其实这个我们通过查看jni.h文件可以知道jstring是C++中定义的对象:
2018051915242745.jpg
而基本类型就是基本数据类型:
20180519152444265.jpg
这个修改没有任何问题的,那么现在问题是修改非基本类型,比如这里的如何返回jstring对象呢?这里我能想到的一个办法就是通过获取NewStringUTF函数指针,通过NativeFunction方法获取函数,然后调用
20180519153330929.jpg
这里看到代码逻辑没什么问题,现在缺的就是NewStringUTF的函数地址了,这个因为在so中没法查看,所以怎么办呢?不着急我们在看看JNIEnv的定义:
20180519153509525.jpg
他是一个结构体,再看看那个函数地址:
20180519153533429.jpg
我们已经有了JNIEnv结构体指针了,每个函数指针都是int类型也就是四个字节,所以从JNIEnv指针开始依次计算就可以得到NewStringUTF函数对应的地址了。不过都说了找不到方法的时候就去官网找,JNIEnv变量其实有对应的方法,这里构造jstring方法其实很简单:
0.png
这个比找函数指正方便多了,其实env有很多方法在这里都有对应的api。
所以到这里我们发现了Frida在Hook底层函数返回jni中的类型的时候有点麻烦了,但是Cydia就不会了,因为他是Android工程,可以引用jni.h头文件的,比如我们用Cydia来修改这个函数的返回值:
20180519154026270.jpg
看到了吧,这样就很方便了因为是Android工程,所以可以直接应用jni.h头文件,然后直接调用NewStringUTF方法返回了,看看hook的结果:
20180519154154937.jpg
也修改成功了。所以这里看到Frida也不是万能的,要看什么问题怎么去分析了。

四、技术总结

到这里我们就把Frida常用的功能和hook常见的用法都说明完了,下面就来总结一下:
第一、Java层代码Hook操作
1、hook方法包括构造方法和对象方法,构造方法固定写法是$init,普通方法直接是方法名,参数可以自己定义也可以使用系统隐含的变量arguments获取。
2、修改方法的参数和返回值,直接调用原始方法通过传入想要修改的参数来做到修改参数的目的,以及修改返回值即可。
3、构造对象和修改对象的属性值,直接用反射进行操作,构造对象用固定写法的$new即可。
4、直接用Java的Exception对象打印堆栈信息,然后通过adb logcat -s AndroidRuntime来查看异常信息跟踪代码。
总结:获取对象的类类型是Java.use方法,方法有重载的话用overload(.......)解决。
第二、Native层代码Hook操作
1、hook导出的函数直接用so文件名和函数名即可。
2、hook未导出的函数需要计算出函数在内存中的绝对地址,通过查看maps文件获取so的基地址+函数的相对地址即可,最后不要忘了+1操作。
总结:Native中最常用的就是内存地址指针了,所以如果要正确的获取值一定要用Memory类作为辅助,特别是字符串信息。

五、Hook家族神器的对比

下面继续来看看Frida,Xposed,SubstrateCydia这三个Hook神器的区别和优缺点:
第一、Xposed的优缺点
优点:在编写Java层hook插件的时候非常好用,这一点完全优越于Frida和SubstrateCydia,因为他也是Android项目,可以直接编写Java代码调用各类api进行操作。而且可以安装到手机上直接使用。
缺点:配置安装环境繁琐,兼容性差,在Hook底层的时候就很无助了。
第二、Frida的优缺点
优点:在上面我们可以看到他的优点在于配置环境很简单,操作也很便捷,对于破解者开发阶段非常好用。支持Java层和Native层hook操作,在Native层hook如果是非基本类型的话操作有点麻烦。
缺点:因为他只适用于破解者在开发阶段,也就是他没法像Xposed用于实践生产中,比如我写一个微信外挂用Frida写肯定不行的,因为他无法在手机端运行。也就是破解者用的比较多。
第三、SubstrateCydia的优缺点
优点:可以运行在手机端,和Xposed类似可以用于实践生产中。支持Java层和Native层的hook操作,但是Java层hook不怎么常用,用的比较多的是Native层hook操作,因为他也是Android工程可以引用系统api,操作更为方便。
缺点:和Xposed一样安装配置环境繁琐,兼容性差。
以上这三个工具可以说是现在用的最多的hook工具了,总结一句话就是写Java层Hook还是Xposed方便,写Native层Hook还是Cydia了,而对于破解者开发那还是Frida最靠谱了。但是不管怎么样,写外挂最难的也是最重要的不是写代码而是寻找hook点,也就是逆向分析app找到那个地方,然后写hook代码实现插件功能。

源码分析及修改方式
关于开机动画的流程主要代码在
framebuffer/base/cmds/bootanimation/bootAnimation.cpp
从 BootAnimation::threadLoop() 中的我们可以看到

if(mZip == NULL) {

    r = android();
} else {
    r = movie();

}
根据 mZip(这是一个叫做 bootanimation.zip 的文件)是否存在,决定调用 android() 接口还是 movie() 接口。

android()
如果没有 zip 文件进入的就是这种方式。
会加载”images/android-logo-mask.png”和”images/android-logo-shine.png” 这两张图片,前者是镂空的 ANDROID 字样,后者是一副很长的银白黑渐进的背景图,通过固定前者,移动后者,实现 ANDROID 字样的反光效果。
想修改android闪动的那两张图片的话,最简单的方法是直接替换图片(图片在 /frameworks/base/core/res/assets/images),如果懂 openGL 的话也可以自己做酷炫的动画。

movie()
如果有 bootanimation.zip 文件进入的就是这种方式。
#define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip"
会加载 bootanimation.zip 中的内容。zip 文件中实际是很多帧图片的组合,通过多帧图片的逐步播放实现动画的效果。
所以把做好的动画拷贝到编译好对应的目录下即可,然后执行make snod整合进 img 包就可以看到效果了。
具体制作 bootanimation.zip 的文章参考这两篇:
http://blog.csdn.net/mlbcday/article/details/7410509
http://luq1985428.blog.163.com/blog/static/12243116220131198011812/
但这样默认是没有音乐的,还需要实现一个 playMusic() 的接口,来同步的播放音乐。
具体实现 playMusic() 接口的方式参考这一篇的 “1.播放音乐”:
http://www.voidcn.com/blog/longtian635241/article/p-2095371.html
从 mp4 中提取音频为 ogg 或者 wav 格式的网站有
http://media.io/
缺点是

  1. 多帧图片由于画面色彩丰富、动画较长,这样做出来的 zip 会比较大,播放效果会出现明显、严重卡顿
  2. 播放时music时可能出现动画和声音不同步

所以我们可以调用 mediaPlayer 的接口来实现播放视频(mp4)

自行添加 video 接口
修改 ThreadLoop 中的判断

// We have no bootanimation file, so we use the stock android logo

 // animation.
  • if (mZip == NULL) {

    • if (mVideo) {//这里的 mVideo 是一个标志位,表示是否有开机视频
  • r = video();
  • }else if (mZip == NULL) {

       r = android();

    } else {

       r = movie();
    

我们在 ReadyToRun 中实现 mVideo 的判断。

@@ -359,6 +362,7 @@ status_t BootAnimation::readyToRun() {

 mFlingerSurfaceControl = control;
 mFlingerSurface = s;
  • mVideo = false;
    // If the device has encryption turned on or is in process
    // of being encrypted we show the encrypted boot animation.
    char decrypt[PROPERTY_VALUE_MAX];
    @@ -366,6 +370,9 @@ status_t BootAnimation::readyToRun() {
 bool encryptedAnimation = atoi(decrypt) != 0 || !strcmp("trigger_restart_min_framework", decrypt);
  • if (access(BOOTANIMATION_VIDEO, R_OK) == 0)
  • mVideo = true;
    +
    ZipFileRO* zipFile = NULL;
    if ((encryptedAnimation

        && (access(SYSTEM_ENCRYPTED_BOOTANIMATION_FILE, R_OK) == 0) 
    

下面可以开始添加 video 接口了

+bool BootAnimation::video()
+{

  • const float MAX_FPS = 60.0f;
  • const bool LOOP = true;
  • const float CHECK_DELAY = ns2us(s2ns(1) / MAX_FPS);
  • sp<IMediaHTTPService> httpService;
  • eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
  • eglDestroySurface(mDisplay, mSurface);
  • /*
  • float asp = 1.0f * mWidth / mHeight;
  • SurfaceComposerClient::openGlobalTransaction();
  • mFlingerSurfaceControl->setPosition(mWidth, 0);
  • mFlingerSurfaceControl->setMatrix(0, 1 / asp, -asp, 0);
  • SurfaceComposerClient::closeGlobalTransaction();
  • */
    +
  • sp<MediaPlayer> mp = new MediaPlayer();
  • mp->setDataSource(httpService, BOOTANIMATION_VIDEO, NULL);//设置播放资源
  • mp->setLooping(true);//确定是否播放循环
  • mp->setVideoSurfaceTexture(mFlingerSurface->getIGraphicBufferProducer());
  • mp->prepare();
  • mp->start();
  • while(true) {
  • if(exitPending())
  • break;
  • usleep(CHECK_DELAY);
  • checkExit();
  • }
  • mp->stop();
  • return false;
    +}

如果要实现开关机动画不同也可以增加一个判断。
这里的 BOOTANIMATION_VIDEO 为 mp4 的路径,setDataSource 接口有多种重载方式,这里采用 url 的方式。

+#define BOOTANIMATION_VIDEO "/system/media/bootanimation.mp4"
+#include <media/IMediaHTTPService.h>

最后修改头文件,添加增加的两个成员变量
/cmds/bootanimation/BootAnimation.h

@@ -106,6 +106,8 @@ private:

 EGLDisplay  mSurface;
 sp<SurfaceControl> mFlingerSurfaceControl;
 sp<Surface> mFlingerSurface;
  • bool mVideo;
  • bool video();
    ZipFileRO *mZip;
    int mHardwareRotation;
    GLfloat mTexCoords[8];

至此已经完成 video() 接口的编写了。
(具体 MediaPlayer 的用法参考的 http://blog.csdn.net/ddna/article/details/5176233
后面可以在 /system/media/ 中添加 bootanimation.mp4 尝试能否播放 mp4。
补丁如下
http://download.csdn.net/detail/dearsq/9623817

开机视频前黑屏 5s
是由于等待电池的后台服务启动导致的,屏蔽如下代码。
frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp

ba061518gw1f7kstpdro9j20mx0btjuz.jpg
屏蔽后黑屏时间减为 1s 左右。