吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9596|回复: 140
上一主题 下一主题
收起左侧

[原创] 解开 Windows 微信 4.0 版本的主数据库

    [复制链接]
跳转到指定楼层
楼主
xuxinhang 发表于 2026-5-2 21:34 回帖奖励
本帖最后由 xuxinhang 于 2026-7-30 22:06 编辑

我之前写的文章(https://www.52pojie.cn/thread-2068774-1-1.html) 讨论了在Windows微信4.0版本中手机备份文件的解密方法,但是新版备份文件的密钥只存在于手机端,无法从Windows微信上提取,而要从安卓手机中读取密钥又必然要拿到管理员权限,步骤上很繁琐,操作也存在风险。所以,对新版微信来说,不如直接从Windows微信自己的数据库(下称主数据库)中读取信息来得更方便。

与解开备份数据文件相比,解开微信主数据库的方法讨论已经相当多了。早在微信4.0公测的时候,0xlane 就已经研究出了解开Windows微信4.0主数据库的方法。可是,按文章所述方法操作,总是拿不到关键信息,直接拿配套的程序运行,也找不出密钥。可能是为老版本开发的方法在新版本中失效了。另外,原文的逻辑分析也讲得较为含糊。

所以,我这次自立更生,再给大家分享另一种提取并解开Windows微信4.0版本主数据库密钥的思路,另一并详解 用于存储聊天图片的 DAT 文件的解密方法

这里使用的微信版本是 Weixin.exe =  4.0.5.18


主数据库解密

微信主数据库位于C:\Users\[用户名]\Documents\xwechat_files\[微信号]\db_storage,其数据库文件形如 contact/contact.db

由WCDB源码分析数据库密钥流转路径

幸亏微信开源了自己数据库框架 WCDB,不然逆向分析如此大型C++项目中的复杂逻辑简直难如登天。分析时自然要选择与微信版本发布时间相近的代码版本。

定位并初步分析入口函数

开始按照 0xlane 和WCDB文档的指引,从WCDB::Database::setCipherKey函数切入。函数中变量CipherConfigName值总为字符串"com.Tencent.WCDB.Config.Cipher";还有常量Configs::Priority::Highest 为数值 0x8000000。这两个显著的标志足以定位该函数。

void Database::setCipherKey(const UnsafeData& cipherKey, int cipherPageSize, CipherVersion cipherVersion) {
    if (cipherKey.size() > 0) {
        m_innerDatabase->setConfig(
            CipherConfigName,
            std::static_pointer_cast<Config>(std::make_shared<CipherConfig>(
                    cipherKey, cipherPageSize, cipherVersion)),
            Configs::Priority::Highest);
    } else {
        m_innerDatabase->removeConfig(CipherConfigName);
    }
}

使用x64dbg启动weixin.exe调试,先不登录。来到“符号”面板,其中的weixin.dll是体积最大的,其它看名字估计是第三方库或是负责其它小功能的,所以进到weixin.dll模块。先右键菜单扫一遍当前模块的字符串,搜索"com.Tencent.WCDB.Config.Cipher"有好几个结果。逐个跳转至对应的汇编,挑出附近同时出现Configs::Priority::Highest  0x8000000的,即定位到setCipherKey函数的具体位置。

包括上面的"com.Tencent.WCDB.Config.Cipher"在内,部分字符串常量在程序运行后动态加载,用IDA等做静态分析无法搜到,需换用 x64dbg、CheatEngine 等动态分析工具。后文同理。

在该函数开头设断点,然后回到微信界面正常登录,程序将在断点处暂停。setCipherKey 函数仅四个参数(类实例自身算第一个参数),故Windows x64下参数均由寄存器传值。第四个参数即源码中的cipherVersion对应汇编的R9值为4,第三个参数cipherPageSize对应R8值为4096。现在知道了调用SQLCipher 的 [文件版本]  和 [页面大小],分别对应 SQLCipher 文档中写的PRAGMA cipher_page_size
PRAGMA cipher_compatibility 两种语句。

细读 SQLCipher 文档,以了解SQLCipher 自定义的这些SQL语句含义与功能。

深入调用层次寻找密钥的踪迹

第二个参数cipherKey对应RD是一个地址,里面理应有具体密钥的地址。尝试使用SQLStudio打开数据库,不成。

只分析到这里还是不好蒙,只得在WCDB源代码中,沿着setCipherKey函数继续探索。setCipherKey 创建了新的 CipherConfig 对象,定位到CipherConfig类,类的主要逻辑在CipherConfig::invoke里,再深入CipherHandle::setCipherKey方法。

CipherHandle::setCipherKey里面有丰富的常量,有数值99、66、67,也有字符串“'x”,但拿它们定位都太模糊。定位起来最方便的,还得是 buffer[66] = '\'' 这条赋值语句。几乎可以肯定它对应的汇编形如 mov BYTE PTR [???], 0x27

bool CipherHandle::setCipherKey(const UnsafeData &data) {
    if (m_isInitializing && data.size() == 99) {
        UnsafeStringView cipher = UnsafeStringView(data.buffer(), data.size());
        if (cipher.hasPrefix("x'") && cipher.hasSuffix("'")) {
            void *buffer = malloc(67 * sizeof(unsigned char));
            memcpy(buffer, data.buffer(), 66 * sizeof(unsigned char));
            ((char *) buffer)[66] = '\'';
            bool ret = InnerHandle::setCipherKey(UnsafeData(buffer, 67));
            return ret;
        }
    }

在IDA中打开weixin.dll,并耐心等待分析完成。首先,在汇编页面中使用 “Search”-“Immediate Value” 搜索立即数0x27。然后,结果窗口中 右键-“Modify Filter” 筛出Address以.text开头的,也就是限定可执行区。最后,挑选出指令内容形如 mov BYTE PTR [???], 0x27  的,只有11条。(IDA自己的筛选不好用我用的是万能的Excel。)

11条不算多,逐条在IDA中定位,在附近凭其它常量(如66、67等数值)判断出哪个确实对应CipherHandle::setCipherKey方法。(这里我们又通过两个常量相互配合来定位函数。)这里对应到的反汇编是 mov byte ptr ds:[rbx+0x42],0x27 。把断点打在这条mov指令处,重新登录微信,正常命中后“步进”一次执行这条mov指令。跳转至RBX寄存器所存地址的内存区间,存储的是形如 x'37ba........2ef'的字符串。

该格式恰恰匹配 SQLCipher 中用于指定密钥的 PRAGMA key 指令,表示32字节的密钥(十六进制表示中有64个字符对应32字节)。断点会命中多次,RBX密钥也出现了好几种,不必关心哪个密钥是用于解开哪个文件的,只需知晓其格式特征就足够了。

编写脚本在内存空间中搜索密钥

到这里可以猜测,密钥将常驻在weixin.exe进程的内存空间中。既然知道了密钥的格式,便可暴力扫描内存,列出所有密钥,再逐一尝试,总会有一个能解开数据库。

很多软件都能以指定的规则搜索内存空间中的特定字节序列,但这里还是自行编写 Python 脚本以实现更复杂的搜索规则。总体思路是:先搜索前缀x',然后检查后继字符格式判定是否为完整密钥,若十六进制字符串表达了48字节,则为含盐的SQLCipher PRAGMA key,则只取前32字节,搜索密钥同时累计相同密钥出现的次数。

内存扫描加逐个尝试来寻找密钥的办法,虽然暴力,但胜在逻辑足够简单,且脚本运行用时实际并不长。运行脚本可搜出多个密钥,后续需逐个尝试,确定哪个是我们需要的。

import pymem, psutil

def find_process():
    processes = [
        (p.pid, p.info['cmdline'])
        for p in psutil.process_iter(['pid', 'name', 'cmdline'])
        if p.info['name'].lower() == 'weixin.exe'
        ]
    pid = min(processes, key=lambda _: len(_[1]))[0]
    return pid

def find_keys(pid):
    keys = []
    p = pymem.Pymem(pid)
    for a in p.pattern_scan_all(b'x\'', return_multiple=True):
        b = p.read_bytes(a, 3+64+32)
        if b[66] != ord('\'') and b[98] != ord('\''):
            continue
        found_key = b[0:66]
        for existed_key in keys:
            if existed_key[0] == found_key:
                existed_key[1] += 1
                break
        else:
            keys.append([found_key, 1])
    keys.sort(key=lambda _: _[1], reverse=True)
    return keys

if __name__ == '__main__':
    pid = find_process()
    keys = find_keys(pid)
    print(*keys, sep='\n')

================ RESTART: F:\wechat\Wechat_Qt_40518\scan_keys.py ===============
[b"x'09d67210196934ad620b9a9676f96ba16302d3a0cbd6af3fada41288ce08a47d", 19]
[b"x'ec93e6219cbb800ffd9006063282102d531f7fbf435950559928e7dc7958c34b", 9]
[b"x'1a45f8ed2ab98f477e0ed550057b679041f08c744661c456caa437517bdb25d4", 7]
[b"x'c837bad1c891f62b73ffa679ac8ffe78db6f79b3cbc385dcc21decf82ec35aab", 6]
[b"x'b12d48b1a13912fcf63400ff7f4482cd8b9f515595c4c019f5d5fbb2130cbafb", 5]
[b"x'03cae888c4d5e24a9a5fbdb359cfd6d1eff879c15a128da47c7fc5b3ab798533", 5]

使用SQLiteStudio验证数据库密钥

打开  SQLiteStudio,“连接数据库”指定文件,“类型”设“SQLCipher”,“密码”留空,“加密算法配置”中按上述分析指定密钥、版本和分页大小参数三条SQL语句。若“测试连接”正常则密钥对了,否则换下个密钥再试。可先从出现次数最多的密钥试起。

PRAGMA KEY = "x'09d67210196934ad620b9a9676f96ba16302d3a0cbd6af3fada41288ce08a47d'";
PRAGMA cipher_page_size = 4096;
PRAGMA cipher_compatibility = 4;

编写Python脚本解密数据库——基于对SQLCipher 4 格式的分析

用SQLiteStudio逐个打开数据库文件太麻烦,还是想用脚本实现自动运行。我在先前发表的文章中摘录过解密3.9版本Windows微信数据库的Python脚本,但新版微信已升级至SQLiteCipher 4版本,密钥形式亦有变化,直接拿老代码解密自然是行不通的。没有在网上找到有关 SQLCipher 4 文件结构的资料,还是得自力更生。

了解 SQLCipher 文件结构的最好方式还是读源代码,SQLCipher应用广泛,但代码结构却不复杂。新的文件结构是在 4.0.0版本引入的,对应的CHANGELOG记录也有参考价值。

包括SQLCipher在内的各类SQLite加密扩展均基于SQLite自身提供的“编解码器”接口实现,所以从 sqlite3Codec函数的重定义入手逐层深入,尤其从指针的加减操作推断出各字段的长度顺序和含义。思路简要提示如下:

sqlite3Codec 是SQLite专门提供的拓展接口
关注offset取值,对第一页要排除文件前16字节的盐值。
if(pgno == 1)
offset = ctx->plaintext_header_sz ? ctx->plaintext_header_sz : FILE_HEADER_SZ;
进入多个分支只关注解密分支 case CODEC_READ_OP
拷贝SQLite文件头
if(pgno == 1) /* copy initial part of file header to buffer */
memcpy(ctx->buffer, plaintext_header_sz ? pData : SQLITE_FILE_HEADER, offset)
调用下一层函数,注意此处据offset调整指针偏移和长度标识
sqlcipher_page_cipher(ctx, cctx, pgno, SQLCIPHER_DECRYPT,  ctx->page_sz - offset, pData + offset, buffer + offset)
——————————————
继续深入sqlcipher_page_cipher
从代码推断IV、HMAC的长度和相对顺序。
size = page_sz - ctx->reserve_sz;
iv_in = in + size;
hmac_in = in + size + ctx->iv_sz;
后继只关注执行解密的分支,注意IV原样拷贝为输出
memcpy(iv_out, iv_in, ctx->iv_sz); /* copy the iv from the input to output buffer */
也进行了HMAC校验(@细节分析略)
sqlcipher_page_hmac(ctx, c_ctx, pgno, in, size + ctx->iv_sz, hmac_out)
注意密钥进行了变换
sqlcipher_shield(c_ctx->key, ctx->key_sz)
来到真正执行解密运算函数调用
ctx->provider->cipher(ctx->provider_ctx, mode, key, key_sz, iv_out, in, size, out);
——————————————
最终来到sqlcipher_openssl_cipher
ctx->provider->cipher 实为 sqlcipher_openssl_cipher
里面调用了 OpenSSL EVP 接口,未见对key、iv和密文做修改,且不进行填充。
EVP_CipherInit_ex(ectx, NULL, NULL, key, iv, mode)
EVP_CIPHER_CTX_set_padding(ectx, 0))) /* no padding */
EVP_CipherInit_ex(ectx, OPENSSL_CIPHER, NULL, NULL, NULL, mode)

所以,第一页结构含盐 『 盐(16)、数据(*)、IV(16)、HMAC(64) 』,而其它页无盐 『 数据(*)、IV(16)、HMAC(64) 』。生成HMAC的算法是 SHA-512。

按此页面结构更新Python代码,核心是 (1)修正各结构的长度位置,(2)直接使用原始密钥而非先前的密码派生。运行脚本即得到解密的数据库文件了。

import hmac
import hashlib
from Crypto.Cipher import AES

def decrypt_msg(path, rawkey):
    KEY_SZ = 32
    PAGE_SZ = 4096
    SQLITE_FILE_HEADER = bytes("SQLite format 3", encoding="ASCII") + bytes(1)

    SALT_SZ= 16
    IV_SZ = 16
    HMAC_SZ = 64
    RESERVE_SZ = (IV_SZ + HMAC_SZ + 15) // 16 * 16

    with open(path, "rb") as f:
        blist = f.read()

    salt = blist[:SALT_SZ]
    # key = hashlib.pbkdf2_hmac
    key = rawkey
    page1 = blist[SALT_SZ:PAGE_SZ]

    mac_salt = bytes(x ^ 0x3a for x in salt)
    mac_key = hashlib.pbkdf2_hmac("sha512", key, mac_salt, 2, KEY_SZ)
    hash_mac = hmac.new(mac_key, digestmod="sha512")
    hash_mac.update(page1[:-RESERVE_SZ+IV_SZ])
    hash_mac.update(bytes.fromhex('01 00 00 00'))
    if hash_mac.digest() != page1[-RESERVE_SZ+IV_SZ:][:HMAC_SZ]:
        raise RuntimeError("Wrong HMAC")

    pages = [page1]
    pages += (blist[i:i+PAGE_SZ] for i in range(PAGE_SZ, len(blist), PAGE_SZ))

    outpath = f"{path[0:-3]}.decrypted.db"
    with open(outpath, "wb") as f:
        f.write(SQLITE_FILE_HEADER)
        for i in pages:
            t = AES.new(key, AES.MODE_CBC, i[-RESERVE_SZ:][:IV_SZ])
            f.write(t.decrypt(i[:-RESERVE_SZ]))
            f.write(i[-RESERVE_SZ:])
    return outpath

if __name__ == "__main__":
    path = "./SampleFiles/contact.db"
    key = bytes.fromhex("09d67210196934ad620b9a9676f96ba16302d3a0cbd6af3fada41288ce08a47d")
    decrypt_msg(path, key)

数据库内容分析

现在已经拿到了解密的数据库文件,这里感兴趣的主要是联系人数据库 contact.db 和 消息数据库 message_?.db。打开这些 SQLite 数据库,从字段名称即可大概了解具体数值数据含义。

WCDB 中 SQL 语句执行流程分析

在执行SQL语句的相关函数上打断点,是数据库逆向中常见的方法。这里简单分析下WCDB中相关代码逻辑。考虑到要配置 SQLCipher 的页面大小等参数必须通过执行SQL语句实现,所以Database::setCipherKey方法一定通向SQL语句执行的地方。

回到Database::setCipherKey

  1. 沿setConfig调用深入InnerDatabase::setConfig
  2. 同时在同一个类中注意到一个名字很有特点的方法InnerDatabase::execute
  3. 再到InnerHandle::execute,这里还是没出现什么标志性的常量。
  4. 再逐层深入InnerHandle::stepHandleStatement::step
  5. HandleStatement::tryReportSQL ,出现了一些独特的字符串(见下)

可以用这些标志性的字符串在IDA中定位该函数位置。使用IDA交叉引用功能逐一查看这些字符串出现的位置,并比较WCDB源码和IDA反编译出的伪代码的结构, 就可以确定哪个是tryReportSQL的位置。定位tryReportSQL之后便可逐层上溯,定位各父函数的位置。

这样就有办法在 x64dbg 中打断点看SQL字符串了。下面我们来实战一下。

bool HandleStatement::step()
{
    int rc = sqlite3_step(m_stmt);
    m_done = rc == SQLITE_DONE;
    if (m_done) {
        tryReportSQL();
    } else {
        m_stepCount++;
    }
    const char *sql = nullptr;
    if (isPrepared()) {
        sql = sqlite3_sql(m_stmt);
    }
    return APIExit(rc, sql);
}

void HandleStatement::tryReportSQL() {
    UnsafeStringView sql = sqlite3_sql(m_stmt);
    if (sql.hasPrefix("SELECT")) {
        m_stream << "RowCount:" << m_stepCount; 
    } else if (sql.hasPrefix("INSERT")) {
        m_stream << "LastInsertedId:" << getHandle()->getLastInsertedRowID();
        // 标志性的字符串常量!

消息数据库message_0.db结构分析

消息数据库message_0.db中许多名如Msg_[会话ID的MD5摘要]的表,如Msg_e667c,分别存储了不同会话下的聊天记录。Name2Id 表保存了该数据库存储中涉及的所有会话ID。

Msg_e667cmessage_content 存储了具体消息内容,文本消息以明文存储,然而不少消息存储的是二进制格式且不能看到有意义的字串,猜测可能是二次加密也可能是压缩,没有头绪,如果是在WCDB外面做的二进制处理那分析起来就更困难了。不论如何,先在反汇编中找到 SQL 语句执行的地方。

按上面分析,在执行SQL语句的相关函数上打断点,断点最好是打在可以方便以字符串形式体现SQL语句的地方。这里打在HandleStatement::step中的字符串赋值 char *sql = sqlite3_sql(m_stmt) 后、APIExit()前。把断点打在这里很方便,恰能借助函数调用约定,通过寄存器定位SQL语句的字符串形式char *sql

仍是对照WCDB源码与IDA反编译伪码,对应到 x64dbg 中下断点。类方法APIExit对应反汇编jmp_sub_183F9F1F0,类实例占其第一参数,根据 Windows x64 调用约定,字符指针变量sql作为第三参数对应寄存器R8,存储了字符串形式的SQL语句。

回到微信界面,切换到任一聊天会话,断点命中。恢复运行后,断点将反复命中,再恢复,再命中。这是由于程序执行了多条SQL语句,但仅关注涉及形如表Msg_e667c的SQL语句,可以看到一个有意思的:

SELECT local_id, server_id, ..., server_seq, origin_source, wcdb_decompress(source, WCDB_CT_source), wcdb_decompress(message_content, WCDB_CT_message_content), packed_info_data FROM Msg_e667cb1b609e900befc6983a9a3a8001

注意到wcdb_decompress(message_content, WCDB_CT_message_content)这个函数显然是自行注册进 SQLite 解释器中的。尝试在WCDB代码库中搜索并定位该函数,然后根据调用逐层深入,调用层次如下:

WCDBLiteralStringDefine(DecompressFunctionName, "wcdb_decompress")
   registerScalarFunction(DecompressFunctionName, ScalarFunctionTemplate<DecompressFunction>::specialize(2));
      functionObject.process(apiObj);
        void DecompressFunction::process(ScalarFunctionAPI& apiObj)
          CompressionCenter::shared().decompressContent()

分析CompressionCenter::decompressContent()函数,一眼看去是ZSTD压缩算法,其中逻辑较为复杂。不过无需研究这些细节,Python里尝试用内置ZSTD库以默认参数解压即可得原内容。

import pyzstd
data = bytes.fromhex('28b52ffd20e07d0500...')
text = pyzstd.decompress(data).decode()
print(text)

此外,表Msg_e667c中的其它字段也有使用Protobuf编码的,Python 中直接使用 blackboxprotobuf 库解码即可,可参考以前的文章。

消息图片存储格式 DAT 文件解密

数据库 message_0.dbpacked_info_data列存储了protobuf结构,对于图片消息,这一字段包含了图片文件名,对应地能在微信存储目录的 msg/attach下找到。文件以.dat扩展名,并非常见的图片格式,无疑又是微信自己的一套。用微信3.9版本的dat文件解密方法没什么用。网上也解密程序 recarto404/WxDatDecrypt.git,但下载运行程序提示“未找到 AES 密钥”。还是得自立更生。不过,可以推测 DAT 格式应该仍被以某种方式加密过。

首先,观察DAT文件其开头固定的07 08 56 32 08 07 00 04 00 00,不妨从这里入手。用 CheatEngine 全局搜,发现旁边有个十六进制串“cfcd20....”,猜测是密钥。这块内存在.data节内,也可用C语言的术语称之为全局变量。我这里将这两段内存分别称呼为 “密钥全局变量” 和 “文件头全局变量”,下面的截图可以看得很清楚。

首先,从密钥全局变量入手。在IDA中转到这个密钥全局变量,再转到唯一一个引用它的函数 sub_1808DCF70 ,反编译发现其内if条件判断有两个分支,上述的密钥地址是if判断的第二个分支返回的。我们不知道两个分支的区别,但这个函数一定有读取并返回密钥的功能。

在IDA中看 sub_1808DCF70 的交叉引用,有多达四个函数调用了该函数,然而不知道上级函数是哪一个。可以借助动态调试分辨其调用来源。用 x64dbg 在 sub_1808DCF70 开头下断点,然后在微信中任意点开一张新图片,断点命中。查看此处x64dbg“调用堆栈”-“返回到”,比对前面的IDA交叉引用进行换算,确定“返回到”地址落在sub_1808E6BD0中。

此外,从文件头全局变量入手再分析一下sub_1808DC590sub_1808DCC60是唯二的两个引用DAT文件头全局变量的函数,到 x64dbg 在两函数开头处分别下断点。

sub_1808DC590断点处x64dbg调用堆栈,从顶开始,计算每个“返回到”地址落在IDA中的哪个sub_???函数中。推断发现sub_1808E6BD0同样位列其调用栈中。换言之,『sub_1808DC590->文件头全局变量』 和 『sub_1808DCF70->密钥全局变量』 两条线都是由sub_1808E6BD0这个公共父函数生出来的。

线程        ID                        
11996        -        主线程                
地址        返回到        返回自        大小        方
00000014D94F91B8        00007FFEF8216C0D*        00007FFEF820CF70        3F0        用户模块
00000014D94F95A8        00007FFEF806F42F        00007FFEF8216C0D        70        用户模块
00000014D94F9618        00007FFEF806F384        00007FFEF806F42F        60        用户模块

线程        ID                                
11996        -        主线程                        
地址        返回到        返回自        大小        方        注释
00000014D94F8FC8        00007FFEF820C87E        00007FFEF820C590        1F0        用户模块        weixin.public:
00000014D94F91B8        00007FFEF8216D19        00007FFEF820C87E        3F0        用户模块        weixin.public:
00000014D94F95A8        00007FFEF806F42F        00007FFEF8216D19        70        用户模块        weixin.public:
00000014D94F9618        00007FFEF806F384        00007FFEF806F42F        60        用户模块        weixin.SetWeixinCallbackFunc+404F3F

__是时候深入公共父函数 sub_1808E6BD0了。__ 在x64dbg中断点在调用 sub_1808DCF70的位置 call 0x7FFD8C9ACF70,然后一点一点步过。每到其它 call 处就停下来看一看,特别是从寄存器看它步过前的传入参数和步过后的传出返回值等。

大致的流转过程如下图所示。在 IDA 的 Graph View 视图下标记颜色也有助于理清流程。

第一个重点子函数call 0x7FFD8DCCF260(IDA中sub_181BFF260

call前寄存器
RDI &"C:\\Users\\...\\xwechat…\\2025-11\\Img\\13_1763360399_t.dat" 是文件路径
RCX 函数传参:对应文件内容前 0x400 字节
RDX 函数传参: L"А"  = 410
R8 函数传参:不知
R9 函数传参:&"5c99084c46e005c5f5d3dcde8fe5f547"  似乎是密钥
call后寄存器
RAX 函数返回值:不知
RBX 函数返回值:内存跳转过去显然有PNG格式头,整块字节大概0x400字节

这个函数的功能是AES加密,估计0x400字节明文,填充16字节,形成0x410字节密文,是AES-ECB模式的特征。IDA进入函数反编译也出现了AesEcbDecrypt字样。用Python脚本可以验证,密钥需要试一试(是使用字符串直接对应的字节序列/解析字符串为字节的序列;密钥长度多少128/192/256)。

第二个需要关注的call 0x7FFD8DCCF6C0 (IDA中sub_181BFF6C0)

call前寄存器
RCX 传参:剩下的文件内容字节
RDX 传参:剩下的文件内容长度
R8 传参:某个立即数?后续分析是异或加密的字节密钥
call后寄存器
RBX 返回值:解密出的PNG格式二进制,可见PNG格式结尾特征字节

用 IDA 反编译这个函数sub_181BFF6C0,AI分析说是异或加密功能,那第三参数R8是单字节密钥。

写Python脚本解密。读入DAT原始文件字节并去除DAT文件头后,拆成前0x400字节部分和剩下的部分,前者做AES-ECB解密,后者逐字节异或回明文,再重新拼起来即完整明文。

def decrypt_dat_file(path):
    with open(path, 'rb') as f:
        blist = f.read()

    aespage = blist[15:15+0x410]
    xorpage = blist[15+0x410:]

    t = AES.new(AESKEY[:16], AES.MODE_ECB)
    aesplaintext = t.decrypt(aespage)[:-16]
    xorplaintext = bytes(_ ^ XORKEY for _ in xorpage)

    with open('decrypted.png', 'wb') as f:
        f.write(aesplaintext)
        f.write(xorplaintext)

if __name__ == "__main__":
    AESKEY = b'5c990805c5f6e03d' + b'cde5d4c48fe5f547'
    # AESKEY = bytes.fromhex('5c990805c5f6e03d' 'cde5d4c48fe5f547')
    XORKEY = 0xD7
    decrypt_dat_file("./1_1762869315_t.dat")

WXGF 文件解码

明文可能是PNG格式也可能是JPEG格式,但更可能是微信的新推出的 WXGF 私有格式。WXGF格式开头有wxgf字样。有关WXGF格式的解码,文章 《WXAM文件格式解析》 已经讲得很详细了。WXGF 解析的相关程序位于动态链接库 VoipEngine.dll ,可以使用其暴露的wxam_dec_wxam2pic_5函数,这个函数的功能是解码并重新编码为JPEG数据。

这里直接经 Python 调用 VoipEngine.dll 重编码JPEG,如下。其中写死的常量可以通过给DLL x64bdg 动态打点确定,暂不关心其实际含义。

def decode_wxgf_data(raw_bytes):
    from ctypes import byref, WinDLL, c_uint32, c_ubyte

    #raw_file_path = r'F:\wechat\Wechat_Qt_40518\DatDec\dat_files\3d096023fac86c06fc290fb9d428fd78.dat'
    #with open(raw_file_path, 'rb') as f:
    #    raw_bytes = f.read()

    assert raw_bytes[:4] == b'wxgf'
    raw_array = (c_ubyte * len(raw_bytes)).from_buffer_copy(raw_bytes)
    raw_size = len(raw_bytes)

    plain_array = (c_ubyte * (8 << 20))()
    plain_size = c_uint32(0x7cfb00)

    call_arg5_t = bytes.fromhex('00 00 00 00 00 00 00 00 00 00 00 00 AA AA AA AA'
                                '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00')
    call_arg5 = (c_ubyte * len(call_arg5_t)).from_buffer_copy(call_arg5_t)

    dll = WinDLL(r'C:\Program Files\Tencent\Weixin\4.0.5.18\VoipEngine.dll')
    dll.wxam_dec_wxam2pic_5(raw_array, c_uint32(raw_size),
                            plain_array, byref(plain_size),
                            call_arg5)

    plain_bytes = bytes(plain_array[:plain_size.value])
    #print(plain_bytes[:128], plain_bytes[-128:])
    return plain_bytes

总结

逆向分析4.0主数据库的思路清晰,也可以说中规中矩,不像之前分析3.9或4.0版本微信备份时连蒙带猜。首先在WCDB的C++源码中寻找标志性的常量,继而定位到关键函数的汇编,再通过动态调试打断点的方式,确定密钥的存储格式特征;然后用脚本在进程内存空间中按该特征扫描出可能的密钥后,逐个密钥尝试解密。DAT文件的解密也按照从标志性常量到切入动态调试开展逻辑分析的思路展开。微信4.0的二进制.data.rdata区已加壳,因此几乎不可能仅从静态分析理清内存布局,看来微信开发团队越来越重视反逆向了。

另,不像3.9版本,微信4.0的主数据库密钥并非每次登录都会换,所以还是应安全妥当地保管好自己的密钥!


到现在,我们分析了微信4.0的主数据库和备份文件机制,以及Windows微信3.9版本的备份文件机制。再加上已经广为流传的3.9版本主数据库逆向,至此我们对微信关键用户数据的存储机理和提取方法的分析算是全面了。

参考

| 使用的工具

| 源码

  • WCDB、SQLCipher - 都在GitHub上

| 资料

免费评分

参与人数 56吾爱币 +56 热心值 +53 收起 理由
microwu + 1 + 1 用心讨论,共获提升!
9324 + 1 热心回复!
maughold + 1 + 1 用心讨论,共获提升!
jantx + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
echovaio + 1 + 1 我很赞同!
Stop1036 + 1 + 1 用心讨论,共获提升!
junjia215 + 1 + 1 谢谢@Thanks!
chenup6 + 1 + 1 我很赞同!
bobbylee0405 + 1 + 1 我很赞同!
Cozy + 1 谢谢@Thanks!
LINDADAN + 1 + 1 我很赞同!
laowanggu + 1 + 1 谢谢@Thanks!
nue12138 + 2 + 1 鼓励转贴优秀软件安全工具和文档!
nich + 1 + 1 热心回复!
heanny + 1 + 1 我很赞同!
vergil0528 + 1 + 1 谢谢@Thanks!
xiaomumu + 1 + 1 我很赞同!
happyfeng + 1 谢谢@Thanks!
止战之删 + 1 + 1 谢谢@Thanks!
hxw555 + 1 + 1 用心讨论,共获提升!
coolXiaoYe + 1 用心讨论,共获提升!
leilin169 + 1 + 1 谢谢@Thanks!
zjdp + 1 + 1 看不懂但热心回复一下
cqrg + 1 我很赞同!
lws4955 + 1 + 1 我很赞同!
2013年 + 1 我很赞同!
vipkwd + 1 + 1 用心讨论,共获提升!
lsb2pojie + 1 + 1 我很赞同!
bairui666 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
damnedvi + 1 + 1 我很赞同!
syfsxyh + 1 + 1 谢谢@Thanks!
happyboy1 + 1 + 1 我很赞同!
联盟少侠 + 2 + 1 谢谢@Thanks!
_小白 + 1 + 1 用心讨论,共获提升!
scz + 1 + 1 谢谢
AIways + 1 + 1 再接再厉!!!
chenfann + 1 + 1 我很赞同!
hdtkdy + 1 + 1 我很赞同!
lvgcom + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
Osilvfe + 1 谢谢@Thanks!
hotwords + 1 + 1 用心讨论,共获提升!
李玉风我爱你 + 2 + 1 我很赞同!
GMCN + 2 + 1 用心讨论,共获提升!
dangerous2026 + 1 + 1 用心讨论,共获提升!
jmzqwh + 1 + 1 我很赞同!
SN1t2lO + 1 + 1 用心讨论,共获提升!
xixicoco + 2 + 1 用心讨论,共获提升!
tzxinqing + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
paltan + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
5omggx + 1 + 1 用心讨论,共获提升!
JimsZack + 1 + 1 用心讨论,共获提升!
jaffa + 1 谢谢@Thanks!
hhtwy + 1 + 1 谢谢@Thanks!
NyghtRose + 1 + 1 我很赞同!
梦入神机 + 3 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
zedex 发表于 2026-5-9 07:57
根据大佬的思路写了一个解密脚本:https://github.com/ZedeX/weixin-decrypte-script
推荐
guhongbo 发表于 2026-5-8 20:58
厉害了!!微信DAT图片解密的AES密钥,怎么找呢!!!有没有快速获取的工具?
推荐
HUIANG 发表于 2026-5-4 04:57
3#
cp99 发表于 2026-5-4 05:43
先收藏了
4#
xiaoyu756295 发表于 2026-5-4 09:02
绝世高手
5#
longelang 发表于 2026-5-4 09:19
牛逼了,学习了。。。
6#
Dontp 发表于 2026-5-4 10:13
算你厉害, 太厉害了吧,这思路,
7#
心中有梦闯天下 发表于 2026-5-4 15:19
企鹅很快就给你一个总监职位
8#
douyacai 发表于 2026-5-4 15:48
我一直在想,如何把微信3.X版本的微信聊天记录恢复,xwechat_files是新版吧微信,以前更新新版微信,导致旧版的微信聊天记录残缺,很多都丢了,另一个硬盘有备份,试过,加载不了以前的聊天记录,不知道有么有办法把以前的老聊天记录导入到新版微信
9#
meeokl 发表于 2026-5-4 21:34
感谢思路分享
10#
dork 发表于 2026-5-4 22:06
本想着解开联系人数据库 contact.db 就可以了,没想到楼主顺便给出来了消息数据库message_0.db结构分析和图片存储格式 DAT 文件的解密,感谢。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-11 21:47

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表