云南中建西部建设有限公司网站广告推广一个月多少钱
1. 背景
有一个游戏包被商店拘审了
拘审理由是:游戏启动后,toast 弹出显示 i m holmes~ 字样,并跳转到快应用
很明显的是 toast 提示:i m holmes(有意思,我是福尔摩斯🐶)
测试:这个问题你排查下,比较紧急,下午需要解决完(此时是上午🌞)
- 你没有游戏工程源码,你只有一个 APK 安装包,如何排查定位 toast 显示问题
- 测试告诉你,复现条件是:关闭开发者选项 + 关闭 USB 调试,启动游戏
2. 排查
2.1 Jadx 静态分析
A 技术:
(撇了一眼)这很简单啊,使用 jadx 反编译安装包,搜索字符串 i m holmes~,就知道是哪里弹出了,再把相关代码删除掉就好了
B 技术: 打开 jadx,把安装包拖到里面反编译,搜索字符串… … 没有发现这个字符串啊
A 技术:
难道字符串被加密了?
B 技术: 不排除这个可能,如果是加密那可太难搞了,先不考虑这种情况
A 技术:
会不会这个字符串是在 assets 目录下的 jar、dex 文件里面
B 技术: assets 目录下确实有 jar 文件,并且文件后缀不是 jar 的,经过简单的重命名,使用 jaxd 反编译也搜索不到字符串 holmes
A 技术:
这是 unity 游戏,让 unity 游戏技术在工程全局搜索看下有没有这个字符串
B 技术: 技术说也没有搜到字符串 holmes
A 技术:
奇怪了!这个字符串会在哪里
初步排查未果。只能本地复现看看到底是怎么个事,再做进一步排查
2.2 Try 本地复现
上面测试说过,复现步骤是:关闭开发者选项 + 关闭 USB 调试,启动游戏
A 技术:
我按照你的复现步骤,在我这个手机怎么没有复现到(此时我的设备连着电脑,USB 连接)
C 测试: 不能吧(他拿着手机走过来),你看我这手机就能复现
测试拿过我的设备,拔掉数据断开 USB 连接,确认关闭开发者选项、确认关闭 USB 调试,启动游戏
C 测试: 能复现啊,你的设备也能复现啊
A 技术:
(一脸懵逼)测试没有操作什么,只是确认拔了数据线连接,确认开关关闭就复现了;难道和连接数据线也有关系?插上数据线连接设备又不能复现了
果然,现在复现步骤变成了:
关闭开发者选项
关闭 USB 调试
断开设备数据线连接(非充电状态)
我又想到了,设备连接数据线意味着设备处于充电状态
,这个充电状态很重要,后面突破的关键
2.3 Hook 吐司显示
A 技术:
使用 Frida hook Toast 显示,拿到调用堆栈,不就知道是哪里的代码调用了
B 技术: 复现条件是设备不能连接 USB,Frida hook 需要连接设备才能使用😓
A 技术:
那你可以 hook 复现问题的条件,条件成立了才会显示 toast
结合 Deepseek 我们很快知道如何使用 Frida hook 上述的复现步骤条件
2.3.1 Hook 开发者选项 & USB 调试
- 开发者选项:
development_settings_enabled
- USB 调试:
adb_enabled
其实做风控的基本都会用到设备的这些信息,用于上报或控制配置下发
所以我们更清晰地知道:被风控的设备是不能复现问题(弹出 toast),我们知道把当前设备设置为非风险设备(既打破风控条件)才能复现问题
function hookUsbDevEnable() {let dev_enable = "development_settings_enabled"let usb_enable = "adb_enabled"let Global = Java.use("android.provider.Settings$Global");Global["getInt"].overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (cr, name, def) {console.log(`Global 重载一:name=${name}, def=${def}`);var ans = this["getInt"](cr, name, def);//ans = 1 表示开启//ans = 0 表示关闭if (name === dev_enable) {console.log(`\t是否开启 开发者选项=${ans}`);printStack()} else if (name === usb_enable) {console.log(`\t是否开启 USB 调试=${ans}`);printStack()}return ans};Global["getInt"].overload('android.content.ContentResolver', 'java.lang.String').implementation = function (cr, name) {console.log(`Global 重载二: name=${name}`);var ans = this["getInt"](cr, name);//ans = 1 表示开启//ans = 0 表示关闭if (name === dev_enable) {console.log(`\t是否开启 开发者选项=${ans}`);printStack()} else if (name === usb_enable) {console.log(`\t是否开启 USB 调试=${ans}`);printStack()}return ans};let Secure = Java.use("android.provider.Settings$Secure");Secure["getInt"].overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (cr, name, def) {console.log(`Secure 重载一:name=${name}, def=${def}`);var ans = this["getInt"](cr, name, def);//ans = 1 表示开启//ans = 0 表示关闭if (name === dev_enable) {console.log(`\t是否开启 开发者选项=${ans}`);printStack()} else if (name === usb_enable) {console.log(`\t是否开启 USB 调试=${ans}`);printStack()} else {console.log(`\tans=${ans}`);}return ans};Secure["getInt"].overload('android.content.ContentResolver', 'java.lang.String').implementation = function (cr, name) {console.log(`Secure 重载二: name=${name}`);var ans = this["getInt"](cr, name);//ans = 1 表示开启//ans = 0 //表示关闭if (name === dev_enable) {console.log(`\t是否开启 开发者选项=${ans}`);printStack()} else if (name === usb_enable) {console.log(`\t是否开启 USB 调试=${ans}`);printStack()}return ans};
}
2.3.2 Hook 充电状态
/*** Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:* integer indicating whether the device is plugged in to a power* source; 0 means it is on battery, other constants are different* types of power sources.*/public static final String EXTRA_PLUGGED = "plugged";// 设备充电状态// values for "status" field in the ACTION_BATTERY_CHANGED Intentpublic static final int BATTERY_STATUS_UNKNOWN = Constants.BATTERY_STATUS_UNKNOWN;public static final int BATTERY_STATUS_CHARGING = Constants.BATTERY_STATUS_CHARGING;public static final int BATTERY_STATUS_DISCHARGING = Constants.BATTERY_STATUS_DISCHARGING;public static final int BATTERY_STATUS_NOT_CHARGING = Constants.BATTERY_STATUS_NOT_CHARGING;public static final int BATTERY_STATUS_FULL = Constants.BATTERY_STATUS_FULL;
function hookCharging() {var Intent = Java.use('android.content.Intent')Intent.getIntExtra.implementation = function (key, def) {var ans = this.getIntExtra(key, def);if (key == "status") {//ans = -1 //表示非充电状态console.log('\t充电:status================ ' + ans)printStack()} else if (key == "plugged") {//ans = -1 //表示非充电状态console.log('\t充电:ans plugged================ ' + ans)printStack()}return ans}
}
hook Toast
function hookToastShow() {var Toast = Java.use('android.widget.Toast')Toast.show.implementation = function () {console.log('Toastshow ================ ')printStack()this.show();}
}
打印当前堆栈
function printStack() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
如何执行上述的 hook 脚本,Frida 安装和使用自行搜索,执行下面命令加载脚本并应用到执行的应用
frida -U -f 包名 -l .\a.js
//a.js
Java.perform(function x() {console.log('重新加载脚本');hookUsbDevEnable()hookCharging()hookToastShow()hookFileCreate()hookDexLoader()hookClassLoader()
})
脚本一执行,修改 hook 方法返回值:关闭开发者选项 + 关闭 USB 调试 + 修改充电状态
能在设备连接数据线情况下复现问题,拿到了 toast 显示的堆栈信息
toast 显示时,输出堆栈:
Toastshow ================
java.lang.Throwableat android.widget.Toast.show(Native Method)at holmes.bridge.core.AcceptorImpl.onTactful(AcceptorImpl.java:30)
2.4 Dex 动态加载
堆栈输出了,以为在 jadx 直接能搜索到这个类holmes.bridge.core.AcceptorImpl
又愣了一下,连holmes
这个包名都没有,更别说这个类了,那代码到底在哪里执行的?
使用 root 手机查看应用目录,发现了 classex.dex
2.4.1 classes.dex
在路径下面目录发现了一个 dex 文件:
/data/data/包名/files/walk/classex.dex
/data/user/0/包名/files/walk/classex.dex
导出文件查看,果然 Toast 代码在这里执行的
2.4.2 hook Dex Loader
能够知道这个 dex 是在哪里加载的呢?,hook 看到没有明显有用的日志,未果
function hookDexLoader() {// .overload('[Ljava.nio.ByteBuffer;', 'java.lang.String', 'java.lang.ClassLoader')// .overload('java.lang.String', 'java.io.File', 'java.lang.String', 'java.lang.ClassLoader')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;')// .overload('java.lang.String', 'java.io.File', 'java.lang.String', 'java.lang.ClassLoader', 'boolean')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;', 'boolean')const DexClassLoader = Java.use("dalvik.system.DexClassLoader");DexClassLoader.$init.overload("java.lang.String", "java.lang.String", "java.lang.String", "java.lang.ClassLoader").implementation = function (dexPath, optimizedDirectory, librarySearchPath, parent) {console.log(`DexClassLoader: dexPath=${dexPath}, optimizedDirectory=${optimizedDirectory},librarySearchPath=${librarySearchPath},parent=${parent}`);this.$init(dexPath, optimizedDirectory, librarySearchPath, parent);};const BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");BaseDexClassLoader.$init.overload('[Ljava.nio.ByteBuffer;', 'java.lang.String', 'java.lang.ClassLoader').implementation = function (dexFiles, librarySearchPath, parent) {console.log(`DexClassLoader: dexFiles=${dexFiles}, librarySearchPath=${librarySearchPath},parent=${parent}`);this.$init(dexFiles, librarySearchPath, parent);};BaseDexClassLoader.$init.overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;', 'boolean').implementation = function (dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter, isTrusted) {console.log(`DexClassLoader: dexPath=${dexPath}, librarySearchPath=${librarySearchPath},parent=${parent},sharedLibraryLoaders=${sharedLibraryLoaders},sharedLibraryLoadersAfter=${sharedLibraryLoadersAfter},isTrusted=${isTrusted}`);this.$init(dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter, isTrusted);};
}
2.4.3 hook Class Loader
hook AcceptorImpl
类加载,执行脚本却只有一点点日志:
hookClassLoader: 找到加载的类 = holmes/bridge/core/AcceptorImpl
java.lang.Throwableat java.lang.ClassLoader.loadClass(Native Method)
function hookClassLoader() {const ClassLoader = Java.use("java.lang.ClassLoader");ClassLoader.loadClass.overload("java.lang.String").implementation = function (name) {if (name.includes("holmes")) {console.log("hookClassLoader: 找到加载的类 = " + name);printStack()}return this.loadClass(name);};
}
2.4.4 hook 文件创建
那么该 classex.dex 文件是如何存在的呢?网络下载吗?
hook 文件创建,也没有看到更多有用的堆栈,不知道是在哪里保存的
堆栈这么少,可能是在 native 下载保存?
创建文件 2 filename=/data/user/0/com.dz.tcsdj.honor/files/walk/bridge
找到 walk 路径
java.lang.Throwableat java.io.File.<init>(Native Method)
function hookFileCreate() {var File = Java.use('java.io.File')let initMethodName = '$init';let overloadedMethods = File[initMethodName].overloads;for (const constructionMethod of overloadedMethods) {constructionMethod.implementation = function () {let result = this[initMethodName].apply(this, arguments);let arg0 = arguments[0];//本次我们只处理第一个参数是 string 类型的构造函数if (typeof arg0 === 'string' && arg0 !== undefined && arg0.length == 1) {console.log('创建文件 1 filename=' + arg0)if (arg0.includes("walk")) {console.log('找到 walk 路径')printStack()}}if (arguments.length == 2) {let arg1 = arguments[1];if (typeof arg0 === 'string' && typeof arg1 === 'string') {let filename = arg0 + arg1;console.log('创建文件 2 filename=' + filename)if (filename.includes("walk")) {console.log('找到 walk 路径')printStack()}}}return result;}}
}
后续稍微跟踪了下,基本可以确定是在 native 动态下载、保存到应用目录,然后加载
由于 native 技术不行,就到此为止了
2.5 完整代码
Java.perform(function x() {console.log('重新加载脚本');hookUsbDevEnable()hookCharging()hookToastShow()hookFileCreate()hookDexLoader()hookClassLoader()
})function hookToastShow() {var Toast = Java.use('android.widget.Toast')Toast.show.implementation = function () {console.log('Toastshow ================ ')this.show();}
}function hookCharging() {var Intent = Java.use('android.content.Intent')Intent.getIntExtra.implementation = function (key, def) {var ans = this.getIntExtra(key, def);if (key == "status") {//表示非充电状态,调试时打开注释ans = -1console.log('\t充电:status================ ' + ans)} else if (key == "plugged") {//表示非充电状态,调试时打开注释ans = -1console.log('\t充电:ans plugged================ ' + ans)}return ans}
}function hookUsbDevEnable() {let dev_enable = "development_settings_enabled"let usb_enable = "adb_enabled"let Global = Java.use("android.provider.Settings$Global");Global["getInt"].overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (cr, name, def) {console.log(`Global 重载一:name=${name}, def=${def}`);var ans = this["getInt"](cr, name, def);if (name === dev_enable) {//表示开启开发者选项,调试时打开注释ans = 0console.log(`\t是否开启 开发者选项=${ans}`);} else if (name === usb_enable) {//表示开启 USB 调试,调试时打开注释ans = 0console.log(`\t是否开启 USB 调试=${ans}`);}return ans};Global["getInt"].overload('android.content.ContentResolver', 'java.lang.String').implementation = function (cr, name) {console.log(`Global 重载二: name=${name}`);var ans = this["getInt"](cr, name);if (name === dev_enable) {//表示开启开发者选项,调试时打开注释ans = 0console.log(`\t是否开启 开发者选项=${ans}`);} else if (name === usb_enable) {//表示开启 USB 调试,调试时打开注释ans = 0console.log(`\t是否开启 USB 调试=${ans}`);}return ans};let Secure = Java.use("android.provider.Settings$Secure");Secure["getInt"].overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (cr, name, def) {console.log(`Secure 重载一:name=${name}, def=${def}`);var ans = this["getInt"](cr, name, def);if (name === dev_enable) {//表示开启开发者选项,调试时打开注释ans = 0console.log(`\t是否开启 开发者选项=${ans}`);} else if (name === usb_enable) {//表示开启 USB 调试,调试时打开注释ans = 0console.log(`\t是否开启 USB 调试=${ans}`);}return ans};Secure["getInt"].overload('android.content.ContentResolver', 'java.lang.String').implementation = function (cr, name) {console.log(`Secure 重载二: name=${name}`);var ans = this["getInt"](cr, name);if (name === dev_enable) {//表示开启开发者选项,调试时打开注释ans = 0console.log(`\t是否开启 开发者选项=${ans}`);} else if (name === usb_enable) {//表示开启 USB 调试,调试时打开注释ans = 0console.log(`\t是否开启 USB 调试=${ans}`);}return ans};
}function hookClassLoader() {const ClassLoader = Java.use("java.lang.ClassLoader");ClassLoader.loadClass.overload("java.lang.String").implementation = function (name) {if (name.includes("holmes")) {console.log("hookClassLoader: 找到加载的类 = " + name);printStack()}return this.loadClass(name);};
}function hookDexLoader() {// .overload('[Ljava.nio.ByteBuffer;', 'java.lang.String', 'java.lang.ClassLoader')// .overload('java.lang.String', 'java.io.File', 'java.lang.String', 'java.lang.ClassLoader')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;')// .overload('java.lang.String', 'java.io.File', 'java.lang.String', 'java.lang.ClassLoader', 'boolean')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;')// .overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;', 'boolean')const DexClassLoader = Java.use("dalvik.system.DexClassLoader");DexClassLoader.$init.overload("java.lang.String", "java.lang.String", "java.lang.String", "java.lang.ClassLoader").implementation = function (dexPath, optimizedDirectory, librarySearchPath, parent) {console.log(`DexClassLoader: dexPath=${dexPath}, optimizedDirectory=${optimizedDirectory},librarySearchPath=${librarySearchPath},parent=${parent}`);this.$init(dexPath, optimizedDirectory, librarySearchPath, parent);};const BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");BaseDexClassLoader.$init.overload('[Ljava.nio.ByteBuffer;', 'java.lang.String', 'java.lang.ClassLoader').implementation = function (dexFiles, librarySearchPath, parent) {console.log(`DexClassLoader: dexFiles=${dexFiles}, librarySearchPath=${librarySearchPath},parent=${parent}`);this.$init(dexFiles, librarySearchPath, parent);};BaseDexClassLoader.$init.overload('java.lang.String', 'java.lang.String', 'java.lang.ClassLoader', '[Ljava.lang.ClassLoader;', '[Ljava.lang.ClassLoader;', 'boolean').implementation = function (dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter, isTrusted) {console.log(`DexClassLoader: dexPath=${dexPath}, librarySearchPath=${librarySearchPath},parent=${parent},sharedLibraryLoaders=${sharedLibraryLoaders},sharedLibraryLoadersAfter=${sharedLibraryLoadersAfter},isTrusted=${isTrusted}`);this.$init(dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter, isTrusted);};
}function hookFileCreate() {var File = Java.use('java.io.File')let initMethodName = '$init';let overloadedMethods = File[initMethodName].overloads;for (const constructionMethod of overloadedMethods) {constructionMethod.implementation = function () {let result = this[initMethodName].apply(this, arguments);let arg0 = arguments[0];//本次我们只处理第一个参数是 string 类型的构造函数if (typeof arg0 === 'string' && arg0 !== undefined && arg0.length == 1) {console.log('创建文件 1 filename=' + arg0)if (arg0.includes("walk")) {console.log('找到 walk 路径')printStack()}}if (arguments.length == 2) {let arg1 = arguments[1];if (typeof arg0 === 'string' && typeof arg1 === 'string') {let filename = arg0 + arg1;console.log('创建文件 2 filename=' + filename)if (filename.includes("walk")) {console.log('找到 walk 路径')printStack()}}}return result;}}
}function printStack() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}