自QQ用了新内核“NT”架构,原有的数据库解密方法也用不了了。查看新版QQ的文件结构可以发现它的新数据库文件:$HOME/.config/QQ/nt_qq_<ID>/nt_db/nt_msg.db
。
我尝试在GitHub上全网搜索“nt_msg.db”,发现仅有两个相关的页面:
- https://github.com/Young-Lord/QQ-History-Backup/issues/9
- https://github.com/QQBackup/qq-win-db-key/blob/02623f5cd547865bbcddc9da5c0ed99584c87b15/nt%20qq%20win%20db%20%E6%95%99%E7%A8%8B.md
逆向不会搞。但得到了两个很有用的信息:key的长度为16,和密钥派生函数迭代次数为4000。并且看那上面有个打开SQLCipher的截图,密钥是直接输进去的,那就应都是可打印字符。我使用内存转储,把转储文件通过strings
命令过滤出字符串,然后再筛选出16长度的行,这样就可以一个一个去试了。
这是试密码的程序:
#!/bin/env rust-script
//! ```cargo
//! [dependencies]
//! rusqlite = { version = "0.29.0", features = ["bundled-sqlcipher"] }
//! ```
#![feature(try_blocks)]
use rusqlite::{params, Connection};
use std::env::args;
use std::io::{stdin, BufRead, BufReader};
use std::process::exit;
fn main() {
let args = args().skip(1).collect::<Vec<_>>();
if args.is_empty() {
println!("Usage: cmd <db> <kdf_iter>");
println!("Stdin: keys");
return;
}
let db_path = &args[0];
let kdf_iter = &args[1];
let stdin = stdin().lock();
let reader = BufReader::new(stdin);
for (i, line) in reader.lines().enumerate() {
let key = line.expect("Line read error");
println!("Trying: {} {}", i + 1, key);
let result: rusqlite::Result<String> = try {
let db = Connection::open(db_path)?;
db.pragma(None, "key", &key, |_| Ok(()))?;
db.pragma(None, "kdf_iter", kdf_iter, |_| Ok(()))?;
let row = db.query_row("select * from sqlite_master", params![], |r| {
r.get::<_, String>(0)
})?;
println!("{}", row);
key
};
match result {
Ok(k) => {
println!("Found it!! {}", k);
exit(0);
}
Err(e) => {
if e.to_string() != "file is not a database" {
println!("{}", e);
}
}
}
}
}
获取QQ PID的操作:
# https://superuser.com/a/822450
~ ❯ ps --forest -o pid=,tty=,stat=,time=,cmd= -g $(ps -o sid= -p $(pgrep linuxqq)) 22:53:27
732596 ? S 00:00:00 /bin/bash /home/bczhc/bin/scripts/non-proxy linuxqq
732597 ? S 00:00:00 \_ /bin/bash /home/bczhc/bin/scripts/exec-override/linuxqq
732598 ? S 00:00:00 \_ /bin/bash /home/bczhc/bin/scripts/non-proxy /bin/linuxqq
732599 ? Sl 00:03:05 \_ /opt/QQ/qq
732604 ? S 00:00:00 \_ /opt/QQ/qq --type=zygote --no-zygote-sandbox
732650 ? Sl 00:02:00 | \_ /opt/QQ/qq --type=gpu-process --crashpad-handler-pid=732633 --enable-crash-reporter
732605 ? S 00:00:00 \_ /opt/QQ/qq --type=zygote
732607 ? S 00:00:00 | \_ /opt/QQ/qq --type=zygote
732771 ? Sl 00:05:33 | \_ /opt/QQ/qq --type=renderer --crashpad-handler-pid=732633 --enable-crash-reporte
829844 ? Sl 00:00:02 | \_ /opt/QQ/qq --type=renderer --crashpad-handler-pid=732633 --enable-crash-reporte
732652 ? Sl 00:00:02 \_ /opt/QQ/qq --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US
813475 ? Sl 00:00:00 \_ /opt/QQ/qq --type=utility --utility-sub-type=audio.mojom.AudioService --lang=en-US --se
使用732599
。
最终的操作:
# https://serverfault.com/a/408929
sudo ../dump-memory <QQ-pid>
cat *.dump > d
cat d | strings | rg '^.{16}$' | sort | uniq > len16
cp ~/.config/QQ/nt_qq_6bb87db59dd2e7d303966b6fc81dc8dd/nt_db/nt_msg.db .
# 哦对,还有要提到的是这个数据库需要跳过1024字节才是真正的SQLite数据库
cat nt_msg.db | tail -c +1025 | sponge db
# find-key为上面的Rust程序
cat len16 | find-key db 4000
运行,很快就找到了key。
转为不加密的SQLite:
sqlcipher db "pragma key = 'Q}MD}n\$3P]9uP)Xm'; pragma kdf_iter = 4000" .d | tail +2 | sqlite3 decrypted-db
其他的数据库应该也用的是同一个key,比如group_msg_fts.db
,这个应该是存群聊天记录。
我的本地快速解密脚本
key='Q}MD}n$3P]9uP)Xm'
rm /tmp/nt_msg.db /tmp/group_msg_fts.db
cd ~/.config/QQ/nt_qq_6bb87db59dd2e7d303966b6fc81dc8dd/nt_db
pv nt_msg.db | tail -c +1025 > /tmp/db
sqlcipher /tmp/db "pragma key = '$key'; pragma kdf_iter = 4000" .d | tail +2 | sed 's/ROLLBACK; -- due to errors/COMMIT;/' | sqlite3 /tmp/nt_msg.db
pv group_msg_fts.db | tail -c +1025 > /tmp/db
sqlcipher /tmp/db "pragma key = '$key'; pragma kdf_iter = 4000" .d | tail +2 | sed 's/ROLLBACK; -- due to errors/COMMIT;/' | sqlite3 /tmp/group_msg_fts.db
好强!请问您愿意把这个 gist 的内容或链接加入 qq-win-db-key 里吗?我来做或者您发 pr 都行。