Zoe

Zoe

beta
实验项目

返回到博客

Rust小工具:微信多开

Zoe

Zoe

2021-07-11

1 MINS

大概4、5年前就在关注和断断续续的学习Rust,大多数从写小工具开始。最近闲暇便整理了一些代码并撰写成文发布。

几年前在数据抓取组时,做过一个微信抓取系统。其中一部分是Windows上微信的Hook和多开,当时是用C++粗糙地完成的。后来自己一直在用Rust做相关重构。这里将「微信多开」这个很小的功能单独领出来,虽然功能就一个,代码很简单,不过也能比较好地展示如何用Rust开展一个小项目。

Windows 微信多开#

微信限制多开其实就是进程的单例。一般来地,可以通过判断MutexEventFile等是否已经存在的方式来实现。

我们只需要找到其如何判断的,然后有两种方式来完成多开的实现:

  • Hook新进程跳过判断
  • 打开已存在进程删除这个标识

下面我们先找到单开的标识。

  1. 在Windows系统上打开一个微信
  2. 使用 Process Explorer 打开微信进程(WeChat.exe)
  3. 翻阅所有的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 process
5 match process::Process::find_first_by_name("WeChat.exe") {
6 None => {},
7 Some(p) => {
8 // get handles of those process
9 let mutants = system::get_system_handles(p.pid()).unwrap()
10 .iter()
11 // match which one is mutex handle
12 .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 handle
18 println!("clean mutant: {}", m.name);
19 let _ = m.close_handle();
20 }
21 }
22 }
23
24 // get wechat start exe location
25 let wechat_key = "Software\\Tencent\\WeChat";
26 match utils::get_install_path(wechat_key) {
27 Some(p) => {
28 // start wehat process
29 // WeChat.exe
30 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 message
4 // so for simple just open as all access flag
5 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 process
17 Some(v) => Process::from_pid(v.pid),
18 None => None
19 }
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 list
11 let mut result: Vec<Process> = Vec::new();
12 let mut _name: String;
13
14 // can't reuse
15 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 entry
20 _name = char_to_string(&entry.szExeFile);
21 // clean entry exefile filed
22 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 name
26 continue;
27 }
28
29 // parse process and push to result vec
30 // TODO: improve reuse the name and other information
31 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 first
38 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 again
4 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 handle
9 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 function
2pub fn get_system_handles(pid: u32) -> Result<Vec<Handle>, io::Error>{
3}

打包发布#

为了二进制的美观,给其增加一个图标

icon

这个需要使用 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编译时好用,减少很多模板代码
  • 宏好用,减少很多重复的代码
  • 返回值和错误处理对函数签名设计有一定要求
  • 产物的二进制小,这点很喜欢

不过,还有很多没有涉及到的点,这些在以后的小工具项目中会陆续涉及,如,

  • 生命周期
  • 定义宏
  • 堆上变量

Rust小工具:窗口管理
我的博客起源

Zoe

Zoe

beta

我将成为我的墓志铭

站点

© 2011 - 2022 Zoe - All rights reserved.

Made with by in Hangzhou.