前言
老早以前就想搞 Xposed
开发了,一直没有机会学习用上,再加上 iPhone
是主要设备,这几年一直没有关注安卓社区,等手中有个安卓备用机后发现现在玩机社区不如过去了,生态越来越封闭,不过这不影响我继续学习 Xposed
模块开发,目前主流 Xposed
框架环境是 LSPosed Framework ,支持 A8.1 ~ A14
,不过开发者好像放弃维护了,不知道后续怎么样就不清楚了。Xposed
很强大,强大到在安卓几乎什么都能干,系统美化、软件破解、去广告等诸多操作,这些操作都不会影响原来应用下进行运行,所以说以前玩机最大的快乐来自 Xposed
。
我想写 Xposed
模块缘由是女朋友一直用醒图,网上对最新醒图破解资源很少,再加上老早以前就想学逆向开发,借此机会就入门了,不过写此教程是记录我学习下来的笔记,不记录醒图逆向开发过程。
准备
想要开发 Xposed
模块需要做到:
- 一台安装好
Xposed
框架环境的手机(EdXposed
,LSPosed
等) - 装有
AS
编辑器(Android Studio
)且有JDK
环境的电脑 - 查看
apk
的反编译工具,例如:jd-gui, JADX 等诸多 - 查看
apk
的布局应用:MT 管理器
,开发助手专业版
- 熟悉的且掌握 Java 反射
本篇不研究怎么破解醒图会员过程,记录在我手机下拉全局搜索,在输入框在联网的情况下会有热点广告词轮播,我的目的将把这个热点删除且还原原来的搜索占位词:
创建项目
打开 AS
编辑器,选择没有 No Activity
, 语言我选择 kotlin
, SDK
选择 API 26
。
创建好项目后引入 Xposed
库,需在在 Gradle Script
下的 settings.gradle.kts
添加:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url=uri("https://api.xposed.info") }
}
}
之后在 app
目录下的 build.gradle.kts
添加 Xposed
声明:
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
compileOnly(libs.api)
}
如果编译的文件很大的话可以删除里面的主题文件即可:
dependencies
保留 只保留:compileOnly(libs.api)
- 移除
src/res/values/themes.xml
里面的主题,注意还有个夜间模式,在values-night
文件夹下 - 移除
AndroidManifest.xml
文件里<application ... />
中的android:theme="xxx"
的一行
之后还需要在 src/res/values/
文件夹新建 arrays.xml
文件,来声明 LSPosed
作用域:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="xposedscope" >
<!-- 这里填写模块的作用域应用的包名,可以填多个。 -->
<item>com.xt.app1</item>
<item>com.xt.app2</item>
</string-array>
</resources>
然后在编辑器运行按钮编辑一下启动配置,勾选 Always install with package manager
并且将 Launch Options
改成 Nothing
即可。
声明模块
创建好项目后需要声明它是一个 Xposed
模块,好让框架发现它,这样才能激活模块 到 AndroidManifest.xml
文件里声明如下:
<application ... >
<!-- 是否为Xposed模块 -->
<meta-data
android:name="xposedmodule"
android:value="true"/>
<!-- 模块的简介(在框架中显示) -->
<meta-data
android:name="xposeddescription"
android:value="我是Xposed模块简介" />
<!-- 模块最低支持的Api版本 一般填54即可 -->
<meta-data
android:name="xposedminversion"
android:value="54"/>
<!-- 模块作用域 -->
<meta-data
android:name="xposedscope"
android:resource="@array/xposedscope"/>
</appication>
然后在 src/main
目录下创建一个文件夹名叫 assets
,并且创建一个文件叫 xposed_init
,内容写模块的入口类名,例如像我的:
com.iiong.xtcracked.MainHook
编写模块
现在在搜索要去广告,得先知道软件包名:
根据 IXposedHookLoadPackage
接口操作如下:
package com.iiong.xtcracked
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
class MainHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
// ColorOS 全局搜索
if (lpparam.packageName == "com.heytap.quicksearchbox") {
XposedBridge.log("找到应用:" + lpparam.packageName)
// hook it
qsbHook(lpparam)
}
}
private fun qsbHook(lpparam: XC_LoadPackage.LoadPackageParam) {
// 具体业务
}
}
然后在作用域声明下包名,打包后调试在手机上可以激活了。
反编译
如果想写具体的业务需要对 apk
进行反编译去读代码,找到关键点去修改即可。将搜索从手机导出 apk
后在电脑端使用 JADX
打开 apk
反编译。
每次打开搜索的时候,发现它的默认占位词是:搜索本机和全网内容
:
所以利用这个作为突破口,在 JADA
搜索这个关键词后发现确实有眉目:
看了一下其中的 dark_word
是用于桌面组件的,寻思我也用不到桌面小组件就不管它了,就从第三个 dock_dark_word_default_all
进入:
入眼就是所谓的判断了,所以切入点直接走这里,直接将 if
判断 给 true
就不会出现热点轮播:
XposedHelpers.findAndHookMethod(
"com.heytap.quicksearchbox.common.helper.EnterSourceManager",
lpparam.classLoader,
"c",
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {}
override fun afterHookedMethod(param: MethodHookParam) {
XposedBridge.log("TestHookLog 1b:$param")
param.result = true
}
})
然后再打包更新确实有效果了,后续又找到一个入口方向,大概是这样的:
直觉给我就是给占位框输入广告词,就用日志输出:
val darkWordSetClass = lpparam.classLoader.loadClass("com.heytap.quicksearchbox.core.db.entity.DarkWordSet")
XposedHelpers.findAndHookMethod(
"com.heytap.quicksearchbox.ui.widget.searchbar.BaseSearchBar",
lpparam.classLoader,
"F0",
darkWordSetClass, String::class.java, String::class.java, Boolean::class.java, Boolean::class.java, String::class.java, Boolean::class.java, object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
XposedBridge.log("TestHookLog 1b for args1:${param.args[0]}")
XposedBridge.log("TestHookLog 1b for args2:${param.args[1]}")
XposedBridge.log("TestHookLog 1b for args3:${param.args[2]}")
XposedBridge.log("TestHookLog 1b for args4:${param.args[3]}")
XposedBridge.log("TestHookLog 1b for args5:${param.args[4]}")
XposedBridge.log("TestHookLog 1b for args6:${param.args[5]}")
XposedBridge.log("TestHookLog 1b for args7:${param.args[6]}")
}
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param)
// XposedBridge.log("TestHookLog 1a for:${param.result}")
}
})
从上面的 args1
和 args2
打印知道传入广告占位的广告词,下面就简单的:
val darkWordSetClass = lpparam.classLoader.loadClass("com.heytap.quicksearchbox.core.db.entity.DarkWordSet")
XposedHelpers.findAndHookMethod(
"com.heytap.quicksearchbox.ui.widget.searchbar.BaseSearchBar",
lpparam.classLoader,
"F0",
darkWordSetClass, String::class.java, String::class.java, Boolean::class.java, Boolean::class.java, String::class.java, Boolean::class.java, object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
param.args[0] = null
param.args[1] = "请输入本机内容和全网内容"
}
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param)
// XposedBridge.log("TestHookLog 1a for:${param.result}")
}
})
打包后确实生效了。
Hook 函数的复杂参数
例如会遇到要 hook
的函数:
public static boolean fun1(String[][] strAry, Map mp1, Map<String,String> mp2, Map<Integer, String> mp3, ArrayList<String> al1, ArrayList<Integer> al2, ArgClass ac)
可以利用 getDeclaredMethods
方法打印出这个函数的参数列表输出如下:
([[Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/ArrayList;Ljava/util/ArrayList;Laqcxbom/xposedhooktarget/ArgClass;)
梳理下如下:
String[][] ==> [[Ljava/lang/String;
Map 数组不论何种形式 ==> Ljava/util/Map;
ArrayList 无论何种形式 ==> Ljava/util/ArrayList;
ArgClass 自定义类给个全路径的事==> Laqcxbom/xposedhooktarget/ArgClass;
在 Xposed
项目下可以尝试这么写:
package com.iiong.xtcracked
import android.util.Log
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
class XposedMain : IXposedHookLoadPackage {
var TAG: String = "AqCxBoM"
private val mStrPackageName = "aqcxbom.xposedhooktarget" //HOOK APP目标的包名
private val mStrClassPath = "aqcxbom.xposedhooktarget.MyClass" //HOOK 目标类全路径
private val mStrMethodName = "fun1" //HOOK 目标函数名
private fun LOGI(ct: String) {
Log.d(TAG, ct)
}
override fun handleLoadPackage(loadPackageParam: LoadPackageParam) {
//判断包名是否一致
if (loadPackageParam.packageName == mStrPackageName) {
LOGI("found target: " + loadPackageParam.packageName)
val ArgClass = XposedHelpers.findClass(
"aqcxbom.xposedhooktarget.ArgClass",
loadPackageParam.classLoader
)
val ArrayList =
XposedHelpers.findClass("java.util.ArrayList", loadPackageParam.classLoader)
val Map = XposedHelpers.findClass("java.util.Map", loadPackageParam.classLoader)
//包名一致时查找是否有匹配参数的类及函数
XposedHelpers.findAndHookMethod(mStrClassPath, //类路径
loadPackageParam.classLoader, //ClassLoader
mStrMethodName, //目标函数名
"[[Ljava.lang.String;", //参数1
Map, //参数2
Map, //参数3
Map, //参数4
ArrayList, //参数5
ArrayList, //参数6
ArgClass, //参数7
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param) //这个函数会在被hook的函数执行前执行
LOGI("beforeHook")
}
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param) //这个函数会在被hook的函数执行后执行
LOGI("afterHooke param: ")
}
})
}
}
}
去广告
现在安卓广告大多数都是国内几大广告商:腾讯、穿山甲、快手等几家,不过后面我懒得自己去定位查找,直接百度搜索发现相关的资料,不过大多数都是 Smail
语法。
比如想去掉穿山甲广告可以这么做:
XposedHelpers.findAndHookMethod("com.bytedance.pangle.Zeus", lpparam.classLoader, "hasInit", object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
}
override fun afterHookedMethod(param: MethodHookParam) {
super.afterHookedMethod(param)
param.result = false
}
})
根据上面的定位可以知道 com.bytedance.pangle.Zeus.hasInit
是用于 SDK
是否初始化成功,如果成功后则加载广告业务,这里直接返回 false
来达到去广告效果,所以其他厂商广告大同小异,故不再详写了。
总结
在后面几天又摸索了几个逆向虽然还是容易绕弯子,不过现在有了入手点和思路无非就是多花点时间,到后续熟悉了话很快找到其中的关键点。