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针对所有进程,类似于系统框架级服务。