前言
我最终还是把毒手伸向了 Mac 应用,只是觉得他们不够好用,但又不能指望找到对应的产品经理提建议,等排期,于是只能自己写 Tweak 。
起因是这样的,Mac QQ 在一次版本更新中,移除了一个类似于 Alfred 的功能,叫做 Swifty。这个功能确实大多数人都不用,毕竟有更好用的 alfred,但是去寻找聊天人的时候。Swifty 有无可比拟的优势,比如我想找 H4x 童鞋聊天,仅需双击 ctrl 后,输入 H4 回车即可。
而现在只能,cmd + shift + z 唤起 QQ,cmd + f 唤起搜索框,输入 h4,回车,用的超级不连贯。我就心想能不能把对应的接口拿出来,给 Alfred 调用。(别说 AppleScript,那个实在是太鬼畜)
初步分析
Mac 应用,干坏事比 iOS 轻松非常多。因为不需要蛋疼证书问题,也没有烦人的 ASLR,更不用砸壳, lldb、class-dump 都可以直接糊上去,但如何找到对应的位置,还是需要下一番功夫的。
Class-dump 出来关于 Search 的东西有一些,但其中的方法还是挺多的,于是就打算 lldb 挂上去瞅瞅,结果发现了一个有趣的现象:
每当我搜索的时候,打一个字符,命令行中就多了一行 NSLog,于是我可以在 NSLog 上下断点,看看是哪个函数打印出来的就好了嘛。
br s -F "NSLog"
果然是 NSLog,bt 看一下堆栈发现有门,但是符号是 unnamed,我们需要魔改一下二进制文件。
杨君大法好
杨君同学前一段开源了一个伟大的项目,restore-symbol,可以把符号通过 class-dump 解析出来,再塞回去。并且配合 IDA Pro 的 Python 脚本,连 Block 的符号都可以还原回去。
跑完了之后,你会发现,无法运行。主要原因是 QQ 是有签名的,需要干掉签名。
用这个命令行工具 https://github.com/steakknife/unsign
,去掉签名即可。
动态调试
我们再次给 NSLog 下断点,发现符号出现了。
是 ContactSearcherInter
中的这个方法
- (void)Query:(long long)arg1 Contacts:(id)arg2 WithKey:(id)arg3;
直接干掉 NSLog 断点,给这个参数下断点:
br dis 1
br s -S "Query:Contacts:WithKey:"
打印三个参数,可以发现
- Query 传入一个 magic number 10
- Contacts 传入一个空的 NSMutableArray
- Key 传入 String,为搜索的 Key
执行完了之后,我们打印断点中获取到的 NSMutableArrary 的地址,发现传入的数组就是最终的结果。
看起来搜索 API 搞定了,之后需要研究如何找到这个 Class 的内存地址。
经过 grep,发现:
- ContactSearchInter 是 MQSearchViewController 的属性
- MQSearchViewController 是MQAIOWindowController2 的属性
- MQAIOWindowController2 是 MQAIOWindow2 的 windowConroller
- MQAIOWindow2 可以通过 [NSApplication sharedApplication].windows 取到
拼图完整了。
如何通信?
这是个非常好的问题,我 Alfred 是单独的一个进程,QQ 是单独的一个进程。
我该如何做进程间通信呢。
方案一:监听文件目录
直接跑个 线程去监控一个约定好的文件修改,收到参数写回去。
这个方法,快槽猛,但是总是让有洁癖的人无法接受。
方案二:notify_post
iOS、 macOS 有个大地图炮的 Notification,叫做 notify_post,全局的。但是有个缺点是:这货是没法传参数的,纯粹的 SendMessage。
所以就成了 Alfred 发送 Notification,QQ 捕获到了之后,写个文件 Notification 回去。Alfred 收到 Notification,读文件。
比第一个方法好很多。但是觉得还是蠢。
方案三:用 macOS 进程间通信 API
看了眼 NSXPCConnection 的 API,晕了,不想碰了。
方案四:开一个 Server
这是我最后选择的方案,虽说 tweak 部分代码变多了。但是对于未来想做 QQ bot 的话方便很多。
方案确定,开始写代码
有了上述的分析之后,后面的事情就轻车熟路了。
找到 ContactSearchInter
调用 API
这里需要注意下,因为传入参数大于两个,没法用 NSObject 内置的
performSelector:<#(SEL)#> withObject:<#(id)#> withObject:<#(id)#>
毕竟参数不够长, 需要用 NSInvocation
对数据进行清洗
因为返回的并不是 NSString 类型,是对应的 Class, Group、Discuss、Buddy。而且还有 NSNull 在捣乱,所以要洗一下数据。
封装成 Server 端 API
塞入 QQ
yolo QQ libQQHook.dylib
完成
对比一下:
后记
这是 Tweak QQ 的第一篇文章,这次一改往日的风格,记录下来的是我思考的过程,而我具体怎么做的一带而过,应该对大家帮助更大些吧?不过文章中很多具体操作的基础都没有写详细,因为我公众号之前都写过了,就不再描述了。
下一步会把唤起 QQ 和切换到对应聊天窗口搞好,记得戳下方的二维码订阅哦。