Frida逆向与协议分析 起因是第七期分享会,有师傅推荐这本书,正好自己在frida脚本上很欠缺,所以买来看看。重点是实操,和C++反汇编不一样,别懒!
0x00 安卓逆向环境搭建 htop:动态查看当前活跃的,占用高的进程。
jnettop:查看下载速度与对应的 IP。
解锁OEM时会出现waiting for any device的情况,解决办法 。刷机过程见P9。adb devices只有进入系统时才能看到,fastboot devices只有进入bootloader时才能看到。
Kali NetHunter刷机:针对移动设备的安卓渗透测试平台。它主要修改安卓内核的内容,这些修改对日常使用无影响。最后使用链接 下载SuperSU-2.82。对于用NetHunter刷完机无法被虚拟机发现的情况,看链接 。
0x01 Frida Hook基础与快速定位 基础 frida基础 两种操作模式:(1)CLI模式:通过命令行将js脚本注入到进程中。(2)RPC模式:通过py脚本间接完成js脚本的注入。本章重点关注CLI模式。
两种操作App的方式:(1)spawn(调用)模式:将启动App的权利交给Frida来控制,即使目标App已启动,在使用Frida对程序进行注入时,还是会重新启动App并注入。frida -f就会调用spawn模式。(2)attach(附加)模式:在目标App已启动的前提下,利用ptrace原理注入程序进而完成Hook操作,默认以attach模式注入。
注:ida attach调试程序时,无法以frida attach模式注入;但是当先进行frida attach模式注入后,可以使用ida attach调试(先后问题)。
对于在App启动时执行的方法(so库的.init_array、.init_proc),使用spawn方式hook。而对于频繁运行的函数,可以使用attach方式hook。
frida连接手机的两种模式:(1)usb数据线:使用adb链接,加入-U参数即可。(2)网络模式:adb链接,frida-server在运行时使用-l监听ip与端口,主机上的frida通过-H参数指定手机的ip和端口。
frida hook基础 xposed使用java代码编写hook模块后,需要重启后才能使hook代码运行起来 。而frida只需在手机上运行frida-server,然后在主机上将js脚本注入即可。frida注入成功后,即使脚本被修改,hook效果也能及时生效。
针对显示->主动显示的钩取如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook ( ){ Java .perform (function ( ){ var settings = Java .use ("com.android.settings.DisplaySettings" ); var getMetricsCategory_func = settings.getMetricsCategory .overload (); getMetricsCategory_func.implementation = function ( ){ var result = this .getMetricsCategory (); console .log ("getMetricsCategory called, result => " , result); return result; } }) } function main ( ){ hook (); } setImmediate (main)
之后运行:frida -U com.android.settings -l hello.js。
注:
(1)所有针对java层函数的hook脚本必须处于Java.perform的包装中。
(2)Java.use获取指定类的handle后,使用类似于调用类静态方法的方式获得对应函数(上述代码中的settings.getMetricsCategory)。如果函数存在多个重载,还需要添加.overload(<signature>)获取指定函数。
如下所示,subString函数有两个重载:String substring(int)与String substring(int, int)。我们只想要钩取substring(int),如下所示:
1 2 3 4 5 6 7 8 9 10 11 function hookSubString ( ){ Java .perform (function ( ){ var String = Java .use ('java.lang.String' ) var subString_int_func = String .substring .overload ('int' ) subString_int_func.implementation = function (index ){ var result = this .substring (index) console .log ("substring called" ,'index =>' ,index,',result =>' ,result) return result } }) }
objection基础 frida提供了API供用户自定义使用,在此基础上可以实现很多具体功能。而objection则是将各种常用功能整合 并可在命令行使用的利器。在安卓中使用objection,可以快速完成内存、类与模块搜索、方法hook、打印参数等。
以movetv.apk为例,通过jadx查看AndroidManifest.xml,获取到包名为com.cz.babySister。安装到手机上,并启动frida-server,之后运行:
1 objection -g com.cz.babySister explore
之后,就可以对进程进行操作:
(1)打印内存:可打印出内存中已加载类的相关信息,从而快速定位app中的关键类。
1 android hooking list classes // 列出进程加载过的类
1 android hooking search classes <pattern> // 列出加载的类名中包含<pattern>格式的类
1 android hooking list class_methods <class_name> // 获取指定类中所有非构造函数的方法名
(2)Hook函数:
objection可以hook一个类中所有函数。
1 android hooking watch class <class_name> // 钩取某个类中所有非构造函数
1 2 3 4 5 // hook 指定函数 // classMethod 为方法名 // option 支持的参数有:--dump-args(函数执行时打印参数内容),--dump-return(打印返回值),--dump-backtrace(打印调用栈) // overload 表示具体是哪个函数(函数可能有重载),如果没指定那么默认为所有重载的函数 android hooking watch class_method <classMethod> <overload> <option>
下图就默认hook了所有setForegroundColor重载函数:
如果要指定钩取参数为int数组的setForegroundColor函数,那么就写为:
1 android hooking watch class_method com.cz.babySister.view.Loading.setForegroundColor "[I" --dump-args --dump-return --dump-backtrace
(3)jobs:hook任务管理,android heap:用于操作内存中类的实例。这些略。
Hook快速定位 逆向分析时遵循hook->主动调用->RPC,这是什么?即,协议分析的流程为:(1)用hook的方式确定关键业务逻辑的位置;(2)通过主动调用实现关键业务逻辑的调用;(3)通过RPC远程过程调用(python+js)的方式进行关键业务逻辑的批量调用。frida将hook工作成功地从xposed模块每次编译都需要重启的循环中解放出来 。
关键类定位:基于trace的枚举 DDMS(Dalvik Debug Monitor Server)是早期android studio中用于调试和监视 Android 设备和模拟器的工具。其中method trace功能可以获得一段时间内app执行的函数记录,但是,它有以下缺点:(1)要求app的debuggable为true。(2)会记录所有函数,包括系统函数。
而frida无需处于debuggable状态,且支持跟踪指定类中的函数,支持跟踪特定类中的所有函数。在此节中,不直接编写frida脚本来进行trace,而是使用frida封装的可用于进行trace定位的工具。
objection trace 以movetv.apk为例,首先搜索包含com.cz.babySister的类:
1 android hooking search classes com.cz.babySister
将输出保存为文件hook,并在每一行开头加入android hooking watch class,表示钩取所有与com.cz.babySister.xxx类相关的函数,如下所示:
通过如下命令,使用objection对所有com.cz.babySister.xxx类相关的函数进行钩取:
1 objection -g com.cz.babySister explore -c hook.txt
此时再触发我们所关心的业务逻辑,就能快速筛选出业务逻辑所在类的范围。
注1:search classes是先获取所有已加载类,之后再筛选。因此,它可能无法涵盖应用中所有的类。因此,在hook之
前,要尽可能多的使用app,以求加载足够多的类。 objection中trace的核心源码见P37。
注2:如果app加壳,那么最好选择attach模式进行trace/hook。否则在应用还未启动时,真实的类还没有被释放。此时会报ClassNotFoundException错误,这是因为app的ClassLoader在运行时的切换问题所导致的。
ZenTracer 具体见P39。
总结一下,frida相比于DDMS,能够依据使用者的想法对执行函数进行跟踪,同时无需样本处于debuggable状态。但是,由于frida本身不太稳定,因此被测试的程序会经常崩溃。
关键类定位:基于内存的枚举 当逆向者通过分析发现某些类可能是关键类时,可以通过对此关键类进行hook去验证分析的结果。相比于xposed,frida不仅不用重启,还支持对内存进程的漫游功能,即:能够通过Java.choose()在目标进程的java堆中寻找和修改已存在的java对象实例 。这使得我们不仅能够对未执行的函数设置hook,还能对已经创建的实例进行操作 。
以OkHttp中的hook抓包进行介绍。OkHttp的核心是拦截器。一个完整的网络请求被拆分为多个步骤,每个步骤都由拦截器完成。拦截器由okhttp3.OkHttpClient类中的List成员_interceptors数组管理,此数组包含Client中的所有拦截器。那么,如果写一个打印日志的LogInterceptor拦截器,并添加到_interceptors数组中,就可以完成对所有数据包的抓取。
如何做?通过Java.choose从内存中找到okhttp3.OkHttpClient的实例对象,并修改_interceptors数组内容,使得此数据包含我们刚才写的LogInterceptor拦截器,从而完成数据的抓包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function searchClient ( ){ Java .perform (function ( ){ var gson2 = Java .use ("com.google.gson.Gson" ); Java .openClassFile ("/data/local/tmp/muok2curl.dex" ).load (); console .log ("loading dex successful" ); const curlInterceptor = Java .use ("com.moczul.ok2curl.CurlInterceptor" ); const loggable = Java .use ("com.moczul.ok2curl.logger.Loggable" ); var Log = Java .use ("android.util.Log" ); var TAG = "okhttpGETcurl" ; var MyLogClass = Java .registerClass ({ name : "okhttp3.MyLogClass" , implements : [loggable], methods : { log : function (MyMessage ){ Log .v (TAG , MyMessage ); } } }) const mylog = MyLogClass .$new(); var curlInter = curlInterceptor.$new(mylog); Java .openClassFile ("/data/local/tmp/okhttplogging.dex" ).load (); var MyInterceptor = Java .use ("com.r0ysue.learnokhttp.okhttp3Logging" ); var MyInterceptorObj = MyInterceptor .$new(); Java .choose ("okhttp3.OkHttpClient" , { onMatch :function (instance ){ console .log ("1. find instance" , instance) console .log ("2. instance.interceptors():" , instance.interceptors ().$className ); console .log ("3. instance._interceptors:" , instance._interceptors ().value .$className ); console .log ("5. interceptors:" , Java .use ("java.util.Arrays" ).toString (instance.interceptors ().toArray ())); var newInter = Java .use ("java.util.ArrayList" ).$new(); newInter.addAll (instance.interceptors ()); console .log ("6. interceptors:" , Java .use ("java.util.Arrays" ).toString (newInter.toArray ())); console .log ("7. interceptors:" , newInter.$className ); newInter.add (MyInterceptorObj ); newInter.add (curlInter); instance._interceptors .value = newInter; }, onComplete :function ( ){ console .log ("Search complete" ); } }) }) } setImmediate (searchClient)
上述代码中,myok2curl.dex负责具体拦截器的实现,okhttplogging.dex负责具体拦截器的实现。这样做的目的是为了避免将Java翻译成js的复杂工作。Java.openClassFile("xx").load()用于导入dex文件,在内存中注册一个类,当然也可以使用Java.registerClass完成。
注:objection的插件WallBreaker,可以快速定位类中所包含的属性与函数,也可以直接通过对象的句柄获取所在类中所有属性的值。其基本原理就是使用Java.choose对内存中对象进行搜索,然后通过对对象的句柄进行反射,从而获得相应成员与函数。
frida也可以在native层进行hook。以脱壳为例,hexdump工具的核心原理是在目标进程的内存空间中遍历搜索包含DEX文件特征的数据,匹配成功则将dex dump出来,它主要使用了frida的Memory.scanSync函数。hexdump仅仅能够解决一代整体加固,对于二代抽取加固,有工具frida_fart。其通过hook app运行过程中的native函数,即LoadMethod函数(此函数处于加载与执行dex文件的ART虚拟机 中),利用此函数的参数,可以获得加载的java函数所在的dex文件与函数代码。
0x02 Frida脚本开发之主动调用与RPC入门 之前的内容讲了如何定位到算法的关键函数,之后我们就需要反复调用此函数,以进行调试,了解此函数真正的程序逻辑。但是如果按部就班的进行调用,函数传入的参数往往是变化的。 因此,就需要主动调用 ,即:目标函数的参数是可控的。远程过程调用(RPC)(就是python脚本的frida)也是与主动调用一起使用的一种技术。
之前说过Frida调试的3板斧:(1)Hook定位关键逻辑;(2)主动调用构造参数进行利用;(3)RPC导出结果进行规模化利用。 Frida实际上是基于Python和JS的进程级Hook框架,其中JS承担Hook函数的工作,Python相当于提供给外界的绑定接口,使用者可通过Python将JS脚本注入到进程中。其中,官方提供了frida-python来进行远程过程调用。
python实现frida注入的基本知识 获取设备 1 2 3 4 5 6 7 8 import fridadevice = frida.get_usb_device() print (device)device = frida.get_device_manager().add_remote_device('192.168.75.1:6666' ) print (device)
获取到设备后,实现进程注入同样有spawn(重新启动app)与attach(附到已启动的app)。如下所示:
注入进程-两种方式 1 2 3 4 5 6 7 8 9 10 11 12 import timeimport fridapid = device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) session = device.attach("com.android.settings" )
成功注入进程后,之后要进行hook脚本的注入,实际上就是将js脚本作为字节流通过frida提供的api加载到相应进程的session中。
hook脚本注入 方式1:js字符串
1 2 3 4 5 6 7 8 9 10 script = session.create_script(""" setImmediate(Java.perform(function(){ console.log("hello python frida"); })) """ )script.load()
方式2:js文件
1 2 3 with open ("hook.js" ) as f: script = session.create_script(f.read()) script.load()
rpc学习 学习rpc实际上是学习js脚本与python进行交互的方式。 rpc脚本示例如下,功能主要是打开settings,并调用两个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from __future__ import print_functionfrom frida.core import Sessionimport fridaimport time device = frida.get_usb_device() pid = device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) script = session.create_script(""" rpc.exports = { hello: function(){ return 'Hello'; }, failPlease: function(){ return 'oops'; } }; """ )script.load() api = script.exports print ("api.hello() => " , api.hello())print ("api.fail_please() => " , api.fail_please())
调用结果如下:
注意js脚本中带大写字母的导出函数变成了小写加下划线的形式,例如failPlease变为fail_please。可以这么说,rpc介绍了在python中主动调用js中函数的方式 。下面再来介绍js主动向python发送数据的方式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from __future__ import print_functionimport sysimport fridaimport timedef on_detached (): print ("on_detached" ) def on_detached_with_reason (reason ): print ("on_detached_with_reason:" , reason) def on_detached_with_varargs (*args ): print ("on_detached_with_varargs:" , args) device = frida.get_usb_device() pid = device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) print ("attached" )session.on('detached' , on_detached) session.on('detached' , on_detached_with_reason) session.on('detached' , on_detached_with_varargs) sys.stdin.read()
上述两个脚本是frida-python给出的两个简单的例子,js还可以用过send函数向python脚本发送数据。frida-python还包括:(1)child_gating.py:子进程注入;(2)bytecode.py:将脚本编译为字节码然后再加载脚本;(3)inject_library文件夹:手动向进程中注入一个动态库。等等。
这对于之前的ZenTracer项目,其真实用于Hook的代码只有traceClass函数,剩下的部分都是js与python之间的数据传递,主要是:通过send将js中的数据传输到python用于接收数据的FridaReceive函数中。
注:frida中session.on、script.on、device.on的区别?
session.on:这个方法用于在Frida会话(Session)对象上注册事件处理程序。Frida会话是与目标应用程序通信的主要接口。通过session.on方法,可以注册多种类型的事件处理程序,如detached(会话与目标应用程序断开连接时触发)和message(接收到来自目标应用程序的消息时触发)等。
script.on:这个方法用于在Frida脚本(Script)对象上注册事件处理程序。Frida脚本是在目标应用程序中执行的用户定义代码。通过script.on方法,可以注册多种类型的事件处理程序,如message(接收到来自目标应用程序的消息时触发)和destroyed(脚本被销毁时触发)等。
device.on:这个方法用于在Frida设备(Device)对象上注册事件处理程序。Frida设备是与目标设备或模拟器通信的接口。通过device.on方法,可以注册多种类型的事件处理程序,如spawned(目标应用程序被启动时触发)和output(设备的输出消息时触发)等。
frida java层主动调用与rpc 以demoso1工程为例,此工程中存在两个native函数,在应用打开后被循环调用。其中method01是静态函数,对输入进行加密并返回,method02是成员函数,对密文解密并返回。其java层声明如下:
注:如果native函数是静态注册的,那么so层最终生成的函数是以Java_开头的导出函数。然而此静态注册方式不安全,因此用RegisterNatives()函数对上述两个函数进行了动态注册,以加强安全性。注册过程如下:
运行日志如下:
首先使用上一章介绍的objection工具来注入目标进程,以确定method01与method02的签名与调用情况。
1 2 3 4 5 6 // 使用 objection 注入到进程 objection -g com.example.demoso1 explore // 获取 com.example.demoso1.MainActivity 中所有非构造函数的方法名 android hooking list class_methods com.example.demoso1.MainActivity // 钩取 com.example.demoso1.MainActivity 中所有非构造函数 android hooking watch class com.example.demoso1.MainActivity
函数签名如下:
调用情况如下:
可以看出,此程序主动调用了method01。之后,我们就要写hook脚本,从而对此函数进行主动调用。主动调用最重要的是参数的构造,主动调用其实就是构造和hook时使用的参数类型一致的参数。首先,编写invoke.js如下:
1 2 3 4 5 6 7 8 9 10 11 12 function hook ( ){ Java .perform (function ( ){ var MainActivity = Java .use ("com.example.demoso1.MainActivity" ); MainActivity .method01 .implementation = function (str ){ var result = this .method01 (str); console .log ("str =>" , str); console .log ("result =>" , result); return result; } }) }
当注入并调用Hook函数时,结果如下所示:
重写一下invoke.js,目的是调用method01函数,并对xcl进行加密:
1 2 3 4 5 6 7 8 9 10 function invokeMethod01 ( ){ Java .perform (function ( ){ var MainActivity = Java .use ("com.example.demoso1.MainActivity" ); var javaString = Java .use ("java.lang.String" ); var plaintext = "xcl" ; var result = MainActivity .method01 (javaString.$new(plaintext)); console .log ("plaintext =>" , plaintext); console .log ("result =>" , result); }) }
结果如下所示:
刚才的method01是静态函数,这类函数的主动调用只需要获得对应类的句柄。而对于一般的函数method02来说,这样调用就会出错,因为此函数需要通过实例进行调用,而不是一个类句柄。而之前学过,Java.choose()能够在内存中获取实例,因此对method02的主动调用脚本如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function invokeMethod02 ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ var javaString = Java .use ("java.lang.String" ) var ciphertext = "0646275a2d4a4e00a36c8b909a405f25" var result = instance.method02 (javaString.$new(ciphertext)) console .log ("ciphertext =>" , ciphertext) console .log ("result =>" , result) }, onComplete ( ){} }) }) }
结果如下所示:
测试好主动调用后,下一步就是rpc(python脚本调用js中的函数)。此时需要将主动调用提供为外部接口,需要两步:
(1)将主动调用参数配置为js函数的参数并将主动调用的结果返回,以方便外部自定义参数进行主动调用,更改后的代码如下所示:
(2)将这两个主动调用的函数导出,最终export.js如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 rpc.exports = { method01 :invokeMethod01, method02 :invokeMethod02 } function invokeMethod01 (plaintext ){ var result; Java .perform (function ( ){ var MainActivity = Java .use ("com.example.demoso1.MainActivity" ); var javaString = Java .use ("java.lang.String" ); result = MainActivity .method01 (javaString.$new(plaintext)); console .log ("plaintext =>" , plaintext); console .log ("result =>" , result); }) return result; } function invokeMethod02 (ciphertext ){ var result; Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ var javaString = Java .use ("java.lang.String" ) result = instance.method02 (javaString.$new(ciphertext)) console .log ("ciphertext =>" , ciphertext) console .log ("result =>" , result) }, onComplete ( ){} }) }) return result }
到此,rpc中js部分完成,此时再写一个Python外部调用的脚本invoke.py,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import timeimport fridaimport jsondef my_message_handler (message, payload ): print (message) print (payload) device = frida.get_usb_device() pid = device.spawn(["com.example.demoso1" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) with open ("export.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load() api = script.exports ciphertext = api.method01("xcl" ) print ("method01 => encode_result: " + ciphertext)print ("method02 => decode_result: " + api.method02(ciphertext))
运行结果如下所示:
再丰富一下上述代码,使其能够通过浏览器访问批量调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from flask import Flask, requestimport jsonapp = Flask(__name__) import timeimport fridaimport jsondef my_message_handler (message, payload ): print (message) print (payload) device = frida.get_usb_device() pid = device.spawn(["com.example.demoso1" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) with open ("export.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load() @app.route('/encrypt' , methods=['POST' ] ) def encrypt_class (): data = request.get_data() json_data = json.loads(data.decode('utf-8' )) postdata = json_data.get("data" ) res = script.exports.method01(postdata) return res @app.route('/decrypt' , methods=['POST' ] ) def decrypt_class (): data = request.get_data() json_data = json.loads(data.decode('utf-8' )) postdata = json_data.get("data" ) res = script.exports.method02(postdata) return res if __name__ == "__main__" : app.run()
启动之后,运行:
1 curl -X POST http://127.0.0.1:5000/encrypt -H "{Content-Type:application/json}" -d '{"data":"xcl"}'
注:-X指定http请求的方法,-H指定http请求的头部,-d指数据体。
运行结果如下:
如果进一步想要将rpc变成批量化集群调用,则可以部署到服务器上。但是需要考虑手机性能、frida版本的稳定性、网络状况等。使用siege压力测试工具进行测试:
1 siege -c10 r100 "http://127.0.0.1:5000/encrypt POST < encrypt.json"
1 2 // encrypt.json {"data":"xcl"}
注:-c10代表并发数为10,-r100代表重复次数为100。结果如下所示:
这个速率还可以。对于method02而言,由于其中有Java.choose(),且此操作非常耗时,因此会降低method02调用的速度。为什么此操作耗时?因为每次调用Java.choose都会在内存中重新搜索实例。
我们改进如下:将对象始终保存在程序的外部全局变量中,这样每次调用method02的时候不至于重新寻找。
但是这利用了app的特殊性,即app不会退出(一直循环调用method01与method02 ),且调用的函数处于MainActivity中,即只要app不退出,MainActivity就会一直存在于内存中。那么,如果在其他app中,对象被系统进行垃圾回收了怎么办?这就需要下一节知识:Native层的主动调用。
frida native层函数的主动调用 虽然method01与method02分别是static与成员函数,但是他们都是native层的。这样的话,我们可以直接在native层进行主动调用,而无需考虑对象是否被回收的问题。Ok,那我们要完成native层的主动调用。继续三板斧。
(1)找到java函数在native层对应的函数符号,并以此找到函数地址。此时分为两种情况:
(a)静态注册的jni函数,其函数符号为Java_<类名>_<函数名>,例如stringFromJNI,此时hook脚本为:
1 2 3 4 5 6 7 8 9 10 11 function hookmethod ( ){ var method01 = Module .findExportByName ('libnative-lib.so' , 'Java_com_example_demoso1_MainActivity_stringFromJNI' ) Interceptor .attach (method01, { onEnter :function (args ){ }, onLeave :function (retval ){ } }) }
如果不确定函数的符号,可以通过Objection来查看导出的函数列表:
1 objection -g com.example.demoso1 explore
列出libnative-lib.so模块中的导出函数。
1 2 // 列出so模块中的导出函数 memory list exports libnative-lib.so
(b)使用RegisterNatives来进行动态注册的函数,例如method01与method02,可以使用frida_hook_libart项目中的hook_RegisterNatives.js脚本获取动态注册后函数所在的地址(注意由于实验版本的frida版本低,所以将脚本中的let改为var)。
1 frida -U -f com.example.demoso1 -l hook_RegisterNatives.js --no-pause
可以发现method01的名称改为:_Z8method01P7_JNIEnvP7_jclassP8_jstring。
(2)写Hook脚本,并改写为主动调用。可以写hook脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hook_native_method (method_name ){ var m = Module .findExportByName ('libnative-lib.so' , method_name) Interceptor .attach (m, { onEnter :function (args ){ console .log ("args[0] =>" , args[0 ]) console .log ("args[1] =>" , args[1 ]) console .log ("args[2] =>" , Java .vm .getEnv ().getStringUtfChars (args[2 ], null ).readCString ()) }, onLeave :function (retval ){ console .log ("result =>" , Java .vm .getEnv ().getStringUtfChars (retval, null ).readCString ()) } }) } function hookmethod01 ( ){ hook_native_method ("_Z8method01P7_JNIEnvP7_jclassP8_jstring" ); }
结果如下:
实际上,书中使用frida_hook_libart项目时只给出了method01在libnative-lib.so中的偏移量offset,此时脚本改为:
1 2 3 4 5 6 7 8 9 10 function hook_native_method (addr ){ Interceptor .attach (addr, { ... }) } function hookmethod01 ( ){ var base = Module .findBaseAddress ('libnative-lib.so' ); var method01_addr = base.add (offset) hook_native_method (method01_addr); }
注:为什么要用Java.vm.getEnv().getStringUtfChars?这是因为Java中的string参数到native层变成了Jstring对象 。
上述脚本并没有涉及函数的主动调用,因为在脚本中并没有主动调用原来的函数。如果主动调用函数的话,应该是这样:
因此,将上述脚本修改如下,这样程序调用method01时,就会执行替换后的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hook_native_method (method_name ){ var m = Module .findExportByName ('libnative-lib.so' , method_name) var addr_func = new NativeFunction (m, 'pointer' , ['pointer' , 'pointer' , 'pointer' ]) Interceptor .replace (m, new NativeCallback (function (arg1, arg2, arg3 ){ var result = addr_func (arg1, arg2, arg3) console .log ("arg3 =>" , Java .vm .getEnv ().getStringUtfChars (arg3, null ).readCString ()) console .log ("result =>" , Java .vm .getEnv ().getStringUtfChars (result, null ).readCString ()) return result }, 'pointer' , ['pointer' , 'pointer' , 'pointer' ] )) } function hookmethod01 ( ){ hook_native_method ("_Z8method01P7_JNIEnvP7_jclassP8_jstring" ); }
可改写为针对method01的主动调用,这样就可以主动调用native层的method01(最终形态):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_native_method (method_name, data ){ var result = null var m = Module .findExportByName ('libnative-lib.so' , method_name) var addr_func = new NativeFunction (m, 'pointer' , ['pointer' , 'pointer' , 'pointer' ]) Java .perform (function ( ){ var env = Java .vm .getEnv () console .log ("data =>" , data) var jstring = env.newStringUtf (data) result = addr_func (env, ptr (1 ), jstring) result = env.getStringUtfChars (result, null ) }) return result } function hookmethod01 ( ){ var result = hook_native_method ("_Z8method01P7_JNIEnvP7_jclassP8_jstring" , "xcl" ) console .log ('result =>' , result) }
注:(1)JNI函数(native层的函数),第一个参数一定是JNIEnv的指针;第二个参数为jclass或jobject,分别表示jni函数在Java层中是静态还是动态函数。由于method01中没有使用第二个参数,因此可以任意传递相同类型的数据,这里使用ptr(1)构造了一个指针。(2)由于函数主动调用时使用了Java.vm.getEnv这个API,因此需要包裹在java.perform()中。
执行结果如下:
(3)对native函数进行导出,并配置rpc,前面说过,因此不再赘述。
native函数调用的另类之法 以method01为例,此法脱离特定apk加载模块并调用native的固有模式。
(1)将apk解压,并将method01所在模块libnative-lib.so导出到/data/app目录下(必须要放到这个目录下 ),并赋予权限。
(2)编写主动调用脚本,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function invoke_func (addr,contents ){ var result = null ; var func = new NativeFunction (addr,'pointer' ,['pointer' ,'pointer' ,'pointer' ]); Java .perform (function ( ){ var env = Java .vm .getEnv (); console .log ("content is " ,contents) var jstring = env.newStringUtf (contents) result = func (env,ptr (1 ),jstring) result = env.getStringUtfChars (result, null ) }) return result; } function invoke_method01 ( ){ var method_name = "_Z8method01P7_JNIEnvP7_jclassP8_jstring" var m = Module .findExportByName ('/data/app/libnative-lib.so' , method_name) var result = invoke_func (m, 'xcl' ) console .log ("result =>" , result.readCString ()) }
注:frida-server仅在python脚本下需要手动启动,在命令行下会自动启动(有时也需要手动),objection使用时也需要手动启动frida-server。
此时注入任何应用都会并调用invoke_method01,都会成功,如下所示(注入了com.android.settings):
注:真实的情况是,脱离app进行模块函数调用时会发生各种问题,例如签名校验,native层调用Java函数 等,这个时候此方法就行不通了。
总结:此章的作用就是非常便捷的模拟app中的某些函数,且仅需要知道函数的参数与返回值。