准备 lsposed xposed仅支持到android 7.0版本, 在更高的版本有许多替代, 最常用的便是lsposed
特性
Xposed
LSPosed
Hook 方式
直接 Hook Zygote
进程
通过 Magisk / Riru 注入 Zygote
兼容性
旧版 Android(7.0-10.0)
适用于 Android 8.0+
模块管理
需要 Xposed Installer
需要 LSPosed Manager
性能影响
修改系统 Zygote
,易损坏
仅 Hook 选定进程,更稳定
安全性
易被检测(如 SafetyNet)
更隐蔽,适用于新版本 Android
不过二者模块是几乎完全兼容的, 语法什么的都差不多
magisk lsposed需要magisk配合才能使用
android studio android开发必备
模块init
android studio创建一个新项目
在main目录下新增一个libs目录, 将XposedBridge.jar放入, 放入后右击选择Add As Library
(注: 该依赖也可以通过在线导入)
到build.gradle中将上一步在dependencies对应产生的引入依赖语句中的implementation
改为 compileOnly
在main目录下新增assets
目录, 其中新建文件xposed_init
, 添加一行内容包名.xposed入口类名
在main目录下新增/res/values
目录, 创建文件arrays.xml
, 用于管理模块作用域
1 2 3 4 5 6 7 8 9 <resources > <string-array name ="xposedscope" > <item > ceui.lisa.pixiv</item > <item > com.xjs.ehviewer</item > <item > com.picacomic.fregata</item > </string-array > </resources >
修改AndroidManifest.xml
在\标签中新增内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <application > <meta-data android:name ="xposedmodule" android:value ="true" /> <meta-data android:name ="xposeddescription" android:value ="Easy example which makes the status bar clock red and adds a smiley" /> <meta-data android:name ="xposedminversion" android:value ="82" /> <meta-data android:name ="xposedscope" android:resource ="@array/xposedscope" /> </application >
文档速览 https://api.xposed.info/
快速过一遍文档中常用部分
XC_MethodHook 钩子回调类, 进行hook时一般需要创建该类的匿名子类
MethodHookParam 封装关于方法调用的信息, 也就是回hook的参数
XposedBridge 提供与 Xposed 框架交互的核心方法
XposedHelper 提供一些实用方法,用于快速hook, 反射和动态操作类、方法、字段等
入口 Xposed/LSPosed 模块的入口类 通常需要实现 Xposed 框架的接口 ,用于接收 Hook 事件并执行 Hook 逻辑, 对于一个模块至少应该实现一个该类接口
主要有四类接口
IXposedHookLoadPackage 用于监听应用的加载过程并hook
重写方法 : handleLoadPackage
参数 :XC_LoadPackage.LoadPackageParam
示例
注意:当app内存在多个包时, 使用参数lpparam.packageName过滤也只能使用应用包名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.XC_LoadPackage;import de.robv.android.xposed.XposedBridge;import de.robv.android.xposed.XposedHelpers;public class MainHook implements IXposedHookLoadPackage { @Override public void handleLoadPackage (XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.target.app" )) return ; XposedBridge.log("Hooking: " + lpparam.packageName); XposedHelpers.findAndHookMethod( "android.app.Application" , lpparam.classLoader, "getPackageName" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { param.setResult("com.fake.package" ); } } ); } }
IXposedHookInitPackageResources 监听资源加载并hook, 用于修改目标应用的 UI 资源,例如替换图片、修改文本等
重写方法 : handleInitPackageResources
参数 :XC_InitPackageResources.InitPackageResourcesParam
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import de.robv.android.xposed.IXposedHookInitPackageResources;import de.robv.android.xposed.callbacks.XC_InitPackageResources;public class ResourceHook implements IXposedHookInitPackageResources { @Override public void handleInitPackageResources (XC_InitPackageResources.InitPackageResourcesParam resparam) throws Throwable { if (!resparam.packageName.equals("com.target.app" )) return ; resparam.res.setReplacement("com.target.app" , "string" , "app_name" , "Hooked App!" ); resparam.res.setReplacement("com.target.app" , "drawable" , "app_icon" , XposedHelpers.findSystemClass("com.myhook.module.R.drawable.new_icon" )); } }
IXposedHookZygoteInit 系统启动时 进行 Hook,影响所有应用
重写方法 :initZygote
参数 :StartupParam
示例 :
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.IXposedHookZygoteInit;import de.robv.android.xposed.XposedBridge;import de.robv.android.xposed.XposedHelpers;public class ZygoteHook implements IXposedHookZygoteInit { @Override public void initZygote (StartupParam startupParam) throws Throwable { XposedBridge.log("Zygote Hook Initialized!" ); XposedHelpers.findAndHookMethod( "android.telephony.TelephonyManager" , null , "getDeviceId" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { param.setResult("000000000000000" ); } } ); } }
接口组合 在一个 Xposed 模块中,可以同时实现多个 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 import de.robv.android.xposed.*;import de.robv.android.xposed.callbacks.*;public class MainHook implements IXposedHookLoadPackage , IXposedHookInitPackageResources { @Override public void handleLoadPackage (XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.target.app" )) return ; XposedBridge.log("Hooking: " + lpparam.packageName); XposedHelpers.findAndHookMethod("android.app.Application" , lpparam.classLoader, "getPackageName" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { param.setResult("com.fake.package" ); } }); } @Override public void handleInitPackageResources (XC_InitPackageResources.InitPackageResourcesParam resparam) throws Throwable { if (!resparam.packageName.equals("com.target.app" )) return ; resparam.res.setReplacement("com.target.app" , "string" , "app_name" , "Hooked App!" ); } }
de.robv.android.xposed.callbacks 包含了 Xposed 框架中的回调接口和类,用于处理模块与框架之间的交互
de.robv.android.xposed.services 提供了与系统服务交互的工具类,通常用于读取文件、获取系统信息等操作
主要就是BaseService
类
案例分析 FDex2源码分析 FDex2是早期针对整体加固的脱壳xposed模块, 其源码并不复杂
其依赖了android7.0以前的两个特殊方法getDex
和getBytes
(这两个方法在android8开始废弃)
android中java.lang.Class类有一个名为getDex的方法, 可以获得某一个类的Dex对象
1 2 3 4 5 6 public Dex getDex () { if (dexCache == null ) { return null ; } return dexCache.getDex(); }
Dex又有一个方法名为getBytes, 可以获取Dex对象的内存数据
1 2 3 4 5 6 7 public byte [] getBytes() { ByteBuffer data = this .data.duplicate(); byte [] result = new byte [data.capacity()]; data.position(0 ); data.get(result); return result; }
FDex2首先就是通过反射获取这两个类的引用
1 2 3 4 5 6 7 8 9 10 11 12 13 public void initRefect () { try { Dex = Class.forName("com.android.dex.Dex" ); Dex_getBytes = Dex.getDeclaredMethod("getBytes" ,null ); getDex = Class.forName("java.lang.Class" ).getDeclaredMethod("getDex" ,null ); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } }
接着使用xposed对java.lang.ClassLoader
的loadClass
方法进行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 42 43 44 45 46 @Override public void handleLoadPackage (XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { initRefect(); XposedBridge.log("目标包名:" + lpparam.packageName); String str = "java.lang.ClassLoader" ; String str2 = "loadClass" ; final String packagename = "com.jiongji.andriod.card" ; if (lpparam.packageName.equals(packagename)){ XposedHelpers.findAndHookMethod(str, lpparam.classLoader, str2, String.class, Boolean.TYPE, new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { super .afterHookedMethod(param); Class cls = (Class) param.getResult(); if (cls == null ){ return ; } String name = cls.getName(); XposedBridge.log("当前类名:" +name); byte [] bArr = (byte []) Dex_getBytes.invoke(getDex.invoke(cls,null )); if (bArr == null ) { XposedBridge.log("数据为空,返回" ); return ; } XposedBridge.log("开始写数据" ); String dex_path = "/data/data/" +packagename+"/" +packagename+"_" +bArr.length+".dex" ; XposedBridge.log(dex_path); File file = new File (dex_path); if (file.exists()){ return ; } writeByte(bArr,file.getAbsolutePath()); } @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); } }); } }
其入口是IXposedHookLoadPackage
, 在应用加载时触发回调
之后使用方法findAndHookMethod
进行hook, 该方法有2个重载
这里选择的是
1 2 3 4 5 6 public static Unhook findAndHookMethod ( String className, // 目标类的全限定名 ClassLoader classLoader, // 目标类的类加载器 String methodName, // 目标方法名 Object... parameterTypesAndCallback // 方法参数类型和回调 )
第四个参数是可变参数, 用于指定目标方法的参数类型 和回调逻辑, 且参数类型的顺序必须与目标方法的签名一致(避免有重载的情况)
源码中就是hook java.lang.ClassLoader
的loadClass
方法
待其执行完毕后从返回值获取到加载得到的类, 并调用getDex
方法和getBytes
方法从而获取到对应的内存字节流并将其保存
1 2 3 4 5 6 7 8 9 10 public void writeByte (byte [] bArr,String str) { try { OutputStream outputStream = new FileOutputStream (str); outputStream.write(bArr); outputStream.close(); } catch (IOException e) { e.printStackTrace(); XposedBridge.log("文件写出失败" ); } }
这个脚本完全也可以通过frida来实现
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 function savedex (dexbytes, dexpath ) { Java .perform (function ( ) { var File = Java .use ("java.io.File" ); var FileOutputStream = Java .use ("java.io.FileOutputStream" ); var fileobj = File .$new(dexpath); var fileOutputStreamobj = FileOutputStream .$new(fileobj); fileOutputStreamobj.write (dexbytes); fileOutputStreamobj.close (); console .warn ("[dumpdex] Dex saved to: " + dexpath); }); } function fdex2 (classname ) { Java .perform (function ( ) { Java .enumerateClassLoadersSync ().forEach (function (loader ) { try { var ThisClass = loader.loadClass (classname); if (ThisClass ) { console .log ("[dumpdex] Found class: " + classname + " in loader: " + loader); var dexobj = ThisClass .getDex (); if (dexobj) { var dexbytearray = dexobj.getBytes (); if (dexbytearray) { var savedexpath = "/sdcard/" + classname.replace (/\./g , "_" ) + ".dex" ; savedex (dexbytearray, savedexpath); } else { console .error ("[dumpdex] Failed to get Dex bytes for class: " + classname); } } else { console .error ("[dumpdex] Failed to get Dex object for class: " + classname); } } } catch (e) { console .error ("[dumpdex] Error while processing loader: " + loader + ", error: " + e.message ); } }); }); } Java .perform (function ( ) { fdex2 ("com.example.target.TargetClass" ); });
实战登山赛车 找款小游戏试试手, 登山赛车版本1.48.18, 没有加壳
游戏打开竟然要求实名
且该窗口无法关闭, 所以首先得想办法pass掉实名验证
jadx打开, 通过activity记录能够知道, 由MyStartActivity
启动了IDCheckDialog
进行验证
一开始的想法是hook点击确定后的doRidCheck函数
但发现这个函数怎么都hook不到, 但他的同级函数却可以被hook, 暂时搞不明白
发现只有当actAsIdVerifyActivity为true时才会进入验证过程, 其又依赖于loginActivityExists的返回值
private boolean actAsIdVerifyActivity = !loginActivityExists();
所以我们直接hook loginActivityExists的返回值为true
先使用frida尝试
1 2 3 4 5 6 7 8 9 Java .perform (function ( ) { let MyStartActivity = Java .use ("com.mygamez.common.MyStartActivity" ); MyStartActivity ["loginActivityExists" ].implementation = function ( ) { console .log ("MyStartActivity.loginActivityExists is called" ); let result = this ["loginActivityExists" ](); console .log ("MyStartActivity.loginActivityExists result=" + result); return true ; }; });
成功加载但显然后续还有其他检测, 通过关键字可以知道游戏检测到了我们是游客模式
在防沉迷类下找到这三个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Java .perform (function ( ) { let MyStartActivity = Java .use ("com.mygamez.common.MyStartActivity" ); MyStartActivity ["loginActivityExists" ].implementation = function ( ) { console .log ("MyStartActivity.loginActivityExists is called" ); let result = this ["loginActivityExists" ](); console .log ("MyStartActivity.loginActivityExists result=" + result); return true ; }; let AntiAddictionManagerInstance = Java .use ("com.mygamez.common.antiaddiction.AntiAddictionManager$AntiAddictionManagerInstance" ); AntiAddictionManagerInstance ["isGuestMode" ].implementation = function ( ) { console .log ("AntiAddictionManagerInstance.isGuestMode is called" ); return false ; }; AntiAddictionManagerInstance ["getUserAge" ].implementation = function ( ) { console .log ("AntiAddictionManagerInstance.getUserAge is called" ); let result = this ["getUserAge" ](); console .log ("AntiAddictionManagerInstance.getUserAge result=" + result); return 20 ; }; });
之后就可以成功进入游戏, 尝试内购
提示需要安装微信, 通过字符串找到检测处
然后其调用处判断返回值是否是空串, 如果是则说明没有错误, 所以hook其返回值始终返回空
接着继续搜索购买失败, 找到对应关键处
可以知道只有getResultCode返回1时才是购买成功, 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 Java .perform (function ( ) { let MyStartActivity = Java .use ("com.mygamez.common.MyStartActivity" ); MyStartActivity ["loginActivityExists" ].implementation = function ( ) { console .log ("MyStartActivity.loginActivityExists is called" ); let result = this ["loginActivityExists" ](); console .log ("MyStartActivity.loginActivityExists result=" + result); return true ; }; let AntiAddictionManagerInstance = Java .use ("com.mygamez.common.antiaddiction.AntiAddictionManager$AntiAddictionManagerInstance" ); AntiAddictionManagerInstance ["isGuestMode" ].implementation = function ( ) { console .log ("AntiAddictionManagerInstance.isGuestMode is called" ); return false ; }; AntiAddictionManagerInstance ["getUserAge" ].implementation = function ( ) { console .log ("AntiAddictionManagerInstance.getUserAge is called" ); let result = this ["getUserAge" ](); console .log ("AntiAddictionManagerInstance.getUserAge result=" + result); return 20 ; }; let BillingWrapperWeChat = Java .use ("com.mygamez.billing.BillingWrapperWeChat" ); BillingWrapperWeChat ["checkIfOkToContinuePaymentPaymentSpecificRequirements" ].implementation = function ( ) { console .log ("BillingWrapperWeChat.checkIfOkToContinuePaymentPaymentSpecificRequirements is called" ); let result = this ["checkIfOkToContinuePaymentPaymentSpecificRequirements" ](); console .log ("BillingWrapperWeChat.checkIfOkToContinuePaymentPaymentSpecificRequirements result=" + result); return "" ; }; let BillingResult = Java .use ("com.mygamez.billing.BillingResult" ); BillingResult ["getResultCode" ].implementation = function ( ) { console .log ("BillingResult.getResultCode is called" ); let result = this ["getResultCode" ](); console .log ("BillingResult.getResultCode result=" + result); return 1 ; }; });
此时已经破解完成, 接下来将frida代码转为xposed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.example.xposed;import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.XC_MethodHook;import de.robv.android.xposed.XposedBridge;import de.robv.android.xposed.XposedHelpers;import de.robv.android.xposed.callbacks.XC_LoadPackage;public class main implements IXposedHookLoadPackage { @Override public void handleLoadPackage (final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.fingersoft.hillclimb.noncmcc" )) { return ; } XposedHelpers.findAndHookMethod("com.mygamez.common.MyStartActivity" , lpparam.classLoader, "loginActivityExists" , new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("loginActivityExists is called" ); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("loginActivityExists result=" + param.getResult()); param.setResult(true ); } }); XposedHelpers.findAndHookMethod("com.mygamez.common.antiaddiction.AntiAddictionManager$AntiAddictionManagerInstance" , lpparam.classLoader, "isGuestMode" , new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("isGuestMode is called" ); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { param.setResult(false ); } }); XposedHelpers.findAndHookMethod("com.mygamez.common.antiaddiction.AntiAddictionManager$AntiAddictionManagerInstance" , lpparam.classLoader, "getUserAge" , new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("getUserAge is called" ); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("getUserAge result=" + param.getResult()); param.setResult(20 ); } }); XposedHelpers.findAndHookMethod("com.mygamez.billing.BillingWrapperWeChat" , lpparam.classLoader, "checkIfOkToContinuePaymentPaymentSpecificRequirements" , new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("checkIfOkToContinuePaymentPaymentSpecificRequirements is called" ); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("checkIfOkToContinuePaymentPaymentSpecificRequirements result=" + param.getResult()); param.setResult("" ); } }); XposedHelpers.findAndHookMethod("com.mygamez.billing.BillingResult" , lpparam.classLoader, "getResultCode" , new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("getResultCode is called" ); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { XposedBridge.log("getResultCode result=" + param.getResult()); param.setResult(1 ); } }); } }
安装并启用模块