某右的五种去广告思路暨 YukiHook 简介

查看 147|回复 11
作者:t00t00   
一、前言
在许多情况下,对一个软件进行 hook 有许多不同的角度。优秀的 hook 往往具有代码简洁且高度适应性的特点,能够在相当长的一段时间内适应原始软件的更新而不需要频繁修改。例如我之前的 某钉反撤回神器 仅使用不到 50 行代码就实现了功能,而且无需任何修改就已经适应了足足超过一年的版本更新。因此,hook 可以被视为一门精炼的艺术。本文旨在以点带面,提供多种思路供学习,通过小范例展示如何做到在不同层面上实现 hook,从而达到事半功倍的效果。
YukiHookAPI 是一个基于 Kotlin 的 hook 框架,它为实现精巧的 hook 功能和便捷的异常捕捉提供了强大支持。可以毫不夸张地称之为现代 hook 技术的首选之一。而在吾爱破解中似乎尚未有文章详细介绍这个强大的框架。
二、简介
某右是一款社交媒体应用,用户可以在平台上分享短视频、图片和文字,展示自己的创意和生活。用户可以互动,通过点赞、评论和分享来表达对内容的喜爱。此外,某右也提供了关注功能,用户可以关注感兴趣的其他用户,及时获取他们的更新内容。这款APP通过个性化的推荐算法,向用户推荐符合其兴趣的内容,使用户能够更好地发现有趣的话题和创作者。
鉴于某右APP存在大量广告,其中包括开屏广告、信息流广告以及评论区广告等形式,为提升使用体验,采取 hook 技术,以有效清除这些广告干扰。
三、工具
[ol]
  • jadx
  • Android Studio
  • YukiHookAPI
    [/ol]
    四、实现思路
    1. 小试牛刀
    现在用 jadx 加载导出的 dex 文件,在左边可以加载出所有的类。dex 文件可以用 frida 等得到,这里就不再赘述。


    image-20230814193524956.png (100.66 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:08 上传

    那么,这么多的类中哪一个才是我们需要的呢?
    首先,已知的情况是:广告都是由各种广告商提供并推送的。因此,如果一个软件能够显示广告,那么显然它已经集成了各类广告 SDK。
    于是,第一个思路就呼之欲出了:直接 hook 掉广告的 SDK 就可以了。


    image-20230814195541214.png (332.55 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:08 上传

    通过搜索,很容易就找到了 BaiduADProvider,这便是百度广告。通过搜索引用,可以发现,在 cn.xiaochuankeji.hermes.HermesSDK.install 使用了这一个类
    public static final void install(Hermes install, Application application, String appID, HermesADConfig hermesADConfig, boolean z, Callback[B] 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。


    image-20230814200502009.png (149.68 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:09 上传

    那么思路就是清空 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()
    可以看到这段代码有两个问题:
    [ol]
  • 代码行数太多,调用 clear() 就 1 行,却使用了 3 行来搜索 field。
  • Q 会随着版本更新或者加壳混淆工具的升级而自动重命名,并不具有通用性。例如重新加壳说不定就变成 R 等等了。
    [/ol]
    通过查找引用,在不远的之前可以找到一个函数 getProviderList$core_release,这个函数会返回同样的变量。


    image-20230814201757717.png (37.89 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:09 上传

    在这里,一方面是不用再通过反射调用 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 (24.57 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:09 上传

    直接返回为空就可以了,这样广告 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 (104.18 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:10 上传

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


    image-20230814203423225.png (214.09 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:10 上传

    检查引用可以看到,这个方法确实被使用,而且被用于状态切换。如果没有被设置为启用,就不会加载。所以直接 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 (43.52 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:10 上传

    所以,直接 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 (214.76 KB, 下载次数: 0)
    下载附件
    2023-8-14 21:11 上传

    于是,可以直接 hook 掉 allRegisteredSDKConfigs,使得返回的每次都是空的字典对。
    "cn.xiaochuankeji.hermes.core.api.entity.ADConfigResponseDataKt".hook {
        injectMember {
            method {
                name = "allRegisteredSDKConfigs"
                paramCount = 1
            }
            beforeHook {
                result = emptyMap()
            }
        }.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,通用性也应当会好很多。
    欢迎各位 Issue、 Pull Requests、 Star、 Fork。

    广告, 代码

  • t00t00
    OP
      


    bhwxha 发表于 2023-8-15 15:44
    直接把广告初始化函数Hermes.init置空,不行吗?
    直接置空会有两个问题
    [ol]
  • 类成员并没有完全初始化,部分还停留在 null。如果之后的代码中有对其进行操作,就会发生异常。
  • Hermes.init 还会初始除广告外的一些必要SDK及组件,所以就算是 hook 了这个函数,也需要在 afterHook 后重现其他组件的初始化。
    [/ol]
    所以为了尽可能简化代码和保证通用性,所以并没有直接对 Hermes.init 进行替换,而是使用了一些迂回技巧。
  • 昨日记忆丶   

    厉害,写得很详细,这款APP是很不错的,但使用的人越来越多,广告也越来越多,后来就卸载了,现在看到去广告的教程也不想折腾了,就怕捣鼓完后更新,一夜回到解放前。
    moruye   

    内容很详细,刚好捣鼓捣鼓
    daddy1   

    路过点赞
    orca007   

    可以很详细
    yiwangzhiqian   

    感谢,愿所有app没有广告
    zz726762565   

    路过看看
    年轻的旅途   

    路过看看
    Puremilk   

    路过看看
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部