这个国庆长假,在晃晃悠悠中渡过了。
除了日常工作以外,抽空对网上现有的一些“微信PC端解密过程类”的文章进行了部分学习。最早的可见于2021年文章,最近的也有近期的文章;各种编程语言的都有,从C++、C#、Java、Python到Rust,基本都看了一些。通过学习,稍微知晓了微信PC端的解密过程,涉及原理分析、逆向跟踪、代码解析等,信息量着实有些大,尤其是后面这里着重对rust这种语言的入门级的认识学习。
注:本篇所有内容,兼为学习,如果你用来做违法的事,跟本人无关;如果内容TX认为不妥,请通知我予以删除。
一、有关内容
关于对微信PC端的解密,网上有很多文章都写得非常地清楚,我也就不多啰嗦了。推荐一些给大家看下,
《【电子取证技巧】解密WX聊天记录文件》,有逆向过程,有源码解密;
《微信解密PC端剖析-解密微信Msg数据库》,有数据库的表及字段说明;
《教你解密微信数据库》,有手机端的解密;
《微信Windows版本解密学习》,SharpWxDump工具;
《【工具】轻松解锁SQLite数据库,一窥微信聊天记录小秘密》,pyWxDump工具;
《电子取证之PC版微信数据库解密》,有逆向,有源码解密;
二、rust版
1、下面,我介绍的这个用Rust语言编写的工具:《wechat-dump-rs》(https://github.com/0xlane/wechat-dump-rs)。github上介绍道:该工具用于导出正在运行中的微信进程的 key 并自动解密所有微信数据库文件以及导出 key 后数据库文件离线解密。
这个工具,跟前面的工具都有所不同,应该是一个进化版了;以前都是有不同版本+不同位置的json,即“一般情况下,key 要在运行的微信进程内存中拿到,内存偏移在每个版本都不一样,大部分工具是对每个版本维护一套偏移,但是当出现新版本的时候都要重新找偏移”,虽然不错但存在着每出一个新版本就要手动找一次的“麻烦”。看这个工具,它采用了“更通用的方法就是内存暴力搜索找到能用于解密的密钥位置”。
用下面这种方式缩小密钥内存范围加快扫描速度:
(1)微信登录设备类型基本只有 iphone、android,在内存中先搜到设备类型所在内存,key 就在它的前面,向前搜就行 ;
(2)key 的内存地址和登录设备类型据我观察是 16 字节对齐的,所以每次向前 16 字节;
同时,作者还介绍了“如何手动寻找偏移”来作验证。
2、调试时的一些错误提示,作个记录方便以后;
当我在RustRover中调试时,出现了2个错误提示,一个如下,
另一个当时没截图,大概意思就是:libclang.dll没找到,
解决如下:
好玩的是,这个解决掉了,上面那个错误也就随之没有了。
三、代码解读
1、源码的思维导图梳理如下,
2、源码解读:
这个工具源码只有两个:main.rs和mod.rs,
(1)main.rs
pub mod procmem;
//pub mod procmem; - 这行代码表示在当前的 Rust 模块中引入名为 procmem 的子模块。
// 通过使用 pub 关键字,这个子模块 procmem 中的内容将对外可见,允许其他模块访问和使用其中定义的内容。
// 当引入子模块后,可以通过 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#"...some text..."#; - 定义一个名为 RULES 的常量,其类型为 &str,
// 并使用原始字符串字面值 (r#"...some text..."#) 来表示多行字符串。
//rule GetPhoneTypeStringOffset - 定义一个名为 GetPhoneTypeStringOffset 的规则。
//strings: - 定义规则中要匹配的字符串节(section)。
// $a = "iphonex00" ascii fullword - 定义一个命名为 $a 的字符串变量,其值为 "iphone"(以 ASCII 编码),fullword 表示完整单词匹配。
// $b = "androidx00" ascii fullword - 定义一个命名为 $b 的字符串变量,其值为 "android"(以 ASCII 编码),fullword 表示完整单词匹配。
// condition: any of them - 定义规则的条件,表示如果任何上面定义的字符串变量匹配成功,则条件成立。
// rule GetDataDir - 定义一个名为 GetDataDir 的规则。
// strings: - 定义规则中要匹配的字符串节(section)。
// $a = /[a-zA-Z]:\(.{1,100}?\){0,1}?WeChat Files\[0-9a-zA-Z_-]{6,20}?\/ - 定义一个命名为 $a 的正则表达式变量,
// 用于匹配类似于 C:somepathWeChat Filessome_folder 这样的路径。
// condition: $a - 定义规则的条件,表示如果正则表达式变量 $a 匹配成功,则条件成立。
const RULES: &str = r#"
rule GetPhoneTypeStringOffset
{
strings:
$a = "iphonex00" ascii fullword
$b = "androidx00" ascii fullword
condition:
any of them
}
rule GetDataDir
{
strings:
$a = /[a-zA-Z]:\(.{1,100}?\){0,1}?WeChat Files\[0-9a-zA-Z_-]{6,20}?\/
condition:
$a
}
"#;
#[derive(Debug, Clone)]
//#[derive(...)]:这是一个属性宏,用于自动为结构体实现指定的 trait。
// Clone:允许对该结构体的实例进行克隆操作,即可以创建一个新的实例与原实例具有相同的数据。
// Debug:允许该结构体的实例使用 {:?} 格式化输出,用于调试目的,方便在控制台打印。
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<u32> {
let mut result = vec![];
unsafe {
for pp in tasklist::Tasklist::new() {
if pp.get_pname().trim_end_matches("x00") == pname {
result.push(pp.get_pid());
}
}
}
result
}
//这段代码定义了一个名为 read_number 的函数,用于从指定进程(通过其 PID)读取一个数值型(如整型、浮点型等)数据,并将其返回。
// 这个函数使用 Rust 的泛型功能,使它能够读取不同类型的数值。
//read_number 函数的主要作用是从指定 PID 进程的内存中读取一个数值型数据。
// 该函数利用了 Rust 的泛型和不安全操作,能够灵活地支持读取多种不同类型的数据,同时确保在读取结束后进行资源的释放。
// 整个函数严重依赖的 Windows API 函数来具体实现对进程内存的读取。
fn read_number<T: Sub + Add + Ord + Default>(pid: u32, addr: usize) -> Result<T> {
//函数名:read_number。
// 泛型参数T:表示将被读取的数据类型,约束条件包括:
// Sub:支持减法操作。
// Add:支持加法操作。
// Ord:支持排序操作(即可以比较)。
// Default:允许使用 T::default() 来创建该类型的默认实例。
// 参数:
// pid: u32:目标进程的 PID(进程标识符)。
// addr: usize:要读取的内存地址。
// 返回值:一个 Result<T>,成功时包含读取数值,失败时包含错误信息。
unsafe {
//调用 Windows API 函数 OpenProcess,以只读和查询信息权限打开指定的进程(PID)。
// 使用按位或运算符组合 PROCESS_VM_READ 和 PROCESS_QUERY_INFORMATION 权限。
// 如果调用成功,返回一个进程句柄 hprocess;如果失败,则返回错误并退出。
let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)?;
//创建一个类型为 T 的变量 result,并将其初始化为该类型的默认值。这是为了在读取内存后存储该数据。
let mut result: T = T::default();
//调用 Windows API 函数 ReadProcessMemory,将指定进程的内存数据读取到 result 变量中。
// hprocess:先前打开的进程句柄。
// addr as _:要读取的内存地址(转换为适当类型)。
// std::mem::transmute(&mut result):将 result 的可变引用转换为指针,以符合 ReadProcessMemory 的参数要求。
// std::mem::size_of::<T>():读取的数据大小,根据泛型类型 T 的大小。
// None:最后一个参数通常用来指定额外的信息,这里未使用。
ReadProcessMemory(
hprocess,
addr as _,
std::mem::transmute(&mut result),
std::mem::size_of::<T>(),
None,
)?;
CloseHandle(hprocess)?;
Ok(result)
}
}
fn read_string(pid: u32, addr: usize, size: usize) -> Result<String> {
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)?),
}
}
}
//这个函数的目的是从指定的进程中读取内存数据。它安全地打开进程和读取其内存,同时确保在读取结束后关闭进程句柄。
// 使用 unsafe 块的操作意味着程序员需要小心以确保安全性。若在任何步骤中发生错误,函数会返回相应的错误信息。
//定义了一个函数 read_bytes,用于从指定 PID 的进程中读取指定数量的字节(size)数据,返回一个字节向量(Vec<u8>)。
fn read_bytes(pid: u32, addr: usize, size: usize) -> Result<Vec<u8>> {
//定义了一个名为 read_bytes 的函数,接收三个参数:
// pid:目标进程的 PID(进程标识符)。
// addr:要读取的内存地址。
// size:要读取的字节数。
// 函数返回一个结果,包含字节向量或可能的错误。
//使用 unsafe 块,表示这些操作可能会影响安全性,允许执行一些低级别的内存操作。这是因为直接访问内存需要比 Rust 编译器更高的权限。
unsafe {
//调用 Windows API 函数 OpenProcess,尝试以只读和查询信息的权限打开指定 PID 的进程。
// 使用按位或运算符组合 PROCESS_VM_READ 和 PROCESS_QUERY_INFORMATION 权限位。
// 如果成功,hprocess 将是一个进程句柄,如果失败则返回错误,? 符号会传播错误。
let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid)?;
//初始化一个字节向量 buffer,大小为 size,所有元素初始值为0。这将用于存储读取到的字节数据。
let mut buffer = vec![0; size];
//调用 Windows API 函数 ReadProcessMemory,从打开的进程 (hprocess) 中读取指定地址 (addr) 的内存数据,并将结果存储在 buffer 中。
// addr as _ 和 buffer.as_mut_ptr() as _ 将内存地址和作为指针的缓冲区转化为适当类型。
// 如果读取失败,? 符号会传播错误。
let _ = ReadProcessMemory(hprocess, addr as _, buffer.as_mut_ptr() as _, size, None)?;
//调用 CloseHandle 函数关闭之前打开的进程句柄,释放相关资源。
// 如果关闭失败,同样会传播错误。
CloseHandle(hprocess)?;
//返回读取到的字节缓冲区(buffer),如果没有发生错误,封装在 Ok 结果中。
Ok(buffer)
}
}
fn get_proc_file_version(pid: u32) -> Option<String> {
unsafe {
tasklist::get_proc_file_info(pid)
.get("FileVersion")
.cloned()
}
}
**/*这段代码的主要作用是通过分析微信进程的内存来提取和解密微信的相关信息(如微信版本、登录设备类型、账号名称、数据目录以及解密密钥)。
它使用了进程内存分析、正则表达式匹配、YARA规则匹配等技术手段。*/**
fn dump_wechat_info(pid: u32, special_data_dir: Option::<&PathBuf>) -> WechatInfo {
let version = get_proc_file_version(pid).unwrap_or_else(|| "unknown".to_owned());
///根据 pid 获取微信进程的文件版本。unwrap_or_else:如果无法获取版本信息,使用默认值 "unknown"。
println!("[+] wechat version is {}", version);
let pmis = procmem::get_mem_list(pid);
///获取指定 pid 的所有内存块信息,返回一个内存块列表 pmis。
let wechatwin_all_mem_infos: Vec<&ProcessMemoryInfo> = pmis
///过滤出包含WeChatWin.dll的内存块:
.iter()
.filter(|x| x.filename.is_some() && x.filename.clone().unwrap().contains("WeChatWin.dll"))
///过滤出 filename 包含 "WeChatWin.dll" 的内存块。
.collect();
///将符合条件的内存块收集到 wechatwin_all_mem_infos 向量中。
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
})
///筛选出权限为可写(例如 PAGE_READWRITE)的内存块。
.map(|x| *x)
.collect();
///将筛选后的结果存入 wechatwin_writable_mem_infos 向量中。
let wechat_writeable_private_mem_infos: Vec<&ProcessMemoryInfo> = pmis
///过滤出可写且为私有的内存块:
.iter()
.filter(|x| {
(x.protect & (PAGE_READWRITE | PAGE_WRITECOPY)).0 > 0 && x.mtype == MEM_PRIVATE
///筛选出权限为可写且类型为 MEM_PRIVATE 的内存块
})
.collect();
///将符合条件的内存块放入 wechat_writeable_private_mem_infos 向量中
// 使用 yara 匹配到登录设备的地址和数据目录
let compiler = Compiler::new().unwrap();
///Compiler::new():创建一个新的 YARA 规则编译器。
let compiler = compiler
.add_rules_str(RULES)
.expect("Should have parsed rule");
/// add_rules_str(RULES):将预定义的 RULES 添加到编译器中。
let rules = compiler
.compile_rules()
.expect("Should have compiled rules");
/// compile_rules():编译 YARA 规则,生成 rules 对象。
let results = rules
.scan_process(pid, 0)
// .scan_file(r"C:Usersthin0DesktopWeChatWin.dll", 0)
.expect("Should have scanned");
/// scan_process(pid, 0):使用 YARA 扫描指定 pid 的进程,匹配规则,返回 results。
///获取手机类型:
let phone_type_str_match = results
.iter()
///遍历 YARA 匹配的结果。
.filter(|x| x.identifier == "GetPhoneTypeStringOffset")
///查找标识符为 "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");
///从指定地址读取 20 字节长的字符串,即手机类型。
///获取数据目录:
let data_dir = if special_data_dir.is_some() {
///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")
///results.iter().filter():从 YARA 结果中获取数据目录地址。
.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")
///String::from_utf8():将匹配到的字节数组转换为 UTF-8 字符串,即数据目录路径。
};
println!("[+] login phone type is {}", phone_type_string);
println!("[+] wechat data dir is {}", data_dir);
///获取微信账号名称:
let align = 2 * std::mem::size_of::<usize>(); // x64 -> 16, x86 -> 8
///计算对齐大小(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<String> = None;
let mut count = 0;
///循环向前查找:在 phone_type_string_addr 之前查找可能的微信账号字符串。
while start >= phone_type_string_addr - align * 20 {
// 名字长度>=16,就会变成指针,不直接存放字符串
let account_name_point_address = read_number::<usize>(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个字符,由字母、数字、下划线组成)
// 微信号是字母、数字、下划线组合,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);
**/*这段代码主要处理从微信进程内存中提取密钥,用于解密其数据库文件(Misc.db)。
这段代码的功能是从内存中寻找并确认获取密钥,用于后续解密微信的数据库文件。
通过读取特定地址的值,再结合 PBKDF2 算法和 HMAC 验证机制,确保获得的密钥是安全且正确的。*/**
const IV_SIZE: usize = 16;
///定义初始化向量(IV)的大小为16字节,这是 AES 加密算法中常用的块大小。
const HMAC_SHA1_SIZE: usize = 20;
///定义 HMAC(Hash-based Message Authentication Code)SHA1 的输出大小为20字节。
const KEY_SIZE: usize = 32;
///定义密钥大小为32字节,这符合 AES-256 的要求。
const AES_BLOCK_SIZE: usize = 16;
///定义 AES 加密时的块大小为16字节。
const SALT_SIZE: usize = 16;
///定义盐的大小为16字节,通常用于生成密钥的过程中,以增加密钥的安全性。
const PAGE_SIZE: usize = 4096;
///定义页面大小为4096字节,这是操作系统常用的内存页面大小。
let db_file_path = data_dir.clone() + "Msg\Misc.db";
///组合数据目录和文件名生成完整的文件路径,指向微信的 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];
///定义一个字节缓冲区,用于临时存储读取的数据库文件内容,大小为一个页面大小(4096字节)。
db_file.read(&mut buf[..]).expect("read Misc.db is failed");
///从打开的文件中读取数据到缓冲区,如果读取失败则抛出错误。
///获取解密密钥:
// key 在微信号前面找
let mut key: Option<String> = None;
///定义一个可选的密钥变量,初始值为 None,该变量将用于存储找到的解密密钥。
let mem_base = phone_type_str_match.base;
///获取手机类型字符串的内存基址(phone_type_str_match 是之前通过 YARA 规则匹配得到的)。
let mut key_point_addr = account_name_addr - align;
///将关键字的查找地址初始化为账户名地址减去对齐值(align),准备向前查找可能的密钥地址。
while key_point_addr >= mem_base {
///进入一个循环,直到关键字地址小于手机类型字符串的基址,确保不越界。
let key_addr = read_number::<usize>(pid, key_point_addr).expect("find key addr failed in memory");
///从内存中读取关键字地址:从当前的 key_point_addr 读取值(可能是密钥的地址),将其存储在 key_addr 变量中。如果读取失败,抛出错误。
if wechat_writeable_private_mem_infos.iter().any(|x| key_addr >= x.base && key_addr <= x.base + x.region_size) {
///检查密钥地址的有效性:检查 key_addr 是否在可写的私有内存范围内,确保该地址是有效的。
let key_bytes = read_bytes(pid, key_addr, KEY_SIZE).expect("find key bytes failed in memory");
///读取密钥字节,从 key_addr 开始读取 KEY_SIZE(32字节)的数据,存储在 key_bytes 中。如果读取失败,抛出错误。
if key_bytes.iter().filter(|&&x| x == 0x00).count() < 5 {
///检查读取到的 key_bytes 中有多少个字节为0,如果不足5个零字节则继续进行验证。 /// 验证密钥的有效性
let start = SALT_SIZE;
let end = PAGE_SIZE;
///定义解密操作的起始位置为盐大小(16字节),结束位置为页面大小(4096字节)。
// 获取到文件开头的 salt
let salt = buf[..SALT_SIZE].to_owned();
///从读取的数据库文件缓冲区中获取盐值,取前面的16个字节。
// salt 异或 0x3a 得到 mac_salt, 用于计算HMAC
///计算 MAC 盐,通过将盐值中的每个字节与0x3A(58十进制,ASCII字符冒号的十六进制)进行异或运算生成。
let mac_salt: Vec<u8> = salt.to_owned().iter().map(|x| x ^ 0x3a).collect();
// 通过 key_bytes 和 salt 迭代64000次解出一个新的 key,用于解密
///使用 PBKDF2-HMAC 方法生成一个新的密钥,以 key_bytes 和 salt 为基础,迭代64000次,以增强密钥的安全性。
let new_key = pbkdf2_hmac_array::<Sha1, KEY_SIZE>(&key_bytes, &salt, 64000);
// 通过 key 和 mac_salt 迭代2次解出 mac_key
let mac_key = pbkdf2_hmac_array::<Sha1, KEY_SIZE>(&new_key, &mac_salt, 2);
///通过新的密钥和 MAC 盐迭代计算出 HMAC 密钥,迭代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
};
///计算哈希检验码的对齐长度,确保其对齐到 AES 块大小的倍数。
// 校验哈希
type HamcSha1 = Hmac<Sha1>;
///定义 HMAC 类型为 SHA-1。
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();
///创建 HMAC 对象并更新数据,包含盐后的数据。最后计算出哈希值。
let hash_mac_start_offset = end - reserve + IV_SIZE;
let hash_mac_end_offset = hash_mac_start_offset + hash_mac.len();
///将计算出的 HMAC 哈希值与缓冲区中相应的哈希值进行比较,如果一致,则认为密钥有效。
if hash_mac == &buf[hash_mac_start_offset..hash_mac_end_offset] {
key = Some(hex::encode(key_bytes));
break;
///如果密钥有效,将其转换为十六进制字符串并存储到 key 变量中,然后跳出循环。
}
}
}
}
///处理未找到密钥的情况,继续向上查找下一个可能的密钥地址。
key_point_addr -= align;
}
///检查是否找到有效的密钥,如果没有,则抛出错误。
if key.is_none() {
panic!("not found key");
}
///创建 WechatInfo 实例,包含提取的进程信息,如 PID、版本号、账户名、手机类型、数据目录和密钥。 /// 如果没有找到密钥,这一行不会被执行,因为之前已经引发了 panic。
WechatInfo {
pid,
version,
account_name,
phone_type: phone_type_string,
data_dir,
key: key.unwrap()
}
}
fn scan_db_files(dir: String) -> Result<Vec<PathBuf>> {
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<Vec<u8>> {
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<Vec<u8>> {
**/*定义了一个名为 decrypt_db_file 的函数,接受两个参数:一个路径(path),指向待解密的数据库文件,
另一个是指向密钥的字符串(pkey)。
函数返回一个结果,内容是一个字节向量(Vec<u8>),并可能返回一个错误(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";
**/*
定义一些常量,用于解密过程: IV_SIZE:AES 初始化向量的大小(16字节)。 HMAC_SHA1_SIZE:HMAC SHA-1 输出的大小(20字节)。 KEY_SIZE:AES-256 密钥的大小(32字节)。 AES_BLOCK_SIZE:AES 加密中块的大小(16字节)。 SQLITE_HEADER:SQLite 文件的文件头标识。 */**
let mut buf = read_file_content(path)?;
//读取指定路径的文件内容并将其存储在字节缓冲区 buf 中。如果读取失败,? 将返回错误。
// 如果开头是 SQLITE_HEADER,说明不需要解密
//检查文件内容的开头是否是 SQLite 的文件头。如果是,则表示文件不需要解密,直接返回原始内容。
if buf.starts_with(SQLITE_HEADER.as_bytes()) {
return Ok(buf);
}
//初始化一个空的字节向量 decrypted_buf,用于存储解密后的内容。
let mut decrypted_buf: Vec<u8> = vec![];
// 获取到文件开头的 salt,用于解密 key
//从读取的文件缓冲区中获取开头的16字节(盐值),并将其存储为 salt。
let salt = buf[..16].to_owned();
// salt 异或 0x3a 得到 mac_salt, 用于计算HMAC
//生成 MAC 盐,通过对盐值的每个字节与0x3A(十六进制)进行异或操作,得到一个新的字节向量 mac_salt。
let mac_salt: Vec<u8> = salt.to_owned().iter().map(|x| x ^ 0x3a).collect();
//进入一个不安全的块,将进行一些不安全的操作,这是因为涉及到原始指针和低级内存操作。
unsafe {
// 通过 pkey 和 salt 迭代64000次解出一个新的 key,用于解密
//将给定的密钥(十六进制编码)解码为字节数组,然后使用 PBKDF2-HMAC 算法与得到的盐值和迭代次数(64000)生成新的密钥。
let pass = hex::decode(pkey)?;
let key = pbkdf2_hmac_array::<Sha1, KEY_SIZE>(&pass, &salt, 64000);
// 通过 key 和 mac_salt 迭代2次解出 mac_key
//通过前面生成的密钥和 MAC 盐,再次使用 PBKDF2 生成 MAC 密钥,仅迭代两次。
let mac_key = pbkdf2_hmac_array::<Sha1, KEY_SIZE>(&key, &mac_salt, 2);
// 开头是 sqlite 头
//将 SQLite 头信息添加到解密后的缓冲区中,并追加一个空字节(0x00)。
decrypted_buf.extend(SQLITE_HEADER.as_bytes());
decrypted_buf.push(0x00);
// hash检验码对齐后长度 48,后面校验哈希用
//计算一个用于哈希校验的保留空间大小,将初始化向量和 HMAC SHA-1 的大小加在一起,如果这个保留空间不是16字节的倍数,则向上对齐到下一个块大小的倍数。
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,分别解密
//定义页面大小为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 {
//如果当前页面是第一页,设置偏移量为16字节(跳过文件头),否则偏移量为0。
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是干啥的,文件大小直接翻倍了
//如果当前页面的所有字节都是0,则将这一页原封不动地添加到解密缓冲区,并终止解密过程。
if buf[start..end].iter().all(|&x| x == 0) {
decrypted_buf.extend(&buf[start..]);
break;
}
// 校验哈希
//定义 HMAC 类型,创建 HMAC 对象并使用 MAC 密钥更新数据,包括当前页面的内容和页面编号。计算出哈希值。
type HamcSha1 = Hmac<Sha1>;
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 解密内容
//使用定义的 AES 解密类型,设置 AES 初始化向量(IV),然后对当前页面的加密内容进行解密,得到解密后的字节,
// 并将其追加到 decrypted_buf 中。注意,此时没有填充,因为消息可能已经根据 AES 的要求进行了填充。
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
let iv = &buf[end-reserve..end-reserve+IV_SIZE];
decrypted_buf.extend(Aes256CbcDec::new(&key.into(), iv.into())
.decrypt_padded_mut::<NoPadding>(&mut buf[start+offset..end-reserve])
.map_err(anyhow::Error::msg)?);
decrypted_buf.extend(&buf[end-reserve..end]);
}
}
//结束不安全块,返回解密后的字节向量 decrypted_buf。
Ok(decrypted_buf)
}
//这一函数的主要功能是从指定的微信进程中提取和解密所有数据库文件,确保输出路径的有效性,并在整个过程中提供用户反馈。
// 对每个文件,函数使用线程并行处理以提高效率,同时借用 Rust 的安全性特性来管理文件系统操作。
//定义了一个名为 dump_all_by_pid 的函数,接受两个参数:
// wechat_info:包含微信进程信息(如数据目录、PID等)的结构体引用。
// output:输出目录的路径。
fn dump_all_by_pid(wechat_info: &WechatInfo, output: &PathBuf) {
//从 wechat_info 中获取数据目录,并添加 “Msg” 子目录,构建消息目录的路径。
let msg_dir = wechat_info.data_dir.clone() + "Msg";
//调用 scan_db_files 函数以扫描找到的数据库文件,并将结果存储在 dbfiles 中(此步使用 unwrap,若发生错误则会导致程序 panic)。
let dbfiles = scan_db_files(msg_dir.clone()).unwrap();
println!("scanned {} files in {}", dbfiles.len(), &msg_dir);
println!("decryption in progress, please wait...");
// 创建输出目录
//检查给定的输出路径是否为文件。如果是文件,程序会触发 panic,提示用户输出路径必须是一个目录。
if output.is_file() {
panic!("the output path must be a directory");
}
//创建一个新的输出目录,具体路径形式为 输出路径wechat_{PID},这里的 {PID} 是当前微信进程的 PID。
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),使用 rayon 库的并行迭代处理(par_iter()),以提升解密速度。
// dbfile 是当前迭代中的数据库文件路径。
dbfiles.par_iter().for_each(|dbfile| {
//初始化两个新的路径对象,分别用于存放数据库文件的目录(db_file_dir)和目标文件路径(dest)。
let mut db_file_dir = PathBuf::new();
let mut dest = PathBuf::new();
db_file_dir.push(&output_dir);
//将当前数据库文件的父目录添加到输出目录路径中,strip_prefix 会去掉消息目录的前缀,保持其相对路径。
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());
//检查计算出的数据库文件目录是否存在,如果不存在则创建该目录。此步也使用 unwrap,若失败则 panic。
if !db_file_dir.exists() {
std::fs::create_dir_all(db_file_dir).unwrap();
}
//调用 decrypt_db_file 函数解密当前数据库文件,并将解密后的内容写入目标路径 dest。若任何一步失败,则 panic。
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.9")
.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> "pid of wechat").value_parser(value_parser!(u32)))
.arg(
arg!(-k --key <KEY> "key for offline decryption of db file")
.value_parser(value_parser!(String)),
)
.arg(arg!(-f --file <PATH> "special a db file path").value_parser(value_parser!(PathBuf)))
.arg(arg!(-d --"data-dir" <PATH> "special wechat data dir path (pid is required)").value_parser(value_parser!(PathBuf)))
.arg(arg!(-o --output <PATH> "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::<PathBuf>("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::<String>("key");
let file_option = matches.get_one::<PathBuf>("file");
let data_dir_option = matches.get_one::<PathBuf>("data-dir");
let pid_option = matches.get_one::<u32>("pid");
** /*
以下代码根据不同的命令行参数组合,处理微信数据的提取和解密操作。它处理了: 1.没有提供 pid、key 和 file 的情况,自动扫描微信进程。 2.提供了 pid 的情况,直接处理该进程的数据。 3.提供了 key 和 file 的情况,解密特定文件或目录中的数据库文件。 4.对于非法的参数组合,程序会报错终止。 */ **
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")
}}
(2)mod.rs
use windows::Win32::{System::{Memory::{MEMORY_BASIC_INFORMATION, PAGE_PROTECTION_FLAGS, PAGE_TYPE, VIRTUAL_ALLOCATION_TYPE, VirtualQueryEx, MEM_MAPPED, MEM_IMAGE}, Threading::{OpenProcess, PROCESS_VM_READ, PROCESS_QUERY_INFORMATION}, ProcessStatus::GetMappedFileNameW}, Foundation::CloseHandle};
//这段代码定义了一个名为 ProcessMemoryInfo 的结构体,
// 并且使用 #[derive(Clone, Debug)] 注解来自动为该结构体实现 Clone 和 Debug trait。
//ProcessMemoryInfo 结构体用于表示一个进程的内存区域的信息,
// 包括基地址、大小、状态、保护标志、类型、文件名(如果有的话)和与之相关的完整内存基本信息。
// 它通过实现 Clone 和 Debug traits 提供了克隆和调试输出的支持,使得在处理和调试时更为方便。
#[derive(Clone, Debug)]
//#[derive(...)]:这是一个属性宏,用于自动为结构体实现指定的 trait。
// Clone:允许对该结构体的实例进行克隆操作,即可以创建一个新的实例与原实例具有相同的数据。
// Debug:允许该结构体的实例使用 {:?} 格式化输出,用于调试目的,方便在控制台打印。
pub struct ProcessMemoryInfo{
//pub:访问修饰符,表示该结构体可以在其他模块中访问。
// struct ProcessMemoryInfo:定义名为 ProcessMemoryInfo 的结构体。
pub base: usize,
//pub base: usize:这是一个公共字段,类型为 usize,存储内存区域的基地址。
pub region_size: usize,
//pub region_size: usize:公共字段,类型为 usize,用于存储内存区域的大小。
pub state: VIRTUAL_ALLOCATION_TYPE,
//pub state: VIRTUAL_ALLOCATION_TYPE:公共字段,
// 类型为 VIRTUAL_ALLOCATION_TYPE,表示内存区域的状态(例如,内存是否被分配、释放等)。
// VIRTUAL_ALLOCATION_TYPE 应该是另一个自定义的类型或枚举,表示不同的内存状态。
pub protect: PAGE_PROTECTION_FLAGS,
//pub protect: PAGE_PROTECTION_FLAGS:公共字段,
// 类型为 PAGE_PROTECTION_FLAGS,表示内存区域的保护标志(例如,读、写、执行权限等)。
// PAGE_PROTECTION_FLAGS 也是一个自定义类型或枚举,定义了不同的保护选项。
pub mtype: PAGE_TYPE,
//pub mtype: PAGE_TYPE:公共字段,
// 类型为 PAGE_TYPE,指明内存区域的类型(例如,代码段、数据段等)。
// 同样,PAGE_TYPE 是一个自定义的类型或枚举,定义了不同的内存类型。
pub filename: Option<String>,
//pub filename: Option<String>:公共字段,
// 类型为 Option<String>,用于存储与内存区域相关联的文件名。
// Option 包裹的是 String 类型,意味着这个文件名可能存在(Some(String))也可能不存在(None)。
pub mbi: MEMORY_BASIC_INFORMATION,
//pub mbi: MEMORY_BASIC_INFORMATION:公共字段,
// 类型为 MEMORY_BASIC_INFORMATION,用以存储完整的内存基本信息。
// 这个字段包含了 MEMORY_BASIC_INFORMATION 结构体的所有信息,以便于后续的访问和操作。
}
//这段代码实现了一个 From trait 的转换,用于将 MEMORY_BASIC_INFORMATION 类型转换为自定义的 ProcessMemoryInfo 类型。
//整个方法的功能是将系统提供的 MEMORY_BASIC_INFORMATION 结构中的数据提取并填充到自定义的 ProcessMemoryInfo 结构中。
// 这种转换使得我们可以更方便地使用 Rust 的类型系统来处理和管理进程内存的信息。通过实现 From trait,
// 允许更自然地在代码中进行类型转换,更提高代码的可读性和简洁性。
// 例如,可以通过 .into() 方法将 MEMORY_BASIC_INFORMATION 转换为 ProcessMemoryInfo。
impl From<MEMORY_BASIC_INFORMATION> for ProcessMemoryInfo {
//实现块:为 ProcessMemoryInfo 提供对 MEMORY_BASIC_INFORMATION 的转换功能。
// From trait:Rust 的标准库中允许将一个类型安全地转换为另一个类型。
// 在这里,From<MEMORY_BASIC_INFORMATION> 说明该实现允许使用 MEMORY_BASIC_INFORMATION
// 类型的值来生成 ProcessMemoryInfo 的实例。
fn from(mbi: MEMORY_BASIC_INFORMATION) -> Self {
//方法名:from。
// 参数:mbi: MEMORY_BASIC_INFORMATION:这是输入参数,代表我们想要转换的内存基本信息。
// 返回值:Self 表示此方法返回一个 ProcessMemoryInfo 类型的实例。
//构造 ProcessMemoryInfo 实例:
// base: mbi.BaseAddress as usize:将 mbi 中的 BaseAddress(内存区域的基地址)转换为 usize 类型,赋值给 base 字段。
// region_size: mbi.RegionSize:将 mbi 中的 RegionSize (内存区域的大小)直接赋值给 region_size 字段。
// state: mbi.State:将 mbi 中的 State(内存区域状态)直接赋值给 state 字段。
// protect: mbi.Protect:将 mbi 中的 Protect(内存区域的保护标志)直接赋值给 protect 字段。
// mtype: mbi.Type:将 mbi 中的 Type(内存区域的类型)直接赋值给 mtype 字段。
// filename: None:初始化 filename 字段为 None,表示该字段在创建时没有文件名信息。
// mbi: mbi:将输入的 mbi 直接赋值给 mbi 字段,确保可以在 ProcessMemoryInfo 中访问完整的 MEMORY_BASIC_INFORMATION 数据。
ProcessMemoryInfo {
base: mbi.BaseAddress as usize,
region_size: mbi.RegionSize,
state: mbi.State,
protect: mbi.Protect,
mtype: mbi.Type,
filename: None,
mbi: mbi,
}
}
}
//这段代码定义了一个名为 get_mem_list 的公共函数,用于获取一个指定进程(根据 PID)内存的基本信息列表,返
// 回一个包含 ProcessMemoryInfo 结构体的向量(Vec<ProcessMemoryInfo>)。
//整个函数的目的是遍历指定 PID 的进程内存,收集所有内存区域的基本信息(类型、基地址、大小等),并尝试获取映射的文件名。
// 最后,它将所有信息收集到一个向量中并返回。该函数使用了 Windows API 来实现对进程内存的查询和访问,
// 同时依赖 Rust 的结构体来组织和返回数据。
pub fn get_mem_list(pid: u32) -> Vec<ProcessMemoryInfo> {
//函数名:get_mem_list。
// 参数:
// pid: u32:目标进程的 PID(进程标识符)。
// 返回值:返回一个包含 ProcessMemoryInfo 结构体的向量,表示所读取的进程内存区域的信息。
//创建一个空的向量 pmis,用于存储进程内存信息结构体(ProcessMemoryInfo)。
let mut pmis = vec![];
unsafe {
//创建一个 MEMORY_BASIC_INFORMATION 结构体的实例 mbi,并将其初始化为默认值。
// 该结构体用于存储有关进程内存区域的信息。
let mut mbi = MEMORY_BASIC_INFORMATION::default();
//调用 Windows API 的 OpenProcess 函数以只读和查询权限打开指定的进程(PID)。
// 使用按位或运算符组合 PROCESS_VM_READ 和 PROCESS_QUERY_INFORMATION 权限。
// 如果调用失败(返回 None),unwrap() 会导致程序 panic 并终止。
let hprocess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid).unwrap();
//初始化一个变量 p,设置为 0x10000(64 KB),表示遍历进程内存时的起始地址。
let mut p: usize = 0x10000;
//使用 VirtualQueryEx 函数查询指定地址 p 的内存信息。传入的参数:
// hprocess:打开的进程句柄。
// Some(p as _):指定要查询的内存地址(强制转换为适当的类型)。
// &mut mbi:用来存储查询结果的内存信息结构体。
// std::mem::size_of::<MEMORY_BASIC_INFORMATION>() as usize:指定读取的大小。
// 如果查询成功(返回值等于 MEMORY_BASIC_INFORMATION 的大小),则进入循环。
while VirtualQueryEx(hprocess, Some(p as _), &mut mbi, std::mem::size_of::<MEMORY_BASIC_INFORMATION>() as usize) == std::mem::size_of::<MEMORY_BASIC_INFORMATION>() {
//使用从 mbi 获取的内存基本信息,创建一个 ProcessMemoryInfo 的实例 pmi,这通常是给定进程中的一块内存区域的描述。
let mut pmi = ProcessMemoryInfo::from(mbi);
//检查当前内存区域的类型,如果是 MEM_IMAGE(可执行映像)或 MEM_MAPPED(映射内存),则执行后续操作。
if mbi.Type == MEM_IMAGE || mbi.Type == MEM_MAPPED {
//初始化一个字符数组 file_name,用于存储从进程内存中获取的文件名(最多 512 个字符)。
let mut file_name = [0u16; 512];
//调用 GetMappedFileNameW 函数,获取当前内存区域对应的文件名,传入的参数:
// hprocess:进程句柄。
// mbi.BaseAddress:内存区域的基地址。
// file_name.as_mut_ptr():用于存储文件名的字符数组指针。
// file_name.len() as u32:字符数组的长度。
// 返回文件名的实际长度。
let file_name_len = GetMappedFileNameW(hprocess, mbi.BaseAddress, file_name.as_mut()) as usize;
//如果文件名长度大于 0,说明成功获取了文件名,将 UFT-16 编码的字符数组转换为 Rust 的 String 类型,
// 并赋值给 pmi.filename。
if file_name_len > 0 {
pmi.filename = Some(String::from_utf16(&file_name[0 .. file_name_len]).unwrap());
}
}
//将当前的 ProcessMemoryInfo 实例 pmi 复制并添加到内存信息列表 pmis 中。
pmis.push(pmi.clone());
//根据当前内存信息中的基地址(base)和区域大小(region_size)更新 p 的值,以移动到下一个内存区域。
p = pmi.base + pmi.region_size;
}
CloseHandle(hprocess).unwrap();
}
//返回收集到的进程内存信息列表 pmis,一个 Vec<ProcessMemoryInfo>。
return pmis;}
四、感受
1、微信也挺有意思,对自己的PC端加密方式、过程被网友逆向公布出来后一直未变,不知为何;
2、Rust这种语言,初看时十分别扭,可以用“丑”来形容,一点都没有pascal的“美雅”;随着慢慢看多了,也能接受了,它的高效确实让特点,因为我是初学小白,也无法用更多的体验来描述;但感觉上确实挺强大的,主打的“安全”也是它的一个特点,看好它。
3、在AI的辅助下,首次对rust程序进行了分析,是个很好的学习过程;在不同的AI分析过程中,我也有了对不同AI的“分析能力”的一些认识,算是一个额外的收获吧。
4、至于里面的逆向过程,因为有了这么多文章都有介绍,我也就没画蛇添足了。
5、这篇文章也算是对国庆假期的一个总结;
6、因为时间短、能力水平也不够,所以理解得不一定对,也不深刻,请勿喷。
原文始发于微信公众号(MicroPest):对微信PC端解密的学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论