返回到博客
Rust小工具:微信多开
大概4、5年前就在关注和断断续续的学习Rust,大多数从写小工具开始。最近闲暇便整理了一些代码并撰写成文发布。
几年前在数据抓取组时,做过一个微信抓取系统。其中一部分是Windows上微信的Hook和多开,当时是用C++粗糙地完成的。后来自己一直在用Rust做相关重构。这里将「微信多开」这个很小的功能单独领出来,虽然功能就一个,代码很简单,不过也能比较好地展示如何用Rust开展一个小项目。
Windows 微信多开#
微信限制多开其实就是进程的单例。一般来地,可以通过判断Mutex
、Event
、File
等是否已经存在的方式来实现。
我们只需要找到其如何判断的,然后有两种方式来完成多开的实现:
- Hook新进程跳过判断
- 打开已存在进程删除这个标识
下面我们先找到单开的标识。
- 在Windows系统上打开一个微信
- 使用 Process Explorer 打开微信进程(WeChat.exe)
- 翻阅所有的
Mutant
类型的句柄,找到_WeChat_App_Instance_Identity_Mutex_Name
这个_WeChat_App_Instance_Identity_Mutex_Name
就是我们要找的单例标识,其完整名称是
1\Sessions\1\BaseNamedObjects\_WeChat_App_Instance_Identity_Mutex_Name
至于如何确认的过程这里就不赘述。下面我们开始用代码来实现一下的步骤:
- 打开微信启动程序
- 检查进程在进程
- 打开所有进程,关闭
Mutex_Name
标识 - 启动微信
如上所述,流程很简单。
初始化 Rust 项目#
通过 cargo
工具创建一个二进制项目,
1cargo new multi-wechat-rs
我们需要使用到Windows的API操作,有2个主流的库(crate)可供选择,
其中windows是编译时生成相关代码,且其API接口更加Rust。不过这次我们选择 winapi。
添加相关以来后,完整的Cargo.toml
内容如下,
1[package]2name = "multi-wechat-rs"3description = "一个完全由Rust实现的微信多开工具。"4license = "Apache-2.0"5version = "0.1.0"6edition = "2018"7
8[dependencies]9ntapi = "0.3.6"10
11[dependencies.winapi]12version = "0.3.9"13features = []
其中dependencies.winapi.features
为边开发时边添加上来的。
然后我们在main.rs
中添加主要的逻辑,
- 查找微信进程
- 关闭句柄
- 启动微信
代码如下
1fn main() {2 println!("Hello, WeChat & Rust!");3
4 // get the wechat process5 match process::Process::find_first_by_name("WeChat.exe") {6 None => {},7 Some(p) => {8 // get handles of those process9 let mutants = system::get_system_handles(p.pid()).unwrap()10 .iter()11 // match which one is mutex handle12 .filter(|x| x.type_name == "Mutant" && x.name.contains("_WeChat_App_Instance_Identity_Mutex_Name"))13 .cloned()14 .collect::<Vec<_>>();15
16 for m in mutants {17 // and close the handle18 println!("clean mutant: {}", m.name);19 let _ = m.close_handle();20 }21 }22 }23
24 // get wechat start exe location25 let wechat_key = "Software\\Tencent\\WeChat";26 match utils::get_install_path(wechat_key) {27 Some(p) => {28 // start wehat process29 // WeChat.exe30 println!("start wechat process => {}", p);31 let exe = format!("{}\\WeChat.exe", p);32 if utils::create_process(exe.as_str(), "").is_err() {33 println!("Error: {}", utils::get_last_error());34 }35 },36 None => {37 println!("get wechat install failed, you can still open multi wechat");38 utils::show_message_box("已关闭多开限制", "无法自动启动微信,仍可手动打开微信。");39 }40 }41}
实现「查找进程」#
新建一个文件 process.rs
作为包(mod
)。
定义进程结构体
1#[derive(Debug, Clone, PartialEq, Eq)]2pub struct Process {3 pid: u32,4 name: String,5 handle: HANDLE,6}
再给Process
添加主要的实例化方法
1impl Process {2 pub fn from_pid(pid: u32) -> Option<Self> {3 // open process by pid, bacause we need to write message4 // so for simple just open as all access flag5 let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) };6 if handle.is_null() {7 return None;8 }9 10 let name = get_process_name(handle);11 Some(Self::new(handle, pid, name.as_str()))12 }13 14 pub fn find_first_by_name(name: &str) -> Option<Self> {15 match find_process_by_name(name).unwrap_or_default().first() {16 // TODO: ugly, shoudl implement copy trait for process17 Some(v) => Process::from_pid(v.pid),18 None => None19 }20 }21}
接下来实现进程查找函数find_process_by_name
的主要功能:
- 创建快照
- 遍历进程
- 匹配进程名
代码如下
1pub fn find_process_by_name(name: &str) -> Result<Vec<Process>, io::Error> {2 let handle = unsafe {3 CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0 as _)4 };5
6 if handle.is_null() || handle == INVALID_HANDLE_VALUE {7 return Err(get_last_error());8 }9 10 // the result to store process list11 let mut result: Vec<Process> = Vec::new();12 let mut _name: String;13
14 // can't reuse15 let mut entry: PROCESSENTRY32 = unsafe { ::std::mem::zeroed() };16 entry.dwSize = mem::size_of::<PROCESSENTRY32>() as u32;17
18 while 0 != unsafe { Process32Next(handle, &mut entry) } {19 // extract name from entry20 _name = char_to_string(&entry.szExeFile);21 // clean entry exefile filed22 entry.szExeFile = unsafe { ::std::mem::zeroed() };23
24 if name.len() > 0 && !_name.contains(name) {25 // ignore if name has set but not match the exefile name26 continue;27 }28
29 // parse process and push to result vec30 // TODO: improve reuse the name and other information31 match Process::from_pid_and_name(entry.th32ProcessID, _name.as_str()) {32 Some(v) => result.push(v),33 None => {},34 }35 }36
37 // make sure the new process first38 result.reverse();39 Ok(result)40}
添加单元测试
1#[cfg(test)]2mod tests {3 use super::*;4 5 #[test]6 fn get_process() {7 println!("get process:");8 match find_process_by_name("Code.exe") {9 Ok(v) => {10 println!("get process count: {}", v.len());11 for x in &v {12 println!("{} {}", x.pid, x.name);13 }14 },15 Err(e) => eprintln!("find process by name error: {}", e)16 }17 }18}
实现「查找句柄」#
定义Handle
结构体,并定义一个的初始化函数
1#[derive(Debug, Clone, PartialEq, Eq)]2pub struct Handle {3 pub pid: u32,4 pub handle: HANDLE,5 pub type_index: u32,6 pub type_name: String,7 pub name: String,8}9
10impl Handle {11 pub fn new(handle: HANDLE, pid: u32, type_index: u32, type_name: String, name: String) -> Self {12 Self{handle, pid, type_index, type_name, name}13 }14}
实现句柄的关闭方法
1impl Handle {2 pub fn close_handle(&self) -> Result<(), io::Error> {3 // open process again4 let process = unsafe{OpenProcess(PROCESS_ALL_ACCESS, FALSE, self.pid as _)};5 if process.is_null() {6 return Err(Error::new(ErrorKind::NotFound, "pid"));7 }8 // duplicate handle to close handle9 let mut nhe: HANDLE = null_mut();10 let r = unsafe{11 DuplicateHandle(12 process, self.handle as _, GetCurrentProcess(),13 &mut nhe, 0, FALSE, DUPLICATE_CLOSE_SOURCE)};14 if r == FALSE {15 println!("duplicate handle to close failed");16 return Err(get_last_error());17 }18 Ok(())19 }20}
就下来还是一个获取句柄的函数get_system_handles
未实现。由于篇幅限制,这里仅给出函数前面,具体实现可以查看项目代码 multi-wechat-rs。
1// TODO: add filter function2pub fn get_system_handles(pid: u32) -> Result<Vec<Handle>, io::Error>{3}
打包发布#
为了二进制的美观,给其增加一个图标
这个需要使用 winres 库来支持,我们在Cargo.toml
中添加依赖,
1[target.'cfg(windows)'.build-dependencies]2winres = "0.1.12"
然后在项目根目录下新建一个build.rs
文件,用于编译时打包图标资源,内容如下,
1extern crate winres;2
3fn main() {4 if cfg!(target_os = "windows") {5 let mut res = winres::WindowsResource::new();6 res.set_icon("wechat-rs.ico");7 res.compile().unwrap();8 }9}
编译出二进制文件
1cargo build --release
希望通过 cargo install
来安装这个小工具,我们可以通过以下命令将包发布至仓库
1cargo publish
完整的代码实现可以查看项目仓库multi-wechat-rs。需要使用的可以去仓库下载编译好的可执行文件。
总结#
通过这一个很小的工具的实现,对于Rust有以下几点体验,
cargo
工具很强大,以至于都想去给Go实现,而且名字就可搭配build.rs
编译时好用,减少很多模板代码- 宏好用,减少很多重复的代码
- 返回值和错误处理对函数签名设计有一定要求
- 产物的二进制小,这点很喜欢
不过,还有很多没有涉及到的点,这些在以后的小工具项目中会陆续涉及,如,
- 生命周期
- 定义宏
- 堆上变量