crackproof Windows 通杀方法
目前只有プリンセスコネクト!Re:Dive 无法 dump,这玩意的 crackproof 不知道改了什么东西,会把完整的 PE 切成几百个分页,不过可以分析安卓版本的,壳子难度中等。
由于 Unity 的源代码得买,所以这里只能用反编译器 + pdb 来分析
LoadScriptingRuntime 这个函数加载了 GameAssembly.dll,相关的加载逻辑在 LoadIl2Cpp 里面,LoadIl2Cpp 的返回值是 GameAssembly.dll 的 handle
解法就很明显了,可以用 frida 拦截 LoadIl2Cpp 返回时候的动作,这时候 GameAssembly.dll 刚刚被 LoadLibraryW 加载上去,并且完成了一些初始化(crackproof 修复导入表,解密解压代码段等等),但是没有执行任何 il2cpp 部分的代码,dump 下来就能获得完全干净的 GameAssembly.dll 了。
dump 下来以后还需要简单的修复一下 PE 头,完整代码如下:
'use strict';
const UNITY_PLAYER = "UnityPlayer.dll";
const TARGET_RVA = ;
const GAMEASSEMBLY = "GameAssembly.dll";
const DUMP_PATH = "D:\\Reverse\\Frida_Hook\\GameAssembly_dump_fix.dll";
const CHUNK_SIZE = ;
function dumpModule(moduleName, outPath) {
try {
const m = Process.getModuleByName(moduleName);
console.log("[*] Found module:", m.name, "Base:", m.base, "Size:", m.size);
const size = m.size;
const base = m.base;
// raw→virtual const localCopy = fixPEHeader(base, size);
if (localCopy === null) {
console.error("[!] Fix PE Header failed");
return;
}
const file = new File(outPath, "wb");
console.log("[*] Output:", outPath);
let offset = 0;
while (offset < size) {
const chunk = Math.min(CHUNK_SIZE, size - offset);
const buf = localCopy.add(offset).readByteArray(chunk);
file.write(buf);
offset += chunk;
}
file.flush();
file.close();
console.log("[*] Dump finished:", outPath);
} catch (e) {
console.error("[!] Dump exception:", e);
}
}
function hookAfterUnityPlayerLoaded(module) {
if (module.name !== UNITY_PLAYER) return;
console.log("[+] UnityPlayer.dll loaded @", module.base);
const targetAddr = module.base.add(TARGET_RVA);
console.log("[*] Hooking LoadDynamicLibrary @", targetAddr);
Interceptor.attach(targetAddr, {
onLeave(retval) {
console.log("[*] LoadDynamicLibrary returned:", retval);
try {
const found = Process.findModuleByName(GAMEASSEMBLY);
if (found) {
console.log("[*] GameAssembly.dll loaded -> dumping...");
dumpModule(GAMEASSEMBLY, DUMP_PATH);
} else {
console.warn("[!] GameAssembly.dll not found yet");
}
} catch (e) {
console.error("[!] Dump error:", e);
}
}
});
}
function fixPEHeader(base, size) {
try {
const localBuf = Memory.alloc(size);
Memory.copy(localBuf, base, size);
const dos = localBuf.readPointer();
const e_lfanew = localBuf.add().readU32();
const nt = localBuf.add(e_lfanew);
const numSections = nt.add().readU16();
const optSize = nt.add().readU16();
const firstSec = nt.add( + optSize);
console.log("[*] Sections:", numSections, "First section @", firstSec);
let secPtr = firstSec;
for (let i = 0; i < numSections; i++) {
const virtualAddress = secPtr.add(0xC).readU32();
const virtualSize = secPtr.add().readU32();
// 把 raw data 指向 virtual
secPtr.add().writeU32(virtualAddress); // PointerToRawData
secPtr.add().writeU32(virtualSize); // SizeOfRawData
secPtr = secPtr.add(); // 下一节
}
return localBuf;
} catch (e) {
console.error("[!] fixPEHeader exception:", e);
return null;
}
}
setImmediate(() => {
console.log("[*] Script started.");
Process.attachModuleObserver({
onAdded(module) {
console.log("[*] Module loaded:", module.name);
if (module.name === UNITY_PLAYER) {
hookAfterUnityPlayerLoaded(module);
}
},
onRemoved(module) { }
});
try {
const existing = Process.getModuleByName(UNITY_PLAYER);
if (existing) hookAfterUnityPlayerLoaded(existing);
} catch (e) { }
});
由于 crackproof hook 了自身的 openprocess 并且进行的 handle 权限过滤,frida-server 是肯定不行了,但是 Windows 这玩意相当开放,有以下方法能把 frida-gadget.dll 塞进去:
- 劫持
version.dll。 - 修改
UnityPlayer.dll的导入表,把frida-gadget.dll导出表的任意函数塞进去。 - 搓一个 ring0 驱动,从内核用 APC 方法把
frida-gadget.dll强行塞进去。
frida-gadget.dll 塞进去了以后还需要写一个配置文件,名称命名为 frida-gadget.config
{
"interaction": {
"type": "script",
"path": "D:\\Reverse\\Frida_Hook\\crackproof\\1.js"
}
}
这样 frida-gadget.dll 加载后就能自动执行脚本,手动连接执行肯定是来不及的,因为 GameAssembly.dll 加载时机非常早。
对于ウマ娘 プリティーダービー这种会检查目录下面有没有多余的 dll,可以把带 crackproof 但是不检查 dll 的启动器复制过去,然后就能随意改导入表了。(不带 crackproof 不检查 dll 的启动器貌似不行,疑似启动器上面的壳子有额外检测)
