Android QQ NT 版数据库解密
以下分析均基于当前最新版本(8.9.78.12275)进行,不保证跨版本兼容性,但是分析思路应该相似。
首先通过简单的定位(文件检测、字符串查找等等)可以得知 QQ NT 版的数据库存放在/data/data/com.tencent.mobileqq/databases/nt_db/nt_qq_{账号相关的hash}/nt_msg.db中,具体的实现代码在libkernel.so中。
根据我们的经验和简单的观察可以发现,QQ NT 版还是采用了 SQLCipher 的加密方案。
SQLCipher 版本定位
由于 SQLCipher 是开源库,为了我们接下来分析逆向代码的方便性,我们可以先定位到其版本,将对应的代码下载下来对照观察。
根据上文内容,可以通过搜索misuse字符串,快速定位到sqlite3_log函数,同时可以得到版本信息872ba256cbf61d9290b571c0e6d82a20c224ca3ad82971edc46b29818d5dalt1。通过搜索这个信息我们可以得到 SQLCipher 的版本为4.5.1(其实在字符串中直接搜索4.也搜得到)。
在 GitHub 上将对应版本的代码下载备用。
解密方式思考
最佳的解密方式肯定是直接将数据库拿出来,自己用 SQLCipher 解密,但是说实话,我折腾了半天都解不出来(其他平台可以很轻松地用原版 SQLCipher 解密)。
于是只能退而求其次,用 Hook 的方式在程序执行中进行解密。此处我们选择 Frida 作为 Hook 工具。
sqlite3_exec 定位
考虑到 QQ 执行过程中肯定会打开聊天消息数据库,我们只需要 Hook 到sqlite3_exec即可。
由于我们手中有源码,定位相当简单,只需要从该函数中挑选一个字符串搜索后查找引用即可。
此处我随意选择了一个调用了sqlite3_exec的函数sqlcipher_check_connection。
static int sqlcipher_check_connection(const char *filename, char *key, int key_sz, char *sql, int *user_version, char** journal_mode) { int rc; sqlite3 *db = NULL; sqlite3_stmt *statement = NULL; char *query_journal_mode = "PRAGMA journal_mode;"; char *query_user_version = "PRAGMA user_version;";
rc = sqlite3_open(filename, &db); if(rc != SQLITE_OK) goto cleanup;
rc = sqlite3_key(db, key, key_sz); if(rc != SQLITE_OK) goto cleanup;
rc = sqlite3_exec(db, sql, NULL, NULL, NULL); if(rc != SQLITE_OK) goto cleanup; ...在 IDA 中查找PRAGMA journal_mode;即可交叉定位到sqlcipher_check_connection函数(0x1CE8634),进而定位到sqlite3_exec函数(0x1CE8DFC)。此处也能定位到sqlite3_key函数,需要寻找 Key 时也可以从这里入手。
通过连接句柄定位文件名
此时还剩下最后一个问题,sqlite3_exec是供全体数据库连接调用的,我们要定位到我们需要的数据库的连接,所以要从句柄中定位到文件名。说实话我还真没找到参考,只能大概糊了一个路线:struct sqlite3->Db *aDb->Btree *pBt->BtShared *pBt->Pager *pPager->char *zFilename。
这个路线如何寻找?由于该项目的代码质量较高,我们全局搜索zFilename,同时这些数据结构中子元素往往对上层元素有反向引用,我们可以一层层上升到句柄结构体(其实我感觉这个活儿肯定有个函数专门处理,但是我没找到)。
Frida 脚本
完成逆向工作后我们就可以编写代码进行 Hook 了,参考网上的资料,我们可以得知在数据库中运行以下指令即可导出没有加密的数据库:
ATTACH DATABASE 'plaintext.db' AS plaintext KEY '';SELECT sqlcipher_export('plaintext');DETACH DATABASE plaintext;事实上这样在安卓上面跑会出错,我猜测是奇怪的文件权限问题,将明文数据库放在权限较低的位置(如/storage/emulated/0/Download/plaintext.db)即可。最终形成的 Frida 脚本如下:
注意,直接对数据库进行操作有可能损坏数据库,请务必做好备份。
// frida -U -f com.tencent.mobileqq -l final.jsconst DATABASE_URI = "/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{CHNAGE_THIS_TO_YOURS}/nt_msg.db";
// FOR LOGlet SQLITE3_EXEC_CALLBACK_LOG = true;let index1 = 0;let xCallback = new NativeCallback( (para, nColumn, colValue, colName) => { if (!SQLITE3_EXEC_CALLBACK_LOG) { return 0; } console.log(); console.log( "------------------------" + index1++ + "------------------------" ); for (let index = 0; index < nColumn; index++) { let c_name = colName .add(index * 8) .readPointer() .readUtf8String(); let c_value = ""; try { c_value = colValue .add(index * 8) .readPointer() .readUtf8String() ?? ""; } catch {} console.log(c_name, "\t", c_value); } return 0; }, "int", ["pointer", "int", "pointer", "pointer"]);
// CODE BELOWlet get_filename_from_sqlite3_handle = function (sqlite3_db) { // full of magic number let zFilename = ""; try { let db_pointer = sqlite3_db.add(0x8 * 5).readPointer(); let pBt = db_pointer.add(0x8).readPointer(); let pBt2 = pBt.add(0x8).readPointer(); let pPager = pBt2.add(0x0).readPointer(); zFilename = pPager.add(208).readPointer().readCString(); } catch (e) {} return zFilename;};
setTimeout(function () { let base_addr = Module.findBaseAddress("libkernel.so"); console.log("libkernel.so base address: " + base_addr);
// sqlite3_exec -> sub_1CFB9C0 let sqlite3_exec_addr = base_addr.add(0x1cfb9c0); console.log("sqlite3_exec_addr: " + sqlite3_exec_addr);
let sqlite3_exec = new NativeFunction(sqlite3_exec_addr, "int", [ "pointer", "pointer", "pointer", "int", "int", ]);
let target_db_handle = null; let js_sqlite3_exec = function (sql) { if (target_db_handle == null) { return -1; } let sql_pointer = Memory.allocUtf8String(sql); return sqlite3_exec(target_db_handle, sql_pointer, xCallback, 0, 0); };
// ATTACH BELOW Interceptor.attach(sqlite3_exec_addr, { onEnter: function (args) { // sqlite3*,const char*,sqlite3_callback,void*,char** let sqlite3_db = ptr(args[0]); let sql = Memory.readCString(args[1]); let callback_addr = ptr(args[2]); let callback_arg = ptr(args[3]); let errmsg = ptr(args[4]); let databasae_name = get_filename_from_sqlite3_handle(sqlite3_db); if (databasae_name == DATABASE_URI) { console.log("sqlite3_db: " + sqlite3_db); console.log("sql: " + sql); target_db_handle = sqlite3_db; } }, }); setTimeout(function () { let ret = js_sqlite3_exec( `ATTACH DATABASE '/storage/emulated/0/Download/plaintext.db' AS plaintext KEY '';SELECT sqlcipher_export('plaintext');DETACH DATABASE plaintext;` ); console.log("js_sqlite3_exec ret: " + ret); }, 4000);}, 1200);