IDA逆向
IDA激活
需要自行配备python3.8以上环境,IDA安装时会自动检测。
磁力链接下载IDA9.1
magnet:?xt=urn:btih:f24cfadb8a66b343bf1ff4f0c1386a5f6991c818&dn=ida91
激活方式,生成License
ThatNotEasy/IDA-Patcher: IDA Pro Patcher & License Generator
main第28行写错了,修改为
end_year = args.end_date or (now.year + 10)
执行命令参数为安装目录,也就是ida.exe的根目录
python main.py -p "D:\Tools\IDA"
成功激活后如下

Zygisk-Il2CppDumper
下载Magisk工具Release Magisk (f9f02c65) (30403) · ayasa520/Magisk
【安卓模拟器安装 Magisk30.4】 https://www.bilibili.com/video/BV1PYWHzCEjE/?share_source=copy_web&vd_source=05320767bb904def1ce7c1e88823462a
获取游戏包名称,以mumu模拟器为例,在这个路径的文件夹名称就是包名。

Fork本体violet-wdream/Zygisk-Il2CppDumper: Using Zygisk to dump il2cpp data at runtime,不需要下载,然后在你Fork的分支actions里面输入包名,构建文件。
原仓库使用的actions太久了需要改成v4版本,修改了build.yml
- uses: actions/upload-artifact@v4

Workflow runs · violet-wdream/Zygisk-Il2CppDumper放置少女jp.glee.girl构建结果模块
安装构建的模块。

启动游戏,会在/data/data/GamePackageName/files/目录下生成dump.cs
这里使用mumu模拟器自带的root explorer查看。选中后选择压缩就会发送到可直接查看的目录/storage/emulated/0/SpeedSoftware/Archives

本机的根目录。

Il2CppDumper
和Zygisk-Il2CppDumper二选一即可。Perfare/Il2CppDumper: Unity il2cpp reverse engineer
使用方法Il2CppDumper/README.zh-CN.md at master · Perfare/Il2CppDumper
这里的放置少女的global-metadata.dat已被加密,所以不适用。
ADB
adb root
adb shell
exit
pm path com.fknzj.qooapp
ls -l /data/data/com.fknzj.qooapp
Frida连接
模拟器frida服务
APP逆向第五课: Frida安装&基础HOOK⽅法_哔哩哔哩_bilibili
模拟器adb shell连接
查看模拟器adb端口

找到adb路径D:\Games\Mumu\MuMuPlayer\nx_device\12.0\shell,getprop查询CPU架构为x86_64
adb.exe connect 127.0.0.1:5555
adb.exe shell
getprop ro.product.cpu.abi

找到对应版本下载。https://github.com/frida/frida/releases/download/16.1.0/frida-server-16.1.0-android-x86_64.xz
解压xz文件后得到一个无拓展名文件。重新命名为frida 根据个人喜好。
传入到模拟器中。这里使用mumu共享文件夹传输文件,然后在共享文件夹再次把文件移动到本机根目录
或者使用push指令一步到位
adb push ...\frida /data/local/tmp

打开root exploreer或者任意root管理器,找到frida文件

再次移动frida,移动到tmp目录,因为其他目录下不能修改这个文件的权限。

修改文件权限为777,全部勾选即可。

接下来cd进这个目录,启动frida
cd /data/local/tmp;
./frida
然后这里会显示权限不够。
`Cetus:/data/local/tmp $ ./frida
Unable to save SELinux policy to the kernel: Permission denied`。
需要提升权限,输入exit返回
exit
adb root
adb shell
cd /data/local/tmp
./frida
然后如果命令行一动不动,说明模拟器frida服务成功开启了

物理机连接frida服务
接下来用物理机连接模拟器的frida服务,新建一个终端,还是在D:\Games\Mumu\MuMuPlayer\nx_device\12.0\shell这个路径下,进行端口转发。
adb forward tcp:27042 tcp:27042
物理机下载frida和frida-tools,参照这个表找到对应版本thelicato/frida-compatibility-matrix: Compatible versions of the frida package for each version of the frida-tools package. Automatically updated through CI/CD
因为我们下载的是16.1.0版本,所以tools下载12.3.0,如果版本下载太高了就无法识别模拟器的frida服务。
pip uninstall frida-tools frida -y #如果下载错了需要先卸载frida和依赖
pip install frida==16.1.0 frida-tools==12.3.0
两个包都需要指定版本,不然会自动下载默认版本,可能会出问题。
如果一切顺利,查询模拟器进程,可以查到PID和对应进程。
frida-ps -U

hook
模拟器frida服务容易掉线,如果出现了问题可以检查一下是不是frida服务掉线了。
最后一个参数是包名,获取方式在Zygisk-Il2CppDumper这个章节,这里是目标游戏放置少女的包名。
在物理机下输入以下指令,会自动启动
frida -U -f "jp.glee.girl"

这里就完成了Frida功能的初步验证。
获取逆向所需文件
打开APK安装包,自己找,只要是unity游戏一定会有。
- libil2cpp.so 所有的C#方法被编译成了C++
- global-metadata.dat 结构化的数据库,存放了定义


启动游戏后,通过Zygisk-Il2CppDumper可以自动获取dump.cs,可以绕过保护。
- dump.cs 反编译出的所有方法的可读函数名
获取关键加密信息
使用IDA导入文件libil2cpp.so
从dump.cs中寻找关键字decrypt encrypt

可以看到这里有对称加密算法的相关方法。
然后可以去IDA按g输入跳转RVA地址,观察是否有值得注意的信息,可以右键切换视图。
继续搜索相关的信息RC4 DES AES等等
public static class CompressUtils
{
// Fields
// Properties
// Methods
// RVA: 0x2472bf4 VA: 0x7d822b773bf4
public static extern Int32 lz4f_compress_file(String inputFilePath, String outputfilePath, out Int64 compressedSize) { }
// RVA: 0x2472cbc VA: 0x7d822b773cbc
public static extern Int32 lz4f_decompress_file(String inputFilePath, String outputfilePath, out Int64 decompressedSize) { }
// RVA: 0x2472d84 VA: 0x7d822b773d84
public static extern Int32 zlib_uncompress([Out] IntPtr dst, out Int32 destLen, [In] IntPtr src, Int32 srcSize) { }
// RVA: 0x2472e2c VA: 0x7d822b773e2c
public static extern Int32 zlib_gzuncompr_cb([In] IntPtr src, Int32 srcSize, ZlibGZUncomprDelegate cb, IntPtr ud) { }
// RVA: 0x2472edc VA: 0x7d822b773edc
public static extern Void RC4_encrypt([In] IntPtr src, Int32 srcSize, [Out] IntPtr dst) { }
// RVA: 0x2472f74 VA: 0x7d822b773f74
private static Void .cctor() { }
// RVA: 0x2472a7c VA: 0x7d822b773a7c
private static Void HandleGZUncompressData(IntPtr d, Int32 len, IntPtr ud) { }
// RVA: 0x247320c VA: 0x7d822b77420c
public static MemoryStream DecompressGZ(Stream inputStream) { }
// RVA: 0x2473548 VA: 0x7d822b774548
public static Int64 LZ4_CompressFile(String fullpath, Boolean suppressError) { }
// RVA: 0x24738bc VA: 0x7d822b7748bc
public static Int64 LZ4_DecompressFile(String fullpath, Boolean suppressError) { }
// RVA: 0x24735bc VA: 0x7d822b7745bc
private static Int64 LZ4_DoBigFile(String fullpath, Boolean compressOrDecompress, Boolean suppressError) { }
// RVA: 0x2473930 VA: 0x7d822b774930
public static Int32 Read7BitEncodedInt(Byte* src, ref Int32 idx) { }
// RVA: 0x24739e0 VA: 0x7d822b7749e0
public static Int32 Zlib_Decompress(NativeDataView src, ref Byte[] dst) { }
// RVA: 0x2473c14 VA: 0x7d822b774c14
public static Int32 RC4_DecryptBufferOverlapped(ref NativeDataView src) { }
// RVA: 0x2473c94 VA: 0x7d822b774c94
public static Int32 RC4_DecryptBufferOverlapped(Byte[] src, Int32 srcLength) { }
}
基本可以确定加密算法是 RC4,需要获取初始向量IV和密钥Key
Gemini编写hook脚本 hook.js
// 假设 libil2cpp.so 已经被加载
var module = Process.findModuleByName("libil2cpp.so");
if (module) {
console.log("[+] libil2cpp.so Found at Base Address: " + module.base);
// RVA 地址 (Relative Virtual Address)
const RVA_KEY_BUILDER_KEY = 0x1b77308; // KeyBuilder::Key(Int32 size)
const RVA_KEY_BUILDER_IV = 0x1b7737c; // KeyBuilder::IV(Int32 size)
const RVA_RC4_DECRYPT_OVERLAP_RET = 0x2473C80; // RC4_DecryptBufferOverlapped 返回前 (获取明文)
// 计算实际的绝对地址 (Native Pointer)
const keyBuilderKeyPtr = module.base.add(RVA_KEY_BUILDER_KEY);
const keyBuilderIVPtr = module.base.add(RVA_KEY_BUILDER_IV);
// 注意:我们 Hook 在解密函数返回前的指令,此时数据已经被原地解密
const rc4DecryptRetPtr = module.base.add(RVA_RC4_DECRYPT_OVERLAP_RET);
// --- Hook 1 & 2: 捕获 Key 和 IV ---
hookKeyAndIV(keyBuilderKeyPtr, "RC4 Key");
hookKeyAndIV(keyBuilderIVPtr, "RC4 IV");
// --- Hook 3: 捕获解密后的缓冲区 ---
hookDecryptedBuffer(rc4DecryptRetPtr);
} else {
console.log("[-] libil2cpp.so NOT found. Check process and module name.");
}
// ----------------------------------------------------------------------
// 辅助函数
// ----------------------------------------------------------------------
function hookKeyAndIV(targetPtr, keyType) {
Interceptor.attach(targetPtr, {
onLeave: function (retval) {
// 在 ARM64 中,返回值通常在 X0 寄存器中,返回的是 Byte[] 对象的指针
// 在 il2cpp 中,Byte[] 对象通常结构为:
// [X0] -> Il2CppArray Object
// + 0x0 (指针) -> Class/VTable
// + 0x8 (指针) -> Bounds (通常为 null)
// + 0x10 (Int32) -> Length
// + 0x14 (Int32) -> Padding
// + 0x18 (指针) -> 实际数据 (元素 0)
// 假设实际数据从偏移 0x18 开始,长度在 0x10
var byteArrPtr = retval;
if (!byteArrPtr.isNull()) {
var length = byteArrPtr.add(0x10).readInt();
var dataPtr = byteArrPtr.add(0x18);
// 为了安全,限制读取长度
if (length > 0 && length < 512) {
var buffer = dataPtr.readByteArray(length);
console.log(`\n[+] CAPTURED ${keyType} (${length} bytes):`);
console.log(hexdump(buffer, { length: length }));
} else {
console.warn(`[!] ${keyType} length ${length} is suspicious or too large. Skipped reading.`);
}
} else {
console.warn(`[!] ${keyType} generation returned null.`);
}
}
});
}
function hookDecryptedBuffer(targetPtr) {
Interceptor.attach(targetPtr, {
onEnter: function (args) {
// 这个 Hook 点位于 RC4_DecryptBufferOverlapped 快返回之前。
// 此时 X19 寄存器(在 onEnter/onLeave 中通过 this.context.x19 访问)
// 仍然持有 NativeDataView/Byte[] 对象的指针 (来自 sub_2473C2C MOV X19, X0)
var byteArrPtr = this.context.x19;
if (!byteArrPtr.isNull()) {
var length = byteArrPtr.add(0x10).readInt(); // 长度在偏移 0x10
var dataPtr = byteArrPtr.add(0x18); // 数据在偏移 0x18
// 捕获解密后的数据 (最大 512 字节,或限制为实际长度,取较小值)
var captureLength = Math.min(length, 512);
// 确保数据指针有效且长度合理
if (length > 0) {
var buffer = dataPtr.readByteArray(captureLength);
console.log(`\n[+] CAPTURED DECRYPTED BUFFER (First ${captureLength}/${length} bytes):`);
console.log(" Address (NativeDataView): " + byteArrPtr);
console.log(" Data Pointer: " + dataPtr);
console.log(hexdump(buffer, { length: captureLength }));
// 检查魔术字来确定压缩类型
if (buffer && buffer.byteLength >= 4) {
var magic = (new Uint8Array(buffer.slice(0, 4)));
console.log(` First 4 bytes (Hex): 0x${magic[0].toString(16).padStart(2, '0')}${magic[1].toString(16).padStart(2, '0')}${magic[2].toString(16).padStart(2, '0')}${magic[3].toString(16).padStart(2, '0')}`);
if (magic[0] === 0x1f && magic[1] === 0x8b) {
console.log(" !!! LIKELY GZIP/ZLIB COMPRESSED DATA !!!");
}
// LZ4 文件头是 0x04224D18 (小端序) 或 0x184D2204 (大端序)
// 字节数组 magic[0]=0x18, magic[1]=0x4d, magic[2]=0x22, magic[3]=0x04
if (magic[0] === 0x18 && magic[1] === 0x4d && magic[2] === 0x22 && magic[3] === 0x04) {
console.log(" !!! LIKELY LZ4 COMPRESSED DATA !!!");
}
}
}
}
}
});
}
关闭游戏,通过frida注入hook启动,需要在hook脚本路径下执行,或者指定hook路径。
这个是动态注入的,也就是说你可以随时更改hook脚本来观察结果。
以下两种方式选择一种,在物理机上执行。
Spawn启动(会重启游戏,如果游戏是开着的)
frida -U -f "jp.glee.girl" -l hook.js
Process.enumerateModules()
Attach到进程启动,注意每次重开游戏PID都会改变,需要重新在物理机上查询。
frida-ps -U #查看PID为 28902
frida -H 127.0.0.1:27042 -p 28902 -l hook.js --realm emulated
#如果报错提示Failed to attach: process is not using emulation则移除 --realm emulated
Process.enumerateModules()
==需要注意的是mumu模拟器当前如果是x86-64架构,而游戏是arm64架构,是无法直接找到libil2cpp.so模块的,而是通过libhoudini.so翻译层执行。==
反斜杠表示转义。

卸载游戏,重启模拟器。试了一下,改成这样直接APK无法下载了。
另一个解法就是换成root过的真机。
hook脚本也确实提示找不到libil2cpp.so,这里在adb shell上查看一下进程内存分配,PID替换为实际进程号
cat /proc/PID/maps
发现有global-metadata.dat,可以确认libil2cpp.so 模块一定在内存中
78d540735000-78d540dfa000 rw-p 00000000 08:23 6423855 /data/media/0/Android/data/jp.glee.girl/files/il2cpp/Metadata/global-metadata.dat
但是却找不到,说明大概率是模块名称被修改了。
未完待续。。。
Comments