Frida逆向与协议分析-2 frida逆向与协议分析第二部分。
0x03 Frida逆向之违法App协议分析与取证实战 之前介绍了frida定位关键类的两种方式:基于trace(objection、Zentracer)、基于内存(Java.choose寻找实例)。本章以两个违法样本为例,对app关键协议进行分析,从而巩固之前的知识。
加固app协议分析 抓包 抓包往往可以快速定位关键接口函数的位置 。
抓包原理:在手机上设置代理,将手机流量数据转发到计算机的代理软件后再完成上网,这样就可以再计算机上监听手机上的流量数据。由于中间人抓包无法应对app使用Https等加密协议进行通信的情况,因此需要将代理软件的证书导入手机系统并添加到证书信任列表中 。如果app不信任用户添加到系统中的证书,那么需要将证书从用户信任去移动到系统信任列表中 。
代理方式有两种:
(1)wifi代理(应用层),如下所示:
这种方式有两个弊端:(a)无法处理非http通信,例如websocket。(b)容易被app检测到,相关代码为:
1 System.getProperty("http.proxyHost")
(2)vpn代理(更加推荐),相当于虚拟新网卡并修改手机路由表,工具为postern(注意要匹配所有地址)。
linux中间人中安装charles,并设置代理:
movetv.apk的登陆界面抓包如下:
注册/登录协议分析 通过抓包分析,name与pass分别代表用户名与密码,而login始终为”login”。而key、rightkey、memi1等字段暂时还不确定。
要对这些字段进行分析,需要找到字段形成的地方
。除了静态工具分析 外,推荐使用前面介绍的快速定位关键类的方式,即基于内存枚举的关键类定位方案
,以确定字段形成的位置。
在此,基于用户登录一定要单击登录
按钮的特性,而按钮button属于view的继承类,因此可以通过hook view类的onClick函数快速得到当前控件的onClick函数所在的类,如下hookEvent.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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 var jclazz = null var jobj = null function getObjClassName (obj ){ if (!jclazz){ var jclazz = Java .use ("java.lang.Class" ) } if (!jobj){ var jobj = Java .use ("java.lang.Object" ) } return jclazz.getName .call (jobj.getClass .call (obj)) } function watch (obj, mtdName ){ var listener_name = getObjClassName (obj) var target = Java .use (listener_name) if (!target || !mtdName in target){ return } target[mtdName].overloads .forEach (function (overload ){ overload.implementation = function ( ){ console .log ("[WatchEvent] " + mtdName + ": " + getObjClassName (this )) return this [mtdName].apply (this , arguments ) } }) } function OnClickListener ( ){ Java .perform (function ( ){ Java .use ("android.view.View" ).setOnClickListener .implementation = function (listener ){ if (listener != null ){ watch (listener, 'onClick' ) } return this .setOnClickListener (listener) } Java .choose ("android.view.View$ListenerInfo" , { onMatch : function (instance ){ instance = instance.mOnClickListener .value if (instance){ console .log ("mOnClickListener : " + getObjClassName (instance)) watch (instance, 'onClick' ) } }, onComplete : function ( ){} }) }) } setImmediate (OnClickListener )
1 2 // 注入到前台 app 中 frida -U -F -l hookEvent.js
结果为:
因此,找到字段形成的地方为com.cz.babySister.activity.LoginActivity
。
之后,要得到此类具体的代码,可以通过静态反编译工具进行。发现进行加壳处理 :
可以利用脱壳工具进行脱壳,例如一代frida_dexdump
,二代抽取脱壳frida_fart|FART
等。在此使用frida_dexdump,流程见链接 。脱壳出5个dex,在dex3中找到:
其中,猜测b()函数应该是提交用户名与密码到服务器的函数。使用objection对b()函数进行注入:
1 2 objection -g com.cz.babySister explore android hooking watch class_method com.cz.babySister.activity.LoginActivity.b --dump-args --dump-return --dump-backtrace
结果如下所示:
因此确定b()传递用户名与密码。在用静态分析跟踪实现,最终确认到了q()函数,但是跟进后发现全为nop,如下所示:
经过查询资料,发现是二代抽取加固 ,使用frida_fart,按照链接 进行脱壳(注意到对movetv.apk设置读取sd卡的权限)。经历千辛万苦,终于找到了正常的逻辑:
依次跟进v5(key)、v6(rightkey)、v4(memi1)的实现逻辑,如下所示:
审计上述代码,发现:v5(key)实际上是app的签名,v6(rightkey)是app所安装的包的证书部分,v4(memi1)是android_id。这些值都是固定值,因此,无需借助app,只需要输入正确的用户名与密码,再传入固定的key、rightkey、memi1即可。
app注册的逻辑与登陆类似。最终可以用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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import requestsrequests.packages.urllib3.disable_warnings() class tv : def __init__ (self ): self.root = "http://39.108.64.125/WebRoot/superMaster/Server" self.memi1 = "34463b7d13ee6ad9" self.rightkey = "376035775" self.key = "308202d5308201bda00302010202041669d9bf300d06092a864886f70d01010b0500301b310b3009060355040613023836310c300a06035504031303776569301e170d3136303731383038313935395a170d3431303731323038313935395a301b310b3009060355040613023836310c300a0603550403130377656930820122300d06092a864886f70d01010105000382010f003082010a028201010095f85892400aae03ca4ed9dcd838d162290ae8dd51939aac6ecfde8282f207c4cd9e507929a279e0a36f1e4847330cb53908c92915b2c6a93d7064be452d073a472093f7ca14f4ab68f827582fe0988e9e4bc8a6ea3b56001cbbbb760f9eec571b0bbc97392e65aaf08c686f0e2ba353896d48a37c36716239977bd0e4dd878025cab497d8164537aec9f6599eefb98577dce972a1b794e211226520e23497beec3fd8548bb5b4d263120d40115cca28116bac32378df5033f536a0d7367fef78c587fefed28c5c9b35ba684ed6e46d9369c40950cf7ad7236d10b7a51dfd2a8f218db72323bbd19f46947410b1191f263012ad4ba8f749223e37591254ee7f50203010001a321301f301d0603551d0e041604143d43284bd5e4b0d322c9962a5b70aad4dcbc3634300d06092a864886f70d01010b050003820101000f04c51ff763311aa011777ba2842b441b15c316373d1e1ed4116cf86e29d55c6ed3fa4c475251b1fb4fac57195dbca0166ebe565d9834552a3758b97c4528bab1f7ab82bb3a9faa932f5bc10943f3daf52e0fe5889ffb58a6be67ea1c9a2fb37dc8aa6f3af476039a467336991a4e52dccd520195cd473eb5b984e702ed9ff638a14c3abb575a7a80ae4062084d1138a06a20e173be9df32df631311b07352898706198ddebaaa011f0da8e5f288f7cfb77505bc943f6476d6cc1feef56b68137aad91f23c4bb772169539d05653a6f0d75f7192164e822b934322f3a975df677903b1667f5dc1e9ddb185da3281d31bfb8f67a84bd23bbcb398f8bb637dd72" def post (self, data=None ): if data is None : data = {} return requests.post(url = self.root, data = data) def register (self, name, pw ): ret = self.post({ "name" :name, "pass" :pw, "memi1" :self.memi1, "key" :self.key, "rightkey" :self.rightkey, "register" :"register" }) print ("register: " , ret.content.decode('utf-8' )) def login (self, name, pw ): ret = self.post({ "name" :name, "pass" :pw, "memi1" :self.memi1, "key" :self.key, "rightkey" :self.rightkey, "login" :"login" }) print ("login: " , ret.content.decode('utf-8' )) if __name__ == "__main__" : tv = tv() print (tv.register("wd2711" , "1111" )) print (tv.login("wd2711" , "1111" ))
结果如下所示:
违法应用取证分析与vip破解 安装fulao2.apk。
vip清晰度破解 手机app对应的服务器好像寄了,如下所示:
ok,那抓不了包了,日了。那直接定位关键类,由于书中写了,清晰度切换是一个按钮控件,这需要vip。因此,我们只需要使用之前抓取按钮的脚本hookEvent.js即可。但是由于我们进不去页面呀(hookEvent.js,此时清晰度控件应该还未加载),只能抓取到:
正常的话,应该抓取到com.ilulutv.fulao2.film.l$t
。
且验证此apk未加壳(PKID不准,movetv.apk放进去也显示这个):
定位到com.ilulutv.fulao2.film.l$t
这个类,如下所示,可以看到是一个判断语句。
之后分析各个判断语句之后执行的操作:
(1)this.d.i
:生成一个对话框,其中要求升级vip。
(2)…
那么,如果将l.d(this.d)
的值设置为true即可。由于此app已无法正常访问,因此以书中为准。使用frida脚本在内存中修改l.d(this.d)
的值。l.d
的实现如下,其中arg0为l类型:
脚本如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookq0 ( ){ Java .perform (function ( ){ Java .choose ("com.ilulutv.fulao2.film.l" , { onMatch :function (instance ){ console .log ("found instance =>" , instance) instance.q0 .value = true }, onComplete :function ( ){ console .log ("completed" ) } }) }) }
最终调用成功了,可惜我这无法验证,呜呜。
图片取证分析 之后分析app协议内容。协议分析的第一步:针对样本流量的抓取与关键字段的定位
。
本次抓包不使用之前的中间人抓包(charles|postern)
,而是使用hook抓包,其优势在于:(1)抓取的流量更加专一,不会受到手机上其他app流量的影响;(2)可以避免app本身对抗抓包的姿势,例如服务器校验客户端,SSL Pinning等
。但是,如果app有对抗frida等工具的手段,那么就需要bypass。(3)hook可以打印调用栈,因此可确定函数之前经过的所有函数。
hook抓包的效果取决于找到的hook点。(1)android有很多封装网络通信的库,例如okhttp3、retrofit。因此,可能需要针对不同的库设置hook点。(2)对于通信协议而言,只抓取http的hook点也不够全面。之后装逼时刻,r0ysue开发了r0capture,hook系统中在socket层发送和接收数据包的关键函数,基本上能抓所有流量
。
虽然跑不了,但是分析一波r0capture ,此书中利用r0capture抓取了图片(挖个坑)。抓取后的图片是加密的。那之后,就要找到图片解密的点。图片要加载的时候,此时图片就被解密
。
android中加载图片的过程:使用BitmapFactory类中的函数加载bitmap,最终通过ImageView加载Bitmap类型的图片。
为了验证这个过程,使用objection插件wallbreaker在内存中搜索Bitmap对象,手动触发图片的加载后再次搜索Bitmap对象,前后数量不一致,因此可以确定此app使用Bitmap对象来保存图片。
android开发中,BitmapFactory中有4个静态方法:decodeFile、decodeResource、decodeStream、decodeByteArray
,分别用于从文件系统、资源、输入流、字节数组
中加载出Bitmap对象。更暴力一点,对BitmapFactory中所有函数进行hook,如下所示:
1 android hooking watch class android.graphics.BitmapFactory
最终发现是使用decodeByteArray
函数(static),此函数的第1个参数就是原始图片的字节信息。为了确认此图片信息为明文,使用frida脚本获取参数并保存。saveBitmap.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 function guid ( ){ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" .replace (/[xy]/g , function (c ){ var r = Math .random () * 16 | 0 var v = c == "x" ? r : (r&0x3 |0x8 ) return v.toString (16 ) }) } function saveBitmap_1 ( ){ Java .perform (function ( ){ var BitmapFactory = Java .use ("android.graphics.BitmapFactory" ) var decodeByteArray_func = decodeByteArray.overload ("[B" , "int" , "int" , "android.graphics.BitmapFactory$Options" ) decodeByteArray_func.implementation = function (data, offset, length, opts ){ var result = this .decodeByteArray (data, offset, length, opts) var path = "/sdcard/Download/tmp" + guid () + ".jpg" console .log ("path =>" , path) var f = Java .use ("java.io.File" ).$new(path) var fos = Java .use ("java.io.FileOutputStream" ).$new(f) fos.write (data) fos.close () return result } }) } setImmediate (saveBitmap_1)
之后,对上述脚本中的saveBitmap_1进行修改,以适应最后的rpc操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function saveBitmap_1 ( ){ Java .perform (function ( ){ var BitmapFactory = Java .use ("android.graphics.BitmapFactory" ) var decodeByteArray_func = BitmapFactory .decodeByteArray .overload ("[B" , "int" , "int" , "android.graphics.BitmapFactory$Options" ) decodeByteArray_func.implementation = function (data, offset, length, opts ){ var result = this .decodeByteArray (data, offset, length, opts) send (data) return result } }) } setImmediate (saveBitmap_1)
相应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 28 29 30 31 32 33 34 35 import fridaimport jsonimport timeimport uuiddef my_message_handler (message, payload ): if message["type" ] == "send" : image = message["payload" ] intArr = [] for m in image: ival = int (m) if ival < 0 : ival += 256 intArr.append(ival) bs = bytes (intArr) fileName = str (uuid.uuid1()) + ".jpg" print ("fileName:" , fileName) f = open (filename, "wb" ) f.write(bs) f.close() device = frida.get_usb_device() target = device.get_frontmost_application() session = device.attach(target.pid) with open ("hookBitmap.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load()
运行saveBitmap.py后,再对图片进行刷新,就可以下载图片啦。但是!我们目的不是为了下载图片,而是为了分析协议,也就是对如何对图片进行的解密
。
我们使用objection hook decodeByteArray并打印调用栈:
1 android hooking watch class_method android.graphics.BitmapFactory.decodeByteArray "[B, int, int, android.graphics.BitmapFactory$Options" --dump-backtrace
发现业务层代码com.ilulutv.fulao2.other.helper.glide.b.a
。使用jeb找到此函数,如下所示:
跟进解密函数,其中arg3为密钥Key,arg4为向量IV,arg5是要解密的数据。
因此,可以使用主动调用的方式对key与IV进行获取,如下所示:
1 2 3 4 5 6 7 8 9 function getKey ( ){ Java .perform (function ( ){ var CipherClient = Java .use ("net.idik.lib.cipher.so.CipherClient" ) var key = CipherClient .decodeImgKey () var iv = CipherClient .decodeImgIv () console .log (key, iv) }) }
获取到key与IV之后,采用rpc的方式来模拟,从而实现最终的脱机抓取图片数据。经过分析,前面抓包得到的图片数据其实是com.ilulutv.fulao2.other.i.b.a
函数的返回值,因此可以写脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 function get_image_ciphertext ( ){ Java .perform (function ( ){ var b = Java .use ("com.ilulutv.fulao2.other.i.b" ) var a_func = b.a .overload ("java.nio.ByteBuffer" ) a_func.implementation = function (obj ){ var result = this .a (obj) send (result) return result } }) }
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 import base64from Crypto.Cipher import AESdef IMGdecrypt (bytearray ): key = "xxxxx" iv = "xxxx" imgkey = base64.decodebytes(bytes (key), encoding="utf-8" ) imgiv = base64.decodebytes(bytes (iv), encoding="utf-8" ) cipher = AES.new(imgkey, AES.MODE_CBC, imgiv) msg = cipher.decrypt(bytearray ) return msg def my_message_handler (message, payload ): if message["type" ] == "send" : image = message["payload" ] intArr = [] for m in image: ival = int (m) if ival < 0 : ival += 256 intArr.append(ival) bs = bytes (intArr) bs = IMGdecrypt(bs) fileName = str (uuid.uuid1()) + ".jpg" print ("fileName:" , fileName) f = open (filename, "wb" ) f.write(bs) f.close() device = frida.get_usb_device() target = device.get_frontmost_application() session = device.attach(target.pid) with open ("final.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load()
0x04 Xposed Hook及主动调用与RPC实现 Xposed 是 Frida 的前辈,其作为系统框架类型的 hook 思想
对安卓安全研究有很大的影响。EdXposed 是 Xposed 的后续产品。本章主要介绍 Xposed 基本使用,并与 frida 做对比。
Xposed 应用 hook Xposed 安装&Xposed插件(模块) 需要在 Root 环境下通过 XposedInstaller App 安装对应系统的 xposed 框架,安装成功后,接着安装相应的 hook 框架并重启,从而完成对目标进程的 hook。xposed 本质上是替换安卓系统中的 zygote 与libart.so 库,来将 XposedBridge.jar 注入到应用中,从而实现进程的 hook。安装 xposed 需要满足:安卓版本小于等于7.1,系统 root。
经过一番周折,终于将 Xposed 安装上了。
Xposed插件以App的形式安装到系统中。举一个xposed插件(app)的例子,以说明xposed插件的开发流程。
(1)修改AndroidManifest.xml,在application节点下增加meta-data属性:
(2)在app目录下新建assets目录,并新建xposed_init文件,文件中填入Xposed模块入口类的完整类名,在此为:com.roysue.xposed1.HookTest
。
(3)MainActivity.java中填入:
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 package com.roysue.xposed1;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.Toast;public class MainActivity extends AppCompatActivity { private Button button; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = (Button) findViewById(R.id.button); button.setOnClickListener(new View .OnClickListener() { public void onClick (View v) { Toast.makeText(MainActivity.this , toastMessage("我未被劫持" ), Toast.LENGTH_SHORT).show(); } }); } public String toastMessage (String message) { return message; } }
若没有hook代码,那么打开此插件后,会提示我未被劫持
。下面,我们hook的目标为:打印toastMessage函数的参数,并且修改返回值为"你已被劫持"
。那么该如何做咧?
(1)之前,在xposed_init中写入了com.roysue.xposed1.HookTest
,从而指定了hook相关的类。我们要在此类中实现IXposedHookLoadPackage
接口。之后,每个由Zygote孵化出的进程启动时都会调用接口中的handleLoadPackage
函数。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.roysue.xposed1;import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.callbacks.XC_LoadPackage;public class HookTest implements IXposedHookLoadPackage { public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { } }
如果想要hook指定进程,就需要对上述代码中的loadPackageParam进行过滤。loadPackageParam中包括一些成员变量,如下所示:
成员变量类型
成员变量名
含义
String
packageName
包名
String
processName
进程名
ClassLoader
classLoader
进程的类加载器
ApplicationInfo
appInfo
其他信息
通常使用packageName进行过滤。
(2)锁定目标进程后,接下来对目标进程的函数进行hook,其中需要使用xposed相关的类:XposedHelpers
。此类中提供了java类、类成员的接口函数。在此例中,需要使用此类中的findAndHookMethod
函数。此函数参数为:要hook函数所在类的handle、函数名、函数参数列表、hook回调类XC_MethodHook
。在将XC_MethodHook
作为参数传给findAndHookMethod
之前,需要实现抽象回调函数:beforeHookedMethod
与afterHookedMethod
。顾名思义,不必多说。
因此,可以完善 logic-1,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import de.robv.android.xposed.XposedBridge;import android.util.Log;if (loadPackageParam.packageName.equals("com.roysue.xposed1" )){ XposedBridge.log("has hooked!" ); XposedBridge.log("inner" + loadPackageParam.processName); Class clazz = loadPackageParam.classLoader.loadClass("com.roysue.xposed1.MainActivity" ); XposedHelpers.findAndHookMethod(clazz, "toastMessage" , String.class, new XC_MethodHook (){ protected void beforeHookedMethod (MethodHookParam param) throws Throwable{ String oldText = (String) param.args[0 ]; Log.d("oldText" , oldText); param.args[0 ] = "您已被劫持" ; } protected void afterHookedMethod (MethodHookParam param) throws Throwable{ } }); }
在Xposed Installer中激活此插件(模块),并重启系统。最终可以得到:
Hook API 详解 本节介绍了被大量使用的Xposed插件(模块),即GravityBox。通过此插件,对Xposed的hook相关API作进一步介绍。GravityBox可以修改状态栏、锁屏、电源等
,源码在此 下载。
GravityBox的分析路径如下:
(1)打开xposed_init,发现其中为:com.ceco.r.gravitybox.GravityBox
。找到此类,此类实现了IXposedHookZygoteInit与IXposedHookLoadPackage的接口,如下所示:
1 2 3 public class GravityBox implements IXposedHookZygoteInit , IXposedHookLoadPackage { }
在此类中,有函数initZygote,用于在Zygote进程启动时执行(每次开机执行一次),正常使用中,initZygote用于初始化工具类。在GravityBox类中,重写initZygote函数,用于初始化配置文件(initZygote重写后调用XSharedPreferences)、打印关键信息,如下所示(logic-2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public void initZygote (StartupParam startupParam) { MODULE_PATH = startupParam.modulePath; if (XposedBridge.getXposedVersion() < 93 ) { prefs = new XSharedPreferences (prefsFileProt); } else { } if (startupParam.startsSystemServer) { XposedBridge.log("GB:Hardware: " + Build.HARDWARE); } }
在logic-2中,还有一个重要的函数,即handleLoadPackage,此函数是进程启动时被调用的函数,作用为完成java函数的hook工作
,其调用时机早于Application.onCreate函数。此函数中有很多if语句,以区分启动的app,从而在一个xposed模块(插件)中hook多个应用。代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void handleLoadPackage (LoadPackageParam lpparam) { if (lpparam.packageName.equals(SystemPropertyProvider.PACKAGE_NAME)) { SystemPropertyProvider.init(prefs, qhPrefs, tunerPrefs, lpparam.classLoader); } if (lpparam.packageName.equals(ModLowBatteryWarning.PACKAGE_NAME)) { ModLowBatteryWarning.init(prefs, qhPrefs, lpparam.classLoader); } if (lpparam.packageName.equals(ModStatusbarColor.PACKAGE_NAME)) { ModStatusbarColor.init(lpparam.classLoader); } }
以控制系统状态栏颜色
为例,跟踪ModStatusbarColor.init函数,发现其对函数的hook都是使用XposedHelpers类进行处理(使用类中的findAndHookMethod函数)。分析ModStatusbarColor.init函数代码,可以得到以下结论(分析过程略):
在afterHookedMethod或beforeHookedMethod函数中,使用param.thisObject
即可拿到被hook函数的实例对象。
frida中获取实例对象的成员值,为实例对象.成员名称.value
,而在xposed中为XposedHelpers.get<type>Field(实例对象,成员名称)
,也有对应的set<type>Field
方法。
frida中需要使用java.cast来完成类型的转换,而xposed本身就是java的,所以强制转换即可。
再来看GravityBox的ModAudio.java部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 XposedHelpers.findAndHookConstructor("android.media.AudioManager" , classLoader, Context.class, new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { Object objService = XposedHelpers.callMethod(param.thisObject, "getService" ); Context mApplicationContext = (Context) XposedHelpers.getObjectField(param.thisObject, "mApplicationContext" ); if (objService != null && mApplicationContext != null ) { XposedHelpers.callMethod(param.thisObject, "disableSafeMediaVolume" ); } } });
可以看出,xposed还可以钩取类的构造函数,此时findAndHookConstructor无需传递函数名称。
因此,可以说:针对app中的类、函数、变量的处理都是通过XposedHelpers类中提供的函数实现的
。那么XposedHelpers是如何实现这些功能的呢?答案是用java的反射
实现的。以XposedHelpers的getBooleanField为例:
1 2 3 4 5 6 7 8 9 10 11 12 public static boolean getBooleanField (Object obj, final String fieldName) { return findField(obj.getClass(), fieldName).getBoolean(obj); } public static Field findField (final Class<?> clazz, final String fieldName) { final Field field = findFieldRecursiveImpl(clazz, fieldName); field.setAccessible(true ); fieldCache.put(fullFieldName, field); return field; } private static Field findFieldRecursiveImpl (Class<?> clazz, final String fieldName) throws NoSuchFieldException { return clazz.getDeclaredField(fieldName); }
Frida与Xposed对比:Xposed可以在一个函数中完成针对所有进程的hook,在zygote重新启动后生效。而frida是单进程级别的hook。
Xposed Hook加固应用 对加固应用进行hook时,如果直接hook应用中的函数,则会提示ClassNotFoundException,同样,使用frida在spawn模式下对加固应用进行hook时,也会提示相同错误(但是frida使用attach进行hook时为什么不提示咧?)。
答案是:时机不对,即类加载器ClassLoader在加固应用启动时切换,从而导致上述情况。具体而言,app中的类都是对应的ClassLoader加载到ART虚拟机中的,如果ClassLoader不正确,那么就无法找到对应的类。当加固应用启动时,app的当前ClassLoader会发生切换,故而出现上述情况
。
那么,如何让xposed在面对加固应用时也可以使用attach注入进程呢?
我们来分析这个问题,首先,xposed注入进程的时机不可更改,即zygote启动时,此时app的Application类并未加载,也就导致用于加载app的业务相关的类的ClassLoader未出现。但是,我们可以手动切换ClassLoader
。
举个例子,当我们静态分析加固app时发现,壳程序总是通过在应用进程中最先获得执行权限的application类中的attachBaseContext与onCreate函数完成对dex的释放与ClassLoader的切换,那么,我们可以hook 这两个函数来获得真实app的上下文。相关代码逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 XposedHelpers.findAndHookMethod("com.xxx.StubApp" , loadPackageParam.classloader, "attachBaseContext" , Context.class, new XC_MethodHook () { @Override protected void afterHookMethod (MethodHookParam param) throws Throwable { super .afterHookedMethod(param); Context context = (Context) param.args[0 ]; ClassLoader finalClassLoader = context.getClassLoader(); XposedHelpers.findAndHookMethod(clzz, finalClassLoader, "method" , ..., new XC_MethodHook (){ ... }); } });
上述方法有一个弊端,即一旦加固厂商改变相应继承Application类的类名,在此理解为方法名,即不一定为attachBaseContext函数,那么上述hook就失效了。那如何解决?
再来补充知识:app被zygote孵化后,会通过ActivityThread.main
启动相应app,在此函数中创建ActivityThread的实例,并进行初始化操作。ActivityThread类至关重要,它根据ActivityManager发送的请求对activities、broadcast Receviers等操作进行调度执行。ActivityThread中的performLaunchActivity函数用于响应与activity相关的操作,ActivityThread中的mInitialApplication存放着当前的ClassLoader
。
以movetv的MainActivity为例,实现hook的代码如下所示(代码放在函数handleLoadPackage中):
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 import android.app.Application;if (loadPackageParam.packageName.equals("com.cz.babySister" )){ XposedBridge.log("hooked " + loadPackageParam.processName); Class ActivityThread = XposedHelpers.findClass("android.app.ActivityThread" , loadPackageParam.classLoader); XposedBridge.hookAllMethods(ActivityThread, "performLaunchActivity" , new XC_MethodHook (){ @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable{ super .afterHookedMethod(param); Application mInitialApplication = (Application) XposedHelpers.getObjectField(param.thisObject, "mInitialApplication" ); ClassLoader finalLoader = mInitialApplication.getClassLoader(); XposedBridge.log("found classloader is => " + finalLoader.toString()); Class BabyLogin = finalLoader.loadClass("com.cz.babySister.activity.LoginActivity" ); XposedBridge.log("Debug -> " + BabyLogin.toString()); XposedBridge.hookAllMethods(BabyLogin, "onCreate" , new XC_MethodHook (){ @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable{ super .beforeHookedMethod(param); XposedBridge.log("LoginActivity onCreate called" ); } }); } }); }
结果如下所示:
Frida 探 Xposed Hook 本节利用frida,来学习Xposed是如何进行hook的。以Xposed 安装&Xposed插件(模块)
一节中的示例插件为例,使用objection注入目标进程,搜索com.roysue.xposed1.HookTest,命令行如下所示:
1 2 objection -g com.roysue.xposed1 explore android hooking search classes HookTest
结果如下:
但是,如果想要执行以下两条命令的任何一条时,就会报错,显示ClassNotFoundException:
1 2 3 4 5 6 // 列出类中的函数 android hooking list class_methods com.roysue.xposed1.HookTest android hooking list class_methods com.roysue.xposed1.HookTest$1 // 钩取类中的所有函数 android hooking watch class com.roysue.xposed1.HookTest android hooking watch class com.roysue.xposed1.HookTest$1
这里之所以报错,是因为:ClassLoader不对,objection中并未结合frida中切换ClassLoader的功能,因此这部分需要自己写脚本完成。
如下traceXposed.js所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ onMatch : function (loader ){ try { if (loader.findClass ("com.roysue.xposed1.HookTest" )){ console .log ("found!" ) console .log (loader) Java .classFactory .loader = loader } } catch (error){} }, onComplete : function ( ){ console .log ("end" ) } }) }) } setImmediate (hook)
1 frida -U -f com.roysue.xposed1 -l traceXposed.js --no-pause
输出此时含有com.roysue.xposed1.HookTest的ClassLoader,如下所示:
完成切换后,对目标类进行hook发现不再报错,在下面的代码中,针对HookTest类中的PrintStack进行Hook,脚本如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hook ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ ... }) Java .use ("com.roysue.xposed1.HookTest" ).PrintStack .implementation = function ( ){ console .log ("hacked by wd" ) return true } }) } setImmediate (hook)
注入之后,点击应用的button,会调用PrintStack,此时会显示”hacked by wd”。作者发现,切换完ClassLoader之后,虽然能够获取com.roysue.xposed1.HookTest的类,但是对于Xposed提供的api,例如XposedBridge.log等,钩取时也会出现ClassNotFoundException错误,这说明:使用Xposed实现的插件与Xposed自身的api不在一个ClassLoader所能加载的范围内
。
我们先查看XposedBridge对应的ClassLoader叫啥?脚本如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ onMatch : function (loader ){ try { if (loader.findClass ("de.robv.android.xposed.XposedBridge" )){ console .log ("found!" ) console .log (loader) Java .classFactory .loader = loader } } catch (error){} }, onComplete : function ( ){ console .log ("end" ) } }) }) } setImmediate (hook)
结果如下所示:
对比发现,Xposed插件在/data/app/com.roysue.xposed1-1/base.apk
中,而Xposed的api在/system/framework/XposedBridge.jar
中。结合代码,可以得出结论,Xposed插件实现了Xposed框架的IXposedHookLoadPackage接口,而XposedBridge则是调用XposedBridge.jar中的函数。
再次分析上面xposed插件的代码,发现beforeHookedMethod与afterHookedMethod函数都在使用new XC_MethodHook
构建的内部匿名类HookTest$1
中,因此需要将ClassLoader切换为HookTest$1
所在的loader。针对上面xposed插件,对beforeHookedMethod与afterHookedMethod进行hook,脚本如下:
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 function traceMethod ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ onMatch : function (loader ){ try { if (loader.findClass ("com.roysue.xposed1.HookTest$1" )){ console .log ("found!" ) console .log (loader) Java .classFactory .loader = loader } } catch (error){} }, onComplete : function ( ){ console .log ("end" ) } }) var targetClassMethod = "com.roysue.xposed1.HookTest$1.beforeHookedMethod" var delim = targetClassMethod.lastIndexOf ("." ) var targetClass = targetClassMethod.slice (0 , delim) var targetMethod = targetClassMethod.slice (delim + 1 , targetClassMethod.length ) var hook = Java .use (targetClass) var overloadCount = hook[targetMethod].overloads .length console .log ("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]" ); for (var i = 0 ; i < overloadCount; i++) { hook[targetMethod].overloads [i].implementation = function ( ) { console .warn ("\n*** entered " + targetClassMethod) for (var j = 0 ; j < arguments .length ; j++) { console .log ("arg[" + j + "]: " + arguments [j]) } var retval = this [targetMethod].apply (this , arguments ) console .log ("retval: " + retval) return retval } } }) }
之后点击button,显示:
同理,钩取afterHookedMethod,显示:
接下来,以GravityBox为例,来描述如何对beforeHookedMethod与afterHookedMethod进行hook。
(1)GravityBox.apk下载链接 ,下载并安装此插件。
(2)以修改状态栏颜色的类com.ceco.nougat.gravitybox.ModStatusbarColor为例,找其相关的匿名类(不使用之前的objection方法):
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 function lookClass ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ onMatch : function (loader ) { try { if (loader.findClass ("com.ceco.nougat.gravitybox.ModStatusbarColor$1" )){ Java .classFactory .loader = loader ; } } catch (error){} }, onComplete : function ( ) {} }) Java .enumerateLoadedClasses ({ onMatch :function (className ){ try { if (className.toString ().indexOf ("gravitybox" ) > 0 && className.toString ().indexOf ("$" ) > 0 ){ if (Java .use (className).class .getSuperclass ()){ var superClass = Java .use (className).class .getSuperclass ().getName () if (superClass.indexOf ("XC_MethodHook" ) > 0 ){ console .log (className) } } } } catch (error){} }, onComplete :function ( ){ console .log ("search completed!" ) } }) }) }
这种寻找方法一个ModStatusbarColor匿名类也没找到,显然是错误的。这是为什么呢?这是因为,由于Frida是进程级别的,而xposed在将代码对进程进行注入时做了进程的判断,例如xposed的ModStatusbarColor类是要对com.android.systemui进行注入,从而改变颜色。因此,只有判断进程是com.android.systemui时,才会初始化ModStatusbarColor类。但是,Frida脚本注入到了插件xposed1中,名字不为com.android.systemui,因此com.android.systemui类并未被初始化,也就找不到其匿名类。
原理如下图所示(Xposed开机时会钩取所有进程,此时给systemui注入了ModStatusbarColor):
那么,之后我们对systemui进程进行frida注入(hook.js代码无需修改),并进行搜索:
1 frida -U com.android.systemui -l hook.js --no-pause
结果如下所示:
之后,对上述寻找到的目标类中所有的函数进行hook,即实现上述代码中注释掉的traceClass函数,如下所示:
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 function traceMethod (targetClassMethod ) { var delim = targetClassMethod.lastIndexOf ("." ); var targetClass = targetClassMethod.slice (0 , delim) var targetMethod = targetClassMethod.slice (delim + 1 , targetClassMethod.length ) var hook = Java .use (targetClass); var overloadCount = hook[targetMethod].overloads .length for (var i = 0 ; i < overloadCount; i++) { hook[targetMethod].overloads [i].implementation = function ( ) { console .warn ("\n*** entered " + targetClassMethod) for (var j = 0 ; j < arguments .length ; j++) { console .log ("arg[" + j + "]: " + arguments [j]) } var retval = this [targetMethod].apply (this , arguments ) console .log ("\nretval: " + retval) return retval } } } function uniqBy (array, key ) { var seen = {}; return array.filter (function (item ) { var k = key (item); return seen.hasOwnProperty (k) ? false : (seen[k] = true ); }); } function traceClass (targetClass ) { var hook = Java .use (targetClass) var methods = hook.class .getDeclaredMethods () hook.$dispose var parsedMethods = [] methods.forEach (function (method ) { parsedMethods.push (method.toString ().replace (targetClass + "." , "TOKEN" ).match (/\sTOKEN(.*)\(/ )[1 ]) }) var targets = uniqBy (parsedMethods, JSON .stringify ) targets.forEach (function (targetMethod ) { traceMethod (targetClass + "." + targetMethod) }) }
上述代码中,traceClass函数必须先获得要钩取类的类名才行。那么,如果我不知道类名,还要hook所有的beforeHookedMethod与afterHookedMethod,该用啥骚姿势?
已经知道,afterHookedMethod/beforeHookedMethod是使用XC_MethodHook的抽象类实现的,前面插件中的HookTest$1,是匿名内部类,也是XC_MethodHook的子类,它有afterHookedMethod/beforeHookedMethod函数。我们只需要关心XC_MethodHook即可。
相关脚本如下所示:
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 function getAllXCMethodHook ( ){ Java .perform (function ( ){ Java .enumerateClassLoaders ({ onMatch : function (loader ) { try { if (loader.findClass ("com.ceco.nougat.gravitybox.ModStatusbarColor$1" )){ Java .classFactory .loader = loader ; } } catch (error){} }, onComplete : function ( ) {} }) Java .enumerateLoadedClasses ({ onMatch :function (className ){ try { if (className.toString ().indexOf ("gravitybox" ) > 0 && className.toString ().indexOf ("$" ) > 0 ){ if (Java .use (className).class .getSuperclass ()){ var superClass = Java .use (className).class .getSuperclass ().getName () if (superClass.indexOf ("XC_MethodHook" ) > 0 ){ console .log (superClass + " | " + className) } } } } catch (error){} }, onComplete :function ( ){ console .log ("search completed!" ) } }) }) }
注入到com.android.systemui
中,上述脚本与hook.js好类似(一模一样),结果输出如下(输出了所有父类为XC_MethodHook的子类):
结合上述描述,Xposed的原理是:通过新建专门用于实现hook的PathClassLoader,并注入目标进程,从而完成对目标函数的插桩。PathClassLoader加载的类列表中包含对目标进程Hook的逻辑,而原生的Xposed API则处于指向XposedBridge.jar文件的ClassLoader。
Xposed 主动调用与 RPC 实现 Xposed 主动调用函数 以example.apk为例,目的是获取pin值,我们要获得正确的pin。app标识符为org.teamsik.ahe17.qualification.easy
。
(1)使用 objection 分析:
1 2 objection -g org.teamsik.ahe17.qualification.easy explore android hooking list classes
发现org.teamsik.ahe17.qualification.MainActivity
类(仅有这一个),之后钩取MainActivity的所有函数:
1 android hooking watch class org.teamsik.ahe17.qualification.MainActivity
之后点击VERIFY PIN
,定位到验证函数:org.teamsik.ahe17.qualification.MainActivity.verifyPasswordClick
。
(2)jeb找到verifyPasswordClick,源码如下,静态分析可以发现,PIN码长度为4,使用SHA1进行加密,并与指定密文对比:
思路:使用Xposed主动调用,暴力穷举,并与密文对比。
Xposed关于主动调用的API为callMethod(用于调用对象实例所的动态静态函数)/callStaticMethod(调用类的静态函数)
。其函数签名各自为:
1 2 3 callMethod(Object obj, String methodName, Object... args) // "Class<?> clazz" 代表 clazz 是类就行 callStaticMethod(Class<?> clazz, String methodName, Object... args)
使用Xposed要主动调用encodePassword,使用callStaticMethod函数。相关代码如下(Xposed插件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ XposedBridge.log("inner: " + loadPackageParam.processName); Class clazz = XposedHelpers.findClass("org.teamsik.ahe17.qualification.Verifier" , loadPackageParam.classLoader); byte [] p = "09042ec2c2c08c4cbece042681caf1d13984f24a" .getBytes(); String pStr = new String (p); for (int i = 999 ; i < 10000 ; i++){ byte [] v = (byte [])XposedHelpers.callStaticMethod(clazz, "encodePassword" , String.valueOf(i)); String vStr = new String (v); if (vStr.equals(pStr)){ XposedBridge.log("right.pin => " + String.valueOf(i)); } } } }
安装好后,之后打开example.apk,再打开日志,发现9083为正确的pin。其实,java本身就存在主动调用函数的方式,即invoke反射,Xposed的主动调用函数本质上也是调用Invoke函数。
因此,可以更改代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ XposedBridge.log("inner: " + loadPackageParam.processName); Class clazz = loadPackageParam.classLoader.loadClass("org.teamsik.ahe17.qualification.Verifier" ); Method encodePassword = clazz.getDeclaredMethod("encodePassword" , String.class); encodePassword.setAccessible(true ); byte [] p = "09042ec2c2c08c4cbece042681caf1d13984f24a" .getBytes(); String pStr = new String (p); for (int i = 999 ; i < 10000 ; i++){ byte [] v = (byte [])encodePassword.invoke(null , String.valueOf(i)); String vStr = new String (v); if (vStr.equals(pStr)){ XposedBridge.log("right.pin => " + String.valueOf(i)); } } } }
与frida主动调用类似,xposed主动调用面对的问题有:参数构造;针对动态函数如何获取对象实例
。
针对参数构造问题,要么就hook相同类型的数据(略),要么就自己构造参数。由于xposed是原生java而言的,因此,xposed相比frida在参数构造上有优势。例如,若想构造verifyPassword(Context,String)
的Context参数,frida代码如下所示:
1 2 3 var ActivityThread = Java .use ("android.app.ActivityThread" )var Context = Java .use ("android.content.Context" );var ctx = Java .case (ActivityThread .currentApplication ().getApplicationContext (), Context )
而xposed代码如下所示:
1 Context context = AndroidAppHelper.currentApplication();
相应的,xposed完整的主动调用verifyPassword函数的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ XposedBridge.log("inner: " + loadPackageParam.processName); Class clazz = loadPackageParam.classLoader.loadClass("org.teamsik.ahe17.qualification.Verifier" ); Method verifyPassword = clazz.getDeclaredMethod("verifyPassword" , Context.class, String.class); Context context = AndroidAppHelper.currentApplication(); for (int i = 999 ; i < 10000 ; i++){ if ((boolean )verifyPassword.invoke(null , context, String.valueOf(i))){ XposedBridge.log("right.pin => " + String.valueOf(i)); } } } }
上面是xposed构造复杂参数。在主动构造实例对象时,可以使用java Constructor类的newInstance进行构造,也可以使用封装过的XposedHelpers.newInstance()进行构造,相应代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ Constructor cons = XposedHelpers.findConstructorExact("org.teamsik.ahe17.qualification.Verifier" , loadPackageParam.classLoader); Object Verifier = cons.newInstance(); Context context = AndroidAppHelper.currentApplication(); for (int i = 999 ; i < 10000 ; i++){ if ((boolean )XposedHelpers.callMethod(Verifier, "verifyPassword" , context, String.valueOf(i))){ XposedBridge.log("right.pin => " + String.valueOf(i)); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ Class clazz = XposedHelpers.findClass("org.teamsik.ahe17.qualification.Verifier" , loadPackageParam.classLoader); Object Verifier = XposedHelpers.newInstance(clazz); Context context = AndroidAppHelper.currentApplication(); for (int i = 999 ; i < 10000 ; i++){ if ((boolean )XposedHelpers.callMethod(Verifier, "verifyPassword" , context, String.valueOf(i))){ XposedBridge.log("right.pin => " + String.valueOf(i)); } } } }
上文并未介绍xposed hook拿到实例对象的方法,这是因为verifier类中函数全为static的,因此verifier类并未初始化,也就hook不到了。
那咋办?换一个类然后再介绍hook拿实例对象呗。以MainActivity中的showSuccessDialog为例(不是静态函数),首先hook MainActivity的onCreate方法,创建MainActivity对象,在hook的afterHookMethod中,主动调用showSuccessDialog函数。
代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("org.teamsik.ahe17.qualification.easy" )){ Class clazz = loadPackageParam.classLoader.loadClass("org.teamsik.ahe17.qualification.MainActivity" ); XposedBridge.hookAllMethods(clazz, "onCreate" , new XC_MethodHook (){ @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable{ super .afterHookedMethod(param); Object mMainActivity = param.thisObject; XposedHelpers.callMethod(mMainActivity, "showSuccessDialog" ); } }); } }
这样的话,打开example.apk就显示congratulation的成功提示。
Xposed 结合 NanoHTTPD 实现 RPC 调用 Xposed本身未提供RPC调用支持,但是可以结合NanoHTTPD(轻量级HTTP服务器)将主动调用导出为web服务实现。以demoso1工程为例,此工程中存在两个native函数,在应用打开后被循环调用。其中method01是静态函数,对输入进行加密并返回,method02是成员函数,对密文解密并返回。
首先用xposed主动调用method01与method02:
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 public void handleLoadPackage (XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("com.example.demoso1" )){ Class clazz = loadPackageParam.classLoader.loadClass("com.example.demoso1.MainActivity" ); XposedBridge.hookAllMethods(clazz, "onCreate" , new XC_MethodHook (){ @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable{ super .beforeHookedMethod(param); Object mMainActivity = param.thisObject; String cipherText = (String)XposedHelpers.callMethod(mMainActivity, "method01" , "wd2711" ); String clearText = (String)XposedHelpers.callMethod(mMainActivity, "method02" , "47fcda3822cd10a8e2f667fa49da783f" ); XposedBridge.log("(1) cipherText => " + cipherText); XposedBridge.log("(1) clearText => " + clearText); } }); Object newMainActivity = XposedHelpers.newInstance(clazz); String cipherText = (String)XposedHelpers.callMethod(newMainActivity, "method01" , "wd2711" ); String clearText = (String)XposedHelpers.callMethod(newMainActivity, "method02" , "47fcda3822cd10a8e2f667fa49da783f" ); XposedBridge.log("(2) cipherText => " + cipherText); XposedBridge.log("(2) clearText => " + clearText); } }
结果如下所示:
接下来,导入NanoHTTPD以进行RPC:
(1)在app/build.gradle文件的dependencies下增加:
1 implementation 'org.nanohttpd:nanohttpd:2.3.1'
Nanohttpd只存在一个抽象类NanoHTTPD,主要有:start(启动web服务器)、stop(停止web服务器)、serve(收到web请求后的回调函数)。serve只有一个参数,为IHTTPSession类型,可用于判断浏览器请求内容,包括请求方法、参数、URL等。
NanoHTTPD的构造函数可以指定web服务监听的端口。在此,简单的以NanoHTTPD实现hello world界面(应该加在上述代码的logic-4中):
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 class App extends NanoHTTPD { public App () throws IOException{ super (8899 ); start(NanoHTTPD.SOCKET_READ_TIMEOUT, true ); XposedBridge.log("Running!" ); } @Override public NanoHTTPD.Response serve (IHTTPSession session) { Method method = session.getMethod(); String uri = session.getUri(); String RemoteIP = session.getRemoteIpAddress(); String RemoteHostName = session.getRemoteHostName(); Log.i("nanohttpd" , "Method => " + method); Log.i("nanohttpd" , "URI => " + uri); Log.i("nanohttpd" , "Remote_IP => " + RemoteIP); Log.i("nanohttpd" , "RemoteHostName => " + RemoteHostName); String msg = "<html><body>Hello wd2711\n" ; return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, msg); } } new App ();
安装好xposed插件,启动demoso1,并访问手机IP+端口
,结果显示如下:
浏览器访问结果如下:
(2)经过上面的测试,NanoHTTPD服务启动正常,接着更改serve内容,以增加函数主动调用:
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 @Override public NanoHTTPD.Response serve (IHTTPSession session) { String uri = session.getUri(); String paramBody = "" ; Map<String, String>params = new HashMap <>(); try { session.parseBody(params); paramBody = session.getQueryParameterString(); } catch (IOException e){ e.printStackTrace(); } catch (ResponseException e){ e.printStackTrace(); } String result = "" ; if (uri.contains("encrypt" )){ result = (String)XposedHelpers.callMethod(getActivity(), "method01" , paramBody); } else if (uri.contains("decrypt" )){ result = (String)XposedHelpers.callMethod(getActivity(), "method02" , paramBody); } return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, result); }
安装好xposed插件,启动demoso1,使用 curl进行post,结果如下:
(3)最后,使用python进行进一步封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsdef encrypt (param ): url = "http://192.168.48.14:8899/encrypt" headers = {"Content-Type" :"application/x-www-form-urlencoded" } r = requests.post(url = url, data = param, headers = headers) print (r.content) def decrypt (param ): url = "http://192.168.48.14:8899/decrypt" headers = {"Content-Type" :"application/x-www-form-urlencoded" } r = requests.post(url = url, data = param, headers = headers) print (r.content) if __name__ == "__main__" : encrypt("wd" ) decrypt("169dc260893dab88cd619d5e35e17634" )
总结 Hook上,frida与xposed不分伯仲。但是在细节上,xposed支持使用setAdditionalInstanceField、setAdditionalStaticField等函数给实例对象添加动静态成员,而frida支持Java.choose从进程堆中搜索目标对象。在宏观上,frida可以热重载(不用重启),作用对象是特定进程,Hook原理类似于调试器,而xposed针对所有进程,类似于系统框架级服务。