本篇文章仅适用于学习交流使用,如有不当,请联系删除。
对于逆向来说,非常的没有档次,算是提供给小白一种学习进步的方法。
一、前置工作
本人在安卓逆向方面属于小白,是跟着正己大佬《安卓逆向这档事》简单学习了一下基础知识,水平算是能够跟着大佬的攻略一步一步走下去的水平,因此本文可能存在大量解释有误的地方,欢迎大家指正。
最近恰逢春节在家无聊,同时刚好gemini 3.0 pro上线,想着通过大模型的能力,看是否能协助我完成“逆向”(本人的能力属实算不上)上的突破。之前学习过漁滒encryptR_client的生成教程,里面仅完成了encryptR_client“补环境”过程,最终没有完成ckey的生成。当时我就搭好了架子,但是同样一直存在一些卡点,最终一直没有完成ckey部分的生成。随着最近大模型能力的提升,我通过gemini(其他大模型大家可以自测)先后完成了某讯ckey、chacha20算法还原,以及本文要说的ckey的unidbg生成。(不得不感叹当前大模型能力的强大,能帮助我这样一个完全看不懂ida伪代码的人,完成算法还原)
回到正题,开工。
- 模拟器 or 真机 (安装frida-server)
- GetVideo 1.3.1(随便找一个应该都行)
- IDEA(unidbg项目)
- python (frida hook脚本调用)
- unidbg 0.9.8(最新版本)
一些基础知识,本文会一笔带过,大家可以通过其他文档学习,一定讲得比我好。另外遇到没讲清楚的地方,可以咨询大模型,本文涉及的所有代码,几乎全部由大模型完成。
二、frida hook,基础参数
需要hook的com.taobao.wireless.security.adapter.JNICLibrary下的doCommandNative方法。因为是动态加载的,所以普通的hook方法不好使,需要在BaseDexClassLoader加载class的时候进行hook。
Hook Logic
大致逻辑:hook BaseDexClassLoader,然后判断dexPath是否有sgmain,如果有,切换class loader之后,就可以hook到doCommandNative了。hook到方法之后,可以使用下面代码将入参和出参完整打印出来。这里就不贴代码了,大模型可以轻松搞定。
递归打印函数
function printRecursive(obj, indent, prefix) {
if (indent === undefined) indent = "";
if (prefix === undefined) prefix = "";
var currentIndent = indent + prefix;
// Null Check
if (obj === null || obj === undefined) {
console.log(currentIndent + "null");
return;
}
// JS Types
var type = typeof obj;
if (type === 'string') {
console.log(currentIndent + "(JS-String) " + JSON.stringify(obj));
return;
}
if (type === 'number') {
console.log(currentIndent + "(JS-Number) " + obj);
return;
}
if (type === 'boolean') {
console.log(currentIndent + "(JS-Boolean) " + obj);
return;
}
// JS Array (doCommandNative 的 n 参数)
if (Array.isArray(obj)) {
console.log(currentIndent + "(JS-Array) Length: " + obj.length + " {");
for (var i = 0; i < obj.length; i++) {
printRecursive(obj[i], indent + " ", "[" + i + "] ");
}
console.log(indent + "}");
return;
}
// Java Object
if (obj.getClass) {
try {
var clsName = obj.getClass().getName();
if (clsName === "[B") {
// === Byte Array ===
// 直接调用上面的终极格式化函数
console.log(currentIndent + formatByteArray(obj));
} else if (clsName.startsWith("[L")) {
// === Java Object Array ===
// 使用反射获取长度和元素
var len = ReflectArray.getLength(obj);
console.log(currentIndent + "(" + clsName + ") Length: " + len + " {");
for (var i = 0; i < len; i++) {
var subElem = ReflectArray.get(obj, i);
printRecursive(subElem, indent + " ", "[" + i + "] ");
}
console.log(indent + "}");
} else {
// === Ordinary Object ===
console.log(currentIndent + "(" + clsName + ") " + obj.toString());
}
} catch (e) {
console.log(currentIndent + "[Error analysing Java Object]: " + e);
}
return;
}
console.log(currentIndent + "(Unknown Type) " + obj);
}
bytearray终极格式化工具:同时输出 Hex 和 ASCII
function formatByteArray(obj) {
try {
var len = ReflectArray.getLength(obj);
if (len === 0) return "(byte[0])";
var hexBuffer = "";
var strBuffer = "";
// 阈值:如果在 Logcat 里打印太长会截断,这里限制一下预览长度
// 如果你想看全量,可以把 limit 调大,或者去掉
var PREVIEW_LIMIT = 512;
var loopLen = (len > PREVIEW_LIMIT) ? PREVIEW_LIMIT : len;
for (var i = 0; i < loopLen; i++) {
var b = ReflectArray.getByte(obj, i);
// 1. 处理 Hex
var h = (b & 0xFF).toString(16);
if (h.length < 2) h = "0" + h;
hexBuffer += h;
// 2. 处理 String (JS 硬解码)
// 判断是否为可见 ASCII 字符 (32-126)
// 0x20(空格) ~ 0x7E(~)
if (b >= 32 && b <= 126) {
strBuffer += String.fromCharCode(b);
} else {
// 不可见字符用点代替,保持长度对齐,方便观察
strBuffer += ".";
}
}
if (len > PREVIEW_LIMIT) {
hexBuffer += "...(truncated)";
strBuffer += "...(truncated)";
}
// 格式化输出
// 如果数据很短,单行显示;很长,分行显示
if (len < 32) {
return "(byte[" + len + "]) hex: " + hexBuffer + " | str: " + strBuffer;
} else {
return "(byte[" + len + "])\n" +
" hex: " + hexBuffer + "\n" +
" str: " + strBuffer;
}
} catch (e) {
return "format_error: " + e;
}
}
三、unidbg
unidbg架子
unidbg补环境主要会涉及JNI、文件以及系统调用,这里我将3种不同环境放到不同文件下。
JNICLibrary
package com.taobao.wireless.security.adapter;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.file.linux.AndroidFileIO;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.linux.android.dvm.wrapper.DvmLong;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.unix.UnixSyscallHandler;
import java.io.File;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JNICLibrary {
private static final Log log = LogFactory.getLog(JNICLibrary.class);
private final AndroidEmulator emulator;
private final VM vm;
private final Memory memory;
private final Module module;
private final DvmClass myjniclass;
public JNICLibrary() {
// syscall override
AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(false) {
@Override
public AndroidEmulator build() {
return new AndroidARMEmulator(processName, rootDir, backendFactories) {
@Override
protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) {
return new MySyscallHandler(svcMemory);
}
};
}
};
emulator = builder
.setProcessName("com.youku.phone")
.setRootDir(new File("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/rootfs"))
.addBackendFactory(new Unicorn2Factory(true))
.build();
emulator.getBackend().registerEmuCountHook(100000);
emulator.getSyscallHandler().setVerbose(true);
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
// 文件处理
emulator.getSyscallHandler().addIOResolver(new MyIOResolver());
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
memory.setCallInitFunction(true);
vm = emulator.createDalvikVM();
vm.setVerbose(true);
// 补环境
vm.setJni(new MyJni());
DalvikModule dalvikModule = vm.loadLibrary(new File("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/lib/libsgmainso-6.4.170.so"), true);
module = dalvikModule.getModule();
vm.callJNI_OnLoad(emulator, module);
myjniclass = vm.resolveClass("com/taobao/wireless/security/adapter/JNICLibrary");
}
public static void main(String[] args) {
JNICLibrary jnicLibrary = new JNICLibrary();
jnicLibrary.init();
}
}
(pkg名称,统一改成了com.youku.phone,没有使用getvideo)
MyJni
package com.taobao.wireless.security.adapter;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MyJni extends AbstractJni {
private static final Log log = LogFactory.getLog(MyJni.class);
}
MyIOResolver
package com.taobao.wireless.security.adapter;
import com.github.unidbg.Emulator;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.file.linux.AndroidFileIO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MyIOResolver implements IOResolver<AndroidFileIO> {
private static final Log log = LogFactory.getLog(MyIOResolver.class);
@Override
public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
// 打印所有文件访问请求,无论是否处理
log.info("[MyIOResolver] ========================> File open request: " + pathname);
return null;
}
}
MySyscallHandler
package com.taobao.wireless.security.adapter;
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.file.linux.AndroidFileIO;
import com.github.unidbg.linux.ARM32SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import unicorn.ArmConst;
public class MySyscallHandler extends ARM32SyscallHandler {
private static final Log log = LogFactory.getLog(MySyscallHandler.class);
public MySyscallHandler(SvcMemory svcMemory) {
super(svcMemory);
setVerbose(true); // 可按需开启详细日志
}
@Override
public void hook(Backend backend, int intno, int swi, Object user) {
Emulator<AndroidFileIO> emulator = (Emulator<AndroidFileIO>) user;
UnidbgPointer pc = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_PC);
int NR = backend.reg_read(ArmConst.UC_ARM_REG_R7).intValue();
// 打印所有系统调用,无论是否处理
log.info("[MySyscallHandler] ========================>");
log.info("syscall intno=0x" + Integer.toHexString(intno) + ", swi=" + swi + ", NR=" + NR + ", pc=" + pc);
super.hook(backend, intno, swi, user);
}
}
初始化
初始化代码
初始化流程其他很多文档都讲清楚了的,大致如下:10101 => 10102(sgmain) => 10102(securitybody) => 10102(avmp)。
(init代码和后面avmp调用方法大家可以参考别的文章,这里就不贴了)
补环境
日志等级调成INFO(DEBUG太慢了),从头开始看运行流程。
1. /dev/properties
创建文件夹即可,其中的内容如果没有似乎不影响。
暂时不用管,测试下来如果从真机pull下来这部分文件,后面补环境会少一些步骤,如果不补,也能够成功。
2. /proc/stat
目测unidbg已经补了?可以不用管。
3. getPackageCodePath
遇到第一个需要补的。
case "com/youku/phone/App->getPackageCodePath()Ljava/lang/String;": {
return new StringObject(vm, dataAppPath + "/base.apk"); // dataAppPath = "/data/app/com.youku.phone"
}
同时,将base.apk复制到对应路径。(因为一开始设置了rootDir,设置的rootDir就是"/"目录,其他文件/目录可以直接往里面复制,不用处理所有文件)
对于不清楚的文件,可以到真机中去看一眼。
这里因为用的是getvideo,所以存在一个隐形的坑,base.apk需要用getvideo解压后里面的一个Youku_xxxx.apk。后续会从该文件中读取关键安全信息。
4. getFilesDir
case "com/youku/phone/App->getFilesDir()Ljava/io/File;": {
return ProxyDvmObject.createObject(vm, new File("/data/user/0/com.youku.phone/files"));
}
同样,相应的文件夹创建好。
5. getAbsolutePath
case "java/io/File->getAbsolutePath()Ljava/lang/String;": {
return new StringObject(vm, dvmObject.getValue().toString());
}
6. getApplicationInfo nativeLibraryDir等
常规需要补的环境,可以上网搜,或者直接问大模型。本文后续只介绍一些可能会踩坑的。
7. /proc/self/status /proc/{PID}/status
这里强烈推荐看看正己大佬的《安卓逆向这档事》第二十五课、Unidbg之补完环境我就睡(中)。很多可能的坑大佬已经介绍了怎么绕过去。
String pkg = emulator.getProcessName();
int pid = emulator.getPid();
if (pathname.equals("/proc/self/status") || pathname.equals("/proc/" + pid + "/status")) {
// 返回一个包含 "TracerPid: 0" 的文件内容,表示未被调试
String statusContent = "Name:\t" + pkg + "\n" +
"Umask:\t0077\n" +
"State:\tS (sleeping)\n" +
"Tgid:\t" + pid + "\n" +
"Pid:\t" + pid +"\n" +
"PPid:\t1\n" +
"TracerPid:\t0\n"; // 关键行
return FileResult.success(new ByteArrayFileIO(oflags, pathname, statusContent.getBytes()));
}
8. /proc/{PID}/stat & /proc/{PID}/wchan
不懂的地方,优先google一下,看看有没有别人遇到过。github issues
if (("/proc/" + emulator.getPid() + "/stat").equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, (emulator.getPid() +
" (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0\n").getBytes()));
}
if (("/proc/" + emulator.getPid() + "/wchan").equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname,
"sys_epoll".getBytes()));
}
9. registerAppLifeCyCleCallBack
参考安卓逆向小案例,很多本文需要补的环境,几乎都能从别人的文档中搜到。
需要注意的就是,尽量所有补的环境都加上log,方便后续debug。
@Override
public void callStaticVoidMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
log.info("callStaticVoidMethod signature=" + signature);
return ;
}
10. SPUtility2->readFromSPUnified
参考安卓逆向小案例。
结论:在/data/user/0/{PKG}/files下面,,有个SGMANAGER_DATA2文件,里面JSON格式保存key-value。获取数据方法是通过arg1 + "_" + arg2作为key去取
case "com/taobao/wireless/security/adapter/common/SPUtility2->readFromSPUnified(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
String arg1 = varArg.getObjectArg(0).getValue().toString();
String arg2 = varArg.getObjectArg(1).getValue().toString();
String key = arg1 + "_" + arg2;
System.out.println("KEY==> "+ key);
JSONObject data = JSONObject.parseObject("{\"dynamicreid_dynamicreid\":\"xxxx\",\"dynamicrsid_dynamicrs这里还有很多,直接省略了\"}");
String result = data.getString(key);
System.out.println("data ==> " + result);
if (result != null) {
return new StringObject(vm, result);
}
return null;
}
11. JNIBridge->registerInfoCallback
返回第二个参数。
12. UserTrackMethodJniBridge->utAvaiable
返回1。
13. SG_INNER_DATA & SG_USER_DATA
- SG_INNER_DATA: 从真机拿到文件,放到rootfs对应路径。这个文件会自动生成一个,替换他。
- SG_USER_DATA: 没看到,暂时空着没管。
14. SPUtility2->saveToFileUnifiedForNative
看起来是存到刚才的JSON里面,当你补了SG_INNER_DATA这个文件之后,这个方法就用不到了,所以不补。
15. UserTrackMethodJniBridge->addUtRecord
返回1就行。感兴趣可以将参数都打印出来看看,应该是用来记录设备行为的。
get方法时将值保存下来,set的时候返回值。
小结
至此,so的初始化就结束了,前面如果遇到乱七八糟的报错,很有可能是文件/目录访问没有处理好,相应的文件和目录都存在的情况下,基本没有什么大坑。
avmp计算ckey
avmp初始化 & ckey计算
流程:通过60901初始化avmp,然后60902获取ckey
经过测试,60901获取到avmp instance之后,应该可以复用。
_str输入为:ccode=01010101&client_ip=192.168.1.1&client_ts=1770000000&utid=xxxx&vid=XMjk4ODAyMzIyOA==
补环境
1. avmp补环境
上面已经讲了base.apk存在坑,如果没有踩这个坑,这里简单补几个环境应该就能完成avmp初始化了。后面很多不太常见的补环境,基本都是依靠gemini帮我补完的,大家也可以试试请教一下大模型。
2. [B->getClass()Ljava/lang/Class;
问大模型:
case "[B->getClass()Ljava/lang/Class;": {
// 获取调用这个方法的对象,也就是 byte 数组本身
Object value = dvmObject.getValue();
// 方法1:如果 value 本身就是 byte[],可以直接获取它的 Class 对象
if (value instanceof byte[]) {
Class<?> clazz = value.getClass();
// 将 Java 的 Class 对象转换为 unidbg 的 DvmObject
// 这里可以直接使用 ProxyDvmObject.createObject 来封装
byte[] byteArray = (byte[]) value;
log.info("=== [B->getClass() 调用 ===");
log.info("Byte数组长度: " + byteArray.length);
log.info("Byte数组内容(hex): " + bytesToHex(byteArray));
log.info("Byte数组内容(ASCII): " + new String(byteArray).replaceAll("[^\\x20-\\x7E]", "."));
log.info("=========================");
return ProxyDvmObject.createObject(vm, clazz);
}
// 方法2:或者直接返回一个代表 byte[] 类型的 Class 对象
// 这种方式更直接,不依赖于实际对象
Class<?> byteArrayClass = (new byte[0]).getClass();
return ProxyDvmObject.createObject(vm, byteArrayClass);
}
3. DeviceInfoCapturer->doCommandForString
获取设备信息,大模型给了hook代码。需要什么参数,获取什么参数。
function doCom(i) {
Java.perform(function() {
Java.enumerateClassLoaders({
"onMatch": function(loader) {
if (loader.toString().indexOf("libsgmain.so") >= 0 ) {
Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的
console.log("loader = ",loader.toString());
}
},
"onComplete": function() {
console.log("success");
}
});
let DeviceInfoCapturer = Java.classFactory.use("com.taobao.wireless.security.adapter.datacollection.DeviceInfoCapturer");
var result = DeviceInfoCapturer.doCommandForString(i);
console.log("result ==> " + result)
})
}
4. ArtMethod 结构体检查(gemini这么说)
直接上图:
这个问题困扰了我很久,一直不知道怎么回事,直到最近问了gemini 3.0 pro,他给了我一个解释:
发生了什么?
1. SO 想要获取设备的 IMEI,所以它去拿 TelephonyManager.getDeviceId() 的方法 ID。
2. Unidbg 作为一个模拟器,为了方便管理,把方法的 Hash 值(0x63bb9035)作为 MethodID 返回给了 SO。
3. 关键来了:在真实的 Android 系统中,MethodID 其实是一个内存指针,指向底层的 ArtMethod 结构体。libsgmain 非常狡猾,它拿到这个 ID 后,并没有立刻去调用方法,而是把它当成内存指针,硬加上偏移量(0x63bb9035 + 5 = 0x63bb903a),试图去读取内存里面的方法的 Access Flags 或者入口点,以此来判断这个方法有没有被 Frida 或 Xposed Hook 掉!
4. 因为 0x63bb9035 只是 Unidbg 算出来的一个 Hash 数字,这块内存地址在 Unicorn 引擎里根本没有被映射(Unmapped),所以 SO 一读就直接触发了内存访问异常,导致进程崩溃。
解决方案
要跑通这个逻辑,我们需要做两步:第一步是“骗”过它的 Hook 检测,第二步是给它返回一个假的 IMEI。
这个解释是否正确,我也不清楚,各位大佬如果有懂的可以评论回复。通过gemini给的方法,我发现压根不用在对应地址写入数据,只需要把内存空间开辟出来,读取内存不报错,这里就可以跑过去了。
当然,有可能是这里处理地不够好,导致后面生成的ckey长度会比frida hook出来的短上一些,好在可以正常使用。
5. svc number: 65
继续给大模型,但是这里他判断错了,他把NR=65的方法给我了。
这里显示NR=20,svc=65。按照正己大佬《安卓逆向这档事》第二十六课、Unidbg之补完环境我就睡(下)中的解释,这里应该是JNI调用(svcNumber 不等于 0x0),但是后面又没有UnsupportedOperationException。目前超出小白的能力范畴了,使用了最简单粗暴的方法,啥也不干,直接return。(留个作业后续再看)
// MySyscallHandler中
if (NR == 20 && swi == 0x41) {
return;
}
5. currentActivityThread
之前抄另一个文档,返回了Thread.currentThread(),后来发现这俩有区别,一个是java/lang/Thread,这里是android/app/ActivityThread,正常补就行。
6. android/app/ActivityThread->mActivities:Ljava/util/HashMap;
暂时new了个空的给过去。
四、总结
至此,大家应该和我一样,拿到最终的ckey了,但是因为很多地方处理并不是很完善,只能说跑出了可用的结果。另外如果想要跑encryptR_client,直接调用应该就能出,环境全部补好了的。
念念不忘,必有回响。前前后后开始->放弃->开始->放弃....重复了n次,最终借用gemini的能力跑通了,虽然在逆向学习中,自己仍然是小白,逆向的知识似乎也没有什么提升,但是通过使用大模型等能力完成多年未完成工程,心里还是十分开心的。
最后,如果还有精力,也会分享使用gemini + unidbg + ida还原算法的方法。整体来说本文难度还是比还原算法简单很多。
完结,撒花🎉🎉!!!
五、参考文档
unidbg调用sgmain的doCommandNative函数生成某酷encryptR_client参数
念念不忘,必有回响
unidbg实现xx请求参数算法
《安卓逆向这档事》系列
unidbg升级到最新版后 跑不起来libsgmain.so
分享9.23 mtop
安卓逆向小案例——某电影票务APP加密参数还原-Unidbg篇
最后贴一个提示词(轻喷)
## 背景
你现在是精通安卓逆向的工程师,需要你帮助完成unidbg调用一个so的签名。
现在我已经通过frida,hook到了签名函数ckey的输入和输出,现在我希望你帮助我使用unidbg模拟so的环境,并成功计算出ckey。其中我可以给你ida解析的so的伪代码,以及帮你使用frida hook更多参数供你分析。其他ida的脚本,我也可以尝试帮你使用。(我是小白,如果需要ida插件,需要详细说明一次)
## 要求
1. unidbg补环境时,对于常见环境,可以直接补充,对于不常见或者拿不准的地方,可以使用frida hook到相应参数再补充。
2. 每次回复,可以简单解释你的分析,不要太多,因为我看不太懂,我不需要知道原理,我只需要协助你运行代码,帮你获取so的伪代码。
## 技能
1. 可以让我协助你hook函数参数,inline hook等各种frida支持的方式。
2. 可以让我给你提供so的伪代码。
3. 可以让我使用ida的插件协助分析(注意,我是小白,如果使用这个技能,需要你详细指导我一次使用方法)。
4. 其他可能有助于你分析的方法,可以教我使用。
## 目前进展
### unidbg脚本
所有unidbg脚本 balabala
### frida hook脚本
hook脚本
### frida hook结果
hook结果
### 简单解释现有hook结果
1. 10101、10102都是初始化工作;
2. 60901初始化avmp,并拿到实例;
3. 60902是输入参数,并获取签名ckey;(重点调用)
4. 10601是获取R,可以暂时忽略。
## 任务
基于目前frida hook的结果,协助我完成unidbg脚本补环境的过程,其中存在大量环境监测的地方,需要你凭借经验和frida hook,拿到真机环境数据,帮助我最终拿到ckey的模拟计算。