实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用

admin 2025年6月23日23:49:36评论3 views字数 6513阅读21分42秒阅读模式
声明: 
截止发文时间,该漏洞尚未修复,所以本文对于 APP 名称、图标、包名、域名、接口、密钥、Hook 脚本等多处均打厚码处理。文章仅供学习思路,切勿滥用。
背景: 
某款小游戏 APP,其中有个类似于消消乐的游戏,游戏规则就是,将一堆卡牌放入一个临时区,在临时区中的三个相同的卡牌可以消掉,如果临时区爆满则游戏失败。其中游戏有三个道具,分别是,撤回1张、移出3张、重置顺序。
在正常 BP 抓包测试的过程中,发现关于道具使用的接口,全都进行了加密,无法直接修改,尝试反编译了 apk 源码,找到几个密钥,但都不对,所以决定使用 hook 方式快速定位。
环境:
  • 电脑使用的是 MacBookPro M1 芯片
  • 测试机为 Google Pixel 2 已 Root 并开启 USB 调试
  • Frida 17.2.0  目前的最新版
  • Frida Server 17.2.0  目前的最新版
正片开始,实在是懒得在排版上浪费时间了,所以凑合着看吧。
01. BP 抓包这样儿,咋整
请求包明显密文,响应包直接是个 stream 流,放弃? 显然不是我风格。
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
02. JADX 反编译
查过了,没壳,所以直接反编译,由于是个新电脑,基本的渗透环境前段时间装好了,但是APP调试的环境还没配置,JADX 也没装,所以直接从头开始。
brew install jadx
jadx-gui /Users/hola/Downloads/xxx.apk
运行结束后会自动打开 jadx-gui 的窗口,就不截图了,因为包名和类名的特征太明显了,截了图也是全部打码,没意义。
运行的过程中可能会报一个 Search 相关的 Error,不重要,貌似是因为最新版本的 jadx 的 search 模块兼容性有点小问题,但不影响反编译的结果。
03. 搜索加密接口和加密相关方法
你品,你细品,这也就是我为什么果断用 hook 方式的原因,我看都懒得看
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
加密接口搜不到,加密方法近 2W
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
04. Hook 环境准备
安装 ADB
brew install adb
检查 adb 连通性
adb devices
正常连通的情况下,会返回一个 device id,如果没有返回,则说明没连通。
如果没连通,检查一下以下几个问题:
  1. 确认 USB 调试模式已经打开,通常这个选项在开发者模式中
  2. 确认默认 USB 传输模式为“文件”或者“MTP”(不同机型显示不同),“仅限充电”模式和“仅限图片传输”模式都不行
  3. 确认 adb 调试是允许(部分机型有这个开关)
  4. 确认 USB 线支持数据传输(有部分线只支持充电)
  5. 如果使用 Mac 拓展坞,需要保证拓展坞也支持数据传输,我的拓展坞就不支持,所以后来换了 C-C 的线直连电脑
安装 Frida
sudo pip3 install frida-tools
安装 Frida Server
# 地址在这里:https://github.com/frida/frida/releases# 下载下来先解压(unxz 命令没有的话自己装一下)unxz frida-server-17.2.0-android-arm64.xz# 解压之后 push 到安卓机上adb push /Users/hola/Downloads/frida-server-17.2.0-android-arm64 /data/local/tmp/frida
进去 adb shell,配置可执行权限
adb shellcd /data/local/tmpchmod 777 frida
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
启动 frida
./frida
如果遇到这个报错
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
运行
setenforce 0
关闭 SELinux 即可,在 Android 10 以上经常会有这个错误,但是这个命令只是临时关闭,下次启动 frida 可能还得运行。
启动成功后,在电脑端运行
frida-ps -U
如果正常输出应用程序列表,则表明环境配置正常。
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
顺便说一下,如果之前经常用 frida 的伙伴可能会习惯使用
frida-ps -U --no-pause
但是新版本的 frida 已经去除了 --no-pause 参数,这是因为,旧版本的 frida 默认启动之后不恢复,所以启动命令经常需要会加上 --no-pause 参数表示启动之后立即恢复,但新版本中,可能开发团队发现大家使用 --no-pause 的场景更多,所以默认变成了自动恢复,所以自然而然就删除了 --no-pause 参数,而多了一个 --pause 参数,用来表示不需要恢复。
05. 确认调用链
确认使用的网络库
Java.perform(() => {    console.log("[*] Frida is working");    const libs = [        "okhttp3.OkHttpClient"        "com.android.volley.toolbox.HurlStack",        "org.apache.http.impl.client.AbstractHttpClient",        "com.google.android.gms.net.CronetUrlRequest"    ];    libs.forEach(lib => {        try {            const cls = Java.use(lib);            console.log(`✅ 检测到网络库: ${lib}`);        } catch (e) {            console.log(`❌ 未找到: ${lib}`);        }    });    // 检测自定义封装    Java.enumerateLoadedClasses({        onMatchfunction(className) {            if (className.includes("Http") ||                 className.includes("Request") ||                 className.includes("Network")) {                console.log(`🔍 疑似网络类: ${className}`);            }        },        onCompletefunction() {}    });});
可以确认使用的是 okhttp3 网络库和 apache 的 AbstractHttpClient
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
拦截网络请求,确认调用链
Java.perform(() => {    // Hook OkHttp 的拦截器    const OkHttpClient = Java.use("okhttp3.OkHttpClient");    const Request = Java.use("okhttp3.Request");    OkHttpClient.newCall.implementation = function(request{        const url = request.url().toString();        if(url.includes("/xxx/xxx")) {            const stackTrace = Java.use("android.util.Log").getStackTraceString(                Java.use("java.lang.Exception").$new()            );            console.log("UseGifts called! Stack trace:\n", stackTrace);        }        return this.newCall.call(this, request);    };});

结果发现,拦截失败,并没有这个接口。

重新静态分析源码,发现整个前端使用了 jsbridge 做了加密混淆,于是找到 jsbridge 相关的类,并且在其中发现了一个 ParamData 类和 security 包(经分析,这里的 security 包只是做加密校验,并不是我们要找的参数加密类),但是这个 ParamData 类看上去极大可能就是对参数进行加密传输的类。

于是,Hook 这个 ParamData 类

Java.perform(function() {    // 1. 核心Hook点:ParamData类    const ParamData = Java.use('com.xxx.model.ParamData');    // Hook toJson方法 - 这是加密前的数据源头    ParamData.toJson.implementation = function() {        const result = this.toJson();        // 关键日志打印        try {            console.log("n🎯 JSBridge参数封转调用");            console.log("  模块: " + this.module.value);            console.log("  标识符: " + this.identifier.value);            if (this.bridgeParam.value) {                const bridgeParams = this.bridgeParam.value.toJson().toString();                console.log("  Bridge参数: " + truncate(bridgeParams, 100));            }            // 打印主参数(这是要被加密的数据)            if (this.param.value) {                const params = this.param.value.toString();                console.log("  主参数: " + truncate(params, 200));                // 如果和礼品相关就打印详细调用栈                if (params.includes("xxx")) {                    console.log("🧩 调用栈:");                    console.log(Java.use("android.util.Log").getStackTraceString(                        Java.use("java.lang.Exception").$new()                    ));                }            }        } catch (e) {            console.log("⚠️ ParamData解析失败:", e.message);        }        return result;    };

顺利的拿到调用栈,发现核心类 XXXManagerImpl 和一个关键性的方法 onSuccess

实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
查看这个 XXXManagerImpl 方法的源码,找到 getInstance 方法,getInstance 方法内又调用了 getEncryptKey 方法,这个方法,极大可能就是参数加密的密钥,但这个方法内部,又是从其他配置中读取数据,索性直接 hook 这个方法
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
hook 脚本核心代码如下(已脱敏处理,只保留核心逻辑,仅供学习)
Java.perform(function() {    // 1. 获取xxxManager实例    const xxxManager = Java.use('com.xxx.xxx.xxx.xxxManager');    const managerInstance = xxxManager.getInstance();    // 2. 延迟获取加密密钥(等待初始化完成)    let encryptKey = null, appSecret = null, appKey = null;    setTimeout(function() {        try {            encryptKey = managerInstance.getEncryptKey();            appSecret = managerInstance.getAppSecret();            appKey = managerInstance.getAppKey();            console.log("🔑 核心加密参数:");            console.log(`  - encryptKey: ${encryptKey}`);        } catch (e) {            console.log("⚠️ 获取密钥失败: " + e.message);        }    }, 5000);    // 3. Hook 请求参数加密前    const xxxManagerImpl = Java.use('com.xxx.xxx.xxxManagerImpl');    xxxManagerImpl.generateRequestWithBuilder.implementation = function(        httpClient,         xxxBuilder,         xxxDiagnosticsContextImpl    ) {        const apiName = xxxBuilder.getName();        if (apiName && apiName.includes("xxx/xxx")) {            console.log("n🎯 捕获礼品请求准备加密");            // 获取原始请求对象            const requestObj = xxxBuilder.getRequest();            // 修改请求对象            let modifiedJson = requestJson;            // modifiedJson = modifiedJson.replace(/"id":d+/g, '"id":999');            // console.log("🔥 修改后请求JSON: " + truncate(modifiedJson, 500));            const modifiedObj = JSON.parseObject(modifiedJson, JSONObject.class);            xxxBuilder.setRequest(modifiedObj);    };    // 4. Hook 响应解密点    xxxManagerImpl.from.implementation = function(response, bArr) {        let decryptedResponse = '';        decryptedResponse = this.from(response, bArr);        const url = response ? response.getRequest().getUrl().toString() : '';        if (url.includes("xxx/xxx")) {            console.log("n🔓 响应解密成功:");            console.log(truncate(decryptedResponse, 500));            // 修改响应 - 针对您的接口具体字段            let modifiedResponse = decryptedResponse;            // ★经过多次测试,发现响应体修改成 stock = 0 时,可以无限使用付费道具            modifiedResponse = '{"status":{"code":0,"message":"OK","description":"成功"},"result":{"status":0,"stock":0}}'            console.log("🔥 修改后响应: " + truncate(modifiedResponse, 500));            return modifiedResponse;    };});
经过多次尝试,发现修改响应包返回参数,可以绕过前端校验,无限使用付费道具。
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用
实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用

原文始发于微信公众号(网安小趴菜):实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月23日23:49:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   实战之某小游戏APP接口加密,Hook大法薅羊毛,付费道具无限用https://cn-sec.com/archives/4195163.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息