微信 macOS 客户端无限多开功能实践
继上一次的 微信 macOS 客户端拦截撤回功能实践 之后,有热心网友给我提了个 issue :macOS微信客户端的多开问题,才发现原来在 macOS 上微信客户端是不能多开的,于是接受挑战~
0x00 传统多开方法
在 macOS 平台上,大部分应用都是支持多开的,比如:
⌘ + N
大法:适用于 QQopen -n /Applications/xxx.app
大法:适用于大部分的应用
那么对于微信客户端来说,以上两种方法都是无效的。其中第一种只能新建新的聊天对象),第二种直接什么反应都没有。
但是也没有难倒机智的广大网民,网上流传的 macOS 平台微信客户端多开方法有:
- 微信客户端 + 网页客户端(最笨的做法)
open /Applications/WeChat.app/Contents/MacOS/WeChat
直接通过命令行打开微信包里的二进制文件(会弹出一个 Terminal 窗口什么鬼,而且还不能关掉=。=)
0x01 准备工作
- 安装各种工具
- Dump 出头文件
- 通过 Hopper Disassembler 导出静态分析文件
- …
这里就不再赘述了,参考之前的实践文。
0x02 找出入口
由上文所说到的网传多开方法得知是通过 open
命令直接打开二进制文件可以实现双开,那么为什么打开第三个就不行呢,执行命令打开第三个微信客户端:
可以看到,命令行运行结果提示 Instance is already running!
,既然有这样的提示,那么就可以作为寻找入口的线索,因为一般这种 Log 都是 Hardcode 在代码里的,于是我们又可以祭出神器: Hopper Disassembler!
通过 Hopper Disassembler 一下子就能定位出该字符串所在的方法:
居然在 EntryPoint()
中,也就是应用的 main()
方法,不过这不是重点,重点可以看出判断客户端是否多开的方法在 if
语句中:if ([CUtility HasWechatInstance] != 0x0) {...}
,因此可以更近一步看看 [CUtility HasWechatInstance]
方法的内部实现:
Hopper Disassembler 解析出的伪代码已经接近源码,阅读难度大大降低。同时可以发现微信客户端是通过读取应用 BundleIdentifier
的对应实例个数来判断应用是否多开,从 if (r12 >= 0x2) {...}
中得知最多可以存在两个实例,这就是为什么上文中通过 open
命令可以双开的原因吧,但是为什么不能通过 open -n
直接打开呢,还没弄清楚。
因此,猜想是通过修改 [CUtility HasWechatInstance]
的返回值来绕过多开检测。
0x03 验证猜想
验证猜想的主要方法是通过动态调试,那么又可以祭出一神器:LLDB!
使用 LLDB 进行调试前我们需要找出断点地址:
上文中提及的 r12
变量即为实例数量变量,因此我们可以在 mov r12, rax
上做手脚,即断点地址为 0x0000000100511644
。由于该方法在 main()
方法中,因此不能通过 attach process
方法来进行动态调试。于是利用
LLDB 创建 Target 并预先设置好断点,通过 LLDB process launch
来启动应用并触发断点:
执行 ni
,并执行 p $r12
查看值:
因为没有打开客户端,因此实例数量值为0,于是可以通过 LLDB 的 register write
将 r12
值修改为 2,并执行 c
让应用跳出断点继续运行:
果然出现提示 Instance is already running!
,然而并没有任何微信客户端实例正在运行,因此可以得出结论猜想是正确的!
0x04 编写 Tweak
同样的使用 constructor
来进行 Tweak。
constructor / destructor
顾名思义,构造器和析构器,加上这两个属性的函数会在分别在可执行文件(或 shared library)load 和 unload 时被调用,可以理解为在 main() 函数调用前和 return 后执行。
参考资料: Clang Attributes 黑魔法小记
__attribute__((constructor(102))) static void multipleInstanceTweak(void) {
Class class = object_getClass(NSClassFromString(@"CUtility"));
SEL selector = NSSelectorFromString(@"HasWechatInstance");
Method method = class_getInstanceMethod(class, selector);
IMP imp = imp_implementationWithBlock(^(id self) {
return 0; //永远返回0
});
class_replaceMethod(class, selector, imp, method_getTypeEncoding(method));
}
以上,通过方法替换,使 [CUtility HasWechatInstance]
永远返回 0,通过 open -n
来打开多个微信便可以实现微信 macOS 客户端无限多开。
0x05 快捷方式多开
通过命令行来多开会不会有点麻烦?通过 Tweak 来添加快捷的多开方式?
快捷多开的位置最终选择了在 Dock Menu 的位置,既不影响应用原有的布局也方便多开的操作。
根据文档中给出的添加 Dock Menu 的方法是 - (NSMenu *)applicationDockMenu:(NSApplication *)sender;
,然而通过 Hopper Disassembler 分析出来并没有这个方法,因为客户端没有实现这个方法。于是又可以利用 Objective-C 的动态特性,动态添加方法。
利用 class_addMethod
添加方法:
@implementation NSObject (WeChatTweak)
static void __attribute__((constructor)) tweak(void) {
class_addMethod(objc_getClass("AppDelegate"), @selector(applicationDockMenu:), method_getImplementation(class_getInstanceMethod(objc_getClass("AppDelegate"), @selector(applicationDockMenu:))), "@:@");
}
+ (BOOL)tweak_HasWechatInstance {
return NO;
}
- (NSMenu *)applicationDockMenu:(NSApplication *)sender {
NSMenu *menu = [[objc_getClass("NSMenu") alloc] init];
NSMenuItem *menuItem = [[objc_getClass("NSMenuItem") alloc] initWithTitle:@"登录新的微信账号" action:@selector(openNewWeChatInstace:) keyEquivalent:@""];
[menu insertItem:menuItem atIndex:0];
return menu;
}
- (void)openNewWeChatInstace:(id)sender {
NSString *applicationPath = [[objc_getClass("NSBundle") mainBundle] bundlePath];
NSTask *task = [[objc_getClass("NSTask") alloc] init];
task.launchPath = @"/usr/bin/open";
task.arguments = @[@"-n", applicationPath];
[task launch];
}
@end
最终效果:
0x06 最后
PS:以上教程代码为原始代码,最终版代码以 GitHub 上的为准,其实就是美化了一下 (:」∠)_
项目源码: WeChatTweak-macOS