安卓逆向第二篇:手搓Xposed RPC绕过算法/框架限制实现抓包和解密
安全工具
大家好,我是拖更博主r0leG3n7。本文将详细介绍如何开发一款Xposed RPC模块在无法得到私钥的情况下解密使用公钥加密算法的APP,这套RPC方案还能应用于Mpass框架和TMF框架的抓包和解密,能解决市面上百分之95的APP无法抓包或者解密的问题。
前言 == 大家好,我是拖更博主r0leG3n7。本文将详细介绍如何开发一款Xposed RPC模块在无法得到私钥的情况下解密使用公钥加密算法的APP,这套RPC方案还能应用于Mpass框架和TMF框架的抓包和解密,能解决市面上百分之95的APP无法抓包或者解密的问题。如有任何错误和不足欢迎各位师傅指正,转载请注明文章出处。 文章背景 ==== 最近测试遇到了个某梆加固的APP,APP使用的是非对称加密算法加密但是找不到私钥,而且我能用的frida都被检测了,但是Xposed能用。之前我能用frida-rpc解决这些使用公钥加密算法、Mpass框架和TMF框架APP的解密和抓包问题,但今天frida的这条腿被打断了,所以我不得不学习怎么开发Xposed RPC模块实现抓包和解密。frida自己有frida-rpc,Xposed自己本身没有RPC,但是市面上还是有一些RPC框架支持联动Xposed,比如Sekiro,但是Sekiro年久失修,在我魔改的lsposed里有些小问题,所以干脆自己手搓一个Xposed RPC模块。 基础知识 ==== 本节我将简单介绍Xposed的原理以及一些常见Xposed API的用法。 Xposed原理 -------- 在 Android 中,所有 App 进程都不是凭空创建的。当你点击图标启动 App 时,系统会通知 Zygote 进程,然后Zygote 会 fork(分裂) 出一个子进程,这个子进程就是你的 App。Xposed在 Zygote 启动时通过修改环境变量或启动参数等方式让 Zygote进程加载XposedBridge.jar,这样Xposed就具备了监听新APP进程创建的能力。当APP创建时,Xposed框架会接管这个过程,调用handleLoadPackage(XC\_LoadPackage.LoadPackageParam loadPackageParam),将模块注入到APP。handleLoadPackage()是 Xposed 模块开发的绝对核心入口,每当 Android 系统启动一个新的应用程序时,Xposed 框架都会主动回调这个方法,并把该应用的“身份信息”打包交给你,你可以在这里决定是否Hook这个APP,是否Hook这个APP的某个方法。 Xposed寻找类 --------- ### XposedHelpers.findClass() XposedHelpers.findClass()用于在运行时动态加载类,第一个参数为需要加载的类名,第二个参数为类加载器,这个类加载器可以从handleLoadPackage传入参数的成员中获取。 ```php public static Class<?> findClass(String className, ClassLoader classLoader) ``` Xposed获取类的静态成员变量 ---------------- ### XposedHelpers.getStaticObjectField() XposedHelpers.getStaticObjectField()用于获取类的静态成员变量 示例源代码: ```php package com.test.test.data.local; public final class LocalData { public static final LocalData INSTANCE = new LocalData(); public static final String SP_CONFIG = "config"; private LocalData() { } public final String getEncryptedAuth() { String string = SPUtils.getInstance(SP_CONFIG).getString("encryptedAuth"); Intrinsics.checkNotNullExpressionValue(string, "getInstance(SP_CONFIG).getString(\"encryptedAuth\")"); return string; } } ``` Xposed获取静态类成员示例: ```php Class<?> localDataCls = XposedHelpers.findClass( "com.test.test.data.local.LocalData", cl); Object localDataInstance = XposedHelpers.getStaticObjectField(localDataCls, "INSTANCE"); String encryptedAuth = (String) XposedHelpers.callMethod(localDataInstance, "getEncryptedAuth"); ``` Xposed调用方法 ---------- ### XposedHelpers.callMethod() XposedHelpers.callMethod()主动调用目标 App 内部的某个方法(调用静态方法要用另外一个函数),它可以绕过 Java 的访问权限检查(private/protected),在运行时动态调用任意对象的任意方法。第一个参数为调用方法的实例对象,第二个参数为要调用的方法名;如果存在多个 重载函数,第三个参数开始为函数的参数类型;其余参数为传给方法的实参。如果遇到多个重载函数的情况,建议使用下面介绍的反射去调用方法。 ```php public static Object callMethod(Object obj, String methodName, Object... args) ``` ### 反射 XposedHelpers.callMethod()常用于没有重载函数,当遇到那种类里面有多个重载函数,就需要用反射去指定参数类型精准调用某个方法。 示例: ```php protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Object oldChain = param.args[0]; Class<?> chainClass = oldChain.getClass(); Method requestMethod = chainClass.getMethod("request"); //这里可以指定参数类型 Object request = requestMethod.invoke(oldChain); } ``` 上面示例中反射类的getMethod是没有指定参数类型的,当需要指定参数类型时的示例: ```php Method setValueMethod = chainClass.getMethod("setValue", int.class, String.class) ``` ### XposedHelpers.findAndHookMethod() XposedHelpers.findAndHookMethod()将寻找类,调用方法并Hook方法三个步骤合在一起。第一个参数为XposedHelpers.findClass()找到的类名,第二个参数是handleLoadPackage传入参数的ClassLoader,第三个参数为要Hook的方法名,第四个参数开始为Hook的方法的传入参数类型,最后一个参数为Hook方法执行的回调函数。 ```php XposedHelpers.findAndHookMethod( "com.example.MainActivity", // 类名 lpparam.classLoader, // ClassLoader "onCreate", // 方法名 Bundle.class, // 方法参数类型(Bundle) new XC_MethodHook() { // 回调钩子 @Override protected void beforeHookedMethod(MethodHookParam param) { // 方法执行前 XposedBridge.log("onCreate 即将执行"); } @Override protected void afterHookedMethod(MethodHookParam param) { // 方法执行后 XposedBridge.log("onCreate 执行完毕"); } } ); ``` Xposed调用静态方法 ------------ ### XposedHelpers.callStaticMethod() 传入参数类型和上面的callMethod()差不多。不同的是callStaticMethod()调用的是类里面用static修饰的静态方法,静态方法和普通方法的区别就是静态方法不需要创建类的实例就可以直接调用。 ```php public static Object callStaticMethod(Class<?> clazz, String methodName, Object... args) ``` Xposed 回调与Hook时机 ---------------- Xposed提供的回调主要分为两种,分别是XC\_MethodHook和XC\_MethodReplacement。两者的区别是XC\_MethodHook不会阻止被Hook方法原本的流程,只在方法执行之前和方法执行完成之后做修改;而XC\_MethodReplacement是完全阻止被Hook方法原本的流程,用其他方法代替原本被Hook的方法,这个回调过程需要改写东西比较多,而且在对APP代码逻辑不清晰的情况下修改原来被Hook方法很容易出错。所以我们通常选择XC\_MethodHook这个回调函数,XC\_MethodHook可重写的函数有两个,分别是beforeHookedMethod()和afterHookedMethod()。 ### beforeHookedMethod() 在原方法执行之前触发,适合: 1、读取原始入参 2、修改入参 3、提前阻断方法执行 ### afterHookedMethod() 在原方法执行之后触发,适合: 1、读取原方法返回值 2、修改返回值 3、在方法执行完成后补日志、补上报 顶级类、内部类 ------- ### 顶级类 先看一段示例代码: ```php package com.apm.insight; import java.io.File; import java.util.List; public class CrashInfoCallback { public File[] crashFileList(CrashType crashType) { return null; } public void onFileUpload(List<File> list) { } } ``` 上面示例代码CrashInfoCallback就属于顶级类,规则是:包名.顶级类名。 寻找内部类(成员类/静态内部类/匿名类的编译名)用 $,$ 是 JVM 字节码里的内部类分隔符,规则是:包名.顶级类名$内部类名。 上面示例代码crashFileList属于普通内部成员类,在hook时候用到该类名需要$进行分隔,比如Xposed寻找类时: ```php XposedHelpers.findClass("com.apm.insight.CrashInfoCallback$crashFileList", loadPackageParam.classLoader); ``` ### 匿名类 再看一段示例代码: ```php package com.test.forPentest; public class Outer { void test() { Runnable r = new Runnable() { ... }; // 编译后常见 Outer$1 } } ``` Runnable()就是顶级类里面的Outer的匿名类,Xposed寻找类匿名内部类常见成 com.test.forPentest.Outer$1 匿名类的应用场景: 1、一次性回调(监听器、Hook 回调) 2、逻辑短,不需要复用 3、希望代码就写在调用处附近 adb端口转发类型 --------- adb forward:将主机端口转发到设备端口,用于从主机访问设备上的服务。 adb reverse:将设备端口转发到主机端口,用于从设备访问主机上的服务。 Xposed开发环境 ========== 调试设备 ---- 调试设备可以选择模拟器或者已经root的设备,我这里为了方便选择了9.0.64版本的雷电模拟器,这个旧版本可以直接安装magisk获取root。root以后安装lsposed框架(跟Xposed差不多,因为xposed-installer不能用了),LSPosed的2.0.2版本已经在TG频道发布了,但Github还没更新。  开发平台 ---- 我刚刚开始入门的时候网上那些Xposed开发教程都推荐使用Android Studio,但是Android Studio用起来真的是一堆坑逼bug,特别是对于我们这种需要用代理去下载gardle和SDK的国内用户,新版的Android Studio用国内的gardle镜像是一堆问题,最让你受不了的是新版的Android Studio虽然他仍然兼容JAVA语言开发,但创建项目的时候开发语言还不给你选JAVA,逼你选Kotlin。而且Android Studio重新加载项目的时候不知道为什么就是会一直卡住(可能是尝试下载gardle,但是一直连不上谷歌),卡到连设置都进入不了。Android Studio基于IntelliJ IDEA开发,但是Android Studio给IntelliJ IDEA舔鞋底都不配,IntelliJ IDEA安装一个安卓的插件就可以做到Android Studio能做的百分之95的事。所以我强烈建议大家不要选Android Studio,esplice都比它好用,下面是我用IntelliJ IDEA搭建Xposed插件开发环境的过程以及踩到一些坑,大家可以参考一下。如果已经搭建好环境,可以跳过本节从下一节开始看。 ### 环境搭建 1、先打开或者创建一个java项目,打开设置,在插件商店里下载Android这个插件  2、点击Tools->Android->Android SDK Manager下载对应的安卓SDK版本以及SDK工具    3、重新打开IntelliJ IDEA,你就会看到有Android这个选项,然后选择"Empty Views Activity"  4、语言选择JAVA,配置语言选Groovy DSL。  5、项目目录/settings.gradle配置国内加速镜像 ```php pluginManagement { repositories { maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } // gradlePluginPortal() // google() // mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { // google() // mavenCentral() maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/central") } maven { url = uri("https://maven.aliyun.com/repository/public") } } } ```  6、项目目录/build.gradle配置 ```php buildscript { repositories { maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } maven { url = uri("https://maven.aliyun.com/repository/central") } } } ```  7、配置项目目录/gradle/wrapper/gradle-wrapper.properties,配置gardle下载路径,我这里用的是腾讯云的镜像  如果在IDEA下载失败,可以直接去<https://mirrors.cloud.tencent.com/gradle/>下载对应的包  8、然后将包放到用户目录C:/Users/<用户名>/.gradle/,在gradle/wrapper/gradle-wrapper.properties配置包路径和包的哈希值 ```php distributionSha256Sum=b21468753cb43c167738ee04f10c706c46459cf8f8ae6ea132dc9ce589a261f2 distributionUrl=file\:///C:/Users/Administrator/.gradle/gradle-9.4.0-all.zip ```  9、在自定义包名下创建一个Xposed的主类,比如我这里创建了一个名为HookDemo类  10、创建完主类以后在 main/assets/ 目录下创建文件xposed\_init,内容为Xposed主类的完整路径,这个文件指定Xposed框架插件的入口。  11、修改AndroidManifest.xml,这个步骤是告诉Xposed框架这个Apk是个Xposed模块,用adb安装这个apk的以后在Xposed模块列表里就能看到这个Xposed模块名以及它的描述,并且说明XposedBridgeAPI的版本是89 ```php <meta-data android:name="xposedmodule" android:value="true" /> <meta-data android:name="xposeddescription" android:value="test module" /> <meta-data android:name="xposedminversion" android:value="89" /> ```  12、在app目录下创建个lib目录,里面放入XposedBridgeAPI-89.jar  13、并在IDEA里右键XposedBridgeAPI-89.jar,点击"Add As Library.."  14、在build.gradle的dependencies内加入如下代码。compileOnly 和implementation的区别是compileOnly这种依赖类型仅参与编译过程,但不参与打包;implementation依赖的库不仅参与编译,还参与打包,这意味着implementation依赖的库不仅是编译时需要用到,在Xposed模块运行时也需要用到这个依赖库。 ```php compileOnly(files("lib\\XposedBridgeAPI-89.jar")) //编译xposed API ```  ### 踩坑 1、如果build项目或更新gradle时执行到Task :prepareKotlinBuildScriptModel UP-TO-DATE 时请求<https://dl.google.com/>报错。 解决方案: 清除C:\\Users\\<用户名>\\.gradle\\gradle.properties的下的所有代理  2、如果build项目报错intelliJ Module: ':app' platform 'android-36' not found. 解决方案: 项目设置界面,下载JDK 11以及安卓API 36。  JAVA的SDK版本选JAVA 11  目标APP逆向分析 ========= 当我提出手搓Xposed RPC的时候 ,总是有无数的人在底下问:博主,博主博主,你这个手搓的Xposed RPC究竟有什么"荣誉"嘛?为什么你还要大费周章写个RPC服务端啊?正常解密不是通过Xposed客户端就可以解决了吗?你是不是为了流量在糊弄我们呀? 正常来说当我们在可以抓包并且逆向分析知道目标APP是对称加密算法的时候,仅依靠Xposed客户端和一些burp插件确实可以解决,但是这都是很理想的测试环境了。如果遇到能抓包,但是目标APP是非对称加密算法且无法得到私钥的情况呢?如果遇到APP是某里的Mpass,某讯的TMF等框架开发的,这些APP在自己的客户端和服务端证书校验过程就可以把burp在目标APP客户端与服务端通信过程的抓包一棒子打死。下面就给大家看下我遇到的测试困境,大家如果有比Xposed RPC更好的解决方案欢迎在评论区交流。 1、下面给大家演示的例子是APP能抓包,但是非对称加密算法,拿不到私钥。因为我看到数据包请求某个参数是密文,但是响应都是明文。如果请求和响应都是密文,那大概率是对称加密算法,即使是非对称加密算法,因为响应是加密,它返回到客户端必定会进行解密,所以客户端JAVA层、native层或者包内必定有非对称算法的私钥。但我这里的响应没有加密,猜测大概率是非对称加密算法。  2、APP脱壳以后反编译源代码,搜索加密关键参数"cipherData"定位到加密的主类,通过公钥以及加密方法的类名基本可以确定APP使用的是SM2公钥加密算法。我尝试过搜索私钥代码关键字,hook它SM2EncDecUtils.decrypt()方法打印传入参数以及Hook一些常见的私钥类的方法都没能找到密钥,结合之前看到的响应没有加密,基本可以确定APP不会在客户端存储私钥进行解密操作,私钥大概率是存储在APP的服务端。  3、"cipherData"参数的值是十六进制编码数据,与SM2的encrypt方法返回值一致,也可以验证我们的猜想。  Xposed客户端开发 =========== 遇到这种无私钥的公钥加密算法我们就真的拿它没办法了吗?真的要给客户出没有漏洞的安全报告了吗?我真的输了吗?  不,我们还有机会!Xposed赋予了我们高贵的调试权限,这意味着我们有监控每个函数输入和输出的能力。公钥加密算法本质还是函数,它传入参数是公钥以及它的明文字节数组,这两个参数在进入公钥加密算法之前还是明文,这时候我们hook这个公钥加密算法,把参数的明文打印出来,这不就实现了解密吗?但是问题又来了,这种解密了又有什么用呢?只能看数据又不能联动Burp之类的抓包工具改数据,这个时候Xposed RPC就有意义了。我们可以创建一个RPC服务端,让Xposed客户端把hook公钥加密算法传入参数的明文转发给RPC服务端,这个过程通过adb以及burp代理,我们就可以在burp代理的过程中修改加密前的明文数据,修改后明文数据经过RPC服务端处理以后返回给Xposed客户端,Xposed客户端将RPC服务端返回的数据以参数形式传入原先的加密函数,这就完成了一次抓包数据修改。**一次向APP服务端发起的http请求会向RPC服务端发起两次数据处理请求,这两次请求分别处理原先向APP服务端发起的请求和响应**。或许我直接这样说大家会有点懵逼,依旧是大伙最喜欢的excel画图环节:  Hook方法选择 -------- 1、说完我的实现思路,接下来实践检验真理。Xposed的最基本的代码框架如下(详解看代码注释,beforeHookedMethod和afterHookedMethod两个方法的重写是核心,稍后详细介绍): ```php package <你的包名>; import de.robv.android.xposed.IXposedHookLoadPackage; // 导入 Xposed 加载包接口 import de.robv.android.xposed.XC_MethodHook; // 导入 Xposed 方法钩子基类 import de.robv.android.xposed.XposedBridge; // 导入 Xposed 桥接工具类,用于输出日志等 import de.robv.android.xposed.XposedHelpers; // 导入 Xposed 助手类,提供反射与 Hook 方法 import de.robv.android.xposed.callbacks.XC_LoadPackage; // 导入 Xposed 包加载回调类 import org.json.JSONObject; public class HookDemo implements IXposedHookLoadPackage{ public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (!loadPackageParam.packageName.equals("com.test.test")) return; //定位目标APP Class<?> interceptorCls = XposedHelpers.findClass( "com.test.test.net.interceptors.SM2Interceptor", loadPackageParam.classLoader); //寻找目标APP加密类 Class<?> chainCls = XposedHelpers.findClass("okhttp3.Interceptor$Chain", loadPackageParam.classLoader);//寻找目标APP加密类的传入参数的类 XposedHelpers.findAndHookMethod(interceptorCls, //目标APP加密类 "intercept", //目标 chainCls, //目标APP加密类的传入参数的类 new XC_MethodHook() { //回调函数 //处理传入参数 protected void beforeHookedMethod(MethodHookParam param) throws Throwable { } //处理函数返回值 protected void afterHookedMethod(MethodHookParam param) throws Throwable { } }); } } ``` 2、写Xposed模块要配合源码去看,我这里Hook了与加密相关的顶级类SM2Interceptor的intercept方法。  3、intercept方法实现的是okhttp3类的Interceptor接口。  4、intercept方法传入参数为Interceptor.Chain,Chain类是Interceptor接口类的内部类,所以用$分隔。 ```php Class<?> chainCls = XposedHelpers.findClass("okhttp3.Interceptor$Chain", loadPackageParam.classLoader); ``` 5、intercept方法的返回值为chain.proceed(),跟进chain.proceed(),在Chain类看到可知其为Response类,Response类是okhttp3处理响应的类。  6、这时候有聪明的小伙伴可能会问:SM2EncDecUtils.encrypt()同样也是加密相关的方法,为什么你要选择SM2Interceptor的intercept()方法而不是SM2EncDecUtils的encrypt()方法?这里我们要结合代码分析,SM2EncDecUtils的encrypt()方法是SM2Interceptor的intercept()方法里的其中一步,我们能看到SM2Interceptor的intercept()方法内不只有加密过程,还有SM3哈希算法生成签名(其实这里严谨一点并不是签名,而是数据摘要),如果Hook你们要的SM2EncDecUtils的encrypt()方法,前面的签名怎么办?再Hook一次签名算法吗?而且Hook SM2Interceptor的intercept()方法比Hook SM2EncDecUtils的encrypt()方法更方便的点在于它的返回值就是Response类,也就是说Hook SM2Interceptor的intercept()方法,通过beforeHookedMethod()修改其传入参数就相当于修改了http请求,通过afterHookedMethod()修改其返回值就相当于修改了http响应。**通过逆向分析选择要Hook的方法是Xposed模块开发最最最重要的一步,选对了能极大减少Xposed 模块开发的代码量,就好像你结婚选择一个好的对象能减少很多不必要的麻烦,所以感情深交选择对的人,Xposed模块开发选择对的Hook方法,m3?**  修改传入参数 ------ 1、接下来介绍beforeHookedMethod(),处理intercept方法的传入参数,详细代码如下(详解看代码注释): ```php protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Object oldChain = param.args[0]; Class<?> chainClass = oldChain.getClass(); Object call = XposedHelpers.callMethod(oldChain, "call"); try { //反射调用Chain的request方法获取request对象 Method requestMethod = chainClass.getMethod("request"); Object request = requestMethod.invoke(oldChain); if (request != null) { Class<?> requestClass = request.getClass(); // 获取请求URL Method urlMethod = requestClass.getMethod("url"); Object url = urlMethod.invoke(request); String urlStr = String.valueOf(url); XposedBridge.log("请求URL: " + urlStr); //获取请求body String bodyPlain = readRequestBodyUtf8(request,loadPackageParam.classLoader ); XposedBridge.log("body: " + bodyPlain); //获取请求方法 String method = String.valueOf(XposedHelpers.callMethod(request, "method")); //构造json格式格式的RPC请求数据 JSONObject rpcReq = new JSONObject(); String traceId = UUID.randomUUID().toString(); rpcReq.put("traceId", traceId); rpcReq.put("type", "hook"); rpcReq.put("phase", "REQUEST"); rpcReq.put("action", "relay"); rpcReq.put("url", url); rpcReq.put("method", method); rpcReq.put("body_base64", ""); rpcReq.put("body", bodyPlain); rpcReq.put("signParam", "signature"); //将RPC请求数据发送给RPC服务端 String rpcRespText = postJsonToRpc(rpcReq.toString(), true); //处理RPC服务端返回的数据 JSONObject rpcResp = new JSONObject(rpcRespText); String newBody = rpcResp.optString("modifiedData", ""); XposedBridge.log("RPC接收到的body: " + newBody); //重构request类 Object newRequest = rebuildRequest(request,newBody, loadPackageParam.classLoader); //重构Chain类 Object newChains = rebuildChain(oldChain,newRequest,loadPackageParam.classLoader); if (traceId != null && traceId.length() > 0) { CALL_TRACE_MAP.put(call, traceId); } //修改传入参数 if (newChains != null) { param.args[0] = newChains; } } } catch (Exception e) { XposedBridge.log("反射调用request()失败: " + e.getMessage()); } } ``` 2、intercept方法的传入参数Interceptor.Chain,它是 OkHttp 传给 intercept() 的“调用上下文”。我们可以学着源码里调用request方法用反射获取request对象。  ```php protected void beforeHookedMethod(MethodHookParam param) throws Throwable { Object oldChain = param.args[0]; Class<?> chainClass = oldChain.getClass(); Method requestMethod = chainClass.getMethod("request"); Object request = requestMethod.invoke(oldChain); } ``` 3、根据request类的源码,我们可以用与上一步相似地步骤使用XposedHelpers.callMethod或反射调用url()、method()和body()方法获取http请求的url、请求方法和body。  4、构造json格式的数据发送给RPC服务器。 ```php private String postJsonToRpc(String jsonBody, boolean waitResponse) throws Exception { URL url = new URL(RPC_URL); HttpURLConnection conn; if (ENABLE_PROXY) { Proxy proxy = new Proxy( Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT) ); conn = (HttpURLConnection) url.openConnection(proxy); } else { conn = (HttpURLConnection) url.openConnection(); } try { conn.setRequestMethod("POST"); conn.setConnectTimeout(10000); conn.setReadTimeout(waitResponse ? 15000 : 300); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); byte[] data = jsonBody.getBytes("UTF-8"); OutputStream os = conn.getOutputStream(); os.write(data); os.flush(); os.close(); if (!waitResponse) { try { conn.getResponseCode(); } catch (Throwable ignored) { } return ""; } int code = conn.getResponseCode(); InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); if (is == null) { throw new java.io.IOException("rpc response stream is null, code=" + code); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int len; while ((len = is.read(buf)) != -1) { bos.write(buf, 0, len); } is.close(); return bos.toString("UTF-8"); } finally { conn.disconnect(); } } ``` 这时候问题来了: 1)本节开始我就提到"**一次向APP服务端发起的http请求会向RPC服务端发起两次数据处理请求,这两次请求分别处理原先向APP服务端的请求和响应**",那么我在burp怎么区分这个是响应还是请求呢?所以我向RPC服务端发送了phase参数,这个参数用于给RPC服务端区分是请求还是响应。 2)Xposed客户端向RPC服务端发起的请求是异步的,在APP点击一个功能可能会有成百上千个请求和响应,http响应里是没有URL的,我怎么知道哪个请求是对应哪个响应呢?所以我向RPC服务端发送了traceId参数,这个参数用可以使发起的请求与其响应一一对应。如果不懂提出这两个问题有什么意义,可以直接拉到下面去看效果演示你就懂了。 5、从RPC服务端接收到的数据是json格式的数据,但是我们intercept方法的传入参数Interceptor.Chain类,要修改传入参数要将json格式的数据转化为request类再转化为Interceptor.Chain类。 ```php private Object rebuildRequest(Object oldRequest, String newPlainBody, ClassLoader cl) { try { Object builder = XposedHelpers.callMethod(oldRequest, "newBuilder"); Object oldBody = XposedHelpers.callMethod(oldRequest, "body"); String method = String.valueOf(XposedHelpers.callMethod(oldRequest, "method")); Object mediaType = null; if (oldBody != null) { mediaType = XposedHelpers.callMethod(oldBody, "contentType"); } if (mediaType == null) { Class<?> mediaTypeCls = XposedHelpers.findClass("okhttp3.MediaType", cl); mediaType = XposedHelpers.callStaticMethod( mediaTypeCls, "parse", "application/json; charset=UTF-8" ); } Class<?> requestBodyCls = XposedHelpers.findClass("okhttp3.RequestBody", cl); byte[] bodyBytes = newPlainBody != null ? newPlainBody.getBytes("UTF-8") : new byte[0]; Object newBody = XposedHelpers.callStaticMethod( requestBodyCls, "create", mediaType, bodyBytes ); XposedHelpers.callMethod(builder, "method", method, newBody); return XposedHelpers.callMethod(builder, "build"); } catch (Throwable t) { XposedBridge.log("[SM2Hook] rebuildRequest error: " + t); return oldRequest; } } private Object rebuildChain(Object oldChain, Object newRequest, ClassLoader cl) { try { Class<?> realChainCls = XposedHelpers.findClass( "okhttp3.internal.http.RealInterceptorChain", cl); Object interceptors = XposedHelpers.getObjectField(oldChain, "interceptors"); Object transmitter = XposedHelpers.getObjectField(oldChain, "transmitter"); Object exchange = XposedHelpers.getObjectField(oldChain, "exchange"); int index = XposedHelpers.getIntField(oldChain, "index"); Object call = XposedHelpers.getObjectField(oldChain, "call"); int connectTimeout = XposedHelpers.getIntField(oldChain, "connectTimeout"); int readTimeout = XposedHelpers.getIntField(oldChain, "readTimeout"); int writeTimeout = XposedHelpers.getIntField(oldChain, "writeTimeout"); return XposedHelpers.newInstance( realChainCls, interceptors, transmitter, exchange, index, newRequest, call, connectTimeout, readTimeout, writeTimeout ); } catch (Throwable t) { XposedBridge.log("[SM2Hook] rebuildChain error: " + t); return oldChain; } } ``` 修改返回值 ----- 1、接下来介绍afterHookedMethod(),处理intercept方法的返回值。跟上面处理传入参数差不多,下面介绍几个关键处理函数,不再过多赘述。 ```php protected void afterHookedMethod(MethodHookParam param) throws Throwable { try { //获取response对象 Object response = param.getResult(); if (response == null) { return; } Object chain = param.args[0]; Object call = XposedHelpers.callMethod(chain, "call"); String traceId = CALL_TRACE_MAP.get(call); if (traceId == null) { traceId = "unknown-" + UUID.randomUUID().toString(); } //读取response的body byte[] responseBodyBytes = readResponseBodyPreview(response); String responseBodyText = new String(responseBodyBytes, "UTF-8"); XposedBridge.log("[RPC][RESPONSE] traceId=" + traceId + " body=" + responseBodyText); final String finalTraceId = traceId; new Thread(new Runnable() { @Override public void run() { try { //发送响应body和TraceId给RPC服务端 RpcResponseResult rpcResult = callRpcResponse( finalTraceId, "RESPONSE", responseBodyText, "", true ); //接收RPC服务端返回的数据 byte[] rebuiltBytes = chooseResponseBytes(rpcResult, responseBodyBytes); //重建response类 Object rebuiltResponse = rebuildResponse(response, rebuiltBytes, loadPackageParam.classLoader); //修改返回值 if (rebuiltResponse != null) { param.setResult(rebuiltResponse); } } catch (Throwable t) { XposedBridge.log("[RPC][RESPONSE] async error: " + t); } } }, "rpc-response-forward").start(); CALL_TRACE_MAP.remove(call); } catch (Throwable t) { XposedBridge.log("[RPC][RESPONSE] hook error: " + t); } } ``` 2、调用response类的 peekBody方法获取响应body的数据  ```php private byte[] readResponseBodyPreview(Object response){ try { Object peekedBody = XposedHelpers.callMethod(response, "peekBody", MAX_PEEK_BODY_SIZE); if (peekedBody == null) { return new byte[0]; } return (byte[]) XposedHelpers.callMethod(peekedBody, "bytes"); } catch (Throwable t) { XposedBridge.log("[RPC] readResponseBodyPreview error: " + t); return new byte[0]; } } ``` 3、调用 ResponseBody类的create方法重构响应Body的数据,注意这里create方法存在多个重载,而且是静态方法,所以我这里调用方法的使用的XposedHelpers.callStaticMethod(),并在最后指定了参数类型MediaType和字节数组。  ```php private Object rebuildResponse(Object oldResponse, byte[] newBytes, ClassLoader cl) { try { Object oldBody = XposedHelpers.callMethod(oldResponse, "body"); Object contentType = null; if (oldBody != null) { contentType = XposedHelpers.callMethod(oldBody, "contentType"); } Class<?> responseBodyCls = XposedHelpers.findClass("okhttp3.ResponseBody", cl); Object newBody = XposedHelpers.callStaticMethod( responseBodyCls, "create", contentType, newBytes != null ? newBytes : new byte[0] ); Object builder = XposedHelpers.callMethod(oldResponse, "newBuilder"); XposedHelpers.callMethod(builder, "body", newBody); return XposedHelpers.callMethod(builder, "build"); } catch (Throwable t) { XposedBridge.log("[RPC][RESPONSE] rebuildResponse error: " + t); return null; } } ``` RPC服务端开发 ======== 这又不得不再提一下我选择对的Hook方法的优点了,因为我是hook原始请求的request,而不是hook加密函数,所以我不需要再次hook签名算法增加Xposed客户端的代码量,所以RPC服务端也可以不用处理Xposed客户端发过来的签名,只做简单的数据中转。下面是我用python的flask开发的轻量化服务端,在这个例子里它的功能是接收Xposed客户端发送过来的数据以后打印数据,然后原封不动地返回给Xposed客户端,它的作用就是为了给Xposed客户端的数据提供一个burp代理的过程,方便我们联动burp等抓包工具查看并修改数据。 ```php import base64 import uuid from flask import Flask, jsonify, request app = Flask(__name__) def normalize_body(data: dict) -> str: body = data.get("body", "") if body: return body body_base64 = data.get("body_Base64", "") if body_base64: try: return base64.b64decode(body_base64).decode("utf-8", errors="replace") except Exception: return "" return "" @app.post("/rpc") def rpc(): req = request.get_json(force=True) or {} trace_id = req.get("traceId") or str(uuid.uuid4()) req_type = req.get("type", "") phase = req.get("phase", "") action = req.get("action", "") url = req.get("url", "") method = req.get("method", "") body_base64 = req.get("body_Base64", "") sign_param = req.get("signParam", "") plain_body = normalize_body(req) print("=" * 60) print(f"type={req_type}") print(f"phase={phase}") print(f"action={action}") print(f"url={url}") print(f"method={method}") print(f"body={plain_body}") print(f"body_Base64={body_base64}") print(f"signParam={sign_param}") print("=" * 60) modified_data = plain_body if not body_base64 and modified_data: body_base64 = base64.b64encode(modified_data.encode("utf-8")).decode("ascii") sign = sign_param return jsonify({ "traceId": trace_id, "phase": phase, "modifiedData": modified_data, "body_Base64": body_base64, "sign": sign, "message": "ok" }) if __name__ == "__main__": app.run(host="127.0.0.1", port=18080, debug=False) ``` 效果演示 ==== 1、burp设置代理  2、Xposed客户端走burp 8080端口代理,因为RPC服务端是在windows上面启动的,所以要用adb进行反向代理,因为是Xposed客户端走windows的代理,所以用adb reverse。 ```php adb reverse tcp:8080 tcp:8080 ``` 3、启动RPC服务  4、IDEA编译Xposed模块安装到安卓设备,lsposed启动模块并勾选目标APP。  5、启动APP,burp抓取APP客户端发起的请求,修改数据  6、RPC服务端收到Xposed客户发送的经过burp代理过程修改的数据,然后原封不动地返回给Xposed客户端。  7、burp抓取APP服务端返回的响应  8、在burp搜traceId的值就可以找到请求与之对应的响应  总结 == APP等C/S架构的应用对数据完整性和保密性的防护是在不断升级的,从开始的校验安卓system根证书、VPN和代理;再到后来的通用flutter框架、双向证书校验;再到现在的如阿里、腾讯、屹通等厂商自研的SDK和框架,这些SDK和框架都在对APP客户端与服务端之间的通信过程中的拦截和篡改行为做严格限制。我这套Xposed RPC抓包/解密方案不改变APP客户端与服务端之间的通信过程,它本质还是通过Xposed框架动态调试Hook修改数据,做RPC只是为了联动Burp等抓包软件,适用于市面上绝大部分的安卓应用,还可以配合f0ng大佬的autoDecoder插件进行对称加密算法的解密,但代码还要做一些修改,到时候看下能不能做成一个插件,大家敬请期待。这套Xposed RPC方案也有个缺点,因为抓的是动态调试的数据,只能实现一抓一改,不能发送到burp的repeater重放。 这里面的难点不仅仅是对目标APP的逆向分析,APP逆向分析的前提是你能通过脱壳看到核心源代码进行静态分析,能过Xposed/Frida检测做动态调试,如果你开发Xposed框架或编写frida脚本还不是很熟练,调试过程会经常导致APP程序崩溃,你的设备指纹可能还会被加固厂商的态势感知拉黑。
发表于 2026-05-27 09:00:00
阅读 ( 660 )
分类:
安全工具
1 推荐
收藏
0 条评论
r0leG3n7
4 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!