某公交 APK 逆向去广告全流程总结

3个月前

某公交 APK 逆向去广告全流程总结

一、项目概述

  • 目标APP: 某公交 (cn.xxx.pay) v3.1.4
  • APK大小: 169MB
  • APP类型: Flutter (Dart AOT编译)
  • 保护方案: 百度加固 (BaiduProtect/Sagittarius) v6.9.4
  • 签名方式: APK Signature Scheme v2+v3
  • 目标: 移除广告弹窗 + 移除权限弹窗 + 去除百度加固

二、环境准备

2.1 工具安装

工具 版本 用途
apktool 2.10.0 APK反编译/重打包
jadx 1.5.0 Java源码反编译(查看)
aapt/aapt2 系统自带 Android资源处理
zipalign 系统自带 APK对齐
apksigner 系统自带 APK签名
openjdk-17 JRE+JDK Java运行环境
gcc-aarch64-linux-gnu 9.4.0 ARM64交叉编译
adb Android SDK 真机调试
sudo apt install -y openjdk-17-jdk-headless
sudo apt install -y gcc-aarch64-linux-gnu
sudo apt install -y adb

# apktool
sudo wget https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar -O /usr/local/bin/apktool.jar
sudo wget https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool -O /usr/local/bin/apktool
sudo chmod +x /usr/local/bin/apktool /usr/local/bin/apktool.jar

# jadx
wget https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip
unzip jadx-1.5.0.zip -d /tmp/jadx
sudo cp /tmp/jadx/bin/jadx /usr/local/bin/
sudo cp -r /tmp/jadx/lib /usr/local/share/jadx/

2.2 测试设备

  • 小米手机 (MIUI, arm64-v8a)
  • 已Root (su可用)
  • USB调试已开启
  • adb连接正常

三、APK结构分析

3.1 反编译

apktool d -f mzt.apk -o mzt_decoded

3.2 DEX文件结构

DEX文件 内容 大小
classes.dex 百度加固壳代码(sagittarius) + 第三方库(alibaba/alipay/bumptech) 7.8MB
classes2.dex bumptech + bytedance(穿山甲) + google 6.0MB
classes3.dex google + huawei + kwad(快手) + kwai + qq(腾讯广告) 5.3MB
classes4.dex ss(下载) + ta + tencent + unionpay 2.4MB
classes5.dex Flutter引擎 + kotlin + okhttp + Flutter插件注册 6.6MB

3.3 百度加固结构

assets/
├── baiduprotect1~6.jar        # 加密的业务dex (共4.6MB)
├── baiduprotect1~6.i.dex      # dex索引 (ZIP包含空壳dex)
├── baiduprotect-sec.dex        # 安全配置
├── baiduprotect.md             # 加密配置
└── libbaiduprotect_x86/x86_64  # x86 so库

lib/
├── arm64-v8a/libbaiduprotect.so    # 核心native库 (OLLVM混淆)
└── armeabi-v7a/libbaiduprotect.so

smali/com/sagittarius/v6/
├── StubApplication.smali    # 壳Application (替换原始Application)
├── A.smali                  # native方法声明 (n1/n2/n3)
├── AppInfo.smali            # APP信息 (包名/so名/路径)
└── b/                       # 加固工具类
    ├── f.smali              # 初始化管理器
    ├── t.smali              # so加载
    ├── h.smali              # dex加载
    └── g.smali              # Application实例化

3.4 广告SDK识别

通过AndroidManifest.xml和smali代码分析,识别出9个广告SDK:

SDK 包名 类型
穿山甲/Pangle com.bytedance.sdk.openadsdk 开屏/插屏/激励视频
Sigmob com.sigmob.sdk 开屏/插屏
Windmill聚合 com.windmill.windmill_ad_plugin Flutter广告桥接
快手Kwad com.kwad.sdk 开屏/全屏/激励视频
腾讯优量汇GDT com.qq.e.ads 横幅/插屏/激励视频
百度Mobads com.baidu.mobads.sdk 激励视频
美数Meishu com.meishu.sdk 插屏/激励视频
章鱼Octopus com.octopus.ad 插屏/浏览器广告
趣盟Qumeng com.qumeng.advlib 插屏广告

四、百度加固签名校验分析与绕过

4.1 签名校验机制

百度加固有两层签名校验:

  1. n1()初始校验: 在StubApplication.attachBaseContext()中调用native方法A.n1(),校验APK签名+解密dex
  2. 后台检测线程: APP启动后约1秒,Thread-9检测线程触发XOX state=519,发现签名不匹配后发送signal 64杀死进程

4.2 StubApplication启动流程

StubApplication.<clinit>()
  └─ b/f.init()                          # 加载加固模块

StubApplication.attachBaseContext(context)
  ├─ b/t.a(app, context)                 # 设置环境变量
  ├─ AppInfo.APKPATH = sourceDir         # 获取APK路径
  ├─ b/t.b()                             # 加载libbaiduprotect.so
  ├─ A.n1(context, pkg, apkpath, ...)    # ★签名校验+dex解密
  ├─ 加载原始Application                  # 实例化FlutterApplication
  ├─ b/g.a(context, application)          # 设置Application
  └─ b/f.attachBaseContext(...)           # 执行加固模块的attachBaseContext

4.3 FakeAPK方案

核心思路: 让n1()读取到原始签名的APK文件

  1. 创建伪造APK (7.5MB),包含原始APK的签名块 + baiduprotect*.jar + classes.dex
  2. 修改StubApplication,在n1()调用前将APKPATH指向伪造APK
  3. n1()从伪造APK验证签名(通过) + 解密dex(正常)
  4. APP正常运行
# 创建伪造APK
# 包含: 原始classes.dex + baiduprotect*.jar + 原始签名块

4.4 内存脱壳

目的: 提取百度加固解密后的真实业务dex

编写C程序(dex_dumper),编译为ARM64静态二进制:

// 核心流程:
// 1. 等待APP进程出现
// 2. kill -STOP 暂停进程
// 3. 读取 /proc/pid/maps
// 4. 搜索 "dalvik-DEX data" 内存区域
// 5. 在内存中搜索 "dex\n" 头
// 6. 读取dex header中的file_size
// 7. dump完整dex到文件
// 8. kill -CONT 恢复进程
aarch64-linux-gnu-gcc -O2 -static -o dex_dumper dex_dumper.c
adb push dex_dumper /data/local/tmp/
# 启动APP后立即执行dump

脱壳结果: 成功提取6个dex文件

文件 大小 内容
dex_0.dex 3.4MB WindmillAdPlugin + JPushPlugin + 第三方库
dex_2.dex 3.9MB TalkingDataSDK + 广告SDK
dex_4.dex 545KB TencentKitPlugin
dex_6.dex 2.4MB FlutterBuglyPlugin + Sigmob
dex_8.dex 1.9MB MainActivity + MztFlutterPlugins + AliyunFace
dex_10.dex 193KB 其他业务代码

五、重打包(去壳+去广告)

5.1 修改方案

  1. 从classes.dex移除sagittarius目录(百度加固代码)
  2. 将脱壳的6个dex反编译为smali,合并为classes6~classes9
  3. AndroidManifest修改:
    • Application: StubApplication → FlutterApplication
    • 移除143个广告SDK的Activity/Service/Provider声明
    • 移除32个不必要权限
    • 移除Unity SDK引用
  4. 删除百度加固文件(baiduprotect*.jar/so/dex)
  5. 创建假的WindmillAdPlugin(返回空响应)
  6. 创建假的PermissionHandler(返回”已授权”)
  7. 修复缺失的Provider类(FluwxFileProvider/ShareFileProvider等)
  8. 添加缺失的构造函数(MainActivity等)

5.2 关键修改

5.2.1 假WindmillAdPlugin

// 注册广告Method Channel但不初始化广告SDK
// 所有方法调用返回success(null)
public class WindmillAdPlugin implements FlutterPlugin {
    public void onAttachedToEngine(FlutterPluginBinding binding) {
        // 注册: com.windmill.ad, com.windmill/splash, 
        //       com.windmill/interstitial, com.windmill/native
        // 所有channel的handler返回null
    }
}

5.2.2 假PermissionHandler

// 所有权限检查返回"已授权"(status=1)
// 不触发权限被拒绝的弹窗逻辑
public void onMethodCall(MethodCall call, Result result) {
    // checkPermissionStatus → 返回1(granted)
    // requestPermissions → 返回{permission: 1}
    // openAppSettings → 返回true
}

5.2.3 权限弹窗文本替换

在libapp.so中用等长空格替换UTF-16编码的弹窗文本:

"您已拒绝权限" → "    " (等长替换)
"温馨提示" → "    "
"无法使用app的部分功能" → "                    "
... 共32处替换

5.3 DEX分包

脱壳的9494个smali文件超过单个dex的65535方法限制,需要分成4个dex:

目录 内容 文件数
smali_classes6 业务代码 + 部分SDK 1862
smali_classes7 非广告SDK 2331
smali_classes8 eidlink/dtf/czhj/jiguang 2871
smali_classes9 sigmob/windmill广告SDK 2430

5.4 编译打包

apktool b mzt_repack -o mzt_noshell.apk -f
zipalign -p 4 mzt_noshell.apk mzt_aligned.apk
apksigner sign --ks mzt_keystore.jks --ks-key-alias mzt \
  --ks-pass pass:123456 --key-pass pass:123456 \
  --out mzt_signed.apk mzt_aligned.apk

六、遇到的问题与解决方案

6.1 百度加固签名校验

问题 方案 结果
纯重签名闪退 分析签名校验流程 确认n1()校验
NOP abort无效 signal 64而非abort 检测线程直接kill
Java hook无效 native层直接读APK文件 不走getPackageInfo
跳过n1()闪退 n1()同时做dex解密 不能跳过
FakeAPK方案 让n1()读伪造APK ✅ 签名校验通过

6.2 后台检测线程(signal 64)

问题 方案 结果
Thread-9发signal 64 修改so无效(OLLVM混淆) 只能脱壳重打包
脱壳重打包 完全移除百度加固 ✅ 无检测线程

6.3 脱壳dex不完整

问题 方案 结果
第一次dump只有2个dex 等待0.3秒太短 部分类缺失
等待0.5秒再dump 获取6个完整dex ✅ 所有类完整

6.4 重打包后的问题

问题 原因 解决方案
VerifyError smali寄存器超限(>=11) 拆分方法,增大.locals
FluwxFileProvider无构造函数 脱壳dex不完整 添加无参构造函数
ShareFileProvider缺失 被百度加固加密 创建空实现类
MainActivity无构造函数 脱壳dex不完整 添加无参构造函数
Unity类找不到 manifest引用 移除Unity meta-data
AbstractMethodError 插件类不完整 用完整dex替换
dex方法数超限 9494个类 分成4个dex
MissingPluginException WindmillAdPlugin未注册 创建假Plugin返回null
广告SDK仍运行 真Plugin初始化SDK 假Plugin不初始化
权限弹窗 Dart代码检查权限 假PermissionHandler返回granted

七、最终APK信息

项目 原始APK 修改后APK
文件大小 169MB 167MB
Application StubApplication FlutterApplication
百度加固 有 无
广告SDK 9个 0个
广告Activity 143个 0个
权限声明 55个 23个
DEX文件 5个 9个
签名 原始证书 自签名

八、完整修改清单

8.1 AndroidManifest.xml

  • Application: com.sagittarius.v6.StubApplication → io.flutter.app.FlutterApplication
  • 移除143个广告SDK的Activity/Service/Provider/meta-data
  • 移除32个不必要权限(广告SDK角标/设备ID/安装包等)
  • 移除Unity SDK的meta-data引用
  • 移除SplashScreenDrawable

8.2 Smali代码

  • 删除 smali/com/sagittarius/ (百度加固壳代码)
  • 删除 smali_assets/ (百度加固反编译代码)
  • 添加 smali_classes6~9/ (脱壳的业务代码,9494个smali文件)
  • 修改 GeneratedPluginRegistrant.smali (移除WindmillAdPlugin注册后又恢复为假实现)
  • 替换 WindmillAdPlugin.smali (假实现,返回null)
  • 替换 MethodCallHandlerImpl.smali (假PermissionHandler,返回granted)
  • 添加缺失的Provider类 (FluwxFileProvider/ShareFileProvider/TencentKitFileProvider/UpdateFileProvider)
  • 添加MainActivity构造函数

8.3 资源文件

  • styles.xml: NormalTheme保持原始定义

8.4 Native库

  • 删除 libbaiduprotect.so (两个架构)
  • 删除 assets/baiduprotect* (所有加固文件)
  • 保留原始 libapp.so (Flutter Dart代码)
  • 在libapp.so中替换32处权限弹窗文本(等长UTF-16空格替换)

8.5 Assets

  • 删除 baiduprotect*.jar/dex/md
  • 删除 bdxadsdk.jar
  • 删除 libbaiduprotect_x86/x86_64

九、关键技术点总结

9.1 百度加固脱壳

百度加固的签名校验在native层(n1方法),通过以下步骤绕过:

  1. FakeAPK: 创建包含原始签名的伪造APK,修改APKPATH让n1()读到正确签名
  2. 内存dump: 用C程序在APP运行时暂停进程,从dalvik-DEX data内存区域提取解密后的dex

9.2 Flutter APP去广告

Flutter APP的广告通过Method Channel与原生端通信:

  1. 假Plugin: 创建假的WindmillAdPlugin,注册Channel但返回空响应
  2. 不初始化SDK: 假Plugin不调用任何广告SDK的初始化代码
  3. 移除Activity: 从manifest移除广告SDK的Activity,即使SDK被调用也无法弹窗

9.3 权限弹窗移除

  1. 假PermissionHandler: 修改permission_handler插件的onMethodCall,所有权限检查返回”已授权”
  2. 文本替换: 在libapp.so中等长替换UTF-16编码的弹窗文本

9.4 libapp.so字符串修改

Flutter的Dart代码编译为AOT机器码(libapp.so),字符串以UTF-16 LE存储:

  • 等长替换: 必须保持字节数完全一致,否则破坏Dart字符串长度字段
  • 中文1字符 = 2字节(UTF-16),空格1字符 = 2字节(UTF-16)

十、文件清单

文件 说明
/home/xxx/mzt.apk 原始APK
/home/xxx/mzt_no_ads.apk 最终去广告APK
/home/xxx/mzt_keystore.jks 签名密钥 (alias=mzt, pass=123456)
/home/xxx/mzt_repack/ apktool反编译目录
/home/xxx/dex_new_*.dex 脱壳的6个dex文件
/home/xxx/dex_dumper_v5.c 内存脱壳C程序
/home/xxx/fake_apk.zip FakeAPK(绕过签名校验用)
/home/xxx/original_cert.der 原始APK签名证书

文档生成时间: 2026-07-07
APK版本: v3.1.4 (versionCode: 443)