WeChat MsgDb Decrypt
提取数据库
ls /data/data/com.tencent.mm/MicroMsg/21159b1820fe6ca555bfa51a3675b28d/EnMicroMsg.db
adb root
adb pull /data/data/com.tencent.mm/MicroMsg/21159b1820fe6ca555bfa51a3675b28d/EnMicroMsg.db
adb pull /sdcard/Download/EnMicroMsg.db
PAGE_SIZE: 1024
KDF iteratorations: 4000
HMAC algorithm: sha1
KDF algorithm: sha1
pysqlcipher3的解密数据库的实现如下,由于该库的编译过程过于复杂,且pip install pysqlcipher3
在python3上无法直接安装,予以放弃,采取自实现解密。
conn = sqlite.connect(in_db_path)
c = conn.cursor()
c.execute("PRAGMA key = '" + passwd + "';")
c.execute("PRAGMA cipher_use_hmac = OFF;")
c.execute("PRAGMA kdf_iter = 4000;")
c.execute("PRAGMA cipher_page_size = 1024;")
c.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA1;")
c.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;")
c.execute("ATTACH DATABASE '" + out_db + "' AS db KEY '';")
c.execute("SELECT sqlcipher_export('db');")
c.execute("DETACH DATABASE db;")
conn.close()
https://sqlitebrowser.org/dl/
安装frida
pip install frida
pip install frida-tools
pip install objection
adb push frida-server-16.1.10-android-arm64 /data/local/tmp/frida-server
cd /data/local/tmp
chmod 777 frida-server
./frida-server
frida unexpected response: PasswordProtected
拔掉其他手机设备
hook相关函数,打印参数用于验证密钥生成算法是否正确。
frida-ps -U -a
objection -g com.tencent.mm explore
android hooking list class_methods com.tencent.wcdb.database.SQLiteDatabase
android hooking watch class_method com.tencent.wcdb.database.SQLiteDatabase.open --dump-args
HMAC
if this context is setup to use hmac checks, generate a seperate and different
key for HMAC. In this case, we use the output of the previous KDF as the input to
this KDF run. This ensures a distinct but predictable HMAC key.
sqlcipher_codec_pragma
static int sqlcipher_codec_ctx_reserve_setup(codec_ctx *ctx) {
int base_reserve = ctx->iv_sz; /* base reserve size will be IV only */
int reserve = base_reserve;
ctx->hmac_sz = ctx->provider->get_hmac_sz(ctx->provider_ctx, ctx->hmac_algorithm);
if(sqlcipher_codec_ctx_get_use_hmac(ctx))
reserve += ctx->hmac_sz; /* if reserve will include hmac, update that size */
/* calculate the amount of reserve needed in even increments of the cipher block size */
if(ctx->block_sz > 0) {
reserve = ((reserve % ctx->block_sz) == 0) ? reserve :
((reserve / ctx->block_sz) + 1) * ctx->block_sz;
}
sqlcipher_log(SQLCIPHER_LOG_DEBUG, "sqlcipher_codec_ctx_reserve_setup: base_reserve=%d block_sz=%d md_size=%d reserve=%d",
base_reserve, ctx->block_sz, ctx->hmac_sz, reserve);
ctx->reserve_sz = reserve;
return SQLITE_OK;
}
第一页
PAGE_SIZE - SALT_SIZE - RESERVE_SIZE
1024 - 16 - 16 = 992
992/16 = 62
后续页
PAGE_SIZE - RESERVE_SIZE
1024-16 = 1008
1008/16 = 63
auth_info_key_prefs.xml
EnMicroMsg.db
由于无hmac验证,根据sqlcipher_codec_ctx_reserve_setup算法可知预留大小发生变化。
形成解密脚本
import os
from typing import Union,List
from Crypto.Cipher import AES
import hashlib
import hmac
import binascii
import sqlite3
SQLITE_FILE_HEADER = b"SQLite format 3\x00"
KEY_SIZE = 32
DEFAULT_PAGESIZE = 1024
DEFAULT_ITER = 4000
# 通过密钥解密数据库
def decrypt(key: str, db_path, out_path):
"""
通过密钥解密数据库
:param key: 密钥 7位16进制字符串
:param db_path: 待解密的数据库路径(必须是文件)
:param out_path: 解密后的数据库输出路径(必须是文件)
:return:
"""
if not os.path.exists(db_path) or not os.path.isfile(db_path):
raise Exception("db_path must be a file")
with open(db_path,"rb") as file:
blist = file.read()
# 每一个数据库文件的开头16字节都保存了一段唯一且随机的盐值,作为HMAC的验证和数据的解密
salt = blist[:16]
byteKey = hashlib.pbkdf2_hmac('sha1',key.encode(),salt,DEFAULT_ITER,dklen=KEY_SIZE)
first = blist[16:DEFAULT_PAGESIZE]
if len(salt) != 16:
raise Exception("salt must be 16 bytes")
block_sz = 16
reserve_sz = 0
# iv size
iv_sz = 16
# hmac size
hmac_sz = 20
reserve_sz = iv_sz
reserve_sz += hmac_sz
if reserve_sz % block_sz != 0:
reserve_sz = ((reserve_sz // block_sz) + 1) * block_sz
print("reserve_sz:",reserve_sz)
reserve_sz = iv_sz
if reserve_sz % block_sz != 0:
reserve_sz = ((reserve_sz // block_sz) + 1) * block_sz
print("reserve_sz:",reserve_sz)
newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)]
with open(out_path,"wb") as deFile:
deFile.write(SQLITE_FILE_HEADER)
# 第一页前16字节为盐值,紧接着是992字节的加密数据段和16字节的保留段
iv = first[-16:]
t = AES.new(byteKey, AES.MODE_CBC, iv)
decrypted = t.decrypt(first[:-16])
deFile.write(decrypted)
deFile.write(first[-16:])
# 后续页均是1008字节长度的加密数据段和16字节的保留段
for i in newblist:
iv = i[-16:]
t = AES.new(byteKey, AES.MODE_CBC, iv)
decrypted = t.decrypt(i[:-16])
deFile.write(decrypted)
deFile.write(i[-16:])
return True,[db_path,out_path]
def get_msgdb_key(uin,imei):
key = imei + uin
md5 = hashlib.md5()
md5.update(key.encode('utf-8'))
key = md5.hexdigest()[:7].lower()
print("key:",key)
return key
def parse_contract(db_path):
if not os.path.exists(db_path):
print("DB not found: ", db_path)
return False
conn = sqlite3.connect(db_path)
c = conn.cursor()
users = c.execute("SELECT username, alias, nickname from rcontact WHERE type=1 OR type=8388611")
for user in users:
username = user[0]
alias = user[1]
nickname = user[2]
# 忽略微信团队和文件助手
if username == "weixin" or username == "filehelper":
continue
print(user)
return True
imei = "1234567890ABCDEF"
uin = "1146048721"
key = get_msgdb_key(uin,imei)
ret = decrypt(key,"EnMicroMsg.db","EnMicroMsg.decrypted.db")
parse_contract("EnMicroMsg.decrypted.db")
https://github.com/BeneficialCode/wechat_msgdb_decrypt