├── image.png ├── pysqlcipher3-1.0.2-py3.6-win32.egg ├── README.md └── via.py /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzyzhangziyu/wechat-db-decrypt/HEAD/image.png -------------------------------------------------------------------------------- /pysqlcipher3-1.0.2-py3.6-win32.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzyzhangziyu/wechat-db-decrypt/HEAD/pysqlcipher3-1.0.2-py3.6-win32.egg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat-db-decrypt 2 | 解密Windows微信聊天记录数据库 3 | 4 | ![image](image.png) 5 | 6 | * 代码是 python3 的 7 | * 代码是一边试,一边堆的,乱的很 8 | * 首先要登陆微信,再打开脚本,脚本会每秒读一次数据库把聊天记录打印出来 9 | * 不知道除了这么轮询之外还有什么方法没有 10 | 11 | 根据这个帖子 https://bbs.pediy.com/thread-222652.htm 手动获取解密数据库的步骤如下 12 | 13 | 1. 打开微信,先不要登陆 14 | 2. OD 附加,搜索字符串 "DBFactory::encryptDB" 15 | 3. 往下有个 `test edx, edx`,在哪里下断,edx 就指向 key,长度 32 16 | 17 | 嘛~ 具体怎么跟没搞清楚~ 18 | 19 | 微信是调用 sqlite3_key 进行解密,所以 key 可以传递包含字符 `\0` 字串,如果用 [DB Browser for SQLite](http://sqlitebrowser.org/) 或者 [pyslite3](https://github.com/rigglemania/pysqlcipher3) 打开时无法要做点处理,具体为什么要看 https://github.com/CovenantEyes/sqlcipher-windows/blob/6747108170c4f8db11d55119414434c13ce5eb80/StaticLib/src/crypto_impl.c#L848 20 | 再为什么会知道是这里,编译 pyslite3 打日志打出来的 21 | 22 | ``` python 23 | def get_password(path, key): 24 | """ 25 | path 是具体的数据库的路径,不如聊天文本的数据库是 ChatMsg.db 26 | """ 27 | salt = open(path, 'rb').read(16) 28 | dk=hashlib.pbkdf2_hmac('sha1', key, salt, 64000, dklen=32) 29 | return binascii.hexlify(dk).decode() 30 | ``` 31 | 32 | 接下来代码里面获取这个 key 是通过读内存读出来的,有 key 之后用 CheatEngine 搜索找到地址挺简单的 33 | 34 | 再来是 Windows 下 pyslite3 编译问题 35 | 1. http://slproweb.com/products/Win32OpenSSL.html OpenSSL-Win32 要装 1.0.x 36 | 2. 编译的时候利用这个 [sqlcipher-windows 37 | ](https://github.com/CovenantEyes/sqlcipher-windows) 38 | 39 | 最后 CheatEngine 得到 wxid 地址,读注册表找到聊天记录路径,全部结合起来可以是个间谍软件 /_\ 40 | -------------------------------------------------------------------------------- /via.py: -------------------------------------------------------------------------------- 1 | import win32com.client 2 | from win32api import( 3 | OpenProcess, 4 | CloseHandle 5 | ) 6 | from win32con import PROCESS_ALL_ACCESS 7 | from win32process import( 8 | EnumProcesses, 9 | EnumProcessModules, 10 | GetModuleFileNameEx, 11 | ) 12 | from ctypes import * 13 | import os 14 | from winreg import * 15 | import hashlib 16 | import binascii 17 | from time import sleep, localtime, strftime 18 | from pysqlcipher3 import dbapi2 as sqlite 19 | 20 | ReadProcessMemory = windll.kernel32.ReadProcessMemory 21 | 22 | WMI = win32com.client.GetObject('winmgmts:') 23 | 24 | def get_pid(exe): 25 | plist = WMI.ExecQuery(f"SELECT * FROM Win32_Process where name = '{exe}'") 26 | return int(plist[0].handle) if len(plist) > 0 else None 27 | 28 | def get_module(process, dll): 29 | for module in EnumProcessModules(process): 30 | name = os.path.basename(GetModuleFileNameEx(process, module)).lower() 31 | if name == 'wechatwin.dll': 32 | return module 33 | return 34 | 35 | def get_mydoc_path(): 36 | key = OpenKey(HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") 37 | value, _ = QueryValueEx(key, "Personal") 38 | return value 39 | 40 | def get_file_save_path(): 41 | key = OpenKey(HKEY_CURRENT_USER, r"Software\Tencent\WeChat") 42 | value, _ = QueryValueEx(key, "FileSavePath") 43 | return value if value != 'MyDocument:' else get_mydoc_path() 44 | 45 | pid = get_pid('wechat.exe') 46 | process = OpenProcess(PROCESS_ALL_ACCESS, False, pid) 47 | module = get_module(process, 'wechatwin.dll') 48 | 49 | wechat_id_addr = c_int32() 50 | ReadProcessMemory(process.handle, module+0xFEDBD8, byref(wechat_id_addr), 4, None) 51 | wechat_id = create_string_buffer(21) 52 | ReadProcessMemory(process.handle, wechat_id_addr, byref(wechat_id), 21, None) 53 | db_path = os.path.join(get_file_save_path(), 54 | 'WeChat Files', wechat_id.value.decode(), 'Msg') 55 | chat_msg_path = os.path.join(db_path, 'ChatMsg.db') 56 | micro_msg_path = os.path.join(db_path, 'MicroMsg.db') 57 | 58 | key_addr = c_int32() 59 | ReadProcessMemory(process.handle, module+0xFF899C, byref(key_addr), 4, None) 60 | key = create_string_buffer(32) 61 | ReadProcessMemory(process.handle, key_addr, byref(key), 32, None) 62 | 63 | def get_password(path, key): 64 | salt = open(path, 'rb').read(16) 65 | dk=hashlib.pbkdf2_hmac('sha1', key, salt, 64000, dklen=32) 66 | return binascii.hexlify(dk).decode() 67 | 68 | chat_msg_passwd = get_password(chat_msg_path, key.raw) 69 | micro_msg_passwd = get_password(micro_msg_path, key.raw) 70 | 71 | micro_msg_conn = sqlite.connect(micro_msg_path) 72 | cur = micro_msg_conn.cursor() 73 | cur.execute('''PRAGMA key="x'%s'"''' % micro_msg_passwd) 74 | cur.execute("PRAGMA cipher_page_size=4096") 75 | contect = dict(cur.execute("select UserName, NickName from Contact").fetchall()) 76 | 77 | 78 | conn = sqlite.connect(chat_msg_path) 79 | cur = conn.cursor() 80 | cur.execute('''PRAGMA key="x'%s'"''' % chat_msg_passwd) 81 | cur.execute("PRAGMA cipher_page_size=4096") 82 | max_seq = -1 83 | 84 | from wcwidth import wcswidth, wcwidth 85 | from prettytable import PrettyTable 86 | from tabulate import tabulate 87 | 88 | def padding(s, l): 89 | return s + ' ' * (l - wcswidth(s)) 90 | 91 | def print_recive(talker, content, timestamp): 92 | content = [''] + \ 93 | [[l] for l in content] +\ 94 | ['', ''] 95 | lines = tabulate(content, [strftime("%Y-%m-%d %H:%M:%S"+' '*20, localtime(timestamp))], tablefmt='psql').split('\n') 96 | lines = [' ' + l for l in lines] 97 | lines[0] = ' /' + lines[0][2:-1] + '\\' 98 | lines[-1] = '/-' + lines[0][2:-1] + '/' 99 | lines.insert(-2, lines[2]) 100 | sign = 'From ' + contect[talker] 101 | lines[-2] = lines[-2][:-wcswidth(sign)-3] + sign + " |" 102 | print('\n'.join(lines)) 103 | print() 104 | 105 | 106 | def print_send(talker, content, timestamp): 107 | content = [''] + \ 108 | [[l] for l in content] +\ 109 | ['', ''] 110 | lines = tabulate(content, [strftime("%Y-%m-%d %H:%M:%S"+' '*20, localtime(timestamp))], tablefmt='psql').split('\n') 111 | width = 30 112 | lines[0] = '/' + lines[0][1:-1] + '\\' 113 | lines[-1] = '\\' + lines[0][1:-1] + '-\\' 114 | lines.insert(-2, lines[2]) 115 | sign = 'To ' + contect[talker] 116 | lines[-2] = lines[-2][:-wcswidth(sign)-3] + sign + " |" 117 | lines = [' ' * (width + (50-len(lines[0]))) + l for l in lines] 118 | print('\n'.join(lines)) 119 | print() 120 | 121 | from math import ceil 122 | import re 123 | import xml.dom.minidom 124 | from bs4 import BeautifulSoup 125 | 126 | def wrap(s, n): 127 | def _wrap(s, n): 128 | result = [] 129 | buf = '' 130 | total_len = 0 131 | for c in s: 132 | l = wcwidth(c) 133 | if (total_len + l > n): 134 | result.append(buf) 135 | buf = c 136 | total_len = 1 137 | buf += c 138 | total_len += l 139 | 140 | result.append(buf) 141 | return result 142 | result = [] 143 | for l in s.split('\n'): 144 | result += _wrap(l, n) 145 | return result 146 | 147 | while True: 148 | seq = cur.execute("select seq from sqlite_sequence where name='ChatCRMsg'").fetchall() 149 | if not seq: 150 | sleep(1) 151 | continue 152 | 153 | seq = seq[0][0] 154 | if max_seq >= seq: 155 | sleep(1) 156 | continue 157 | 158 | msg = cur.execute("select type, CreateTime, IsSender, strTalker, strContent from ChatCRMsg where localId > ?", (max_seq,)) 159 | for type_id, create_time, is_sender, talker, content in msg: 160 | if type_id == 49: 161 | dom = BeautifulSoup(content, "html.parser") 162 | appname = dom.find('appname').text 163 | if (appname == '网易云音乐'): 164 | title = dom.find('title').text 165 | des = dom.find('des').text 166 | url = dom.find('url').text 167 | content = f'[音乐] {title} - {des}\n{url}' 168 | if appname == '微信支付': 169 | display_name = dom.find('display_name').text 170 | words = dom.select('value word') 171 | money = '支付金额' + words[0].text 172 | explains = '\n'.join([' * ' + w.text for w in words[1:]]) 173 | content = f'[微信支付凭证] {display_name}\n\n {money}\n\n{explains}' 174 | else: 175 | content = '[动画表情]' 176 | elif type_id == 47: 177 | content = '[动画表情]' 178 | elif type_id == 3: 179 | content = '[图片]' 180 | elif type_id == 42: 181 | dom = xml.dom.minidom.parseString(content) 182 | nickname = dom.documentElement.getAttribute('nickname') 183 | username = dom.documentElement.getAttribute('username') 184 | alias = dom.documentElement.getAttribute('alias') 185 | content = f'[名片] {nickname} - {alias if alias else username}' 186 | elif type_id == 43: 187 | content = '[视频]' 188 | elif type_id == 34: 189 | content = '[语音]' 190 | 191 | content = re.sub('(.+)', r'<\1>', content) 192 | 193 | content = wrap(content, 50) 194 | 195 | if (is_sender): 196 | print_send(talker, content, create_time) 197 | else: 198 | print_recive(talker, content, create_time) 199 | 200 | max_seq = seq --------------------------------------------------------------------------------