某右的五种去广告思路暨 YukiHook 简介
本帖最后由 t00t00 于 2023-8-14 21:18 编辑## 一、前言
在许多情况下,对一个软件进行 hook 有许多不同的角度。优秀的 hook 往往具有代码简洁且高度适应性的特点,能够在相当长的一段时间内适应原始软件的更新而不需要频繁修改。***例如我之前的 [某钉反撤回神器](https://github.com/kazutoiris/anti-dingtalk-recall) 仅使用不到 50 行代码就实现了功能,而且无需任何修改就已经适应了足足超过一年的版本更新。***因此,hook 可以被视为一门精炼的艺术。本文旨在以点带面,提供多种思路供学习,通过小范例展示如何做到在不同层面上实现 hook,从而达到事半功倍的效果。
YukiHookAPI 是一个基于 Kotlin 的 hook 框架,它为实现精巧的 hook 功能和便捷的异常捕捉提供了强大支持。可以毫不夸张地称之为现代 hook 技术的首选之一。而在吾爱破解中似乎尚未有文章详细介绍这个强大的框架。
## 二、简介
某右是一款社交媒体应用,用户可以在平台上分享短视频、图片和文字,展示自己的创意和生活。用户可以互动,通过点赞、评论和分享来表达对内容的喜爱。此外,某右也提供了关注功能,用户可以关注感兴趣的其他用户,及时获取他们的更新内容。这款APP通过个性化的推荐算法,向用户推荐符合其兴趣的内容,使用户能够更好地发现有趣的话题和创作者。
鉴于某右APP存在大量广告,其中包括开屏广告、信息流广告以及评论区广告等形式,为提升使用体验,采取 hook 技术,以有效清除这些广告干扰。
## 三、工具
1. (https://github.com/skylot/jadx)
2. Android Studio
3. (https://github.com/fankes/YukiHookAPI)
## 四、实现思路
### 1. 小试牛刀
现在用 jadx 加载导出的 dex 文件,在左边可以加载出所有的类。dex 文件可以用 frida 等得到,这里就不再赘述。
那么,这么多的类中哪一个才是我们需要的呢?
首先,已知的情况是:广告都是由各种广告商提供并推送的。因此,如果一个软件能够显示广告,那么显然它已经集成了各类广告 SDK。
于是,第一个思路就呼之欲出了:直接 hook 掉广告的 SDK 就可以了。
通过搜索,很容易就找到了 `BaiduADProvider`,这便是百度广告。通过搜索引用,可以发现,在 `cn.xiaochuankeji.hermes.HermesSDK.install` 使用了这一个类
```java
public static final void install(Hermes install, Application application, String appID, HermesADConfig hermesADConfig, boolean z, Callback<Boolean> callback) {
if (PatchProxy.proxy(new Object[]{install, application, appID, hermesADConfig, new Byte(z ? (byte) 1 : (byte) 0), callback}, null, changeQuickRedirect, true, C2428R2.attr.tabContentStart, new Class[]{Hermes.class, Application.class, String.class, HermesADConfig.class, Boolean.TYPE, Callback.class}, Void.TYPE).isSupported) {
return;
}
Intrinsics.checkNotNullParameter(install, "$this$install");
Intrinsics.checkNotNullParameter(application, "application");
Intrinsics.checkNotNullParameter(appID, "appID");
Intrinsics.checkNotNullParameter(hermesADConfig, "hermesADConfig");
Hermes.init(application, appID, hermesADConfig, new ADProvider[]{PangleADProvider.Companion.create$default(PangleADProvider.Companion, application, z, null, 4, null), TencentADProvider.Companion.create(application, z), MimoADProvider.Companion.create(application, z), XinguADProvider.Companion.create(application, z), KuaishouADProvider.Companion.create$default(KuaishouADProvider.Companion, application, z, null, 4, null), (ADProvider) JingdongADProvider.Companion.create$default(JingdongADProvider.Companion, application, z, (InfoProvider) null, 4, (Object) null), XcADProvider.Companion.create$default(XcADProvider.Companion, application, z, null, 4, null), BJXinguADProvider.Companion.create(application, z), BaiduADProvider.Companion.create(application, z), QuMengADProvider.Companion.create(application, z), GroMoreADProvider.Companion.create(application, z), TanxADProvider.Companion.create(application, z)}, callback);
}
```
可以看到,在第 9 行初始化了 `PangleADProvider`、`TencentADProvider`、`MimoADProvider`、`XinguADProvider`、`KuaishouADProvider`、`JingdongADProvider`、`XcADProvider`、`BaiduADProvider`、`QuMengADProvider`、`GroMoreADProvider`、`TanxADProvider`。
那么思路就是清空 `providers`。
但是问题来了,怎么清空呢?由于 `ADProvider[]` 是一种不可变类型,所以是没有 `clear()` 方法的,因此直接操作传入参数不太方便。
于是,看到第 3 行,provider 转换为 MutableList 并保存在 f5876Q 中。MutableList 就可变了,内置了 `clear` 方法,这可比改传入参数方便多了。那么,这时候可以在 afterHook 通过反射调用 f5876Q 的 `clear()` 方法。
不过,这样写并不优雅,以下提供了一小段 Java 伪代码。
```java
Class c = XposedHelpers.findClass("cn.xiaochuankeji.hermes.core.Hermes", lpparam.classLoader);
Field field = XposedHelpers.findField(c, "Q");
field.setAccessible(true);
field.clear()
```
可以看到这段代码有两个问题:
1. 代码行数太多,调用 `clear()` 就 1 行,却使用了 3 行来搜索 field。
2. `Q` 会随着版本更新或者加壳混淆工具的升级而自动重命名,并不具有通用性。例如重新加壳说不定就变成 `R` 等等了。
通过查找引用,在不远的之前可以找到一个函数 `getProviderList$core_release`,这个函数会返回同样的变量。
在这里,一方面是不用再通过反射调用 `clear()`,其 result 原生就支持 `clear()`;另一方面,这个函数名是加壳混淆不会进行修改的,因此具有一定的通用性。
```kotlin
"cn.xiaochuankeji.hermes.core.Hermes".hook {
injectMember {
method {
name = "getProviderList\$core_release"
}
afterHook {
(result as MutableList<*>).clear()
}
}.onAllFailure {
loggerE(msg = "Hook getProviderList fail: ${it.message}")
}
}
```
以上代码通过一行就直接清空了,代码相当的少。
### 2. 换个角度
> 首先,已知的情况是:广告都是由各种广告商提供并推送的。因此,如果一个软件能够显示广告,那么显然它已经集成了各类广告 SDK。
现在来想这么一个问题:如果要接入广告需要怎么做?是不是需要登录广告商官网,注册账户,**获取广告位 ID**,然后将 SDK 集成到应用代码中,按照指南进行配置和调用。
那么,如果没有进行正确的配置,SDK 不就不能拉取到广告了嘛。
于是,很容易的就找到了配置的位置。
直接返回为空就可以了,这样广告 SDK 就没办法获得正确的配置,也就无法展示广告了。
这种 hook 方式比之前虽然代码量增加了,但是写起来更为干净,只需要处理基本的数据结构,而不需要进行 List 等等类的处理。
```kotlin
"cn.xiaochuankeji.hermes.core.provider.ADProvider".hook {
injectMember {
method {
name = "getChannel"
}
beforeHook {
result = 0
}
}.onAllFailure {
loggerE(msg = "Hook getChannel fail: ${it.message}")
}
}
"cn.xiaochuankeji.hermes.core.provider.ADProvider".hook {
injectMember {
method {
name = "getConfigKey"
}
beforeHook {
result = ""
}
}.onAllFailure {
loggerE(msg = "Hook getConfigKey fail: ${it.message}")
}
}
```
### 3. 看看有没有更简单的
其实上下翻一翻代码,不远处可以看到一个 `getEnable` 方法。
首先,需要确认以下这个方法有没有被使用。往往 IDE 会自动生成所有字段的 getter/setter 方法,有些不一定会用到。所以不能够盲目地 hook。
检查引用可以看到,这个方法确实被使用,而且被用于状态切换。如果没有被设置为启用,就不会加载。所以直接 hook 这个方法,返回 false 就行了。
因此,可以写出新的代码。新的代码就比上面更少了。
```kotlin
"cn.xiaochuankeji.hermes.core.api.entity.ADSDKConfigResponseData".hook {
injectMember {
method {
name = "getEnable"
}
replaceToFalse()
}.onAllFailure {
loggerE(msg = "Hook getEnable fail: ${it.message}")
}
}
```
### 4. 看一看赋值的来源
查找一下引用就可以找到,在 `new ADSDKConfigResponseData` 传入的 `enable` 来自于 `transformSDk.getEnable()`。
所以,直接 hook 掉 `transformSDk.getEnable()` 比前面更为彻底。尽管代码行数没有任何变化,但是覆盖面要比前面的广。
```kotlin
"cn.xiaochuankeji.hermes.core.api.entity.ThirdSDKConfigResponse".hook {
injectMember {
method {
name = "getEnable"
}
replaceToFalse()
}.onAllFailure {
loggerE(msg = "Hook getEnable fail: ${it.message}")
}
}
```
### 5. 从热更新就拦截
其实,上面七七八八说了一大堆,都是得调用类的一些方法才可以达到效果。假如没有任何类,即下发的配置为空,那不就没有上面这么多事了。
于是,可以直接 hook 掉 `allRegisteredSDKConfigs`,使得返回的每次都是空的字典对。
```kotlin
"cn.xiaochuankeji.hermes.core.api.entity.ADConfigResponseDataKt".hook {
injectMember {
method {
name = "allRegisteredSDKConfigs"
paramCount = 1
}
beforeHook {
result = emptyMap<Any, Any>()
}
}.onAllFailure {
loggerE(msg = "Hook allRegisteredSDKConfigs fail: ${it.message}")
}
}
```
## 五、为什么使用 YukiHookAPI
```kotlin
package com.xxx.adfree
import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed
import com.highcapable.yukihookapi.hook.factory.configs
import com.highcapable.yukihookapi.hook.factory.encase
import com.highcapable.yukihookapi.hook.log.loggerE
import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit
@InjectYukiHookWithXposed
object HookEntry : IYukiHookXposedInit {
override fun onInit() = configs {
isDebug = false
}
override fun onHook() = encase {
loadApp("xxx") {
"yyy".hook {
injectMember {
method {
name = "getEnable"
}
replaceToFalse()
}.onAllFailure {
loggerE(msg = "Hook getEnable fail: ${it.message}")
}
}
}
}
}
```
以上代码通过 `YukiHookAPI` 完整的实现了一个 hook 的方式。可以看出,相比于原始的 Xposed 的方式,它可以自动管理包名,不需要手动修改 `array.xml` 来管理适配应用。而且写法也比较符合正常语言书写逻辑,不需要提供参数的具体类型,只要提供数量即可。错误捕捉日志打印也比较现代化,可以说一用就回不去了。
## 六、横向对比
目前已有的一些项目是通过 hook 每一个广告 SDK 的 `init` 方法实现的,代码行数比较多。而且,一旦广告 SDK 增加或删除就需要修改代码,不太能跟随原始软件的版本更新而保持通用,在最新版就已经失效了。
在我的仓库 ***(https://github.com/kazutoiris/zuiyou-adfree)*** 中将五种方法全部都开启了。实际上,采用任何一种都足以达到最后的效果。可以看到,我上文所写的每一种方法代码都只有 `~10` 行左右,并没有使用任何的 `magic number`,通用性也应当会好很多。
欢迎各位 `Issue`、 `Pull Requests`、 `Star`、 `Fork`。 bhwxha 发表于 2023-8-15 15:44
直接把广告初始化函数Hermes.init置空,不行吗?
直接置空会有两个问题
1. 类成员并没有完全初始化,部分还停留在 null。如果之后的代码中有对其进行操作,就会发生异常。
2. `Hermes.init` 还会初始除广告外的一些必要SDK及组件,所以就算是 hook 了这个函数,也需要在 afterHook 后重现其他组件的初始化。
所以为了尽可能简化代码和保证通用性,所以并没有直接对 `Hermes.init` 进行替换,而是使用了一些迂回技巧。 {:301_1003:}厉害,写得很详细,这款APP是很不错的,但使用的人越来越多,广告也越来越多,后来就卸载了,现在看到去广告的教程也不想折腾了,就怕捣鼓完后更新,一夜回到解放前。 路过点赞 可以很详细 感谢,愿所有app没有广告 路过看看 路过看看 路过看看 可以,学习到了,这就去试试 感谢楼主热心分享