用 Frida 给一个 Android App 去广告:从抓包到 Hook

逆向工程 · 7 分钟阅读· 10 次阅读

这篇记录一次纯粹为了自己用着舒服的逆向练习:给手机上一个工具类 App 去广告。

声明:只在自己设备上、对已安装的应用做本地分析,不修改分发、不绕过付费功能。 目的是学习动态调试。

一、先看流量

第一步不是反编译,是看这个 App 在干什么。

手机连电脑,用 Charles 抓包(Android 7 以上需要装用户证书到系统证书区,或者用 root + 迁移)。

开屏广告的特征很明显:启动后 1~2 秒内,有一个请求打到广告 SDK 的域名,返回 JSON 里带图片地址和落地页。

记录几个关键域名:

api.xxx-ads.com
img.xxx-cdn.com

再看请求内容,有个参数叫 slot_id,不同位置(开屏、banner、信息流)值不一样。

这一步的价值:确定了广告是远程下发的,不是本地硬编码。 所以去广告的思路有两条:

  1. 拦截网络请求(需要 root + hosts 或 VPN)
  2. Hook 广告 SDK 的方法,让它拿不到数据

我选第二条,因为 hosts 拦截会导致 App 某些功能异常(有些 SDK 把广告请求和配置请求放同一个域名)。

二、反编译,找 SDK

apktool d app.apk -o app_src

先看 AndroidManifest.xml 里注册了哪些 SDK:

<meta-data android:name="com.xxx.ads.APP_ID" android:value="..."/>

拿到 SDK 包名,比如 com.xxx.ads。

再用 jadx 打开 dex,搜索这个包名,找到核心类。通常的结构是:

com.xxx.ads
  ├─ AdManager        广告管理器(加载、展示)
  ├─ AdLoader         加载器
  ├─ AdView           视图
  └─ AdListener       回调

关键类是 AdManager,看它的方法:

public class AdManager {
    public void loadSplashAd(AdSlot slot, AdListener listener) { ... }
    public boolean isAdReady() { ... }
    public void show() { ... }
}

三、Frida Hook

思路:让 loadSplashAd 直接回调「加载失败」,App 就会走无广告的分支。

先起 frida-server(需要 root):

adb push frida-server /data/local/tmp/
adb shell "su -c '/data/local/tmp/frida-server &'"

写 hook 脚本:

Java.perform(function () {
    var AdManager = Java.use('com.xxx.ads.AdManager');

    // 让加载直接失败
    AdManager.loadSplashAd.implementation = function (slot, listener) {
        console.log('[hook] loadSplashAd intercepted, slot=' + slot);
        if (listener !== null) {
            // 走「加载失败」回调
            listener.onAdFailed(0, 'no fill');
        }
        return;
    };

    // 兜底:即使有缓存也不展示
    AdManager.show.implementation = function () {
        console.log('[hook] show blocked');
    };
});

运行:

frida -U -f com.xxx.app -l hook.js --no-pause

如果 onAdFailed 的方法签名不对,Frida 会报 Error: expected argument types。这时候用 Java.enumerateMethods 列出所有重载:

Java.enumerateMethods('*AdListener*!*/onAdFailed*/isu')

四、遇到的两个问题

问题一:类被混淆了

第一版脚本报 ClassNotFoundException。用 jadx 看到的类名是 com.xxx.ads.AdManager,但运行时找不到。

原因:App 用了 R8/ProGuard 混淆,jadx 显示的是反混淆后的名字(如果开了 deobfuscation 或者有 mapping 文件),运行时实际的类名是 a.b.c。

解决:在运行时枚举类:

Java.enumerateLoadedClasses({
    onMatch: function (name) {
        if (name.indexOf('xxx') !== -1) console.log(name);
    },
    onComplete: function () {}
});

或者用 Java.choose 遍历实例。

问题二:SDK 有完整性校验

Hook 上去以后,广告没了,但 App 某些功能报错(「网络异常」)。

查日志发现,SDK 启动时会校验自己的签名:

// 伪代码
if (!Signature.check(context)) {
    throw new RuntimeException("invalid");
}

解决:Hook 掉校验方法,让它永远返回 true:

var Sig = Java.use('com.xxx.ads.utils.Signature');
Sig.check.implementation = function (ctx) {
    return true;
};

这一步之后功能恢复正常。

这也说明:广告 SDK 的自我保护比很多付费功能还严。

五、结果

开屏广告没了,banner 位置变成空白(不是布局塌陷,App 有占位处理)。

副作用:

  • 某个「签到领积分」功能会检查广告观看记录,去广告后领不了(无所谓)
  • App 启动快了一点(少了广告图片下载)

六、这件事学到了什么

1. 抓包永远第一步。 不抓包直接反编译,容易在几万个类里迷失。

2. 混淆不是不可绕过。 运行时枚举类名比静态分析有效。

3. Hook 的位置比 Hook 的方法重要。 我第一版 Hook 的是 show(),结果广告不显示但 SDK 认为展示成功,会触发「曝光」上报,反而让数据更脏。Hook 在「加载」这一步更干净。

4. 完整性校验无处不在。 现代 SDK 基本都有,Hook 之前先想好要不要一起处理。

七、防守视角

反过来想,如果我是这个 App 的开发者,怎么防?

  • 广告请求和业务请求混在一起,让「拦截网络」的成本变高
  • SDK 加签名校验和运行时检测(Frida 检测:扫 /proc/self/maps 里的 frida 字样)
  • 关键逻辑放 native 层(so),提高逆向成本

但要承认:只要在用户设备上运行,就没有绝对的安全。所有这些手段只是提高成本。

对于我这样的普通用户,用 Frida 去掉自己手机上一个 App 的广告,属于「修改自己设备上的运行状态」,不涉及破解付费功能、不涉及分发修改版。这个边界要清楚。

评论 (0)

还没有评论,来说两句吧~

发表评论

验证码

提交后需站长审核,通过后才会显示。