Android 高级逆向实战(二):突破商业 App「洋葱」鉴权

0 阅读5分钟

声明:本文仅用于安全研究与技术交流,请勿用于未授权分析或商业用途。所有操作均在作者自有设备与测试环境中完成。


0x00 为什么新手改 isVip() 总是失效?

拿到脱壳后的 DEX,最常见的操作是:

  1. 全局搜索 isVipisSVipisPremium
  2. 找到 LoginUtils 相关方法,强制返回 true
  3. 重打包安装

结果往往是:

  • 个人中心 VIP 徽章确实亮了
  • 界面显示「尊贵 SVIP」
  • 但一点「下载高清壁纸」或「设置动态锁屏」,立刻弹出:
    • 「原创资源请先解锁再下载」
    • 「观看 15 秒激励视频解锁」

很多人会怀疑是不是改错了位置、有没有 Native 校验、服务端是不是有加密。
其实,你只是撞上了现代商业 App 的**「洋葱多层防御模型」**。


0x01 商业 App 的「洋葱」多层防御模型

现代商业 App 绝不会把权限只押在一个全局布尔变量上。对目标壁纸 App 的完整调用链审计后,其鉴权实际分为 4 层

flowchart TD
    subgraph layer4["第4层:UI 显示门卫"]
        A["WallpaperLoginUtils.isVip() / isSVip()"] -->|只控制个人中心徽章| B["UI VIP 标识亮起"]
    end
    
    subgraph layer3["第3层:本地资源解锁状态机"]
        C["OriginManager.isUnLocked(resId)"] -->|查询本地 unlock 表| D["是否已购/已解锁"]
    end
    
    subgraph layer2["第2层:远程鉴权门禁"]
        E["OriginImpl.checkOriginalUrl(...)"] -->|调用 ApiService.getOriginalUrl| F{"服务端鉴权接口"}
        F -->|"未登录/未购买 (-700)"| G["Toast: 原创资源请先解锁 ❌"]
        F -->|"鉴权通过"| H["IAction.work(id, url) 回调"]
    end
    
    subgraph layer1["第1层:下载拦截器与广告漏斗"]
        I["DownPreController.down()"] --> J{"DownRewardAd.isShowRewardAd()"}
        J -->|"是"| K["弹出激励视频弹窗 ❌"]
        J -->|"否"| L["调用 Native 下载引擎落盘 ✅"]
    end

只改第 4 层的 isVip(),请求走到第 2 层时,服务端因缺少合法 Token 直接返回 -700,客户端在 onFail 处直接中断下载。


0x02 破局关键:移动端的「本地信封」特性

服务端接口会返回 -700,难道必须去逆向签名算法或破解支付?

完全不需要。

移动端为了追求首页瀑布流和动态壁纸滑动的极致秒开体验,在分页拉取列表时(GET /api/v1/wallpaper/list),服务端就已经把真实 CDN 高清直链随列表数据下发到了客户端内存中的 VideoData.url 字段。

后置的 ApiService.getOriginalUrl 本质上只是一个形式主义的门禁检测,它并不负责解密密文,原画地址早已经在你的手机内存里了。

// 反编译源码:VideoData.java
public class VideoData extends BaseData {
    public String url;          // 核心原画 CDN 直链(列表加载时已就绪)
    public String previewUrl;   // 预览低清图
    public int isOrigin;        // 是否为原创壁纸
    public int videoWidth;
    public int videoHeight;
}

这就是「本地信封」模式:为了性能,核心资产提前下发,鉴权只是事后拦截。


0x03 逐层穿透:Smali 级实战改造

明确了「洋葱模型」与「本地信封」后,修补策略就很清晰:从内到外,全面短路

1. 穿透第 2 层:改写 OriginImpl.checkOriginalUrl

原方法会发起异步网络请求,失败时弹 Toast。我们直接改写为:检测到媒体对象后,从本地提取 URL 并同步触发回调。

.method public checkOriginalUrl(Lcom/xxx/wallpaper/model/BaseData;ZLcom/xxx/wallpaper/manager/OriginManager$IAction;)V
    .registers 6
    .param p1, "baseData"
    .param p2, "z"
    .param p3, "iAction"

    # 参数合法性检查
    if-eqz p1, :cond_finish
    if-nez p3, :cond_check_video

    :cond_finish
    return-void

    # 1. 尝试强转为 VideoData(动态壁纸)
    :cond_check_video
    instance-of v0, p1, Lcom/xxx/wallpaper/model/VideoData;
    if-eqz v0, :cond_check_image

    move-object v0, p1
    check-cast v0, Lcom/xxx/wallpaper/model/VideoData;
    iget-object v1, v0, Lcom/xxx/wallpaper/model/VideoData;->url:Ljava/lang/String;
    goto :cond_dispatch

    # 2. 尝试强转为 WallpaperData(静态高清壁纸)
    :cond_check_image
    instance-of v0, p1, Lcom/xxx/wallpaper/model/WallpaperData;
    if-eqz v0, :cond_finish

    move-object v0, p1
    check-cast v0, Lcom/xxx/wallpaper/model/WallpaperData;
    iget-object v1, v0, Lcom/xxx/wallpaper/model/WallpaperData;->url:Ljava/lang/String;

    # 3. 核心:直接回调下游,跳过任何远程网络请求
    :cond_dispatch
    iget v0, p1, Lcom/xxx/wallpaper/model/BaseData;->id:I
    invoke-interface {p3, v0, v1}, Lcom/xxx/wallpaper/manager/OriginManager$IAction;->work(ILjava/lang/String;)V
    return-void
.end method

2. 穿透第 3 层:解锁状态机全量开放

修改 OriginManager / OriginImpl 中的 isUnLocked 方法,强制返回 true

.method public isUnLocked(I)Z
    .registers 3
    .param p1, "resId"

    # 强制标记为已解锁
    const/4 v0, 0x1
    return v0
.end method

3. 穿透第 1 层:修剪下载控制器中的广告与弹窗

DownPreController 中,剥离所有激励广告与二次弹窗判断,直接调用底层下载管道:

.method public checkDown(Lcom/xxx/wallpaper/model/BaseData;)V
    .registers 6
    .param p1, "baseData"

    const/4 v2, 0x0
    const/4 v4, 0x1
    const/4 v3, 0x0

    # 1. 记录下载上下文
    invoke-direct {p0, p1, v2}, Lcom/xxx/wallpaper/video/DownPreController;->m(Lcom/xxx/wallpaper/model/BaseData;Z)V

    # 2. 提取当前 URL
    iget-object v1, p0, Lcom/xxx/wallpaper/video/DownPreController;->mUrl:Ljava/lang/String;

    # 3. 直连真实下载方法,0 广告 0 弹窗
    invoke-direct {p0, v1, v4, v3}, Lcom/xxx/wallpaper/video/DownPreController;->l(Ljava/lang/String;IZ)V
    return-void
.end method

0x04 真机验证与实测效果

完成上述 Smali 修补并重打包安装到未 Root 真机后:

  1. 零弹窗无感下载
    详情页点击「下载」或「设置壁纸」,原版的「请先解锁」Toast 与激励视频弹窗全部消失。

  2. 高速落盘验证
    通过 ADB 检查物理文件系统,高清原画 MP4 无水印瞬间落盘:

    $ adb shell ls -lh /storage/emulated/0/DCIM/xxx/Wallpaper/video/
    -rw-rw---- 1 root everybody 11.7M 2026-09-02 15:20 2qpd3mxhxjwmr7av439io2ufu.mp4
    -rw-rw---- 1 root everybody  2.1M 2026-09-02 15:17 5ypffcan0z1hh1m99640tvzf5.mp4
    
  3. 系统动态壁纸调起成功
    点击「一键设为桌面/锁屏」,Android 原生 WallpaperManager 服务被正常调起,实时渲染生效。


0x05 总结与架构思考

  1. 认清多层防御的本质
    逆向商业 App 时,务必沿着 UI 事件 → Controller → Manager → 网络/本地 Gate → 核心引擎 梳理完整调用链,切忌只改全局布尔变量。

  2. 善用性能设计的副产物
    移动端为了秒开与预加载体验,往往把核心资产提前放进「本地信封」。在很多场景下,短路控制流比逆向加密算法更直接、更优雅。

  3. 从内到外逐层短路
    先解决数据通路(本地 URL 直接回调),再处理状态机与广告拦截,才能实现真正的「无感」效果。

下一篇《Android 高级逆向实战(三)》将分享如何搭建一套工业级 APK 差量重打包与 AXML 字节级注入系统,彻底告别卡顿的 apktool b 和 AAPT2 资源编译噩梦,15 秒自动化生成纯净发布包。