第一部分
一、前言
Xposed 能干嘛?我可以告诉你 Root + Xposed ,真的可以为所欲为。而 Android 开源,为“搞机”带了更多的乐趣的同时,当然也引入安全性问题,部分流氓软件在 Root 下,会盗取用户私密信息,例如:号码、照片、短信、密码······,所以需要慎重使用 Root,此外本文仅作为技术学习。
二、Xposed 安装
1.首先你的手机 必须 Root,关于各安卓机型的 Root 方法,请自行到对应机型论坛和贴吧找找 (注:Root 有风险,失败可能导致手机变砖,风险自行承担)
2.下载「Xposed Installer」软件并安装,需要留意的是你的手机系统版本,不同版本下载对应的 apk,如果上不了该网址,可百度搜索——Xposed Installer 最新版本下载。
Xposed 官方网址:
https://repo.xposed.info/module/de.robv.android.xposed.installer
Android 5.0 及以上版本的下载地址:
3.激活 Xposed 框架(确保你手机刷入了第三方 Recovery),激活后可能会使系统变的有些卡顿,但为了技术(装B),我们牺牲点性能还是值得~
点击【安装/更新】,选择【Install via recovery】
等待【下载】,重启到【Recovery】模式,期间请勿操作
耐心等待,刷完会自动重启,打开【Xposed Installer】,显示框架已激活。
三、配置 Xposed 插件
如何配置 Android Studio 项目为 Xposed 插件?
1、配置项目 Gradle 的依赖
compileOnly 'de.robv.android.xposed:api:82' compileOnly 'de.robv.android.xposed:api:82:sources'
注:需要 compileOnly 来依赖,如果不想通过 Gradle 配置,也可以下载 XposedBridgeApi.jar ,放到项目 libs 目录。
2、配置 AndroidManifest.xml
<meta-data android:name="xposedmodule" android:value="true" /> <meta-data android:name="xposeddescription" android:value="XXX插件" /> <meta-data android:name="xposedminversion" android:value="89" />
xposedmodule:是否配置为 Xposed 插件,设置为 true
xposeddescription:模块名称
xposedminversion:最低版本号
3、新建 Hook 入口类
该类需要实现接口 IXposedHookLoadPackage
,并实现里面关键方法handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam)
,该方法会在每个软件被启动的时候回调,所以一般需要通过目标包名过滤。
/** * @author zhicheng.chen */public class TargetHook implements IXposedHookLoadPackage { @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { //通过目标包名过滤 if (lpparam.packageName.equals("com.xxx.xxx")) { XposedBridge.log("启动了xxx软件"); } } }
4、Xposed 免重启调试
Xposed插件每次代码改动,都需要重启手机才能生效,有时候重启一次还不生效(我的手机有一次重启 3 次,才看到生效,还好是公司测试机,不心疼),所以代码最好写上相关 Log 信息,来看代码生效没。
XposedBridge.log("启动了xxx软件");
不过这里分享一个免重启调试的方法,方法来自网上,感谢 DX :
/** * @author DX * 这种方案建议只在开发调试的时候使用,因为这将损耗一些性能(需要额外加载apk文件),调试没问题后,直接修改xposed_init文件为正确的类即可 * 可以实现免重启,由于存在缓存,需要杀死宿主程序以后才能生效 * 这种免重启的方式针对某些特殊情况的hook无效 * 例如我们需要implements IXposedHookZygoteInit,并将自己的一个服务注册为系统服务,这种就必须重启了 * Created by DX on 2017/10/4. */public class HookLoader2 implements IXposedHookLoadPackage { //按照实际使用情况修改下面几项的值 /** * 当前Xposed模块的包名,方便寻找apk文件 */ private final String modulePackage = "com.xxx.plugin"; /** * 宿主程序的包名(允许多个),过滤无意义的包名,防止无意义的apk文件加载 */ private static List<String> hostAppPackages = new ArrayList<>(); static { // TODO: Add the package name of application your want to hook! hostAppPackages.add("com.eg.android.AlipayGphone"); hostAppPackages.add("com.xxx.plugin"); } /** * 实际hook逻辑处理类 */ private final String handleHookClass = TargetHook.class.getName(); /** * 实际hook逻辑处理类的入口方法 */ private final String handleHookMethod = "handleLoadPackage"; @Override public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (hostAppPackages.contains(loadPackageParam.packageName)) { //将loadPackageParam的classloader替换为宿主程序Application的classloader,解决宿主程序存在多个.dex文件时,有时候ClassNotFound的问题 XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { Context context=(Context) param.args[0]; loadPackageParam.classLoader = context.getClassLoader(); invokeHandleHookMethod(context,modulePackage, handleHookClass, handleHookMethod, loadPackageParam); } }); } } /** * 安装app以后,系统会在/data/app/下备份了一份.apk文件,通过动态加载这个apk文件,调用相应的方法 * 这样就可以实现,只需要第一次重启,以后修改hook代码就不用重启了 * @param context context参数 * @param modulePackageName 当前模块的packageName * @param handleHookClass 指定由哪一个类处理相关的hook逻辑 * @param loadPackageParam 传入XC_LoadPackage.LoadPackageParam参数 * @throws Throwable 抛出各种异常,包括具体hook逻辑的异常,寻找apk文件异常,反射加载Class异常等 */ private void invokeHandleHookMethod(Context context, String modulePackageName, String handleHookClass, String handleHookMethod, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { File apkFile=findApkFile(context,modulePackageName); if (apkFile==null){ throw new RuntimeException("寻找模块apk失败"); } //加载指定的hook逻辑处理类,并调用它的handleHook方法 PathClassLoader pathClassLoader = new PathClassLoader(apkFile.getAbsolutePath(), ClassLoader.getSystemClassLoader()); Class<?> cls = Class.forName(handleHookClass, true, pathClassLoader); Object instance = cls.newInstance(); Method method = cls.getDeclaredMethod(handleHookMethod, XC_LoadPackage.LoadPackageParam.class); method.invoke(instance, loadPackageParam); } /** * 根据包名构建目标Context,并调用getPackageCodePath()来定位apk * @param context context参数 * @param modulePackageName 当前模块包名 * @return return apk file */ private File findApkFile(Context context, String modulePackageName){ if (context==null){ return null; } try { Context moudleContext = context.createPackageContext(modulePackageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); String apkPath=moudleContext.getPackageCodePath(); return new File(apkPath); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return null; } }
四、反编译工具
1、TopActivity.apk
安装这个工具可以直接查看当前 App 显示的最前 Activity,后面我们接触别人写的项目,这个工具可以很方便跟踪代码的入口,项目原理是通过 AccessibilityService
服务,监听所有的界面变化,读取当前界面 Activity,需要给软件开启【辅助权限】。
当然也可以通过 Adb 命令,获取 Dumpsys 当前 Activity 的信息:
adb shell dumpsys activity top
2、BDOpener.apk
这是 Xposed 的插件模块,也就是说需要刷入 Xposed 框架后,才能使用该,通过安装这个软件,我们可以使手机安装的软件,变为可调试状态,这样就可以通过 Android Studio 的 Monitor 工具,查看所有的进程状态,并 dumpsys 方法调用信息。
安装这个软件之后,需要在 Xposed 里面的【模块】里面,把该软件勾选后,【重启手机】才能生效。
有人说找不到 Monitor 工具,该工具在新的 As 版本把入口取消了,我们可以在:C:\Users\Administrator\AppData\Local\Android\Sdk\tools\monitor.bat
目录下找到。
开启调试状态之前,只能看到 AS 调试安装的进程:
开启调试状态之后,可以看到所有的进程信息:
3、jadx-gui 工具
关于这个工具的介绍,这里我就不再赘述,贴一篇我觉得写得很好的博文,作者讲得很详细易懂。
Android 反编译利器,jadx 的高级技巧:https://www.jianshu.com/p/e5b021df2170
注:如果出现 Jdk 错误,请安装 Jdk 64 位版本。
第二部分
一、前言
2018 年眨眼就结束,2019 年即将新年,在外工作拼搏一年,看着身边的朋友一个个升职加薪,买房买车,你是不是很羡慕!今天看我如何一步一步改掉我的支付宝账户余额的,进阶成为「百万富翁」
虽然不是真的账户余额,但是看着这个数字还是很感人的~
二、寻找 Hook 入口
1、结合 Top-Activity
获取到当前首页的 Activity 名称,可以看到截图支付宝首页的界面的 Activity 名称是 com.eg.android.AlipayGphone.AlipayLogin
,然后我们打开 Jadx-gui , 并打开下载好的 alipay_wap_main.apk
,反编译出源码
2、首页这样的排版,能想到这是常见的布局形式:TabHost + Fragment
。那重点去找【财富】对应的 Tab 下的 Fragment 类,打开 AlipayLogin 源码,它是继承 LauncherActivity
,不出意外的话,应该可以在这个类找到下面四个 Tab 各自对应的 Fragment。
不过尴尬的是,这里面我反复找了下,没能找到 Fragment 相关信息
3、通过 Monitor 去跟踪一下里面的方法的调用流程,看看是怎么个执行顺序
在点击【财富】按钮前,马上点击 Monitor
里的 Start Method Profiling
,等到页面完切过去【财富页】,并且加载数据完毕,点下停止即可,这样就能跟踪到这个进程的方法调用信息了。
在 trace 里面,重点关注包名为 com.alipay.xxx
开头的类的方法,可以找到几个可疑的方法调用,里面有 TabHost、TabLauncherFragment、FortureWidgetGroup、FortureHomeView
,最主要是 Fortune 这个单词是财富的意思
基本可以肯定最里面的是 FortureHomeView
这个作为界面视图,下个步骤就是怎么在 FortureHomeView
找到我们需要改的【总资产】这个布局,还有这个布局赋值的地方
继续跟踪,找到 FortureHomeView.updateFortureHead()
方法,由方法名可知更新头部,展开这个方法,看看里面调用了什么
点击 AssetCardV2View.renderData()
方法名大概知道是 View 处理数据地方
越来越清晰了,继续找下去。看到里面的 AssetHeaderV2View.setData()
知道是头部的设置数据
展开里面的 AssetHeaderV2View.setData()
看到有调用 TextView 的 setText() 方法,还有一个 NumRunningTextView 的 setRunningText() ,这里结合我们平时看到的界面效果,这是个自定义的带动画效果的View,可以猜测是【昨日收益】的 TextView,因为受益为了更好地用户体验,会带滚动效果,平时有留意就知道了。
知道具体是 AssetHeaderV2View
,在设置界面的值,现在去看看它的源码是咋回事。
4、第一个框框里面,有个 string 的命名是 hide_status_text
,我觉得就是我们隐藏资产的那个【*】符号
然后再看看条件 else if(assetsCardModel != null)
后面执行的代码,在第二个框里面,发现 setRunningText()
这个方法,而且方法有个参数是:totalYesterdayProfitView
,意思是昨天总收益,那这里应该就是【昨日收益】设值的地方
那就是说前一个setText()
方法,如果没有错误的话,就是设值【总资产】的地方,设值变量为 latestTotalView
。
为了确保没有错误,跟一下变量 a 和变量 b 具体是什么类型,可以看到一个是 AUAutoResizeTextView
另一个 NumRunningTextView
,这应该是自定义的控件,为啥总资产要定义为自动调整大小的View呢?想一下如果写死宽度的话或者字体大小的,就兼容不了每个人的资产数目,如:1000000 和 1 长度的区别。
AssetsCardModel
应该是存储用户资产信息的 Model 类,框框里面的变量,就是存储着最新的【总资产】信息
好的,到这里分析完了,总结一下具体是怎么个流程:
LauncherActivity -> TabHost -> TabLauncherFragment -> FortureHomeView.updateFortureHead() -> AssetCardV2View.renderData() -> AssetHeaderV2View.setData() -> AUAutoResizeTextView.setText()
三、Hook 代码
现在只需要对 AssetHeaderV2View.setData()
加工处理,在调用这个方法之前,对参数进行更改,通过反射把里面的 latestTotalView
,改成你想要的金额。
/** * @author zhicheng.chen * @date 2018/11/26 */public class AlipayHook implements IXposedHookLoadPackage { @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (lpparam.packageName.equals("com.eg.android.AlipayGphone")) { XposedBridge.log("load alipay"); ClassLoader classLoader = lpparam.classLoader; Class<?> aClass = classLoader.loadClass("com.alipay.android.render.engine.viewbiz.AssetsHeaderV2View"); Class<?> aClass2 = classLoader.loadClass("com.alipay.android.render.engine.model.AssetsCardModel"); if (aClass != null) { XposedHelpers.findAndHookMethod(aClass, "setData", aClass2, boolean.class, boolean.class, boolean.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); Object arg = param.args[0]; try { Log.w("czc", arg.getClass().getField("latestTotalView").get(arg).toString()); arg.getClass().getField("latestTotalView").set(arg,"1000000.00"); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); } }); } } } }