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 就是我们要找的单例标识,其完整名称是
\Sessions\1\BaseNamedObjects\_WeChat_App_Instance_Identity_Mutex_Name
至于如何确认的过程这里就不赘述。下面我们开始用代码来实现一下的步骤:
- 打开微信启动程序
- 检查进程在进程
- 打开所有进程,关闭
Mutex_Name标识 - 启动微信
如上所述,流程很简单。
初始化 Rust 项目
通过 cargo 工具创建一个二进制项目,
cargo new multi-wechat-rs
我们需要使用到Windows的API操作,有2个主流的库(crate)可供选择,
其中windows是编译时生成相关代码,且其API接口更加Rust。不过这次我们选择 winapi。
添加相关以来后,完整的Cargo.toml内容如下,
[package]
name = "multi-wechat-rs"
description = "一个完全由Rust实现的微信多开工具。"
license = "Apache-2.0"
version = "0.1.0"
edition = "2018"
[dependencies]
ntapi = "0.3.6"
[dependencies.winapi]
version = "0.3.9"
features = []
其中dependencies.winapi.features为边开发时边添加上来的。
然后我们在main.rs中添加主要的逻辑,
- 查找微信进程
- 关闭句柄
- 启动微信
代码如下
fn main() {
println!("Hello, WeChat & Rust!");
// get the wechat process
match process::Process::find_first_by_name("WeChat.exe") {
None => {},
Some(p) => {
// get handles of those process
let mutants = system::get_system_handles(p.pid()).unwrap()
.iter()
// match which one is mutex handle
.filter(|x| x.type_name == "Mutant" && x.name.contains("_WeChat_App_Instance_Identity_Mutex_Name"))
.cloned()
.collect::<Vec<_>>();
for m in mutants {
// and close the handle
println!("clean mutant: {}", m.name);
let _ = m.close_handle();
}
}
}
// get wechat start exe location
let wechat_key = "Software\\Tencent\\WeChat";
match utils::get_install_path(wechat_key) {
Some(p) => {
// start wehat process
// WeChat.exe
println!("start wechat process => {}", p);
let exe = format!("{}\\WeChat.exe", p);
if utils::create_process(exe.as_str(), "").is_err() {
println!("Error: {}", utils::get_last_error());
}
},
None => {
println!("get wechat install failed, you can still open multi wechat");
utils::show_message_box("已关闭多开限制", "无法自动启动微信,仍可手动打开微信。");
}
}
}
实现「查找进程」
新建一个文件 process.rs 作为包(mod)。
定义进程结构体
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Process {
pid: u32,
name: String,
handle: HANDLE,
}
再给Process添加主要的实例化方法
impl Process {
pub fn from_pid(pid: u32) -> Option<Self> {
// open process by pid, bacause we need to write message
// so for simple just open as all access flag
let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) };
if handle.is_null() {
return None;
}
let name = get_process_name(handle);
Some(Self::new(handle, pid, name.as_str()))
}
pub fn find_first_by_name(name: &str) -> Option<Self> {
match find_process_by_name(name).unwrap_or_default().first() {
// TODO: ugly, shoudl implement copy trait for process
Some(v) => Process::from_pid(v.pid),
None => None
}
}
}
接下来实现进程查找函数find_process_by_name的主要功能:
- 创建快照
- 遍历进程
- 匹配进程名
代码如下
pub fn find_process_by_name(name: &str) -> Result<Vec<Process>, io::Error> {
let handle = unsafe {
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0 as _)
};
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return Err(get_last_error());
}
// the result to store process list
let mut result: Vec<Process> = Vec::new();
let mut _name: String;
// can't reuse
let mut entry: PROCESSENTRY32 = unsafe { ::std::mem::zeroed() };
entry.dwSize = mem::size_of::<PROCESSENTRY32>() as u32;
while 0 != unsafe { Process32Next(handle, &mut entry) } {
// extract name from entry
_name = char_to_string(&entry.szExeFile);
// clean entry exefile filed
entry.szExeFile = unsafe { ::std::mem::zeroed() };
if name.len() > 0 && !_name.contains(name) {
// ignore if name has set but not match the exefile name
continue;
}
// parse process and push to result vec
// TODO: improve reuse the name and other information
match Process::from_pid_and_name(entry.th32ProcessID, _name.as_str()) {
Some(v) => result.push(v),
None => {},
}
}
// make sure the new process first
result.reverse();
Ok(result)
}
添加单元测试
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_process() {
println!("get process:");
match find_process_by_name("Code.exe") {
Ok(v) => {
println!("get process count: {}", v.len());
for x in &v {
println!("{} {}", x.pid, x.name);
}
},
Err(e) => eprintln!("find process by name error: {}", e)
}
}
}
实现「查找句柄」
定义Handle结构体,并定义一个的初始化函数
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
pub pid: u32,
pub handle: HANDLE,
pub type_index: u32,
pub type_name: String,
pub name: String,
}
impl Handle {
pub fn new(handle: HANDLE, pid: u32, type_index: u32, type_name: String, name: String) -> Self {
Self{handle, pid, type_index, type_name, name}
}
}
实现句柄的关闭方法
impl Handle {
pub fn close_handle(&self) -> Result<(), io::Error> {
// open process again
let process = unsafe{OpenProcess(PROCESS_ALL_ACCESS, FALSE, self.pid as _)};
if process.is_null() {
return Err(Error::new(ErrorKind::NotFound, "pid"));
}
// duplicate handle to close handle
let mut nhe: HANDLE = null_mut();
let r = unsafe{
DuplicateHandle(
process, self.handle as _, GetCurrentProcess(),
&mut nhe, 0, FALSE, DUPLICATE_CLOSE_SOURCE)};
if r == FALSE {
println!("duplicate handle to close failed");
return Err(get_last_error());
}
Ok(())
}
}
就下来还是一个获取句柄的函数get_system_handles未实现。由于篇幅限制,这里仅给出函数前面,具体实现可以查看项目代码 multi-wechat-rs。
// TODO: add filter function
pub fn get_system_handles(pid: u32) -> Result<Vec<Handle>, io::Error>{
}
打包发布
为了二进制的美观,给其增加一个图标
这个需要使用 winres 库来支持,我们在Cargo.toml中添加依赖,
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"
然后在项目根目录下新建一个build.rs文件,用于编译时打包图标资源,内容如下,
extern crate winres;
fn main() {
if cfg!(target_os = "windows") {
let mut res = winres::WindowsResource::new();
res.set_icon("wechat-rs.ico");
res.compile().unwrap();
}
}
编译出二进制文件
cargo build --release
希望通过 cargo install 来安装这个小工具,我们可以通过以下命令将包发布至仓库
cargo publish
完整的代码实现可以查看项目仓库multi-wechat-rs。需要使用的可以去仓库下载编译好的可执行文件。
总结
通过这一个很小的工具的实现,对于Rust有以下几点体验,
cargo工具很强大,以至于都想去给Go实现,而且名字就可搭配build.rs编译时好用,减少很多模板代码- 宏好用,减少很多重复的代码
- 返回值和错误处理对函数签名设计有一定要求
- 产物的二进制小,这点很喜欢
不过,还有很多没有涉及到的点,这些在以后的小工具项目中会陆续涉及,如,
- 生命周期
- 定义宏
- 堆上变量

Zoe
全栈开发 · AI 工具制造者 · Go / Flutter / Rust · 开源偏执狂
