Zoe
Zoe
返回博客

Rust小工具:微信多开

9 分钟阅读
Rust小工具微信HookWindows

大概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 就是我们要找的单例标识,其完整名称是

\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>{
}

打包发布

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

icon

这个需要使用 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

Zoe

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

https://zoe.im

评论