BlueArchive蔚蓝档案国际服剧情文本取得

看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的开发者

相关推荐
玫幽倩7 天前
2026黄河流域公安院校-电子物证单项赛(程序逆向分析+服务器取证)
运维·服务器·python·电子取证·逆向·程序分析·服务器取证
小小工匠10 天前
当 Agent 遇上逆向:拆解 reverse-skill 的技能路由架构
架构·逆向·reverse-skill
枝头玉1 个月前
新160个CrackMe048-monkeycrackme1、049-THraw-crackme8、050-daxxor逆向分析
安全·逆向·crackme·reserve
夜猫逐梦2 个月前
【UE基础】01.环境与引擎心智模型
游戏·逆向·ue·unreal·actionrpg
嫂子的姐夫2 个月前
047-MD5:飞卢网
爬虫·python·js逆向·逆向
嫂子的姐夫2 个月前
050-wx小程序合肥住房
爬虫·python·小程序·逆向
Android小码家2 个月前
白帽子之逆向Crackme2.apk - 反调试(一)
逆向
himobrinehacken2 个月前
C/C++中字符编码与指针应用全解析
c语言·逆向
shandianchengzi2 个月前
【记录】Ghidra|Ubuntu 26.04 下 Ghidra 界面缩放完整指南
linux·ubuntu·逆向·ghidra