震惊!防止 Mac 微信撤回消息竟然只要两行代码


#1

看了 http://iosre.com/t/mac/7014/6 一贴之后,消息防撤回才是更具备痛点的需求。

没有参考前人(后来才发现 Google 一下可以省掉我一些逆向的时间)的成果,直接 hopper 在 WeChat 可执行文件里找 recall,仅仅找到了 -[MessageService RecallMsgWithChatName:msgData:erroHandler:]:,是触发撤回的动作。然后继续看这个 MessageService 类,找到了另一个方法 -[MessageService onRevokeMsg:]:

妹的,怎么又变成 revoke 了。代码就不全贴了:

void -[MessageService onRevokeMsg:](void * self, void * _cmd, void * arg2) {
    r12 = self;
    r15 = [arg2 retain];
    if ([r15 length] == 0x0) goto loc_1007cfbae;
    goto loc_1007cf72c;

loc_1007cfbae:
    rax = [r15 release];
    return;

loc_1007cf72c:
    r14 = @selector(alloc);
    rcx = [NSString alloc];
    r13 = @selector(initWithFormat:);
    rbx = [rcx initWithFormat:@"INFO: Revoke msg: %@", r15];
    [MMLogger logWithMMLogLevel:0x4 module:"Message" file:"MessageService+Internal.mm" line:0x18b func:"-[MessageService(Internal) onRevokeMsg:]" message:rbx];
    [rbx release];
    sub_1008833c4(var_48, 0x1);
    var_50 = r12;
    rbx = objc_retainAutorelease(r15);
    r12 = [rbx UTF8String];
    rax = [rbx lengthOfBytesUsingEncoding:0x4];
    sub_100883ae6(var_48, r12, LODWORD(rax));
    rax = loc_100883d04(var_48, r12);
    if (rax == 0x0) goto loc_1007cf9e5;
    goto loc_1007cf813;

loc_1007cf9e5:
    rcx = [NSString alloc];
    rbx = [rcx initWithFormat:rdx];
    [MMLogger logWithMMLogLevel:0x2 module:"Message" file:"MessageService+Internal.mm" line:0x192 func:"-[MessageService(Internal) onRevokeMsg:]" message:rbx];

这个方法就是微信在从服务器上获得一个撤回动作的时候执行的。经过分析这个方法完全可以安全地屏蔽掉。

下面来折腾 hook。

接下来的 hook 就是老生常谈了,完全可以写成一个 dylib 插件打包到应用可执行文件里。

为了偷懒,下面用 frida.re 做原型。这也是为什么我们的代码这么短的原因……

frida 是一个类似 cycript 的工具,可以用脚本执行一些 hook 逻辑的编写,除 iOS 之外还支持 Android、Windows 和 Linux,语法完全基于 javascript。具体可以参考 frida 的文档:

Mac 上自带 python,当然 brew 装一个更好用。默认读者机器有 python 环境。用包管理 pypi 安装 frida,一条命令就搞定了:[sudo] pip install frida

这样就多了 frida, frida-ps 等命令,具体的用法还是继续参考官方文档啦。

然后就是使用 frida 附加到进程。需要注意的是,在较新的 OS X 上需要关闭 SIP。frida WeChat 附加到微信的进程,这时候我们可以看到一个控制台:

➜  GitHub frida WeChat
     ____
    / _  |   Frida 8.0.0 - A world-class dynamic instrumentation framework
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/

[Local::ProcName::WeChat]->

在这个控制台里可以输入任意的 javascript 并获得输出结果,类似 cycript 的操作。做函数 hook 的时候我们需要用到 Interceptor

Interceptor 提供了两个 hook 方法:attach 和 replace。这两者的区别是,attach 会在函数进入之前和返回之后增加两个回调函数,可以打印参数、堆栈、返回值等功能,但不能『阻止』这个函数的调用。replace 方法则是整个替换掉函数,也就是完全可以避免执行到函数里的功能。

首先我们需要得到这个方法……使用 ObjC 对象的 classes 成员可以枚举当前进程中所有的 ObjC 类:

[Local::ProcName::WeChat]-> ObjC.classes
{
    "_NSViewAnimator_NSBrowserView": {
        "handle": "0x7fff9c27fc78"
    },
    "_NSViewAnimator_NSClipView": {
        "handle": "0x7fff9c280c90"
    },
    "_NSViewAnimator_NSClipView_NSClipView": {
        "handle": "0x600001054190"
    },
    "_NSViewAnimator_NSCollectionView": {
        "handle": "0x7fff9c29dd40"
    },
...

列表很长就不全列了。这是一个 js 的字典对象,每一个 key 对应一个 class 的名字。

[Local::ProcName::WeChat]-> ObjC.classes.MessageService
{
    "handle": "0x10f6d5d28"
}

这样便获得了 MessageService 类。此外使用 ObjC.classes.类名.$methods 可以列出一个类所有可用的方法。

定位到我们要的方法:

[Local::ProcName::WeChat]-> ObjC.classes.MessageService["- onRevokeMsg:"]
function

在 Javascript 里甚至可以直接调用这个方法,当然需要有合适的参数。接着修改其 .implementation 属性即可替换函数实现。而在 frida 中将普通的 javascript 函数转换成 ObjectiveC 可以调用的函数,还需要使用 ObjC.implement。

最后只要两行代码:

var method = ObjC.classes.MessageService["- onRevokeMsg:"];
method.implementation = ObjC.implement(method, function (self, sel) {})

2.2.0 测试可用。

Mac 上无法撤回

手机上已经撤回成功

当然,这样简单地处理连自己撤回的消息都会失败,有兴趣的话自己在函数里判断撤回消息的 fromUsrName,再进一步决定是否要拦截。


#2

最后还是改成了插件实现,本来打算从 0 开始新建工程,但是遇到了一些找不到链接库之类琐碎的问题,于是 fork 了一份现成的代码来改


#4

只是一个防撤回的话,hopper开启来
-[MessageService onRevokeMsg:]
下直接return下。更快吧…