From 5c9a88df8fdae7e596dcb629b0d4f65043d57bac Mon Sep 17 00:00:00 2001 From: xaoyaoo Date: Tue, 5 Dec 2023 11:17:52 +0800 Subject: [PATCH] =?UTF-8?q?parse.py=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pywxdump/wx_info/get_bias_addr.py | 35 +- pywxdump/wx_info/test.rb | 625 ++++++++++++++++++++++++++++++ 2 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 pywxdump/wx_info/test.rb diff --git a/pywxdump/wx_info/get_bias_addr.py b/pywxdump/wx_info/get_bias_addr.py index 6e53120..e793b10 100644 --- a/pywxdump/wx_info/get_bias_addr.py +++ b/pywxdump/wx_info/get_bias_addr.py @@ -251,6 +251,23 @@ class BiasAddr: key, bais = verify_key(maybe_key, wx_db_path) return bais + def test(self): + phone_type1 = "iphone\x00" + phone_type2 = "android\x00" + Regex = re.compile(r"^[a-zA-Z0-9_]+$") + # 内存搜索 + module = pymem.process.module_from_name(self.pm.process_handle, self.module_name) + print(hex(module.lpBaseOfDll)) + phone_type1_bias = self.pm.pattern_scan_module(phone_type1.encode(), self.module_name, return_multiple=True) + phone_type2_bias = self.pm.pattern_scan_module(phone_type2.encode(), self.module_name, return_multiple=True) + phone_type_bias = phone_type1_bias + phone_type2_bias + print(len(phone_type1_bias)) + for i in phone_type_bias[::-1]: + for j in range(i, i - 1000, -16): + a = get_info_without_key(self.process_handle, j, 32) + if Regex.match(a) and len(a) >= 6: + print(a) + def run(self, logging_path=False, version_list_path=None): if not self.get_process_handle()[0]: return None @@ -263,6 +280,8 @@ class BiasAddr: key_bias = self.get_key_bias2(self.db_path, account_bias) if key_bias <= 0 and self.db_path else key_bias rdata = {self.version: [name_bias, account_bias, mobile_bias, 0, key_bias]} + print(rdata) + self.test() if version_list_path and os.path.exists(version_list_path): with open(version_list_path, "r", encoding="utf-8") as f: data = json.load(f) @@ -276,4 +295,18 @@ class BiasAddr: elif logging_path: print("{版本号:昵称,账号,手机号,邮箱,KEY}") print(rdata) - return rdata \ No newline at end of file + 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" + + +if __name__ == '__main__': + account, mobile, name, key, db_path = "test", "test", "test", "0000", "test" + bias_addr = BiasAddr(account, mobile, name, key, db_path) + bias_addr.run() diff --git a/pywxdump/wx_info/test.rb b/pywxdump/wx_info/test.rb new file mode 100644 index 0000000..517e82e --- /dev/null +++ b/pywxdump/wx_info/test.rb @@ -0,0 +1,625 @@ +pub mod procmem; + +use std::{ + fs::{self, File}, + io::Read, + ops::{Add, Sub}, + path::PathBuf, +}; + +use rayon::prelude::*; +use aes::cipher::{KeyIvInit, BlockDecryptMut, block_padding::NoPadding}; +use anyhow::{Ok, Result}; +use hmac::{Hmac, Mac}; +use pbkdf2::pbkdf2_hmac_array; +use regex::Regex; +use sha1::Sha1; +use windows::Win32::{ + Foundation::CloseHandle, + System::{ + Diagnostics::Debug::ReadProcessMemory, + Memory::{PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_READWRITE, PAGE_WRITECOPY, MEM_PRIVATE}, + Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}, + }, +}; +use yara::Compiler; + +use crate::procmem::ProcessMemoryInfo; + +const RULES: &str = r#" + rule GetPhoneTypeStringOffset + { + strings: + $a = "iphone\x00" ascii fullword + $b = "android\x00" ascii fullword + + condition: + any of them + } + + rule GetDataDir + { + strings: + $a = /[a-zA-Z]:\\.{0,100}?\\WeChat Files\\[0-9a-zA-Z_-]{6,20}?\\/ + + condition: + $a + } +"#; + +#[derive(Debug, Clone)] +struct WechatInfo { + pub pid: u32, + pub version: String, + pub account_name: String, + pub phone_type: String, + pub data_dir: String, + pub key: String, +} + +impl std::fmt::Display for WechatInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"======================================= +ProcessId: {} +WechatVersion: {} +AccountName: {} +PhoneType: {} +DataDir: {} +key: {} +======================================= +"#, + self.pid, + self.version, + self.account_name, + self.phone_type, + self.data_dir, + self.key + ) + } +} + +fn get_pid_by_name(pname: &str) -> Vec { + let mut result = vec![]; + unsafe { + for pp in tasklist::Tasklist::new() { + if pp.get_pname() == pname { + result.push(pp.get_pid()); + } + } + } + + result +} + +fn read_number(pid: u32, addr: usize) -> Result { + unsafe { + let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)?; + + let mut result: T = T::default(); + + ReadProcessMemory( + hprocess, + addr as _, + std::mem::transmute(&mut result), + std::mem::size_of::(), + None, + )?; + + CloseHandle(hprocess)?; + Ok(result) + } +} + +fn read_string(pid: u32, addr: usize, size: usize) -> Result { + unsafe { + let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)?; + + let mut buffer = vec![0; size]; + let _ = ReadProcessMemory(hprocess, addr as _, buffer.as_mut_ptr() as _, size, None); + + CloseHandle(hprocess)?; + + match buffer.iter().position(|&x| x == 0) { + Some(pos) => Ok(String::from_utf8(buffer[..pos].to_vec())?), + None => Ok(String::from_utf8(buffer)?), + } + } +} + +fn read_bytes(pid: u32, addr: usize, size: usize) -> Result> { + unsafe { + let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)?; + + let mut buffer = vec![0; size]; + let _ = ReadProcessMemory(hprocess, addr as _, buffer.as_mut_ptr() as _, size, None)?; + + CloseHandle(hprocess)?; + + Ok(buffer) + } +} + +fn get_proc_file_version(pid: u32) -> String { + unsafe { + tasklist::get_proc_file_info(pid) + .get("FileVersion") + .expect("read file version failed") + .to_string() + } +} + +fn dump_wechat_info(pid: u32, special_data_dir: Option::<&PathBuf>) -> WechatInfo { + let version = get_proc_file_version(pid); + println!("[+] wechat version is {}", version); + + let pmis = procmem::get_mem_list(pid); + + let wechatwin_all_mem_infos: Vec<&ProcessMemoryInfo> = pmis + .iter() + .filter(|x| x.filename.is_some() && x.filename.clone().unwrap().contains("WeChatWin.dll")) + .collect(); + + let wechatwin_writable_mem_infos: Vec<&ProcessMemoryInfo> = wechatwin_all_mem_infos + .iter() + .filter(|x| { + (x.protect + & (PAGE_READWRITE + | PAGE_WRITECOPY + | PAGE_EXECUTE_READWRITE + | PAGE_EXECUTE_WRITECOPY)) + .0 + > 0 + }) + .map(|x| *x) + .collect(); + + let wechat_writeable_private_mem_infos: Vec<&ProcessMemoryInfo> = pmis + .iter() + .filter(|x| { + (x.protect & (PAGE_READWRITE | PAGE_WRITECOPY)).0 > 0 && x.mtype == MEM_PRIVATE + }) + .collect(); + + // 使用 yara 匹配到登录设备的地址和数据目录 + let compiler = Compiler::new().unwrap(); + let compiler = compiler + .add_rules_str(RULES) + .expect("Should have parsed rule"); + let rules = compiler + .compile_rules() + .expect("Should have compiled rules"); + let results = rules + .scan_process(pid, 0) + // .scan_file(r"C:\Users\thin0\Desktop\WeChatWin.dll", 0) + .expect("Should have scanned"); + + let phone_type_str_match = results + .iter() + .filter(|x| x.identifier == "GetPhoneTypeStringOffset") + .next() + .expect("unbale to find phone type string") + .strings + .iter() + .filter(|x| { + x.matches.iter().any(|y| { + wechatwin_writable_mem_infos + .iter() + .any(|z| y.base == z.base) + }) + }) + .next() + .expect("unbale to find phone type string") + .matches + .iter() + .filter(|x| { + wechatwin_writable_mem_infos + .iter() + .any(|y| x.base == y.base) + }) + .next() + .expect("unable to find phone type string"); + let phone_type_string_addr = phone_type_str_match.base + phone_type_str_match.offset; + let phone_type_string = + read_string(pid, phone_type_string_addr, 20).expect("read phone type string failed"); + let data_dir = if special_data_dir.is_some() { + special_data_dir.unwrap().clone().into_os_string().into_string().unwrap() + } else { + let data_dir_match = results + .iter() + .filter(|x| x.identifier == "GetDataDir") + .next() + .expect("unable to find data dir") + .strings + .first() + .expect("unable to find data dir") + .matches + .iter() + .filter(|x| wechat_writeable_private_mem_infos.iter().any(|pmi| pmi.base == x.base)) + .next() + .expect("unable to find data dir"); + String::from_utf8(data_dir_match.data.clone()).expect("data dir is invalid string") + }; + + println!("[+] login phone type is {}", phone_type_string); + println!("[+] wechat data dir is {}", data_dir); + + let align = std::mem::size_of::(); // x64 -> 16, x86 -> 8 + + // account_name 在 phone_type 前面,并且是 16 位补齐的,所以向前找,离得比较近不用找太远的 + let mut start = phone_type_string_addr - align; + let mut account_name_addr = start; + let mut account_name: Option = None; + let mut count = 0; + while start >= phone_type_string_addr - align * 20 { + // 名字长度>=16,就会变成指针,不直接存放字符串 + let account_name_point_address = read_number::(pid, start) + .expect("read account name point address failed"); + let result = if pmis.iter().any(|x| { + account_name_point_address >= x.base && account_name_point_address <= x.base + x.region_size + }) { + read_string(pid, account_name_point_address, 100) + } else { + read_string(pid, start, align) + }; + + if result.is_ok() { + let ac = result.unwrap(); + + // 微信号是字母、数字、下划线组合,6-20位 + let re = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); + if re.is_match(&ac) && ac.len() >= 6 && ac.len() <= 20{ + // 首次命中可能是原始的 wxid_,第二次是修改后的微信号,找不到第二次说明注册后没改过微信号 + account_name = Some(ac); + account_name_addr = start; + count += 1; + if count == 2 { + break; + } + } + } + + start -= align; + } + + if account_name.is_none() { + panic!("not found account name address"); + } + let account_name = account_name.unwrap(); + println!("[+] account name is {}", account_name); + + // 读取一个文件准备暴力搜索key + const IV_SIZE: usize = 16; + const HMAC_SHA1_SIZE: usize = 20; + const KEY_SIZE: usize = 32; + const AES_BLOCK_SIZE: usize = 16; + const SALT_SIZE: usize = 16; + const PAGE_SIZE: usize = 4096; + let db_file_path = data_dir.clone() + "Msg\\Misc.db"; + let mut db_file = std::fs::File::open(&db_file_path).expect(format!("{} is not exsit", &db_file_path).as_str()); + let mut buf = [0u8; PAGE_SIZE]; + db_file.read(&mut buf[..]).expect("read Misc.db is failed"); + + // key 在微信号前面找 + let mut key: Option = None; + let mem_base = phone_type_str_match.base; + let mut key_point_addr = account_name_addr - align; + while key_point_addr >= mem_base { + let key_addr = read_number::(pid, key_point_addr).expect("find key addr failed in memory"); + + if wechat_writeable_private_mem_infos.iter().any(|x| key_addr >= x.base && key_addr <= x.base + x.region_size) { + let key_bytes = read_bytes(pid, key_addr, KEY_SIZE).expect("find key bytes failed in memory"); + if key_bytes.iter().filter(|&&x| x == 0x00).count() < 5 { + // 验证 key 是否有效 + let start = SALT_SIZE; + let end = PAGE_SIZE; + + // 获取到文件开头的 salt + let salt = buf[..SALT_SIZE].to_owned(); + // salt 异或 0x3a 得到 mac_salt, 用于计算HMAC + let mac_salt: Vec = salt.to_owned().iter().map(|x| x ^ 0x3a).collect(); + + // 通过 key_bytes 和 salt 迭代64000次解出一个新的 key,用于解密 + let new_key = pbkdf2_hmac_array::(&key_bytes, &salt, 64000); + + // 通过 key 和 mac_salt 迭代2次解出 mac_key + let mac_key = pbkdf2_hmac_array::(&new_key, &mac_salt, 2); + + // hash检验码对齐后长度 48,后面校验哈希用 + let mut reserve = IV_SIZE + HMAC_SHA1_SIZE; + reserve = if (reserve % AES_BLOCK_SIZE) == 0 { + reserve + } else { + ((reserve / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE + }; + + // 校验哈希 + type HamcSha1 = Hmac; + + unsafe { + let mut mac = HamcSha1::new_from_slice(&mac_key).expect("hmac_sha1 error, key length is invalid"); + mac.update(&buf[start..end-reserve+IV_SIZE]); + mac.update(std::mem::transmute::<_, &[u8; 4]>(&(1u32)).as_ref()); + let hash_mac = mac.finalize().into_bytes().to_vec(); + + let hash_mac_start_offset = end - reserve + IV_SIZE; + let hash_mac_end_offset = hash_mac_start_offset + hash_mac.len(); + if hash_mac == &buf[hash_mac_start_offset..hash_mac_end_offset] { + key = Some(hex::encode(key_bytes)); + break; + } + } + } + } + + key_point_addr -= align; + } + + if key.is_none() { + panic!("not found key"); + } + + WechatInfo { + pid, + version, + account_name, + phone_type: phone_type_string, + data_dir, + key: key.unwrap() + } +} + +fn scan_db_files(dir: String) -> Result> { + let mut result = vec![]; + + for entry in fs::read_dir(dir)?.filter_map(Result::ok) { + let path = entry.path(); + if path.is_dir() { + result.extend(scan_db_files(path.to_str().unwrap().to_string())?); + } else if let Some(ext) = path.extension() { + if ext == "db" { + result.push(path); + } + } + } + + Ok(result) +} + +fn read_file_content(path: &PathBuf) -> Result> { + let mut file = File::open(path)?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) +} + +fn decrypt_db_file(path: &PathBuf, pkey: &String) -> Result> { + const IV_SIZE: usize = 16; + const HMAC_SHA1_SIZE: usize = 20; + const KEY_SIZE: usize = 32; + const AES_BLOCK_SIZE: usize = 16; + const SQLITE_HEADER: &str = "SQLite format 3"; + + let mut buf = read_file_content(path)?; + + // 如果开头是 SQLITE_HEADER,说明不需要解密 + if buf.starts_with(SQLITE_HEADER.as_bytes()) { + return Ok(buf); + } + + let mut decrypted_buf: Vec = vec![]; + + // 获取到文件开头的 salt,用于解密 key + let salt = buf[..16].to_owned(); + // salt 异或 0x3a 得到 mac_salt, 用于计算HMAC + let mac_salt: Vec = salt.to_owned().iter().map(|x| x ^ 0x3a).collect(); + + unsafe { + // 通过 pkey 和 salt 迭代64000次解出一个新的 key,用于解密 + let pass = hex::decode(pkey)?; + let key = pbkdf2_hmac_array::(&pass, &salt, 64000); + + // 通过 key 和 mac_salt 迭代2次解出 mac_key + let mac_key = pbkdf2_hmac_array::(&key, &mac_salt, 2); + + // 开头是 sqlite 头 + decrypted_buf.extend(SQLITE_HEADER.as_bytes()); + decrypted_buf.push(0x00); + + // hash检验码对齐后长度 48,后面校验哈希用 + let mut reserve = IV_SIZE + HMAC_SHA1_SIZE; + reserve = if (reserve % AES_BLOCK_SIZE) == 0 { + reserve + } else { + ((reserve / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE + }; + + // 每页大小4096,分别解密 + const PAGE_SIZE: usize = 4096; + let total_page = (buf.len() as f64 / PAGE_SIZE as f64).ceil() as usize; + for cur_page in 0..total_page { + let offset = if cur_page == 0 { + 16 + } else { + 0 + }; + let start: usize = cur_page * PAGE_SIZE; + let end: usize = if (cur_page + 1) == total_page { + start + buf.len() % PAGE_SIZE + } else { + start + PAGE_SIZE + }; + + // 搞不懂,这一堆0是干啥的,文件大小直接翻倍了 + if buf[start..end].iter().all(|&x| x == 0) { + decrypted_buf.extend(&buf[start..]); + break; + } + + // 校验哈希 + type HamcSha1 = Hmac; + + let mut mac = HamcSha1::new_from_slice(&mac_key)?; + mac.update(&buf[start+offset..end-reserve+IV_SIZE]); + mac.update(std::mem::transmute::<_, &[u8; 4]>(&(cur_page as u32 + 1)).as_ref()); + let hash_mac = mac.finalize().into_bytes().to_vec(); + + let hash_mac_start_offset = end - reserve + IV_SIZE; + let hash_mac_end_offset = hash_mac_start_offset + hash_mac.len(); + if hash_mac != &buf[hash_mac_start_offset..hash_mac_end_offset] { + return Err(anyhow::anyhow!("Hash verification failed")); + } + + // aes-256-cbc 解密内容 + type Aes256CbcDec = cbc::Decryptor; + + let iv = &buf[end-reserve..end-reserve+IV_SIZE]; + decrypted_buf.extend(Aes256CbcDec::new(&key.into(), iv.into()) + .decrypt_padded_mut::(&mut buf[start+offset..end-reserve]) + .map_err(anyhow::Error::msg)?); + decrypted_buf.extend(&buf[end-reserve..end]); + } + } + + Ok(decrypted_buf) +} + +fn dump_all_by_pid(wechat_info: &WechatInfo, output: &PathBuf) { + let msg_dir = wechat_info.data_dir.clone() + "Msg"; + let dbfiles = scan_db_files(msg_dir.clone()).unwrap(); + println!("scanned {} files in {}", dbfiles.len(), &msg_dir); + println!("decryption in progress, please wait..."); + + // 创建输出目录 + if output.is_file() { + panic!("the output path must be a directory"); + } + let output_dir = PathBuf::from(format!("{}\\wechat_{}", output.to_str().unwrap(), wechat_info.pid)); + if !output_dir.exists() { + std::fs::create_dir_all(&output_dir).unwrap(); + } + + dbfiles.par_iter().for_each(|dbfile| { + let mut db_file_dir = PathBuf::new(); + let mut dest = PathBuf::new(); + db_file_dir.push(&output_dir); + db_file_dir.push(dbfile.parent().unwrap().strip_prefix(PathBuf::from(msg_dir.clone())).unwrap()); + dest.push(db_file_dir.clone()); + dest.push(dbfile.file_name().unwrap()); + + if !db_file_dir.exists() { + std::fs::create_dir_all(db_file_dir).unwrap(); + } + + std::fs::write(dest, decrypt_db_file(&dbfile, &wechat_info.key).unwrap()).unwrap(); + }); + println!("decryption complete!!"); + println!("output to {}", output_dir.to_str().unwrap()); + println!(); +} + +fn cli() -> clap::Command { + use clap::{arg, value_parser, Command}; + + Command::new("wechat-dump-rs") + .version("1.0.5") + .about("A wechat db dump tool") + .author("REinject") + .help_template("{name} ({version}) - {author}\n{about}\n{all-args}") + .disable_version_flag(true) + .arg(arg!(-p --pid "pid of wechat").value_parser(value_parser!(u32))) + .arg( + arg!(-k --key "key for offline decryption of db file") + .value_parser(value_parser!(String)), + ) + .arg(arg!(-f --file "special a db file path").value_parser(value_parser!(PathBuf))) + .arg(arg!(-d --"data-dir" "special wechat data dir path (pid is required)").value_parser(value_parser!(PathBuf))) + .arg(arg!(-o --output "decrypted database output path").value_parser(value_parser!(PathBuf))) + .arg(arg!(-a --all "dump key and decrypt db files")) +} + +fn main() { + // 解析参数 + let matches = cli().get_matches(); + + let all = matches.get_flag("all"); + let output = match matches.get_one::("output") { + Some(o) => PathBuf::from(o), + None => PathBuf::from(format!("{}{}", std::env::temp_dir().to_str().unwrap(), "wechat_dump")) + }; + + let key_option = matches.get_one::("key"); + let file_option = matches.get_one::("file"); + let data_dir_option = matches.get_one::("data-dir"); + let pid_option = matches.get_one::("pid"); + + match (pid_option, key_option, file_option) { + (None, None, None) => { + for pid in get_pid_by_name("WeChat.exe") { + let wechat_info = dump_wechat_info(pid, None); + println!("{}", wechat_info); + println!(); + + // 需要对所有db文件进行解密 + if all { + dump_all_by_pid(&wechat_info, &output); + } + } + }, + (Some(&pid), None, None) => { + let wechat_info = dump_wechat_info(pid, data_dir_option); + println!("{}", wechat_info); + println!(); + + // 需要对所有db文件进行解密 + if all { + dump_all_by_pid(&wechat_info, &output); + } + }, + (None, Some(key), Some(file)) => { + if !file.exists() { + panic!("the target file does not exist"); + } + + match file.is_dir() { + true => { + let dbfiles = scan_db_files(file.to_str().unwrap().to_string()).unwrap(); + println!("scanned {} files in {}", dbfiles.len(), &file.to_str().unwrap()); + println!("decryption in progress, please wait..."); + + // 创建输出目录 + if output.is_file() { + panic!("the output path must be a directory"); + } + if !output.exists() { + std::fs::create_dir_all(&output).unwrap(); + } + + for dbfile in dbfiles { + let mut db_file_dir = PathBuf::new(); + let mut dest = PathBuf::new(); + db_file_dir.push(&output); + db_file_dir.push(dbfile.parent().unwrap().strip_prefix(PathBuf::from(&file)).unwrap()); + dest.push(db_file_dir.clone()); + dest.push(dbfile.file_name().unwrap()); + + if !db_file_dir.exists() { + std::fs::create_dir_all(db_file_dir).unwrap(); + } + + std::fs::write(dest, decrypt_db_file(&dbfile, &key).unwrap()).unwrap(); + } + println!("decryption complete!!"); + println!("output to {}", output.to_str().unwrap()); + println!(); + }, + false => { + std::fs::write(&output, decrypt_db_file(&file, &key).unwrap()).unwrap(); + println!("output to {}", output.to_str().unwrap()); + } + } + }, + _ => panic!("param error") + } +} \ No newline at end of file