Unity手游APK去广告实战:华为渠道包反编译、广告拦截与“网络不佳“弹窗修复全记录

Unity手游APK去广告实战:华为渠道包反编译、广告拦截与"网络不佳"弹窗修复全记录

本文仅用于技术学习与安全研究,请遵守相关法律法规,尊重开发者劳动成果。修改后的APK仅限个人学习测试使用。

摘要

朋友拿了一个华为渠道的手游 APK------《切割模拟器》(一款休闲解压类游戏,Unity 引擎,约 148MB)------希望帮忙去掉广告。整个过程中踩了不少坑:先是定位广告 SDK 的调用链,找到拦截点后成功去掉全部广告;随后又遇到改包后"一直提示网络不佳,请检查网络设置"的弹窗问题,最终定位到是华为渠道 SDK 的签名校验失败导致。本文完整记录了从工具链搭建、APK 结构分析、广告拦截点挖掘、smali 补丁编写,到重新打包签名验证的全过程,希望能给同样在做 APK 分析学习的同学一些参考。


一、环境与工具准备

1.1 原始材料

复制代码
com.ylly.qgmnq.huawei.apk   (约 147MB,华为渠道)

1.2 工具链

Windows 环境下需要以下工具:

工具 用途 说明
JDK 17 (Temurin) Java 运行环境 apktool/jadx 均基于 Java
apktool 2.11.1 APK 解码/回编译 将 dex 反汇编为 smali,改完再回编译
jadx 1.5.1 反编译为 Java 源码 快速阅读逻辑,定位广告代码
uber-apk-signer 1.3.0 重签名 + zipalign 一步完成签名与对齐

踩坑提示:国内网络直连 GitHub 不稳定,可用 gh-proxy.com 等镜像加速下载:

bash 复制代码
curl -L -o apktool.jar "https://gh-proxy.com/https://github.com/iBotPeaches/Apktool/releases/download/v2.11.1/apktool_2.11.1.jar"

1.3 反编译

bash 复制代码
# 1. 解码 APK(得到 smali 代码 + 资源 + Manifest)
java -jar apktool.jar d -f -o decoded com.ylly.qgmnq.huawei.apk

# 2. jadx 转 Java 源码(便于阅读)
jadx -d jadx_out --no-res com.ylly.qgmnq.huawei.apk

二、APK 结构初探

2.1 文件清单分析

bash 复制代码
unzip -l com.ylly.qgmnq.huawei.apk | grep -E "classes[0-9]*\.dex|\.so$"

关键发现:

复制代码
classes.dex        6.2MB   ← Java 层代码(SDK 主要在这)
classes2.dex       325KB
libil2cpp.so       54MB    ← Unity IL2CPP 游戏逻辑(C# 编译产物,不可直接改)
libunity.so        18MB

结论 :这是一个 Unity + IL2CPP 游戏。游戏本体逻辑在 libil2cpp.so 里(无法像 DEX 那样直接改),但广告 SDK 是纯 Java 实现,全部在 classes.dex 中------这就是突破口。

2.2 广告 SDK 识别

AndroidManifest.xml 中能明显看到华为广告 SDK 的痕迹:

xml 复制代码
<meta-data android:name="com.huawei.hms.client.service.name:ads-banner"
           android:value="ads-banner:13.4.82.300"/>
<meta-data android:name="com.huawei.hms.client.service.name:ads-splash"
           android:value="ads-splash:13.4.82.300"/>
...
<activity android:name="com.huawei.openalliance.ad.activity.PPSLauncherActivity"/>

同时资源目录下有一堆 hiad_* 前缀的 drawable(华为广告素材),以及 splash_layout 等广告布局。

再结合 assets/lt_plugin_config.xml 确认了完整的 SDK 插件结构:

xml 复制代码
<plugins>
  <!-- 渠道插件:华为 -->
  <plugin name="com.lt.sdk.channel.huawei.HuaweiChannel" type="channel" .../>
  <!-- 广告插件:华为广告 -->
  <plugin name="com.lt.sdk.ad.huawei.HuaweiAd" type="ad" .../>
  <!-- 其他插件:游客登录、实名、统计 -->
  <plugin name="com.lt.sdk.other.vl.VisitorLogin" type="other" .../>
</plugins>

这是一个典型的 MAR SDK(国内手游聚合 SDK) 架构:渠道、广告、账号、统计全部插件化。


三、广告调用链分析(核心)

3.1 从 Unity 桥接到广告展示

游戏 C# 代码(il2cpp)通过 Unity 的 AndroidJavaObject 机制调用 Java 层。入口是 MarMainActivity.ReciveMessageFromUnity(int type, String data)

java 复制代码
// MarMainActivity.java(jadx 反编译)
public void ReciveMessageFromUnity(int Type, String data) {
    FromUnity fromUnity = FromUnity.getTypeByKeys(Type);
    switch (...) {
        case 5:  showInters();        break;  // 插屏广告
        case 6:  showVideo();         break;  // 激励视频
        case 8:  showBanner(num);     break;  // 横幅广告
        case 9:  showSplash();        break;  // 开屏广告
        case 27: showNativeInters();  break;  // 原生插屏
        ...
    }
}

完整调用链:

复制代码
Unity C# (il2cpp)
  → MarMainActivity.ReciveMessageFromUnity()
    → MARGgPlatform.showXxx()          // MAR SDK 广告网关
      → SDKAdPlatform.showXxx()        // 广告平台分发
        → PluginManager → HuaweiAd    // 华为广告插件
          → HuaweiAdCtrl (extends AdCtrl)
            → AdHandler.show()         // ★ 广告处理器基类
              → 华为 HMS 广告 SDK → 渲染广告

3.2 关键发现:所有广告都要过 isReady()

通读 AdCtrl(广告控制器基类,1500 行)后,发现所有广告类型的展示路径都依赖同一个方法

java 复制代码
// AdHandler.java ------ 广告处理器基类
public boolean isReady() {
    this.loadFailTimes = 0;
    return this.loadState == AdLoadState.LOADED;  // 广告加载成功才算就绪
}

它在全代码库中被引用 14 处,覆盖所有广告类型:

java 复制代码
// 插屏广告
public boolean getIntersFlag() {
    ...
    if (adHandler.isReady()) { hasReady = true; ... }
    return canShow && hasReady;
}

// 激励视频
public boolean getVideoFlag() {
    ...
    if (adHandler.isReady()) { hasReady = true; ... }
    return canShow && hasReady;
}

// 横幅
public void showBanner(int pos) {
    ...
    if (adHandler.isReady()) { hasReady = true; this.canShowBannerList.add(adHandler); }
    if (canShow && hasReady) { this.bannerShowing.show(); }
}

// 弹窗广告(ejectAd)
public void ejectAd(AdType type, AdInstType instType, boolean isReplenish, IEjectAdCallback cb) {
    ...
    if (adHandler.isReady()) { hasReady = true; canShowAdHandlers.add(adHandler); }
    if (!hasReady) { cb.onEjectAdResult(false, "not exist load success ad"); return; }
    ...
}

这就是最优雅的拦截点 :只要让 isReady() 永远返回 false,所有广告都无法通过展示检查,而 API 结构完全保留,游戏不会崩溃。

3.3 为什么不直接删掉广告 SDK?

游戏 C# 代码(il2cpp)里仍会请求广告。若直接删掉华为广告 SDK 的类,运行时 ClassNotFoundException 直接闪退。"堵门"优于"拆门"------让广告永远"未就绪",比让它不存在更安全。

3.4 回调机制的妙用

拦截后游戏为什么不会卡死?因为 SDK 的失败回调仍然触发,游戏会走"广告未准备好/已关闭"的正常分支:

复制代码
游戏请求插屏 → isReady()=false → showInters() 触发 onResult(1002, "广告已关闭")
  → MARSDK → callUnity(LoginSuccess 同通道) → 游戏 C# 继续流程 ✓

游戏请求激励视频 → isReady()=false → showVideo() 触发 onResult(1004, "广告未准备好")
  → 游戏弹提示后继续 ✓

四、smali 补丁编写(5处广告拦截)

4.1 核心补丁:AdHandler.isReady() 永远返回 false

找到反编译产物中的 smali:

smali 复制代码
# decoded/smali/com/lt/sdk/base/plugin/ad/AdHandler.smali
.method public isReady()Z
    .locals 3
    const/4 v0, 0x0
    iput v0, p0, Lcom/lt/sdk/base/plugin/ad/AdHandler;->loadFailTimes:I
    iget-object v1, p0, Lcom/lt/sdk/base/plugin/ad/AdHandler;->loadState:Lcom/lt/sdk/base/model/AdLoadState;
    sget-object v2, Lcom/lt/sdk/base/model/AdLoadState;->LOADED:Lcom/lt/sdk/base/model/AdLoadState;
    if-ne v1, v2, :cond_0
    const/4 v0, 0x1
    :cond_0
    return v0
.end method

替换为:

smali 复制代码
.method public isReady()Z
    .locals 1
    const/4 v0, 0x0
    return v0
.end method

smali 小知识:return v0 返回的是 v0 寄存器中的值;const/4 v0, 0x0 把 0 赋给 v0。.locals 1 声明 1 个局部寄存器(v0),参数 p0 是 this。注意局部寄存器数量必须 ≥ 实际使用的寄存器编号+1,否则回编译报错。

4.2 开屏广告补丁(2处)

开屏广告不走 isReady(),是直接加载 SplashView 的,需要单独处理:

SplashAdActivity.onCreate() ------ 启动时的华为开屏广告页:

smali 复制代码
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 1
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    invoke-direct {p0}, Lcom/lt/sdk/ad/huawei/SplashAdActivity;->jump()V   # 直接跳到游戏启动页

    return-void
.end method

HuaweiAdCtrl.testShowSplashAd() ------ 从后台切回前台时触发的开屏,改为直接恢复游戏:

smali 复制代码
.method public testShowSplashAd()V
    .locals 3

    invoke-static {}, Lcom/lt/sdk/base/control/AdControl;->getInstance()Lcom/lt/sdk/base/control/AdControl;
    move-result-object v0
    const/4 v1, 0x1
    invoke-virtual {v0, v1}, Lcom/lt/sdk/base/control/AdControl;->setInoutReason(I)V

    invoke-static {}, Lcom/lt/sdk/base/control/AdControl;->getInstance()Lcom/lt/sdk/base/control/AdControl;
    move-result-object v0
    iget-object v1, p0, Lcom/lt/sdk/ad/huawei/HuaweiAdCtrl;->context:Landroid/app/Activity;
    const/16 v2, 0xfc
    invoke-virtual {v0, v2, v1}, Lcom/lt/sdk/base/control/AdControl;->sendActivityListener(ILandroid/app/Activity;)V

    return-void
.end method

4.3 原生插屏弹窗补丁(2处)

游戏内还有一种"原生插屏"(伪装成原生 UI 的广告弹窗),它不经过 AdHandler ,是单例直接加载的。它的 getFlag() 决定能否展示:

smali 复制代码
# SpecialNativeIntersAd.smali / SpecialNativeInters2Ad.smali
# 原始实现遍历 isLoadSuccess[] 数组,任一成功即返回 true
.method public getFlag()Z
    .locals 1
    const/4 v0, 0x0
    return v0
.end method

4.4 补丁清单汇总

# 方法 修改
1 AdHandler isReady() 恒 false(拦截横幅/插屏/视频/悬浮/原生等全部)
2 SplashAdActivity onCreate() 跳过启动开屏
3 HuaweiAdCtrl testShowSplashAd() 跳过回前台开屏
4 SpecialNativeIntersAd getFlag() 恒 false(原生插屏样式1)
5 SpecialNativeInters2Ad getFlag() 恒 false(原生插屏样式2)

五、重新打包与签名

5.1 回编译

bash 复制代码
java -jar apktool.jar b -f -o nopad-unsigned.apk decoded

5.2 签名(uber-apk-signer 一步完成)

bash 复制代码
java -jar uber-apk-signer.jar -a nopad-unsigned.apk --overwrite

输出确认:

复制代码
- zipalign verified
- signature verified [v2, v3]

六、踩坑:改包后"一直提示网络不佳"

6.1 现象

装上修改版后,游戏反复弹出"网络不佳,请检查网络设置",点"确定"重试又弹,无限循环。

6.2 排查过程

第一步:全代码库搜索该字符串

bash 复制代码
grep -rn "网络不佳" jadx_out/sources/

只有一处:

java 复制代码
// HuaWeiRNDialog.java
content.setText("网络不佳,请检查网络设置");

第二步:查这个 Dialog 的触发点

它是华为渠道 SDK(账号/游戏服务)在 init / login / 实名验证 失败时弹的。也就是说------根本不是广告加载失败,而是华为渠道初始化失败

第三步:确认根因

华为游戏服务(HMS JosApps)初始化时会校验应用签名是否与华为应用市场登记的一致。我们改包后用调试签名重新签名,签名不匹配 → 初始化返回错误码 → 弹窗 → 点击重试 → 再次失败 → 死循环。

再交叉验证:IL2CPP 的 global-metadata.dat(包含所有 C# 字符串字面量)里搜"网络"相关字符串,一个都没有,进一步证明提示来自 Java 侧 SDK,而非游戏逻辑。

6.3 修复思路

改包后的 APK 本质上是"华为账号体系不可用"的,硬走华为登录永远失败。正确做法是让 SDK 不再依赖华为服务器

  1. 禁弹窗HuaWeiRNDialog.showTip() → 空操作
  2. 初始化永远成功HuaweiSDK.initGameService() → 不再调用 HMS,直接置 initSuccess=true 并上报 onInitResult(10)
  3. 登录走游客回退HuaweiSDK.login() → 直接回调空结果,触发 SDK 内置的游客登录 插件(VisitorLogin 已在插件配置中注册,是 SDK 的标准降级机制)
smali 复制代码
# HuaweiSDK.login() 补丁
.method public login()V
    .locals 2
    iget-object v0, p0, Lcom/lt/sdk/channel/huawei/HuaweiSDK;->channelListener:Lcom/lt/sdk/base/plugin/channel/IChannelListener;
    if-eqz v0, :cond_skip
    const-string v1, ""
    const-string v2, "visitor login"
    invoke-interface {v0, v1, v2}, Lcom/lt/sdk/base/plugin/channel/IChannelListener;->onLoginResult(Ljava/lang/String;Ljava/lang/String;)V
    :cond_skip
    return-void
.end method

6.4 修复后的自动登录链路

复制代码
启动 → 隐私协议 → (跳过广告开屏) → 游戏启动页 → 主界面
  ↓
华为SDK init → 直接成功 → SDK自动触发登录(onInitResult(10) 钩子)
  ↓
login → 空结果 → handleChannelLoginResult 检测到游客插件
  ↓
游客登录成功 (visitorId/visitorToken) → callUnity(LoginSuccess) → 进入游戏

这个链路是 SDK 内部自动完成的:SDKPlatform.setSelfSDKListener() 里注册了 onInitResult(10) 后自动调用 PluginManager.login() 的逻辑。所以改完这三个方法后,不需要游戏端做任何配合。


七、最终验证

7.1 补丁验证(对最终 APK 二次反编译)

用 apktool 重新解码签名后的 APK,逐一确认 8 处补丁都在:

复制代码
[广告拦截 - 5处]
1) AdHandler.isReady            → return false
2) SpecialNativeIntersAd.getFlag → return false
3) SpecialNativeInters2Ad.getFlag → return false
4) HuaweiAdCtrl.testShowSplashAd → 跳过开屏
5) SplashAdActivity.onCreate     → 直接 jump

[网络不佳弹窗修复 - 3处]
6) HuaWeiRNDialog.showTip        → no-op
7) HuaweiSDK.initGameService     → 永远成功
8) HuaweiSDK.login               → 游客登录回退

7.2 签名与完整性验证

bash 复制代码
# zip 完整性
unzip -t final.apk            → OK

# 签名 + 对齐
uber-apk-signer -a final.apk --onlyVerify
  → zipalign verified
  → signature verified [v2, v3]

# 文件数与资源一致性(原包 837 个文件,改后仍 837 个)

7.3 效果

  • ✅ 开屏广告:直接跳过
  • ✅ 插屏/弹窗广告:不再弹出
  • ✅ 激励视频:不再播放
  • ✅ 横幅/悬浮/原生广告:全部不显示
  • ✅ "网络不佳"弹窗:消除
  • ✅ 游戏正常进入(游客模式)

八、总结与思考

  1. 找对拦截点比蛮力删除重要 :一个 isReady() 方法拦截了 90% 的广告类型,这就是"关键路径分析"的价值------先梳理调用链,找到所有广告类型的公共汇聚点。

  2. 改包的本质是"与签名共存亡":任何渠道包改签名后,渠道 SDK(华为、小米、OPPO 等)的账号/支付/云服务都会失效,这是平台侧的校验,无法绕过(也不应该绕过)。本文的做法是让 SDK 走自身的降级路径(游客登录),而不是伪造渠道凭据。

  3. IL2CPP 游戏改 Java 层的优势:游戏逻辑在原生库中无法修改,但 SDK 层是 Java,改动后对游戏透明------这是此类"改 SDK 不改游戏"方案的通用价值。

  4. 工具链:apktool(smali 级修改)+ jadx(源码阅读)+ uber-apk-signer(签名)是 APK 修改的黄金组合,全部开源免费。


附:工具下载地址

工具 地址
apktool https://github.com/iBotPeaches/Apktool/releases
jadx https://github.com/skylot/jadx/releases
uber-apk-signer https://github.com/patrickfav/uber-apk-signer/releases
Temurin JDK https://adoptium.net/

再次声明:本文技术内容仅用于学习研究。请勿将修改后的 APK 用于商业传播或侵权用途,支持正版,尊重每一位开发者的劳动。

详细APK见资源

相关推荐
k4m7v2pz19 分钟前
Godot 4 嵌入式试玩窗口尺寸与适配策略详解
编辑器·游戏引擎·godot·游戏开发·嵌入式运行
Behaviour12 小时前
Unity UI循环列表UIScrollViewContent实现
ui·unity·c#·游戏引擎
_ZHOURUI_H_1 天前
Unity EasyECS:并不是所有字段都适合 SoA,Unity 项目中应该怎样拆数据
游戏·unity·性能优化·架构·游戏引擎
TO_ZRG1 天前
Unity 编辑器之 SettingsProvider、Hierarchy、Project、SearchField
java·unity·编辑器
yj爆裂鼓手1 天前
spine绑定骨骼运动工具&spine纹理压缩自动设置工具
游戏·unity
惊鸿醉2 天前
Unity 实战:用讯飞 WebAPI 做一个“说普通话、播方言“的语音程序
unity·c#·游戏引擎·语音识别
略略略咯咯2 天前
C#Unity
开发语言·unity·c#
小贺儿开发2 天前
Unity 局域网遥控幻灯片展示工具 1.0
unity·网络通信·工具·ppt·控制·网页·互动
WarPigs2 天前
Unity未激活的物体无法响应事件?
java·unity·游戏引擎