微信PC端多开的秘密
微信电脑端也能多开
昨天,偶然从好朋友小林处得知,他的电脑居然可以同时上两个微信号。
手机端多开微信我知道,像华为、小米等手机系统都对此做了支持,不过在运行Windows系统的电脑上怎么启动两个微信呢,这倒是一下引起了我的好奇。
小林告诉我他是这样做的,写了一个批处理:
start D:WeChatWeChat.exe
start D:WeChatWeChat.exe
然后直接双击批处理文件,就能启动两个微信进程。
我试了一下,果然如此!
随后我又加了一行,竟然还能启动3个:
接着我在网络上搜了一下,原来这一招早就被人用过了,看来是我火星了。不过到底为什么用这种方式就能多开,我倒是很想知道这个迷底。
TIPS:如果对技术分析部分不感兴趣,可以跳过直接来到后面的真相部分。
微信的单例模式
正常情况下,直接手动双击微信图标启动,后面启动的进程会进行全局单例模式检查,如果发现已经存在微信进程,就会直接把对应进程的微信窗口激活,定位到桌面最前面,随后自己退出。
但为什么用上面的方式就能启动俩呢?我们来一探究竟。
首先,分析一下上面描述的微信单个实例是如何实现的。
做过Windows平台应用程序开发的朋友可能对此比较熟悉,一般是进程启动后创建一个全局唯一名字的互斥体,创建成功则正常启动,创建失败则判断一下是否这个互斥体已经存在。如果已经存在则说明已经有对应程序之前启动。
带着这种猜想,用工具procexp查看一下微信进程打开的所有内核对象,并找到互斥体部分:
果然,这其中有一个名字叫_WeChat_App_Instance_Identity_Mutex_Name的互斥体,从这个名字可以猜出,这个跟微信的单例模式绝对有关系。
接着,启动神器APIMonitor,它可以帮你监控指定进程的API调用情况,勾选上CreateMutex和GetLastError这两个Windows API函数。在已经有微信在运行的情况下,用这个工具再启动一个微信进程,看一下函数调用情况:
可以看到,创建这个名字的互斥体后,随后又调用了GetLastError函数,并返回了0x000000b7,查看手册,其含义:
表示已经存在。
来看一下,这个CreateMutex调用的堆栈,看看是哪个地方的代码在创建这个全局互斥体:
从堆栈看出,调用来自于微信目录下的一个动态库WeChatWin.dll。具体位置在偏移0x8e271b处的前一条指令。
接下来就要祭出神器中的神器,大名鼎鼎的反汇编软件IDA,这家伙支持x86、x64、ARM、MIPS等多种处理器架构和Windows、Linux、Android、MacOS、JVM等多种系统平台的程序分析。
用IDA打开这个WeChatWin.dll文件,并定位到偏移0x8e271b处:
如上图所示,创建互斥体的动作,发生在函数sub_108e26d0。
上层是sub_108e2660函数在调用它:
上面这张图反映了创建互斥体后的判断逻辑:
- 如果sub_108e26d0的返回值为0,表示没有错误,当前函数也直接返回0。
- 如果sub_108e26d0的返回值不为0,表示出现了错误,则依次判断WeChatMainWndForPC和WeChatLoginWndForPC两个窗口是否存在,如果存在则使用BringWindowToTop函数将其置顶弹出。这两个窗口分别代表的是微信的主界面窗口和登陆界面窗口,如果一个微信实例已经存在,则势必处于这两种状态之一。
问题就出在上面这个判断中,汇编代码看起来有点辣眼睛,咱们F5来还原一下C代码(还原效果只能凑合看,能看清楚逻辑就行):
上面图片的注解已经说明了,函数sub_108e2660的返回值将决定是否启动微信实例进程,还是直接退出。
真相只有一个
事情到这里就真相大白了,来总结一下。
微信判断是否启动的2个条件:
- 如果能成功创建互斥体对象,则启动微信
- 如果不能创建互斥体:
- 如果找到对应窗口,则置顶之,自己退出
- 如果没有找到,则启动微信
用伪代码来表示一下:
if (CreateMutex() == SUCCESS) {
启动微信
} else {
if (FindWindow() == SUCCESS) {
将已有窗口置顶
} else {
启动微信
}
}
而直接使用脚本启动的多个进程,虽然操作系统内核层面保证了互斥体的唯一,但由于启动速度相差不大,相应的窗口还没有来得及创建出来,导致走入上面的第二个启动逻辑,从而可以启动多个实例。
小发现
在分析的过程中,发现了一个有趣的事情:
在WeChatWin.dll中,上面的创建互斥体再上一级函数名字叫StartWaChat,也是作为导出函数被该DLL导出:
这里不知道是故意还是不小心把微信的WeChat
写成了WaChat
,如果是笔误,这位程序员同学看到了赶紧偷偷去改一下吧。
- 40个重要的HTML 5面试问题及答案
- js调用原生API--陀螺仪和加速器
- OpenDaylight开发-DataStoreChange监听器三种类型
- express模拟接口
- spring boot开发的日志系统
- elasticsearch 5.0.1安装analysis-ik分词器
- Spring Cloud中Feign如何统一设置验证token
- laravel+react+webpack+babel+gulp的配置
- OpenvSwitch系列之浅析main函数
- 没有公式如何看懂EM算法?
- Google用来处理海量文本去重的simhash算法原理及实现
- Open vSwitch系列之openflow版本兼容
- R预设配色系统及自定义色板
- SDN实战团分享(十二):Service Function Chain
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- windows mysql 8.0 ERROR 1045 (28000): Access denied for user 'ODBC'@'localhost' (using password: NO)
- 树莓派基础实验9:蜂鸣器实验
- 浅谈文件上传漏洞(客户端JS检测绕过)
- java字符串操作:如何实现字符串的反转及替换?
- 树莓派基础实验10:干簧管传感器实验
- Java 字节流 字符流 io流
- 写一个函数,要求输入一个字符串和一个字符长度,对该字符串进行分 隔。
- 树莓派基础实验11:U型光电传感器实验
- java 缓冲流+原理图解
- 写一个函数,2 个参数,1 个字符串,1 个字节数,返回截取的字符串,要 求字符串中的中文不能出现乱码
- Java 编程,打印昨天的当前时刻.
- git merge后 丢失文件 以及 代码
- 树莓派基础实验12:PCF8591模数转换器实验
- spring boot多数据源的代码实现
- 树莓派基础实验13:雨滴探测传感器实验