看52pojie上有大佬发了关于提取国际服ba剧情文本的帖子,正好有项目需要用到剧情文本,就想试试自己提取。 结果,SQLite数据库居然打不开!一看文件头才知道,原来数据库文件被加密了。
初步分析
这款游戏使用unity引擎开发,于是使用il2cppdumper提取c#结构。很幸运,这一版本的游戏并没有对il2cpp的相关文件做修改。很轻松地拿到了文件。
将libil2cpp.so导入ida反汇编,再使用il2cppdumper的ida脚本恢复函数名和符号。
根据52的那篇帖子,决定先从ScenarioScriptExcel结构体相关的逻辑入手。
静态分析
在函数列表中搜索ScenarioScriptExcel,发现位于0x206222C的MX_Data_ScenarioData__GetScenarioScriptExcels方法返回了一个ScenarioScriptExcel数组。我想这个应该就是获取剧情脚本的函数。 接着看函数调用,发现其中调用
根据il2cpp中处理泛型的方式,可以得出类似的C#代码
c#
object = ((DataBaseForSQLite)this).get<ScenarioScriptExcelRepository>();
跳转到MX_Data_DataBaseForSQLite__Get_object_方法,其主要逻辑是从自身的repositoryDic字段中获取继承自ISQLiteRepository的泛型的实例。推测其逻辑大概是由DataBaseForSQLite加载数据库,ISQLiteRepository对象对应了数据库中部分表序列化后的数据。
这时候我在撞运气分析DataBaseForSQLite中的其他函数,想看看有没有初始化逻辑。突然发现MX_Data_DataBaseForSQLite__Initialize_31878536在泛型方法MX_Data_DataManager__Load_object_中被调用了:

v2的类型是由传入的泛型决定的,当传入的泛型继承自DataBaseForSQLite时,函数则初始化DataBaseForSQLite并返回对象。 ExcelDB.db是在游戏初始化阶段就被加载的,于是搜索MX_Data_DataManager__Load_object_在文件中的调用,找到了MX_Data_GameDataInitializer__InitInBuildDataManager中的调用
这正好也是DataManager的初始化逻辑 其中LocalizeInBuildData继承自DataBaseForSQLite,由此确定DataManager确实管理了ExcelDB.db的加载。
动态调试
我这时并不清楚解密逻辑具体的位置,网上有文章提到Windows上的ba使用的是SQLCipher。于是我便找到了MX_Data_DataManager__SetSQLCipher方法,查看MX_Data_DataManager__SetSQLCipher的调用点。只有一个直接调用,在QueuingGetCryptoKeysNetworkTask__HandleMessage方法里。
看了一眼逻辑,函数的四个参数都是从网络包获取的,看来静态调试已经走到头了。
本来想直接hook QueuingGetCryptoKeysNetworkTask__HandleMessage拿到SetSqlcipher的参数的,结果看了一眼SetSqlcipher中还使用MX.Crypto中的方法对这四个参数做了额外处理。索性直接hook SetSQLCipher这个方法。
SetSQLCipher中调用的游戏加密函数调用是

这个函数的逻辑并不在libil2cpp.so中,从函数逻辑来看,似乎是il2cpp动态加载函数😓
受限于我的水平,我对加密具体的算法的研究就止步于此了,好在获取到密钥并不需要知道具体的加密函数。直接hook MX_Core_Crypto_O955132dac5d41feabf746f1417de25a752632adde964512864135725661e5e1f__set_Key的调用
类似的C#代码就是
C#
this.SQLKey.set_Key(v17);
set_Key函数只是将传入的的Byte\[\] value拆分存入了类的part1,part2,part3字段。类中并没有其他的加密逻辑。
直接hook对set_key的调用,抓取其第二个参数的值:
编写frida脚本,在onEnter时hook,从X0寄存器找到Byte\[\] value的地址,抓取数据:
javascript
var HOOK_RVA = 0x1E68768;
function hookWhenReady() {
var m = Process.findModuleByName("libil2cpp.so");
if (!m) { setTimeout(hookWhenReady, 1000); return; }
var addr = m.base.add(HOOK_RVA);
Interceptor.attach(addr, {
onEnter: function() {
var x0 = this.context.x0;
console.log("\n[X0] = " + x0);
var len = x0.add(0x18).readU32();
console.log("\n[len @+0x18] = " + len);
if (len > 0 && len <= 128) {
var hex = "";
for (var i = 0; i < len; i++) {
hex += ("0" + x0.add(0x20).add(i).readU8().toString(16)).slice(-2);
}
console.log("[hex @+0x20] = " + hex);
}
console.log("ok")
}
});
console.log("ready");
}
hookWhenReady();
成功获取到了key。
解密数据库
在dump出来的stringliterial中找到了:
json
{
"value": "PRAGMA cipher_license = \u0027",
"address": "0x6AF5408"
},...
{
"value": "PRAGMA key = \u0022x\u0027",
"address": "0x6AF5418"
}
推断游戏使用了SQLCipher来加密数据库。
使用SQLCipher 交互式命令行连接到数据库。在测试了不同版本的加密参数后,确定了SQLCipher配置为:
sql
PRAGMA cipher_page_size = 4096;
PRAGMA kdf_iter = 256000;
PRAGMA cipher_hmac_algorithm = HMAC_SHA512;
PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;
成功加载了数据库。
剧情文本取得
之后便是dump了数据库到未加密的数据库文件。
ScenarioScriptExcel结构体和数据库内部没有其他的改动,直接按照52上大佬的方式导出剧情文本就好。

感谢il2cppdumper的开发者