手游 SQLite3 数据库解密实例
milkory 🐇

近期有新游上线,久违的写篇流水账。

在 Android 目录下可以找到热更新包的配置文件,后缀名是 .bytes,直接打开发现是加密过的,基本没有规律可言。

image

加密的配置文件

首先找 libil2cpp.soglobal-metadata.dat,两个文件都没有加密,可以直接用 Il2CppDumper 解出。打开导出的 stringliteral.json 文件,搜索 .bytes,发现确实存在该字符串,位于地址 0x5249578

在 IDA 中打开 libil2cpp.so,定位到这个地址,搜索其交叉引用,可以找到一个函数 SQLiteLoader$$OnLoadFile,疑似用于加载配置文件。

image

交叉引用

在反汇编结果中发现该函数只是读取了配置文件内容,没有对文件解密的过程,在相关函数中寻找也一无所获。但是在另一个函数 ConfigDBMap$$IsDBInUse 中发现调用了 MUGame.LuaUtil$$IsFileLocked,怀疑配置文件读取发生在 lua 脚本中。

热更新包中可以找到分散的 lua 文件,其中有一个文件名为 DBTools.bytes,文件头是 1B 4C 4A,标准的 LuaJIT 文件头,尝试使用 ljd 反编译,可惜失败。重新打开文件,发现存在字符串字面量 dbPassword,最后可以找到一串显然格格不入的字符串 apj20240312(已脱敏),疑似数据库密码。

Google 搜索「SQLite3 加密」,基本上都指向同一个软件 SQLCipher。软件付费分发但是免费开源。在 WSL2 上克隆储存库之后按照 README.md 中的方法 2 编译成功,之后尝试解密。

1
2
3
4
5
6
7
8
9
10
11
12
milkory@mky0:~/test$ ./sqlcipher
SQLite version 3.44.2 2023-11-24 11:41:44 (SQLCipher 4.5.6 community)
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open ConfDataBasic.bytes
sqlite> PRAGMA key='apj20240312';
ok
sqlite> .tables
ConfActivityPalace ConfOWDayType
ConfActivityTrackAD ConfOWDivination
(以下略)

可见解密成功。接下来要写脚本导出数据,我选择使用 Python 库 pysqlcipher3 来导出。这个库也需要自己构建。

构建后,编写解包脚本,其中核心代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pysqlcipher3 import dbapi2 as sqlite

def unpack(db_path, out_path):
conn = sqlite.connect(db_path)
cur = conn.cursor()
cur.execute("PRAGMA key='%s';"%db_password)
tables = cur.execute("SELECT name FROM sqlite_master WHERE type='table';").fetchall()
table_names = [t[0] for t in tables]

for table in table_names:
data = cur.execute("SELECT * FROM %s;"%table).fetchall()
columns = [desc[0] for desc in cur.description]
rows = [dict(zip(columns, row)) for row in data]
with open(os.path.join(out_path, '%s.json')%table, 'w') as f:
json.dump(rows, f, indent=4, ensure_ascii=False)

conn.close()

以上是对游戏数据的解包流程。该游戏的游戏资源也无法直接解出,但不在本文讨论范围之内。这里简单提一下,只要把文件头中多余的 UnityFS 片段删去,就能用 Studio 处理了。