微信 macOS 客户端无限多开功能实践

继上一次的 微信 macOS 客户端拦截撤回功能实践 之后,有热心网友给我提了个 issue :macOS微信客户端的多开问题,才发现原来在 macOS 上微信客户端是不能多开的,于是接受挑战~

0x00 传统多开方法

在 macOS 平台上,大部分应用都是支持多开的,比如:

  1. ⌘ + N 大法:适用于 QQ
  2. open -n /Applications/xxx.app 大法:适用于大部分的应用

那么对于微信客户端来说,以上两种方法都是无效的。其中第一种只能新建新的聊天对象),第二种直接什么反应都没有。

但是也没有难倒机智的广大网民,网上流传的 macOS 平台微信客户端多开方法有:

  1. 微信客户端 + 网页客户端(最笨的做法)
  2. open /Applications/WeChat.app/Contents/MacOS/WeChat 直接通过命令行打开微信包里的二进制文件(会弹出一个 Terminal 窗口什么鬼,而且还不能关掉=。=)

0x01 准备工作

  1. 安装各种工具
  2. Dump 出头文件
  3. 通过 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 writer12 值修改为 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