某公交 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 签名校验机制
百度加固有两层签名校验:
- n1()初始校验: 在
StubApplication.attachBaseContext()中调用native方法A.n1(),校验APK签名+解密dex - 后台检测线程: 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文件
- 创建伪造APK (7.5MB),包含原始APK的签名块 + baiduprotect*.jar + classes.dex
- 修改StubApplication,在n1()调用前将APKPATH指向伪造APK
- n1()从伪造APK验证签名(通过) + 解密dex(正常)
- 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 修改方案
- 从classes.dex移除sagittarius目录(百度加固代码)
- 将脱壳的6个dex反编译为smali,合并为classes6~classes9
- AndroidManifest修改:
- Application: StubApplication → FlutterApplication
- 移除143个广告SDK的Activity/Service/Provider声明
- 移除32个不必要权限
- 移除Unity SDK引用
- 删除百度加固文件(baiduprotect*.jar/so/dex)
- 创建假的WindmillAdPlugin(返回空响应)
- 创建假的PermissionHandler(返回”已授权”)
- 修复缺失的Provider类(FluwxFileProvider/ShareFileProvider等)
- 添加缺失的构造函数(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方法),通过以下步骤绕过:
- FakeAPK: 创建包含原始签名的伪造APK,修改APKPATH让n1()读到正确签名
- 内存dump: 用C程序在APP运行时暂停进程,从dalvik-DEX data内存区域提取解密后的dex
9.2 Flutter APP去广告
Flutter APP的广告通过Method Channel与原生端通信:
- 假Plugin: 创建假的WindmillAdPlugin,注册Channel但返回空响应
- 不初始化SDK: 假Plugin不调用任何广告SDK的初始化代码
- 移除Activity: 从manifest移除广告SDK的Activity,即使SDK被调用也无法弹窗
9.3 权限弹窗移除
- 假PermissionHandler: 修改permission_handler插件的onMethodCall,所有权限检查返回”已授权”
- 文本替换: 在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)
版权声明: 本文采用 BY-NC-SA 协议进行授权,如无注明均为原创,转载请注明转自 Dreamc's blog
本文链接: 某公交 APK 逆向去广告全流程总结
本文链接: 某公交 APK 逆向去广告全流程总结