导出WeChat&QQ聊天记录
Jackie

导出WeChat&QQ聊天记录

本文仅供学习交流使用,严禁用于商业用途及非法用途,否则后果自负!

参考资料

微信

微信历史版本下载

SharpWxDump

wx_dump_rs

wechat-dump-rs(支持微信4.0版本)

留痕

获取基址、密钥详见如下(有逆向基础最好)

  • SharpWxDump:./CE获取基址.md

  • 留痕:./app/decrypt/get_bias_addr.py

  • wechat-dump-rs

  • wx_dump_rs


获取基址

拿VisualStudio2022打开SharpWxDump项目后报错缺少.NET4.0,在此链接下载对应版本(右侧的Download package)

下载完之后修改后缀名为.zip然后直接解压

打开如下地址

microsoft.netframework.referenceassemblies.net48.1.0.3\build\.NETFramework

将v4.n文件夹复制到

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework

此处提供如下三种方法获取基址

  1. 可根据SharpWxDump帮助文档利用CE手动获取基址(建议参考上面提到的其他几个项目)

获取到后打开Program.cs仿照格式填写,保存,选择release、x86编译运行可得到key

  1. wechat-dump-rs一键获取密钥Key

  2. 自用代码(微信昵称、微信账号、微信手机号、微信KEY都有)

    致谢:留痕

import ctypes
import hashlib
import json
import os
import re
import sys

import psutil
from win32com.client import Dispatch
from pymem import Pymem
import pymem
import hmac

ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
void_p = ctypes.c_void_p
KEY_SIZE = 32
DEFAULT_PAGESIZE = 4096
DEFAULT_ITER = 64000

def validate_key(key, salt, first, mac_salt):
byteKey = hashlib.pbkdf2_hmac("sha1", key, salt, DEFAULT_ITER, KEY_SIZE)
mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE)
hash_mac = hmac.new(mac_key, first[:-32], hashlib.sha1)
hash_mac.update(b'\x01\x00\x00\x00')

if hash_mac.digest() == first[-32:-12]:
return True
else:
return False

def get_exe_bit(file_path):
"""
获取 PE 文件的位数: 32 位或 64 位
:param file_path: PE 文件路径(可执行文件)
:return: 如果遇到错误则返回 64
"""
try:
with open(file_path, 'rb') as f:
dos_header = f.read(2)
if dos_header != b'MZ':
print('get exe bit error: Invalid PE file')
return 64
# Seek to the offset of the PE signature
f.seek(60)
pe_offset_bytes = f.read(4)
pe_offset = int.from_bytes(pe_offset_bytes, byteorder='little')

# Seek to the Machine field in the PE header
f.seek(pe_offset + 4)
machine_bytes = f.read(2)
machine = int.from_bytes(machine_bytes, byteorder='little')

if machine == 0x14c:
return 32
elif machine == 0x8664:
return 64
else:
print('get exe bit error: Unknown architecture: %s' % hex(machine))
return 64
except IOError:
print('get exe bit error: File not found or cannot be opened')
return 64

def get_exe_version(file_path):
"""
获取 PE 文件的版本号
:param file_path: PE 文件路径(可执行文件)
:return: 如果遇到错误则返回
"""
file_version = Dispatch("Scripting.FileSystemObject").GetFileVersion(file_path)
return file_version

def find_all(c: bytes, string: bytes, base_addr=0):
"""
查找字符串中所有子串的位置
:param c: 子串 b'123'
:param string: 字符串 b'123456789123'
:return:
"""
return [base_addr + m.start() for m in re.finditer(re.escape(c), string)]

class BiasAddr:
def __init__(self, account, mobile, name, key, db_path):
print(f"[+] 初始化参数:")
print(f" 账号: {account}")
print(f" 手机: {mobile}")
print(f" 名称: {name}")
print(f" 密钥: {key[:10]}..." if key else " 密钥: 无")
print(f" 数据库路径: {db_path}\n")

self.account = account.encode("utf-8")
self.mobile = mobile.encode("utf-8")
self.name = name.encode("utf-8")
self.key = bytes.fromhex(key) if key else b""
self.db_path = db_path if db_path and os.path.exists(db_path) else ""

self.process_name = "WeChat.exe"
self.module_name = "WeChatWin.dll"

self.pm = None # Pymem 对象
self.is_WoW64 = None # True: 32位进程运行在64位系统上 False: 64位进程运行在64位系统上
self.process_handle = None # 进程句柄
self.pid = None # 进程ID
self.version = None # 微信版本号
self.process = None # 进程对象
self.exe_path = None # 微信路径
self.address_len = None # 4 if self.bits == 32 else 8 # 4字节或8字节
self.bits = 64 if sys.maxsize > 2 ** 32 else 32 # 系统:32位或64位

def get_process_handle(self):
try:
print("[+] 正在获取微信进程...")
self.pm = Pymem(self.process_name)
self.pm.check_wow64()
self.is_WoW64 = self.pm.is_WoW64
self.process_handle = self.pm.process_handle
self.pid = self.pm.process_id
self.process = psutil.Process(self.pid)
self.exe_path = self.process.exe()
self.version = get_exe_version(self.exe_path)

print(f"[+] 进程信息:")
print(f" PID: {self.pid}")
print(f" 路径: {self.exe_path}")
print(f" 版本: {self.version}")
print(f" WoW64: {self.is_WoW64}\n")

version_nums = list(map(int, self.version.split("."))) # 将版本号拆分为数字列表
if version_nums[0] <= 3 and version_nums[1] <= 9 and version_nums[2] <= 2:
self.address_len = 4
else:
self.address_len = 8
return True, ""
except pymem.exception.ProcessNotFound:
return False, "[-] WeChat No Run"

def search_memory_value(self, value: bytes, module_name="WeChatWin.dll"):
# 创建 Pymem 对象
module = pymem.process.module_from_name(self.pm.process_handle, module_name)
ret = self.pm.pattern_scan_module(value, module, return_multiple=True)
ret = ret[-1] - module.lpBaseOfDll if len(ret) > 0 else 0
return ret

def get_key_bias1(self):
try:
byteLen = self.address_len # 4 if self.bits == 32 else 8 # 4字节或8字节

keyLenOffset = 0x8c if self.bits == 32 else 0xd0
keyWindllOffset = 0x90 if self.bits == 32 else 0xd8

module = pymem.process.module_from_name(self.process_handle, self.module_name)
keyBytes = b'-----BEGIN PUBLIC KEY-----\n...'
publicKeyList = pymem.pattern.pattern_scan_all(self.process_handle, keyBytes, return_multiple=True)

keyaddrs = []
for addr in publicKeyList:
keyBytes = addr.to_bytes(byteLen, byteorder="little", signed=True) # 低位在前
may_addrs = pymem.pattern.pattern_scan_module(self.process_handle, module, keyBytes,
return_multiple=True)
if may_addrs != 0 and len(may_addrs) > 0:
for addr in may_addrs:
keyLen = self.pm.read_uchar(addr - keyLenOffset)
if keyLen != 32:
continue
keyaddrs.append(addr - keyWindllOffset)

return keyaddrs[-1] - module.lpBaseOfDll if len(keyaddrs) > 0 else 0
except:
return 0

def search_key(self, key: bytes):
key = re.escape(key) # 转义特殊字符
key_addr = self.pm.pattern_scan_all(key, return_multiple=False)
key = key_addr.to_bytes(self.address_len, byteorder='little', signed=True)
result = self.search_memory_value(key, self.module_name)
return result

def get_key_bias2(self, wx_db_path):

addr_len = get_exe_bit(self.exe_path) // 8
db_path = wx_db_path

def read_key_bytes(h_process, address, address_len=8):
array = ctypes.create_string_buffer(address_len)
if ReadProcessMemory(h_process, void_p(address), array, address_len, 0) == 0: return "None"
address = int.from_bytes(array, byteorder='little') # 逆序转换为int地址(key地址)
key = ctypes.create_string_buffer(32)
if ReadProcessMemory(h_process, void_p(address), key, 32, 0) == 0: return "None"
key_bytes = bytes(key)
return key_bytes

def verify_key(key, wx_db_path):
KEY_SIZE = 32
DEFAULT_PAGESIZE = 4096
DEFAULT_ITER = 64000
with open(wx_db_path, "rb") as file:
blist = file.read(5000)
salt = blist[:16]
byteKey = hashlib.pbkdf2_hmac("sha1", key, salt, DEFAULT_ITER, KEY_SIZE)
first = blist[16:DEFAULT_PAGESIZE]

mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE)
hash_mac = hmac.new(mac_key, first[:-32], hashlib.sha1)
hash_mac.update(b'\x01\x00\x00\x00')

if hash_mac.digest() != first[-32:-12]:
return False
return True

phone_type1 = "iphone\x00"
phone_type2 = "android\x00"
phone_type3 = "ipad\x00"

pm = pymem.Pymem("WeChat.exe")
module_name = "WeChatWin.dll"

MicroMsg_path = os.path.join(db_path, "MSG", "MicroMsg.db")

module = pymem.process.module_from_name(pm.process_handle, module_name)

type1_addrs = pm.pattern_scan_module(phone_type1.encode(), module, return_multiple=True)
type2_addrs = pm.pattern_scan_module(phone_type2.encode(), module, return_multiple=True)
type3_addrs = pm.pattern_scan_module(phone_type3.encode(), module, return_multiple=True)
type_addrs = type1_addrs if len(type1_addrs) >= 2 else type2_addrs if len(
type2_addrs) >= 2 else type3_addrs if len(type3_addrs) >= 2 else "None"
if type_addrs == "None":
return 0
for i in type_addrs[::-1]:
for j in range(i, i - 2000, -addr_len):
key_bytes = read_key_bytes(pm.process_handle, j, addr_len)
if key_bytes == "None":
continue
if verify_key(key_bytes, MicroMsg_path):
return j - module.lpBaseOfDll
return 0

def run(self, logging_path=False, version_list_path=None):
if not self.get_process_handle()[0]:
return {}

print("[+] 开始搜索内存偏移...")
mobile_bias = self.search_memory_value(self.mobile, self.module_name)
print(f" 手机号偏移: 0x{mobile_bias:X}")

name_bias = self.search_memory_value(self.name, self.module_name)
print(f" 用户名偏移: 0x{name_bias:X}")

account_bias = self.search_memory_value(self.account, self.module_name)
print(f" 账号偏移: 0x{account_bias:X}")

print("[+] 开始搜索密钥偏移...")
key_bias = 0
key_bias = self.get_key_bias1()
if key_bias <= 0 and self.key:
print(" 方法1失败,尝试方法2...")
key_bias = self.search_key(self.key)
if key_bias <= 0 and self.db_path:
print(" 方法2失败,尝试方法3...")
key_bias = self.get_key_bias2(self.db_path)
print(f" 密钥偏移: 0x{key_bias:X}\n")

rdata = {self.version: [name_bias, account_bias, mobile_bias, 0, key_bias]}
print("[+] 搜索完成!")
print(f" 结果: {json.dumps(rdata, indent=4)}\n")
return rdata

def get_info_without_key(h_process, address, n_size=64):
array = ctypes.create_string_buffer(n_size)
if ReadProcessMemory(h_process, void_p(address), array, n_size, 0) == 0: return "None"
array = bytes(array).split(b"\x00")[0] if b"\x00" in array else bytes(array)
text = array.decode('utf-8', errors='ignore')
return text.strip() if text.strip() != "" else "None"

def get_user_input():
print("\n[*] 请输入以下信息:")
account = input("微信账号: ").strip()
mobile = input("手机号码: ").strip()
name = input("用户名称: ").strip()

print("\n[*] 以下信息为可选,直接回车可跳过:")
key = input("密钥(可选): ").strip()
db_path = input("数据库路径(可选): ").strip()

return account, mobile, name, key, db_path

def main():
try:
account, mobile, name, key, db_path = get_user_input()

if not all([account, mobile, name]):
print("\n[-] 错误: 账号、手机号和用户名为必填项")
sys.exit(1)

bias = BiasAddr(
account=account,
mobile=mobile,
name=name,
key=key,
db_path=db_path
)
result = bias.run()

if not result:
print("\n[-] 未找到微信进程或搜索失败")
sys.exit(1)

except KeyboardInterrupt:
print("\n\n[-] 用户取消操作")
sys.exit(0)
except Exception as e:
print(f"\n[-] 发生错误: {str(e)}")
sys.exit(1)

if __name__ == '__main__':
main()

数据库文件在Document\WeChat Files\<微信原始ID>\Msg\Multi下,这里是各个数据库简述

使用如下脚本解密数据库

pip3 install psutil pymem pywin32 pycryptodome
from Crypto.Cipher import AES
import ctypes
import hashlib
import hmac

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

input_pass = input('请输入密钥: ')
input_dir = input('请输入数据库文件路径: ')

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:])

可使用Navicat等软件打开,有了各个数据库的基础后直接查询,或者使用wx_dump_rs留痕可视化查看

QQ

因无RE进阶知识,此处放相关链接

 评论
评论插件加载失败
正在加载评论插件