国际服 Blue Archive 剧情文本提取记录

看 52pojie 上有大佬发了关于提取国际服 BA 剧情文本的帖子,正好有项目需要用到剧情文本,就想试试自己提取。

结果,SQLite 数据库居然打不开!一看文件头才知道,原来数据库文件被加密了。

初步分析

这款游戏使用 Unity 引擎开发,于是使用 IL2CPPDumper 提取 C# 结构。很幸运,这一版本的游戏并没有对 IL2CPP 的相关文件做修改,很轻松地拿到了文件。

将 libil2cpp.so 导入 IDA 反汇编,再使用 IL2CPPDumper 的 IDA 脚本恢复函数名和符号。

根据 52 的那篇帖子,决定先从 ScenarioScriptExcel 结构体相关的逻辑入手。

静态分析

在函数列表中搜索 ScenarioScriptExcel,发现位于 0x206222C 的 MX_Data_ScenarioData__GetScenarioScriptExcels 方法返回了一个 ScenarioScriptExcel 数组。我想这个应该就是获取剧情脚本的函数。

接着看函数调用,发现其中调用

屏幕截图 2026-08-17 224922

根据 IL2CPP 中处理泛型的方式,可以得出类似的 C# 代码

1
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_ 中被调用了:

屏幕截图 2026-08-17 232039

v2 的类型是由传入的泛型决定的,当传入的泛型继承自 DataBaseForSQLite 时,函数则初始化 DataBaseForSQLite 并返回对象。

ExcelDB.db 是在游戏初始化阶段就被加载的,于是搜索 MX_Data_DataManager__Load_object_ 在文件中的调用,找到了 MX_Data_GameDataInitializer__InitInBuildDataManager 中的调用:

屏幕截图 2026-08-17 232609

这正好也是 DataManager 的初始化逻辑。

其中 LocalizeInBuildData 继承自 DataBaseForSQLite,由此确定 DataManager 确实管理了 ExcelDB.db 的加载。

动态调试

我这时并不清楚解密逻辑具体的位置,网上有文章提到 Windows 上的 BA 使用的是 SQLCipher。于是我便找到了 MX_Data_DataManager__SetSQLCipher 方法,查看 MX_Data_DataManager__SetSQLCipher 的调用点。只有一个直接调用,在 QueuingGetCryptoKeysNetworkTask__HandleMessage 方法里。

屏幕截图 2026-08-17 233751

看了一眼逻辑,函数的四个参数都是从网络包获取的,看来静态调试已经走到头了。

本来想直接 hook QueuingGetCryptoKeysNetworkTask__HandleMessage 拿到 SetSQLCipher 的参数的,结果看了一眼 SetSQLCipher 中还使用 MX.Crypto 中的方法对这四个参数做了额外处理。索性直接 hook SetSQLCipher 这个方法。

SetSQLCipher 中调用的游戏加密函数调用是

屏幕截图 2026-08-17 234414

这个函数的逻辑并不在 libil2cpp.so 中,从函数逻辑来看,似乎是 IL2CPP 动态加载函数😓

受限于我的水平,我对加密具体的算法的研究就止步于此了。好在获取到密钥并不需要知道具体的加密函数,直接 hook MX_Core_Crypto_O955132dac5d41feabf746f1417de25a752632adde964512864135725661e5e1f__set_Key 的调用:

屏幕截图 2026-08-17 235048

类似的 C# 代码就是

1
this.SQLKey.set_Key(v17);

set_Key 函数只是将传入的 Byte[] value 拆分存入了类的 part1、part2、part3 字段,类中并没有其他的加密逻辑。

直接 hook 对 set_Key 的调用,抓取其第二个参数的值:

屏幕截图 2026-08-17 235913

编写 Frida 脚本,在 onEnter 时 hook,从 X0 寄存器找到 Byte[] value 的地址,抓取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 出来的 string literal 中找到了:

1
2
3
4
5
6
7
8
{
"value": "PRAGMA cipher_license = \u0027",
"address": "0x6AF5408"
},...
{
"value": "PRAGMA key = \u0022x\u0027",
"address": "0x6AF5418"
}

推断游戏使用了 SQLCipher 来加密数据库。

使用 SQLCipher 交互式命令行连接到数据库。在测试了不同版本的加密参数后,确定了 SQLCipher 配置为:

1
2
3
4
PRAGMA cipher_page_size = 4096;
PRAGMA kdf_iter = 256000;
PRAGMA cipher_hmac_algorithm = HMAC_SHA512;
PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;

屏幕截图 2026-08-18 001708

成功加载了数据库。

剧情文本取得

之后便是 dump 了数据库到未加密的数据库文件。

ScenarioScriptExcel 结构体和数据库内部没有其他的改动,直接按照 52 上大佬的方式导出剧情文本就好。

屏幕截图 2026-08-17 142625

感谢 il2cppdumper 的开发者。


国际服 Blue Archive 剧情文本提取记录
http://example.com/2026/08/18/ba-story-extraction/
作者
_Achelous
发布于
2026年8月18日
许可协议