-
电脑使用的是 MacBookPro M1 芯片 -
测试机为 Google Pixel 2 已 Root 并开启 USB 调试 -
Frida 17.2.0 目前的最新版 -
Frida Server 17.2.0 目前的最新版
brew install jadx
jadx-gui /Users/hola/Downloads/xxx.apk
brew install adb
adb devices
-
确认 USB 调试模式已经打开,通常这个选项在开发者模式中 -
确认默认 USB 传输模式为“文件”或者“MTP”(不同机型显示不同),“仅限充电”模式和“仅限图片传输”模式都不行 -
确认 adb 调试是允许(部分机型有这个开关) -
确认 USB 线支持数据传输(有部分线只支持充电) -
如果使用 Mac 拓展坞,需要保证拓展坞也支持数据传输,我的拓展坞就不支持,所以后来换了 C-C 的线直连电脑
sudo pip3 install frida-tools
# 地址在这里: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
cd /data/local/tmp
chmod 777 frida
./frida
setenforce 0
frida-ps -U
frida-ps -U --no-pause
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({
onMatch: function(className) {
if (className.includes("Http") ||
className.includes("Request") ||
className.includes("Network")) {
console.log(`🔍 疑似网络类: ${className}`);
}
},
onComplete: function() {}
});
});
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
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大法薅羊毛,付费道具无限用
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论