

本文仅用于学习和技术研究,读者利用本文所提供的信息造成的任何直接或间接的影响和损失均由该读者负责,文章作者以及公众号不为此承担任何责任,请遵守国家网络安全法,维护良好的网络环境。


大家好,我是拖更博主r0leG3n7。本文将详细介绍如何开发一款Xposed RPC模块在无法得到私钥的情况下解密使用公钥加密算法的APP,这套RPC方案还能应用于Mpass框架和TMF框架的抓包和解密,能解决市面上百分之95的APP无法抓包或者解密的问题。月更真有点困难,如果不发社区,这篇文章四月份就发出来了,社区文章审核和文章排期非常久,为了几百块稿费又拖更了,再次跟大伙真诚致歉。其实三月底的时候我本来想蹭一下SysWhispers4和那个蓝锤Defender的提权漏洞的热度写一两篇文章的,但是最后又没发出来的原因是感觉内容不太充实而且大家都在写,我再水一篇同类型的也没啥意思,我的初衷还是想写点不一样而且对大家有用的[狗头],当然这些都是次要原因了,主要原因还是因为我是条懒狗。
还想跟大家汇报一个事,我打算换工作了,想跟网络安全毕业生一起去体验一下现在的网络安全市场,最近在疯狂复习web渗透、提权、逆向和免杀的笔记。如果对方允许,我会把我面试遇到的困难的或者比较有意思的问题总结成经验分享出来,给最近也在找工作的大伙提供一些参考,欢迎各位大佬过来拷打我这个菜鸡。
如有任何错误和不足欢迎各位师傅指正,转载请注明文章出处。本文首发于奇安信攻防社区,链接地址:
https://forum.butian.net/share/4881


ping不通的主机就别ping了,telnet不通的端口就别继续尝试了。你的执着源于她告诉你她是台慢热的单机服务器,但可能实际上她的RJ45接口已经连接了别人的网线,连接着别人的局域网甚至是公共网络,她的端口对于别人都是Listenning状态,你发的每一段数据都被她的防火墙拦截,但是别人的报文却可以直通她的应用层。你ping不通的她可能还关闭了防火墙,缺乏安全防护的她可能正在被其他主机狠狠地DDOS,最可悲的是你明明也知道真相,但还为了揭穿这个ping不通的谎言去翻看她被DDOS的日志。


最近测试遇到了个某梆加固的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的用法。
在 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的某个方法。
XposedHelpers.findClass()用于在运行时动态加载类,第一个参数为需要加载的类名,第二个参数为类加载器,这个类加载器可以从handleLoadPackage传入参数的成员中获取。
public static Class<?> findClass(String className, ClassLoader classLoader)XposedHelpers.getStaticObjectField()用于获取类的成员变量
示例源代码:
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获取静态类成员示例:
Class<?> localDataCls = XposedHelpers.findClass("com.test.test.data.local.LocalData", cl);Object localDataInstance = XposedHelpers.getStaticObjectField(localDataCls, "INSTANCE");String encryptedAuth = (String) XposedHelpers.callMethod(localDataInstance, "getEncryptedAuth");
XposedHelpers.callMethod()主动调用目标 App 内部的某个方法(调用静态方法要用另外一个函数),它可以绕过 Java 的访问权限检查(private/protected),在运行时动态调用任意对象的任意方法。第一个参数为调用方法的实例对象,第二个参数为要调用的方法名;如果存在多个 重载函数,第三个参数开始为函数的参数类型;其余参数为传给方法的实参。如果遇到多个重载函数的情况,建议使用下面介绍的反射去调用方法。
public static Object callMethod(Object obj, String methodName, Object... args)XposedHelpers.callMethod()常用于没有重载函数,当遇到那种类里面有多个重载函数,就需要用反射去指定参数类型精准调用某个方法。
示例:
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是没有指定参数类型的,当需要指定参数类型时的示例:
Method setValueMethod = chainClass.getMethod("setValue", int.class, String.class)XposedHelpers.findAndHookMethod()将寻找类,调用方法并Hook方法三个步骤合在一起。第一个参数为XposedHelpers.findClass()找到的类名,第二个参数是handleLoadPackage传入参数的ClassLoader,第三个参数为要Hook的方法名,第四个参数开始为Hook的方法的传入参数类型,最后一个参数为Hook方法执行的回调函数。
XposedHelpers.findAndHookMethod("com.example.MainActivity", // 类名lpparam.classLoader, // ClassLoader"onCreate", // 方法名Bundle.class, // 方法参数类型(Bundle)new XC_MethodHook() { // 回调钩子@Overrideprotected void beforeHookedMethod(MethodHookParam param) {// 方法执行前XposedBridge.log("onCreate 即将执行");}@Overrideprotected void afterHookedMethod(MethodHookParam param) {// 方法执行后XposedBridge.log("onCreate 执行完毕");}});
传入参数类型和上面的callMethod()差不多。不同的是callStaticMethod()调用的是类里面用static修饰的静态方法,静态方法和普通方法的区别就是静态方法不需要创建类的实例就可以直接调用。
public static Object callStaticMethod(Class<?> clazz, String methodName, Object... args)Xposed提供的回调主要分为两种,分别是XC_MethodHook和XC_MethodReplacement。两者的区别是XC_MethodHook不会阻止被Hook方法原本的流程,只在方法执行之前和方法执行完成之后做修改;而XC_MethodReplacement是完全阻止被Hook方法原本的流程,用其他方法代替原本被Hook的方法,这个回调过程需要改写东西比较多,而且在对APP代码逻辑不清晰的情况下修改原来被Hook方法很容易出错。所以我们通常选择XC_MethodHook这个回调函数,XC_MethodHook可重写的函数有两个,分别是beforeHookedMethod()和afterHookedMethod()。
在原方法执行之前触发,适合:
1、读取原始入参
2、修改入参
3、提前阻断方法执行
在原方法执行之后触发,适合:
1、读取原方法返回值
2、修改返回值
3、在方法执行完成后补日志、补上报
先看一段示例代码:
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寻找类时:
XposedHelpers.findClass("com.apm.insight.CrashInfoCallback$crashFileList", loadPackageParam.classLoader);再看一段示例代码:
package com.test.forPentest;public classOuter{void test() {Runnable r = new Runnable() { ... }; // 编译后常见 Outer$1}}
Runnable()就是顶级类里面的Outer的匿名类,Xposed寻找类匿名内部类常见成 com.test.forPentest.Outer$1
匿名类的应用场景:
1、一次性回调(监听器、Hook 回调)
2、逻辑短,不需要复用
3、希望代码就写在调用处附近
adb forward:将主机端口转发到设备端口,用于从主机访问设备上的服务。
adb reverse:将设备端口转发到主机端口,用于从设备访问主机上的服务。


本节我将介绍一下Xposed开发调试设备和工具的选择以及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配置国内加速镜像
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配置
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配置包路径和包的哈希值
distributionSha256Sum=b21468753cb43c167738ee04f10c706c46459cf8f8ae6ea132dc9ce589a261f2distributionUrl=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
<meta-dataandroid:name="xposedmodule"android:value="true" /><meta-dataandroid:name="xposeddescription"android:value="test module" /><meta-dataandroid: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模块运行时也需要用到这个依赖库。
compileOnly(files("lib\\XposedBridgeAPI-89.jar")) //编译xposed API1、如果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



当我提出手搓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赋予了我们高贵的调试权限,这意味着我们有监控每个函数输入和输出的能力。公钥加密算法本质还是函数,它传入参数是公钥以及它的明文字节数组,这两个参数在进入公钥加密算法之前还是明文,这时候我们hook这个公钥加密算法,把参数的明文打印出来,这不就实现了解密吗?但是问题又来了,这种解密了又有什么用呢?只能看数据又不能联动Burp之类的抓包工具改数据,这个时候Xposed RPC就有意义了。我们可以创建一个RPC服务端,让Xposed客户端把hook公钥加密算法传入参数的明文转发给RPC服务端,这个过程通过adb以及burp代理,我们就可以在burp代理的过程中修改加密前的明文数据,修改后明文数据经过RPC服务端处理以后返回给Xposed客户端,Xposed客户端将RPC服务端返回的数据以参数形式传入原先的加密函数,这就完成了一次抓包数据修改。一次向APP服务端发起的http请求会向RPC服务端发起两次数据处理请求,这两次请求分别处理原先向APP服务端发起的请求和响应。或许我直接这样说大家会有点懵逼,依旧是大伙最喜欢的excel画图环节:

1、说完我的实现思路,接下来实践检验真理。Xposed的最基本的代码框架如下(详解看代码注释,beforeHookedMethod和afterHookedMethod两个方法的重写是核心,稍后详细介绍):
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; //定位目标APPClass<?> 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接口类的内部类,所以用$分隔。
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方法的传入参数,详细代码如下(详解看代码注释):
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();// 获取请求URLMethod urlMethod = requestClass.getMethod("url");Object url = urlMethod.invoke(request);String urlStr = String.valueOf(url);XposedBridge.log("请求URL: " + urlStr);//获取请求bodyString 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对象。

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服务器。
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类。
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方法的返回值。跟上面处理传入参数差不多,下面介绍几个关键处理函数,不再过多赘述。
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的bodybyte[] 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() {@Overridepublic 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的数据

privatebyte[] 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和字节数组。

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;}}


这又不得不再提一下我选择对的Hook方法的优点了,因为我是hook原始请求的request,而不是hook加密函数,所以我不需要再次hook签名算法增加Xposed客户端的代码量,所以RPC服务端也可以不用处理Xposed客户端发过来的签名,只做简单的数据中转。下面是我用python的flask开发的轻量化服务端,在这个例子里它的功能是接收Xposed客户端发送过来的数据以后打印数据,然后原封不动地返回给Xposed客户端,它的作用就是为了给Xposed客户端的数据提供一个burp代理的过程,方便我们联动burp等抓包工具查看并修改数据。
import base64import uuidfrom flask import Flask, jsonify, requestapp = Flask(__name__)def normalize_body(data: dict) -> str:body = data.get("body", "")if body:return bodybody_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_bodyif not body_base64 and modified_data:body_base64 = base64.b64encode(modified_data.encode("utf-8")).decode("ascii")sign = sign_paramreturn 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。
adb reverse tcp:8080 tcp:80803、启动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程序崩溃,你的设备指纹可能还会被加固厂商的态势感知拉黑。
作者微信👇,欢迎催更和骚扰


r0leG3n7
r0leG3n7
安卓逆向第一篇:刷机与root(补充未解锁BL设备实现Apatch root方案,解锁BL设备转SukiSU和Root隐藏)
r0leG3n7



夜雨聆风