初入iOSRE - 逆向全国最大同性交友App

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。

总之还是很开心的,总算走出改别人的逆向项目自己从头写了个插件。欢迎大家交流呀~

2 Likes

image

好文章

一时竟没看懂这个梗,Google才明白:joy:

:crazy_face:
快进到号没了

崆(恐)峒(同)