0x1
最近从安卓机换回了“尊贵的” iPhone 13 Pro 远峰蓝,一直怀念安卓上某蓝软件的阅后即焚 block 插件,找了一圈并没有 iOS 的,心血来潮自己写了一个。但是我并没有 iOS App 正向开发的经验,平时后端开发和 web 开发居多,RN 写过一些,但也完全不搭嘎,OC 是一点都不会,甚至基础都没有。写这个插件基本上是面向 Google 编程,也走了不少弯路,分享下经验路程,共同交流学习。
0x2
砸壳、classdump、monkeydev、frida/llvm/flex/reveal/passionfruit/ida 调试 这些就不提了(是的我是菜逼能用的调试手段我都用了),跳过 iosre 的基础,我们直接进入正题。顺便感谢下各位前辈,感谢庆哥、狗神、霜神、瓜神等等感谢提供这么好的工具,能让我们站在巨人的肩膀上,我一直都觉得,做二进制逆向的都是神。
本次编写插件的目标:
- 转换闪照消息为普通照片消息
- 阻止消息撤回
- 涉及到信息被处理的,UI 上给到友好提示
0x3 转换闪照消息为普通照片消息
某蓝这软件头文件 dump 出来 9,276 个项目,非常臃(xi)肿(làn)。找到对应的方法我走了不少弯路。
我最开始是想按照逆向微信的思路去找类似于 message manager 的 class 所以最初关注到 BDChatMessageManager
这个类,passionfruit 里看聊天记录的数据库有个 messageTable 表,跑到 ida 里搜 xrefs 也基本上是定位到这个类,里面有个方法:
@class BDChatMessageManager
- (void)didReceiveGRPCStreamMessage:(id)arg1;
于是给这个方法打断点,但是这个方法的参数拿到是一个 GPBAny
类型的数据,属于还未处理的 protobuf 包。并且奇怪的是,消息撤回情况并不能触发这个函数,可能是除了长链接以外还有其他的数据传输方式。
后面基本上就是大海捞针似的搜 message 相关的方法,关注到一个:
@class GJIMSessionService, GJIMMessageService
- (void)addMessage:(id)arg1;
这个方法 GJIMSessionService
, GJIMMessageService
两个类都有实现,其中 GJIMMessageService
只会在聊天列表触发,GJIMSessionService
会在接收/发送到消息后任意页面触发。传入的参数正是对应的消息 Model。
整理出常用的消息类型:
- 1: 普通文本消息
- 2: 图片消息
- 3: 语音消息
- 6: 大表情
- 24: 闪照
- 55: 撤回消息
到这里我们就能实现拦截闪照了,只需要把消息类型从 24 闪照
修改为 2 普通照片
:
if (msg.type == 24) {
msg.type = 2;
return msg;
}
兴奋的看效果,结果是图片是裂的。
仔细看一下闪照的消息数据,是被加密过的:
那么 flex 上场,让我们来看看 imageview 是怎么解密这个数据的:
可以看到对应类名为:
BDAutoDestoryImageViewController
到 ida 中看到可疑方法:
就是他了!
改一下拦截代码:
if (msg.type == 24) {
msg.type = 2;
msg.content = [objc_getClass("BDEncrypt") decryptVideoUrl:msg.content];
return msg;
}
轻松搞定!可以看到正确的图片了。
0x4 消息防撤回
基于前序的工作,消息防撤回是不是很简单呢?
我是不是只需要这样就可以:
// #hook updateMessage
if (msg.type == 55) {
msg.type = 1;
return msg;
}
事实上并不能,因为程序执行到这一步以后是拿不到原始消息类型和消息内容的,相当于消息数据丢了。
还有一个副作用就是自己发出的消息也会被处理,尽管我尝试了对比当前用户userid,去避免这个问题,但是仍解决不了数据丢失的问题。
我一直很想去查询 addMessage
/ updateMessage
的 backtrace 看到对应的调用栈,说来惭愧,我尝试了 frida、lldb、ida xrefs 都找不到。这个程序大概率是异步调用的或者通过闭包函数调用的。
一番无目的的摸索,我注意到GJIMSessionService
有这么个推送相关的方法:
- (void)gji_responsePushPackage:(id)arg1;
打断点后发现是属于消息 model 更上层的数据,这时被撤回消息并没有被改动,存在近期消息列表的内存实例中。并且只有发送方的数据。调用原函数后就可以获得对应的消息 model。
很好,整理下代码:
switch (pkg.messageType) {
case 55: // 撤回
{
NSLog(@"[BLUEDHOOK] %@ 撤回消息已被拦截。", pkg.name);
// 获取原始消息
NSDictionary *serviceDict = [self messageServiceDict];
GJIMMessageService *service = [serviceDict objectForKey:[NSString stringWithFormat:@"2+%d", pkg.from]];
if (service == nil) {
NSLog(@"[BLUEDHOOK] Warning: cannot find msgid %llu from %d in message service, canceled tagging.", pkg.messageId, pkg.from);
return nil;
}
GJIMMessageModel *targetMsg;
for (GJIMMessageModel *msg in [service messageArr]) {
if (msg.msgId == pkg.messageId) {
targetMsg = msg;
break;
}
}
targetMsg.msgExtra = @{@"BLUED_HOOK_IS_RECALLED": @1};
[self updateMessage:targetMsg];
return nil;
}
break;
case 24:
pkg.messageType = 2;
pkg.contents = [objc_getClass("BDEncrypt") decryptVideoUrl:pkg.contents];
pkg.msgExtra = @{@"BLUEDHOOK_IS_SNAPIMG": @1};
break;
default:
break;
}
return CHSuper1(GJIMSessionService, p_handlePushPackage, pkg);
可以正常工作了
0x5 添加UI反馈
你可能已经注意到了,我利用原生 msgExtra
的字段对消息做了标记。这是为了方便渲染 cell 的时候对消息进行判断。
其实最初的时候,我还是想着用类似微信逆向的思路通过 addmessage 添加系统提示,但是很可惜,这个 app 没有对应的实现(或者我还没找到),所以只能从渲染入手。
由于对 UIKit 并不熟悉这里我也走了很多弯路,补了一些 UITableView 的相关基础,最初注意到的是
BDMessageViewController
它继承自 UITableView
,其中有实现
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2;
我尝试过 hook 这个方法去处理诸如 BDChatBasicCell
之类的 view,但是实现都不是很理想,App 渲染优化可能有问题,一条数据基本是会调用三次这个方法。就连最后一次都不能拿到 bubbleview 的正确高度。
最后我干脆 hook 了 UITableViewCell 然后对类名进行判断:
NSString *cellClassName = [NSString stringWithFormat:@"%@", ((UIView*)self).class];
if (![cellClassName containsString:@"PrivateOther"]) {
return CHSuper0(UITableViewCell, contentView);
}
UI 部分代码比较长,基本上的思路就是取到当前 viewcell 对应的 msg model,然后根据标记渲染即可。
取得了很好的效果,也就是我们的成品了:
最后打包重签名就可以在 iPhone 13 Pro 上运行啦~
0x6 总结
项目代码:Github - bluedhook
其实目前还存在一些问题不知道怎么解决:
- 重签名推送保活
这 App 似乎只走 apns,即便 App 在后台也收不到推送 - 处理过两次的照片信息显示不完整
比如闪照转换为普通照片又撤回了的情况,但是我尝试修改过 cell 的 frame 并不能生效,不清楚 frame 高度具体是被谁控制的,或者如何修改 autolayout 的情况。两条消息以上的话,确实需要去修改高度,否则超出部分会被 clip。
总之还是很开心的,总算走出改别人的逆向项目自己从头写了个插件。欢迎大家交流呀~