吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5312|回复: 56
收起左侧

[Android 原创] 某右的五种去广告思路暨 YukiHook 简介

  [复制链接]
t00t00 发表于 2023-8-14 21:18
本帖最后由 t00t00 于 2023-8-14 21:18 编辑

一、前言

在许多情况下,对一个软件进行 hook 有许多不同的角度。优秀的 hook 往往具有代码简洁且高度适应性的特点,能够在相当长的一段时间内适应原始软件的更新而不需要频繁修改。例如我之前的 某钉反撤回神器 仅使用不到 50 行代码就实现了功能,而且无需任何修改就已经适应了足足超过一年的版本更新。因此,hook 可以被视为一门精炼的艺术。本文旨在以点带面,提供多种思路供学习,通过小范例展示如何做到在不同层面上实现 hook,从而达到事半功倍的效果。

YukiHookAPI 是一个基于 Kotlin 的 hook 框架,它为实现精巧的 hook 功能和便捷的异常捕捉提供了强大支持。可以毫不夸张地称之为现代 hook 技术的首选之一。而在吾爱破解中似乎尚未有文章详细介绍这个强大的框架。

二、简介

某右是一款社交媒体应用,用户可以在平台上分享短视频、图片和文字,展示自己的创意和生活。用户可以互动,通过点赞、评论和分享来表达对内容的喜爱。此外,某右也提供了关注功能,用户可以关注感兴趣的其他用户,及时获取他们的更新内容。这款APP通过个性化的推荐算法,向用户推荐符合其兴趣的内容,使用户能够更好地发现有趣的话题和创作者。

鉴于某右APP存在大量广告,其中包括开屏广告、信息流广告以及评论区广告等形式,为提升使用体验,采取 hook 技术,以有效清除这些广告干扰。

三、工具

  1. jadx
  2. Android Studio
  3. YukiHookAPI

四、实现思路

1. 小试牛刀

现在用 jadx 加载导出的 dex 文件,在左边可以加载出所有的类。dex 文件可以用 frida 等得到,这里就不再赘述。

image-20230814193524956.png

那么,这么多的类中哪一个才是我们需要的呢?

首先,已知的情况是:广告都是由各种广告商提供并推送的。因此,如果一个软件能够显示广告,那么显然它已经集成了各类广告 SDK。

于是,第一个思路就呼之欲出了:直接 hook 掉广告的 SDK 就可以了。

image-20230814195541214.png

通过搜索,很容易就找到了 BaiduADProvider,这便是百度广告。通过搜索引用,可以发现,在 cn.xiaochuankeji.hermes.HermesSDK.install 使用了这一个类

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 行初始化了 PangleADProviderTencentADProviderMimoADProviderXinguADProviderKuaishouADProviderJingdongADProviderXcADProviderBaiduADProviderQuMengADProviderGroMoreADProviderTanxADProvider

image-20230814200502009.png

那么思路就是清空 providers

但是问题来了,怎么清空呢?由于 ADProvider[] 是一种不可变类型,所以是没有 clear() 方法的,因此直接操作传入参数不太方便。

于是,看到第 3 行,provider 转换为 MutableList 并保存在 f5876Q 中。MutableList 就可变了,内置了 clear 方法,这可比改传入参数方便多了。那么,这时候可以在 afterHook 通过反射调用 f5876Q 的 clear() 方法。

不过,这样写并不优雅,以下提供了一小段 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,这个函数会返回同样的变量。

image-20230814201757717.png

在这里,一方面是不用再通过反射调用 clear(),其 result 原生就支持 clear();另一方面,这个函数名是加壳混淆不会进行修改的,因此具有一定的通用性。

"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 不就不能拉取到广告了嘛。

于是,很容易的就找到了配置的位置。

image-20230814202623139.png

直接返回为空就可以了,这样广告 SDK 就没办法获得正确的配置,也就无法展示广告了。

这种 hook 方式比之前虽然代码量增加了,但是写起来更为干净,只需要处理基本的数据结构,而不需要进行 List 等等类的处理。

"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 方法。

image-20230814203249661.png

首先,需要确认以下这个方法有没有被使用。往往 IDE 会自动生成所有字段的 getter/setter 方法,有些不一定会用到。所以不能够盲目地 hook。

image-20230814203423225.png

检查引用可以看到,这个方法确实被使用,而且被用于状态切换。如果没有被设置为启用,就不会加载。所以直接 hook 这个方法,返回 false 就行了。

因此,可以写出新的代码。新的代码就比上面更少了。

"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()

image-20230814204052998.png

所以,直接 hook 掉 transformSDk.getEnable() 比前面更为彻底。尽管代码行数没有任何变化,但是覆盖面要比前面的广。

"cn.xiaochuankeji.hermes.core.api.entity.ThirdSDKConfigResponse".hook {
    injectMember {
        method {
            name = "getEnable"
        }
        replaceToFalse()
    }.onAllFailure {
        loggerE(msg = "Hook getEnable fail: ${it.message}")
    }
}

5. 从热更新就拦截

其实,上面七七八八说了一大堆,都是得调用类的一些方法才可以达到效果。假如没有任何类,即下发的配置为空,那不就没有上面这么多事了。

image-20230814204642521.png

于是,可以直接 hook 掉 allRegisteredSDKConfigs,使得返回的每次都是空的字典对。

"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

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 增加或删除就需要修改代码,不太能跟随原始软件的版本更新而保持通用,在最新版就已经失效了。

在我的仓库 kazutoiris/zuiyou-adfree 中将五种方法全部都开启了。实际上,采用任何一种都足以达到最后的效果。可以看到,我上文所写的每一种方法代码都只有 ~10 行左右,并没有使用任何的 magic number,通用性也应当会好很多。

欢迎各位 IssuePull RequestsStarFork

免费评分

参与人数 26威望 +1 吾爱币 +49 热心值 +25 收起 理由
status_0 + 1 + 1 我很赞同!
phoenixlhs + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Rytter + 1 + 1 谢谢@Thanks!
jnb20000 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
alentonly + 1 + 1 我很赞同!
Imabetsu + 1 + 1 我很赞同!
xxxesa6xxx + 1 + 1 谢谢@Thanks!
Yangzaipython + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
sifeng + 1 + 1 谢谢@Thanks!
Project430 + 1 + 1 我很赞同!
nobody0721 + 1 + 1 我很赞同!
aerian + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
as614001 + 1 + 1 我很赞同!
昨日记忆丶 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
yizhiyuanmo007 + 1 用心讨论,共获提升!
ufldh + 1 + 1 热心回复!
林伊轩 + 3 + 1 大牛!!
忆魂丶天雷 + 2 + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Phantom可 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jyboyabc + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
又飘小雪 + 1 + 1 我很赞同!
Some + 1 + 1 我很赞同!
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
hkq666 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| t00t00 发表于 2023-8-16 13:23
bhwxha 发表于 2023-8-15 15:44
直接把广告初始化函数Hermes.init置空,不行吗?

直接置空会有两个问题

  1. 类成员并没有完全初始化,部分还停留在 null。如果之后的代码中有对其进行操作,就会发生异常。
  2. Hermes.init 还会初始除广告外的一些必要SDK及组件,所以就算是 hook 了这个函数,也需要在 afterHook 后重现其他组件的初始化。

所以为了尽可能简化代码和保证通用性,所以并没有直接对 Hermes.init 进行替换,而是使用了一些迂回技巧。

昨日记忆丶 发表于 2023-8-15 15:47
厉害,写得很详细,这款APP是很不错的,但使用的人越来越多,广告也越来越多,后来就卸载了,现在看到去广告的教程也不想折腾了,就怕捣鼓完后更新,一夜回到解放前。
头像被屏蔽
moruye 发表于 2023-8-14 21:38
daddy1 发表于 2023-8-14 21:48
路过点赞
orca007 发表于 2023-8-14 22:01
可以很详细
yiwangzhiqian 发表于 2023-8-14 22:06
感谢,愿所有app没有广告
zz726762565 发表于 2023-8-14 22:07
路过看看
年轻的旅途 发表于 2023-8-14 22:26
路过看看
Puremilk 发表于 2023-8-14 22:29
路过看看
BonnieRan 发表于 2023-8-14 22:37
可以,学习到了,这就去试试
zf1391834498 发表于 2023-8-14 22:52
感谢楼主热心分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-1 03:15

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表