重写架构,支持微信4.0
This commit is contained in:
514
wxManager/decrypt/wx_info_v4.py
Normal file
514
wxManager/decrypt/wx_info_v4.py
Normal file
@@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@Time : 2025/1/10 2:36
|
||||
@Author : SiYuan
|
||||
@Email : 863909694@qq.com
|
||||
@File : wxManager-wx_info_v4.py
|
||||
@Description :
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import multiprocessing
|
||||
import os.path
|
||||
|
||||
import hmac
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
import pymem
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Hash import SHA512
|
||||
import yara
|
||||
|
||||
from wxManager.decrypt.common import WeChatInfo
|
||||
from wxManager.decrypt.common import get_version
|
||||
|
||||
# 定义必要的常量
|
||||
PROCESS_ALL_ACCESS = 0x1F0FFF
|
||||
PAGE_READWRITE = 0x04
|
||||
MEM_COMMIT = 0x1000
|
||||
MEM_PRIVATE = 0x20000
|
||||
|
||||
# Constants
|
||||
IV_SIZE = 16
|
||||
HMAC_SHA256_SIZE = 64
|
||||
HMAC_SHA512_SIZE = 64
|
||||
KEY_SIZE = 32
|
||||
AES_BLOCK_SIZE = 16
|
||||
ROUND_COUNT = 256000
|
||||
PAGE_SIZE = 4096
|
||||
SALT_SIZE = 16
|
||||
|
||||
finish_flag = False
|
||||
|
||||
|
||||
# 定义 MEMORY_BASIC_INFORMATION 结构
|
||||
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("BaseAddress", ctypes.c_void_p),
|
||||
("AllocationBase", ctypes.c_void_p),
|
||||
("AllocationProtect", ctypes.c_ulong),
|
||||
("RegionSize", ctypes.c_size_t),
|
||||
("State", ctypes.c_ulong),
|
||||
("Protect", ctypes.c_ulong),
|
||||
("Type", ctypes.c_ulong),
|
||||
]
|
||||
|
||||
|
||||
# Windows API Constants
|
||||
PROCESS_VM_READ = 0x0010
|
||||
PROCESS_QUERY_INFORMATION = 0x0400
|
||||
|
||||
# Load Windows DLLs
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
|
||||
# 打开目标进程
|
||||
def open_process(pid):
|
||||
return ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
|
||||
|
||||
|
||||
# 读取目标进程内存
|
||||
def read_process_memory(process_handle, address, size):
|
||||
buffer = ctypes.create_string_buffer(size)
|
||||
bytes_read = ctypes.c_size_t(0)
|
||||
success = ctypes.windll.kernel32.ReadProcessMemory(
|
||||
process_handle,
|
||||
ctypes.c_void_p(address),
|
||||
buffer,
|
||||
size,
|
||||
ctypes.byref(bytes_read)
|
||||
)
|
||||
if not success:
|
||||
return None
|
||||
return buffer.raw
|
||||
|
||||
|
||||
# 获取所有内存区域
|
||||
def get_memory_regions(process_handle):
|
||||
regions = []
|
||||
mbi = MEMORY_BASIC_INFORMATION()
|
||||
address = 0
|
||||
while ctypes.windll.kernel32.VirtualQueryEx(
|
||||
process_handle,
|
||||
ctypes.c_void_p(address),
|
||||
ctypes.byref(mbi),
|
||||
ctypes.sizeof(mbi)
|
||||
):
|
||||
if mbi.State == MEM_COMMIT and mbi.Type == MEM_PRIVATE:
|
||||
regions.append((mbi.BaseAddress, mbi.RegionSize))
|
||||
address += mbi.RegionSize
|
||||
return regions
|
||||
|
||||
|
||||
rules_v4 = r'''
|
||||
rule GetDataDir {
|
||||
strings:
|
||||
$a = /[a-zA-Z]:\\(.{1,100}?\\){0,1}?xwechat_files\\[0-9a-zA-Z_-]{6,24}?\\db_storage\\/
|
||||
condition:
|
||||
$a
|
||||
}
|
||||
|
||||
rule GetPhoneNumberOffset {
|
||||
strings:
|
||||
$a = /[\x01-\x20]\x00{7}(\x0f|\x1f)\x00{7}[0-9]{11}\x00{5}\x0b\x00{7}\x0f\x00{7}/
|
||||
condition:
|
||||
$a
|
||||
}
|
||||
rule GetKeyAddrStub
|
||||
{
|
||||
strings:
|
||||
$a = /.{6}\x00{2}\x00{8}\x20\x00{7}\x2f\x00{7}/
|
||||
condition:
|
||||
all of them
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def read_string(data: bytes, offset, size):
|
||||
try:
|
||||
return data[offset:offset + size].decode('utf-8')
|
||||
except:
|
||||
# print(data[offset:offset + size])
|
||||
# print(traceback.format_exc())
|
||||
return ''
|
||||
|
||||
|
||||
def read_num(data: bytes, offset, size):
|
||||
# 构建格式字符串,根据 size 来选择相应的格式
|
||||
if size == 1:
|
||||
fmt = '<B' # 1 字节,unsigned char
|
||||
elif size == 2:
|
||||
fmt = '<H' # 2 字节,unsigned short
|
||||
elif size == 4:
|
||||
fmt = '<I' # 4 字节,unsigned int
|
||||
elif size == 8:
|
||||
fmt = '<Q' # 8 字节,unsigned long long
|
||||
else:
|
||||
raise ValueError("Unsupported size")
|
||||
|
||||
# 使用 struct.unpack 从指定 offset 开始读取 size 字节的数据并转换为数字
|
||||
result = struct.unpack_from(fmt, data, offset)[0] # 通过 unpack_from 来读取指定偏移的数据
|
||||
return result
|
||||
|
||||
|
||||
def read_bytes(data: bytes, offset, size):
|
||||
return data[offset:offset + size]
|
||||
|
||||
|
||||
# def read_bytes_from_pid(pid, offset, size):
|
||||
# with open(f'/proc/{pid}/mem', 'rb') as mem_file:
|
||||
# mem_file.seek(offset)
|
||||
# return mem_file.read(size)
|
||||
|
||||
|
||||
# 导入 Windows API 函数
|
||||
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
|
||||
OpenProcess = kernel32.OpenProcess
|
||||
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
||||
OpenProcess.restype = wintypes.HANDLE
|
||||
|
||||
ReadProcessMemory = kernel32.ReadProcessMemory
|
||||
ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t,
|
||||
ctypes.POINTER(ctypes.c_size_t)]
|
||||
ReadProcessMemory.restype = wintypes.BOOL
|
||||
|
||||
CloseHandle = kernel32.CloseHandle
|
||||
CloseHandle.argtypes = [wintypes.HANDLE]
|
||||
CloseHandle.restype = wintypes.BOOL
|
||||
|
||||
|
||||
def read_bytes_from_pid(pid: int, addr: int, size: int):
|
||||
# 打开进程
|
||||
hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
||||
if not hprocess:
|
||||
raise Exception(f"Failed to open process with PID {pid}")
|
||||
buffer = b''
|
||||
try:
|
||||
# 创建缓冲区
|
||||
buffer = ctypes.create_string_buffer(size)
|
||||
|
||||
# 读取内存
|
||||
bytes_read = ctypes.c_size_t(0)
|
||||
success = ReadProcessMemory(hprocess, addr, buffer, size, ctypes.byref(bytes_read))
|
||||
if not success:
|
||||
CloseHandle(hprocess)
|
||||
return b''
|
||||
raise Exception(f"Failed to read memory at address {hex(addr)}")
|
||||
|
||||
# 关闭句柄
|
||||
CloseHandle(hprocess)
|
||||
except:
|
||||
pass
|
||||
# 返回读取的字节数组
|
||||
return bytes(buffer)
|
||||
|
||||
|
||||
def read_string_from_pid(pid: int, addr: int, size: int):
|
||||
bytes0 = read_bytes_from_pid(pid, addr, size)
|
||||
try:
|
||||
return bytes0.decode('utf-8')
|
||||
except:
|
||||
return ''
|
||||
|
||||
|
||||
def is_ok(passphrase, buf):
|
||||
global finish_flag
|
||||
if finish_flag:
|
||||
return False
|
||||
# 获取文件开头的 salt
|
||||
salt = buf[:SALT_SIZE]
|
||||
# salt 异或 0x3a 得到 mac_salt,用于计算 HMAC
|
||||
mac_salt = bytes(x ^ 0x3a for x in salt)
|
||||
# 使用 PBKDF2 生成新的密钥
|
||||
new_key = PBKDF2(passphrase, salt, dkLen=KEY_SIZE, count=ROUND_COUNT, hmac_hash_module=SHA512)
|
||||
# 使用新的密钥和 mac_salt 计算 mac_key
|
||||
mac_key = PBKDF2(new_key, mac_salt, dkLen=KEY_SIZE, count=2, hmac_hash_module=SHA512)
|
||||
# 计算 hash 校验码的保留空间
|
||||
reserve = IV_SIZE + HMAC_SHA512_SIZE
|
||||
reserve = ((reserve + AES_BLOCK_SIZE - 1) // AES_BLOCK_SIZE) * AES_BLOCK_SIZE
|
||||
# 校验 HMAC
|
||||
start = SALT_SIZE
|
||||
end = PAGE_SIZE
|
||||
mac = hmac.new(mac_key, buf[start:end - reserve + IV_SIZE], SHA512)
|
||||
mac.update(struct.pack('<I', 1)) # page number as 1
|
||||
hash_mac = mac.digest()
|
||||
# 校验 HMAC 是否一致
|
||||
hash_mac_start_offset = end - reserve + IV_SIZE
|
||||
hash_mac_end_offset = hash_mac_start_offset + len(hash_mac)
|
||||
if hash_mac == buf[hash_mac_start_offset:hash_mac_end_offset]:
|
||||
print(f"[v] found key at 0x{start:x}")
|
||||
finish_flag = True
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_chunk(chunk, buf):
|
||||
global finish_flag
|
||||
if finish_flag:
|
||||
return False
|
||||
if is_ok(chunk, buf):
|
||||
return chunk
|
||||
return False
|
||||
|
||||
|
||||
def verify_key(key: bytes, buffer: bytes, flag, result):
|
||||
if len(key) != 32:
|
||||
return False
|
||||
if flag.value: # 如果其他进程已找到结果,提前退出
|
||||
return False
|
||||
if is_ok(key, buffer): # 替换为实际的目标检测条件
|
||||
print("Key found!", key)
|
||||
with flag.get_lock(): # 保证线程安全
|
||||
flag.value = True
|
||||
return key
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_key_(keys, buf):
|
||||
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count() // 2)
|
||||
results = pool.starmap(check_chunk, ((key, buf) for key in keys))
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
for r in results:
|
||||
if r:
|
||||
print("Key found!", r)
|
||||
return bytes.hex(r)
|
||||
return None
|
||||
|
||||
|
||||
def get_key_inner(pid, process_infos):
|
||||
"""
|
||||
扫描可能为key的内存
|
||||
:param pid:
|
||||
:param process_infos:
|
||||
:return:
|
||||
"""
|
||||
process_handle = open_process(pid)
|
||||
rules_v4_key = r'''
|
||||
rule GetKeyAddrStub
|
||||
{
|
||||
strings:
|
||||
$a = /.{6}\x00{2}\x00{8}\x20\x00{7}\x2f\x00{7}/
|
||||
condition:
|
||||
all of them
|
||||
}
|
||||
'''
|
||||
rules = yara.compile(source=rules_v4_key)
|
||||
pre_addresses = []
|
||||
for base_address, region_size in process_infos:
|
||||
memory = read_process_memory(process_handle, base_address, region_size)
|
||||
# 定义目标数据(如内存或文件内容)
|
||||
target_data = memory # 二进制数据
|
||||
if not memory:
|
||||
continue
|
||||
# 加上这些判断条件时灵时不灵
|
||||
# if b'-----BEGIN PUBLIC KEY-----' not in target_data or b'USER_KEYINFO' not in target_data:
|
||||
# continue
|
||||
# if b'db_storage' not in memory:
|
||||
# continue
|
||||
# with open(f'key-{base_address}.bin', 'wb') as f:
|
||||
# f.write(target_data)
|
||||
matches = rules.match(data=target_data)
|
||||
if matches:
|
||||
for match in matches:
|
||||
rule_name = match.rule
|
||||
if rule_name == 'GetKeyAddrStub':
|
||||
for string in match.strings:
|
||||
instance = string.instances[0]
|
||||
offset, content = instance.offset, instance.matched_data
|
||||
addr = read_num(target_data, offset, 8)
|
||||
pre_addresses.append(addr)
|
||||
keys = []
|
||||
key_set = set()
|
||||
for pre_address in pre_addresses:
|
||||
if any([base_address <= pre_address <= base_address + region_size - KEY_SIZE for base_address, region_size in
|
||||
process_infos]):
|
||||
key = read_bytes_from_pid(pid, pre_address, 32)
|
||||
if key not in key_set:
|
||||
keys.append(key)
|
||||
key_set.add(key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_key(pid, process_handle, buf):
|
||||
process_infos = get_memory_regions(process_handle)
|
||||
|
||||
def split_list(lst, n):
|
||||
k, m = divmod(len(lst), n)
|
||||
return (lst[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n))
|
||||
|
||||
keys = []
|
||||
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count() // 2)
|
||||
results = pool.starmap(get_key_inner, ((pid, process_info_) for process_info_ in
|
||||
split_list(process_infos, min(len(process_infos), 40))))
|
||||
pool.close()
|
||||
pool.join()
|
||||
for r in results:
|
||||
if r:
|
||||
keys += r
|
||||
key = get_key_(keys, buf)
|
||||
return key
|
||||
|
||||
|
||||
def get_wx_dir(process_handle):
|
||||
rules_v4_dir = r'''
|
||||
rule GetDataDir {
|
||||
strings:
|
||||
$a = /[a-zA-Z]:\\(.{1,100}?\\){0,1}?xwechat_files\\[0-9a-zA-Z_-]{6,24}?\\db_storage\\/
|
||||
condition:
|
||||
$a
|
||||
}
|
||||
'''
|
||||
rules = yara.compile(source=rules_v4_dir)
|
||||
process_infos = get_memory_regions(process_handle)
|
||||
wx_dir_cnt = {}
|
||||
for base_address, region_size in process_infos:
|
||||
memory = read_process_memory(process_handle, base_address, region_size)
|
||||
# 定义目标数据(如内存或文件内容)
|
||||
target_data = memory # 二进制数据
|
||||
if not memory:
|
||||
continue
|
||||
if b'db_storage' not in memory:
|
||||
continue
|
||||
matches = rules.match(data=target_data)
|
||||
if matches:
|
||||
# 输出匹配结果
|
||||
for match in matches:
|
||||
rule_name = match.rule
|
||||
if rule_name == 'GetDataDir':
|
||||
for string in match.strings:
|
||||
content = string.instances[0].matched_data
|
||||
wx_dir_cnt[content] = wx_dir_cnt.get(content, 0) + 1
|
||||
return max(wx_dir_cnt, key=wx_dir_cnt.get).decode('utf-8') if wx_dir_cnt else ''
|
||||
|
||||
|
||||
def get_nickname(pid):
|
||||
process_handle = open_process(pid)
|
||||
if not process_handle:
|
||||
print(f"无法打开进程 {pid}")
|
||||
return {}
|
||||
process_infos = get_memory_regions(process_handle)
|
||||
# 加载规则
|
||||
r'''$a = /(.{16}[\x00-\x20]\x00{7}(\x0f|\x1f)\x00{7}){2}.{16}[\x01-\x20]\x00{7}(\x0f|\x1f)\x00{7}[0-9]{11}\x00{5}\x0b\x00{7}\x0f\x00{7}.{25}\x00{7}(\x3f|\x2f|\x1f|\x0f)\x00{7}/s'''
|
||||
rules_v4_phone = r'''
|
||||
rule GetPhoneNumberOffset {
|
||||
strings:
|
||||
$a = /[\x01-\x20]\x00{7}(\x0f|\x1f)\x00{7}[0-9]{11}\x00{5}\x0b\x00{7}\x0f\x00{7}/
|
||||
condition:
|
||||
$a
|
||||
}
|
||||
'''
|
||||
nick_name = ''
|
||||
phone = ''
|
||||
account_name = ''
|
||||
rules = yara.compile(source=rules_v4_phone)
|
||||
for base_address, region_size in process_infos:
|
||||
memory = read_process_memory(process_handle, base_address, region_size)
|
||||
# 定义目标数据(如内存或文件内容)
|
||||
target_data = memory # 二进制数据
|
||||
if not memory:
|
||||
continue
|
||||
# if not (b'db_storage' in target_data or b'USER_KEYINFO' in target_data):
|
||||
# continue
|
||||
# if not (b'-----BEGIN PUBLIC KEY-----' in target_data):
|
||||
# continue
|
||||
matches = rules.match(data=target_data)
|
||||
if matches:
|
||||
# 输出匹配结果
|
||||
for match in matches:
|
||||
rule_name = match.rule
|
||||
if rule_name == 'GetPhoneNumberOffset':
|
||||
for string in match.strings:
|
||||
instance = string.instances[0]
|
||||
offset, content = instance.offset, instance.matched_data
|
||||
# print(
|
||||
# f"匹配字符串: {identifier} 内容: 偏移: {offset} 在地址: {hex(base_address + offset + 0x10)}")
|
||||
# print(string)
|
||||
with open('a.bin','wb') as f:
|
||||
f.write(target_data)
|
||||
phone_addr = offset + 0x10
|
||||
phone = read_string(target_data, phone_addr, 11)
|
||||
|
||||
# 提取前 8 个字节
|
||||
data_slice = target_data[offset:offset + 8]
|
||||
# 使用 struct.unpack() 将字节转换为 u64,'<Q' 表示小端字节序的 8 字节无符号整数
|
||||
nick_name_length = struct.unpack('<Q', data_slice)[0]
|
||||
# print('nick_name_length', nick_name_length)
|
||||
nick_name = read_string(target_data, phone_addr - 0x20, nick_name_length)
|
||||
a = target_data[phone_addr - 0x60:phone_addr + 0x50]
|
||||
account_name_length = read_num(target_data, phone_addr - 0x30, 8)
|
||||
# print('account_name_length', account_name_length)
|
||||
account_name = read_string(target_data, phone_addr - 0x40, account_name_length)
|
||||
# with open('a.bin', 'wb') as f:
|
||||
# f.write(target_data)
|
||||
if not account_name:
|
||||
addr = read_num(target_data, phone_addr - 0x40, 8)
|
||||
# print(hex(addr))
|
||||
account_name = read_string_from_pid(pid, addr, account_name_length)
|
||||
return {
|
||||
'nick_name': nick_name,
|
||||
'phone': phone,
|
||||
'account_name': account_name
|
||||
}
|
||||
|
||||
|
||||
def worker(pid, queue):
|
||||
nickname_dic = get_nickname(pid)
|
||||
queue.put(nickname_dic)
|
||||
|
||||
|
||||
def dump_wechat_info_v4(pid) -> WeChatInfo | None:
|
||||
wechat_info = WeChatInfo()
|
||||
wechat_info.pid = pid
|
||||
wechat_info.version = get_version(pid)
|
||||
process_handle = open_process(pid)
|
||||
if not process_handle:
|
||||
print(f"无法打开进程 {pid}")
|
||||
return wechat_info
|
||||
queue = multiprocessing.Queue()
|
||||
process = multiprocessing.Process(target=worker, args=(pid, queue))
|
||||
|
||||
process.start()
|
||||
|
||||
wechat_info.wx_dir = get_wx_dir(process_handle)
|
||||
# print(wx_dir_cnt)
|
||||
if not wechat_info.wx_dir:
|
||||
return wechat_info
|
||||
db_file_path = os.path.join(wechat_info.wx_dir, 'biz', 'biz.db')
|
||||
with open(db_file_path, 'rb') as f:
|
||||
buf = f.read()
|
||||
wechat_info.key = get_key(pid, process_handle, buf)
|
||||
ctypes.windll.kernel32.CloseHandle(process_handle)
|
||||
wechat_info.wxid = '_'.join(wechat_info.wx_dir.split('\\')[-3].split('_')[0:-1])
|
||||
wechat_info.wx_dir = '\\'.join(wechat_info.wx_dir.split('\\')[:-2])
|
||||
process.join() # 等待子进程完成
|
||||
if not queue.empty():
|
||||
nickname_info = queue.get()
|
||||
wechat_info.nick_name = nickname_info.get('nick_name', '')
|
||||
wechat_info.phone = nickname_info.get('phone', '')
|
||||
wechat_info.account_name = nickname_info.get('account_name', '')
|
||||
if not wechat_info.key:
|
||||
wechat_info.errcode = 404
|
||||
else:
|
||||
wechat_info.errcode = 200
|
||||
return wechat_info
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
freeze_support()
|
||||
st = time.time()
|
||||
pm = pymem.Pymem("Weixin.exe")
|
||||
pid = pm.process_id
|
||||
w = dump_wechat_info_v4(pid)
|
||||
print(w)
|
||||
et = time.time()
|
||||
print(et - st)
|
||||
Reference in New Issue
Block a user