前言

本文介绍微信本地数据库的导出和部分数据表字段分析。

起因是看到一篇文章,博主用微信聊天记录和博客文章,配合ChatGPT克隆了一个数字版自己。俺没这么高级,单纯想看看某个闲聊群谁是龙王。

废话不多说,下面进行实操

数据库解密

环境如下:win10 + pc版微信(3.9.6.33)

数据取证

微信数据库进行了加密,需要用对客户端进行取证得到解密的秘钥。解密工具用 SharpWxDump ,需要自行编译,得到SharpWxDump.exe

接下来正式取证,首先登录微信,然后在命令提示符(CMD)中执行SharpWxDump.exe即可得到信息,WeChatKey就是秘钥,记得复制下等会要用

Untitled

数据库文件

微信本地数据库用的是SQLite,文件存放在 $WeChat Files/wxid_xxxxxxx\Msg\Multi 中。

$WeChat Files:表示微信的文件存放路径,在微信 → 设置 → 文件管理中可以查看

wxid_xxxxxxx:当前电脑登录过多少个微信,就有多少个wxid_xxxxxxx文件夹,可以进入wxid_xxxxxxx文件夹,查看第一个文件夹account_xxxx确认是哪个微信,格式是 account_微信号

在 Multi 文件夹中有很多文件,我们需要用到的数据库文件格式为 MSGx.db ,这里的表示0123,例如 MSG0.db MSG1.db等。接下来的操作,我们需要关闭微信,不然复制的文件的时候可能会有问题。符合条件的我们都复制到新的文件夹,以便后续使用。

解密脚本

解密数据库文件需要用到WeChatKey + python解密脚本,以下是脚本代码

# jiemi.py
from Crypto.Cipher import AES
import hashlib, hmac, ctypes, sys, getopt

SQLITE_FILE_HEADER = bytes('SQLite format 3', encoding='ASCII') + bytes(1)
IV_SIZE = 16
HMAC_SHA1_SIZE = 20
KEY_SIZE = 32
DEFAULT_PAGESIZE = 4096
DEFAULT_ITER = 64000
opts, args = getopt.getopt(sys.argv[1:], 'hk:d:')
input_pass = ''
input_dir = ''

for op, value in opts:
    if op == '-k':
        input_pass = value
    else:
        if op == '-d':
            input_dir = value

password = bytes.fromhex(input_pass.replace(' ', ''))

with open(input_dir, 'rb') as (f):
    blist = f.read()
print(len(blist))
salt = blist[:16]
key = hashlib.pbkdf2_hmac('sha1', password, salt, DEFAULT_ITER, KEY_SIZE)
first = blist[16:DEFAULT_PAGESIZE]
mac_salt = bytes([x ^ 58 for x in salt])
mac_key = hashlib.pbkdf2_hmac('sha1', key, mac_salt, 2, KEY_SIZE)
hash_mac = hmac.new(mac_key, digestmod='sha1')
hash_mac.update(first[:-32])
hash_mac.update(bytes(ctypes.c_int(1)))

if hash_mac.digest() == first[-32:-12]:
    print('Decryption Success')
else:
    print('Password Error')
blist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)]

with open(input_dir, 'wb') as (f):
    f.write(SQLITE_FILE_HEADER)
    t = AES.new(key, AES.MODE_CBC, first[-48:-32])
    f.write(t.decrypt(first[:-48]))
    f.write(first[-48:])
    for i in blist:
        t = AES.new(key, AES.MODE_CBC, i[-48:-32])
        f.write(t.decrypt(i[:-48]))
        f.write(i[-48:])

使用方法

python3 .\jiemi.py -k WeChatKey -d .\MSGx.db

jiemi.py:就是python解密脚本

WeChatKey:数据取证得到

MSGx.db:上一步得到的数据库文件(多个文件就执行多次)

数据库分析

用数据库工具以SQLite方式打开MSG0.db,可以看到4个表,此处我们关注 MSG 表,这是保存聊天记录的表。

表字段很多,我们重点关注其中几个字段

  • Type:消息类型
  • SubType:消息类型子分类
  • IsSender:是否是自己发出的消息(1是 0否)
  • CreateTime:消息创建时间的秒级时间戳
  • StrTalker:聊天对象标识(如果是群里,格式就是xxx@chatroom)
  • StrContent:字符串格式的数据
  • CompressContent:压缩的数据(引用回复的消息会放在这里,StrContent为空)

详细的分析可以看这篇文章:微信PC端各个数据库文件结构与功能简述

如果只需要看文本消息,则可以如下搜索

SELECT * FROM MSG WHERE Type=1 OR (Type=49 AND SubType=57)

Type=1 :表示普通文本消息

Type=49 AND SubType=57 :带有引用的文本消息(这种类型下 StrContent 为空,发送和引用的内容均在 CompressContent 中)

CompressContent 的内容是被压缩过的,压缩算法用的是lz4,解压出来的内容是XML格式。以下是解压代码

import lz4.block as lb

unzipStr = lb.decompress(CompressContent, uncompressed_size=0x10004)
text = unzipStr.decode('utf-8')
print(text )

代码中CompressContent就是压缩后的内容,text是解压后的。

还有一个最重要的问题,如何确认哪条消息是谁发的。

一对一私聊很简单,通过StrTalker确认聊天对象,IsSender判断是不是自己发的消息即可

在群聊中,通过StrTalker只能锁定群里消息,至于是哪个群友发的消息,则需要通过BytesExtra字段中的内容判断。而开心的是,这个字段也是BLOB类型,我也找不到格式化的方法。

我判断是谁发的消息只能对BytesExtra进行iso-8859-1解码,然后判断BytesExtra是否包含了wxid_xxxx这样的方式来判断。。。

感谢评论区的热心大神 , BytesExtra 是 protobuf 格式的数据,解析可以参考这个 无源protobuf二进制流反序列化学习

总结

那么问题来了,谁是龙王。。。

参考文章:

微信PC端各个数据库文件结构与功能简述

红队攻防之PC端微信个人信息与聊天记录取证