乐趣区

关于ios:Mac逆向手撸钉钉机器人

浏览此文档的过程中遇到任何问题,请关注公众号【挪动端 Android 和 iOS 开发技术分享】或加 QQ 群【812546729

1. 指标

钉钉在企业中的利用越来越宽泛。官网也有对应的自定义机器人服务,然而,如果 1 分钟内发消息超过 20 条,则会会限流 10 分钟。作为技术人,说干就干,申请个小号,手撸一个无限度的机器人。

2. 操作环境

  • mac 零碎
  • frida:动静调试工具
  • Python:解决钉钉收到的工作
  • Redis:钉钉和 python 间的通信

3. 流程

动态剖析

应用 frida-trace 的 frida-trace -m "*[* *endMsg*]" -m "*[* *end*Message*]" 钉钉 (必须敞开 Mac 零碎的 sip) 命令跟踪钉钉。任意发送一条音讯后,发现要害日志如下:

126282 ms  -[DTChatInputTextView sendMessage]
126282 ms     | +[DTMojoGraySwitchManager isEnableRemoveSendMessageTrim]
126282 ms     | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126282 ms     |    | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126282 ms     | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126282 ms     |    | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126283 ms     | +[DTMojoGraySwitchManager isEnableRemoveSendMessageTrim]
126283 ms     | -[DTChatInputTextView sendOrdinaryMessage:0x600000b2eba0]
126284 ms     |    | -[DTChatInputTextView sendMessageByType:0x1 body:0x0 attrString:0x600000b2eba0]
126284 ms     |    |    | -[DTChatInputTextView sendMessageByType:0x1 body:0x0 attrString:0x600000b2eba0 toConversationModel:0x0]
126284 ms     |    |    |    | -[DTChatInputTextView sendTxtMessageWithOption:0x0 attrString:0x600000b2eba0]
126284 ms     |    |    |    |    | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126284 ms     |    |    |    |    |    | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126284 ms     |    |    |    |    | -[DTChatInputTextView sendTextMessage:0xd9f80e4ec6f6596b option:0x0]
126284 ms     |    |    |    |    |    | -[DTConversationModel sendTextMessage:0xd9f80e4ec6f6596b withOption:0x0 completionHandler:0x7ffeeb4aef78]
126284 ms     |    |    |    |    |    |    | -[DTMojoMessageService sendTextMessage:0xd9f80e4ec6f6596b withCid:0x60000076d280 option:0x0 completionHandler:0x7ffeeb4aef78]
126308 ms     |    |    |    | -[DTChatContentController inputTextViewDidSendMessage:0x7fd674a41400]
126308 ms     | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]
126308 ms     |    | +[DTMojoGraySwitchManager isTranslateSendMsgEnabled]

发现要害类 DTMojoMessageService,再应用命令frida-trace -m "*[DTMojoMessageService *]" 钉钉,跟踪 DTMojoMessageService 类,去查找收到音讯调用的办法,当收到音讯后的日志如下:

 54153 ms  +[DTMojoMessageService sharedService]
 54153 ms  -[DTMojoMessageService didMessageReadStatus:0x600000a7fce0 localId:0x600000a7d1e0 msgid:0x600000a7c960 unreadCount:0x2 totalCount:0x4]
 57462 ms  +[DTMojoMessageService sharedService]
 57463 ms  -[DTMojoMessageService didReceiveNotSilenceMsg:0x600000a74aa0]
 57463 ms  +[DTMojoMessageService sharedService]
 57463 ms  -[DTMojoMessageService didUpdatedNewMessagesWithCid:0x600000a74aa0 newMessages:0x600001243c30]

批改 frida-trance 生成 DTMojoMessageService 类的 didUpdatedNewMessagesWithCid 办法,打印具体参数,js 代码如下:

{onEnter(log, args, state) {log(`-[DTMojoMessageService didUpdatedNewMessagesWithCid:${new ObjC.Object(args[2])}]`);
    var array = new ObjC.Object(args[3]);
    var count = array.count().valueOf();
    for (var i = 0; i !== count; i++) {var element = array.objectAtIndex_(i);
      var msg = new ObjC.Object(element);
      log(`-[DTMojoMessageService newMessages:${msg.messageContent().text()}]`);
    }
  },
  onLeave(log, retval, state) {}}

日志输入如下:

 5051 ms  -[DTMojoMessageService didUpdatedNewMessagesWithCid:36545771520]
  5051 ms  -[DTMojoMessageService newMessages:222]

最终确定钉钉收到音讯的办法为:

[DTMojoMessageService didUpdatedNewMessagesWithCid:newMessages:]

回复该音讯后的日志如下:

150594 ms  +[DTMojoMessageService sharedService]
150594 ms  -[DTMojoMessageService sendReplyMessage:0x7ff5a6f059b0 completion:0x7ffee9653f18]
150626 ms  +[DTMojoMessageService sharedService]
150626 ms  -[DTMojoMessageService didReceiveNotSilenceMsg:0x7ff5a745a030]
150627 ms  +[DTMojoMessageService sharedService]
150627 ms  -[DTMojoMessageService didUpdatedNewMessagesWithCid:36545771520]
150718 ms  +[DTMojoMessageService sharedService]
150718 ms  -[DTMojoMessageService didMessageSendSuccess:0x7ff5a04ae750]
150721 ms  +[DTMojoMessageService sharedService]
150721 ms  -[DTMojoMessageService didMessageSendSuccess:0x7ff5a99549e0]
150841 ms  +[DTMojoMessageService sharedService]
150841 ms  -[DTMojoMessageService didMessageExtensionChange:0x7ff5a6a2c8b0 mid:0xaeab680c5dd extension:0x7ff5a6ae6a40]
150895 ms  +[DTMojoMessageService sharedService]
150895 ms  -[DTMojoMessageService didMessageExtensionChange:0x7ff5a0491d90 mid:0xaeab68143d6 extension:0x7ff5a639c910]
151586 ms  +[DTMojoMessageService sharedService]
151586 ms  -[DTMojoMessageService didMessageReadStatus:0x7ff5a9ba0bc0 localId:0x7ff5a01fb510 msgid:0x7ff5a99636b0 unreadCount:0x2 totalCount:0x4]

最终确定钉钉回复音讯的办法为:

[DTMojoMessageService sendReplyMessage:completion:]

通过以上的剖析,确定了接管和回复音讯的办法别离为:

[DTMojoMessageService didUpdatedNewMessagesWithCid:newMessages:]

[DTMojoMessageService sendReplyMessage:completion:]

实现钉钉机器人的音讯接管

编写一个 DingTalkRobot.dylib 动静库,将钉钉接管到的音讯入 redis 队列,要害代码如下:

CHOptimizedMethod2(self, void, DTMojoMessageService, didUpdatedNewMessagesWithCid, id, arg1, newMessages, NSArray <DTMessageImp*>*, arg2) {CHSuper2(DTMojoMessageService, didUpdatedNewMessagesWithCid, arg1, newMessages, arg2);

    DTMessageImp *message =arg2.firstObject;

    // @我并且是文本音讯
    YYLog(@"atOpenIds=%@=", message.atOpenIds); // 从该日志里获取到本人的 openid
    if ([message.atOpenIds.allKeys containsObject:[NSNumber numberWithLongLong:263527137]] &&
        message.messageType == 1) {
        @try {id<DTMessageContentText> messageContent = (id)message.messageContent;
            NSString *content = [DingTalkRobot messageContent:messageContent.text];

            NSString *nickName = [[DingTalkRobot contactService] getNickByUid:message.senderId];

            // 0 内容,1 音讯 id,2,发送人 id,3 发送人昵称,4 会话 id
            NSArray *messageInfo = @[content,[NSString stringWithFormat:@"%lld", message.messageId], [NSString stringWithFormat:@"%lld", message.senderId], nickName, message.conversationId];
            NSString *messageString = [messageInfo componentsJoinedByString:@"|"];
            YYLog(@"redis rpush=%@=", messageString);
            [[[[DingTalkRobot shared].redis rpush:task_queue value:messageString] then:^id(id value) {YYLog(@"redis rpush result =%@=", value);
                return nil;
            }] catch:^id(NSError *err) {YYLog(@"redis rpush err =%@=", err);
                return nil;
            }];
        } @catch (NSException *exception) {} @finally {}
    }
}

全副源码见文末。将开发好的 dylib 注入钉钉利用,参考 https://bbs.iosre.com/t/topic…

解决 redis 队列里的音讯

此代码收到 redis 音讯后,调用图灵 API 去拿到后果,而后再存入 redis 回调队列。其余业务场景,如主动打包等其余业务场景可在此基础上扩大。源码如下:

import time
import redis
import itertools
import requests

redis_cli = redis.StrictRedis(host='localhost', decode_responses=True)

TASK_QUEUE = 'redis_task_queue'  # 工作队列
CALLBACK_QUEUE = 'redis_callback_queue'  # 工作状态队列
REDIS_TIME_OUT = 10  # redis 读取超时时长


def loop():
    for i in itertools.count(1):
        task = redis_cli.blpop(TASK_QUEUE, REDIS_TIME_OUT)
        if not task:
            print(f'[{i}]暂无工作')
            continue

        task: str = task[1]
        # // 0 内容,1 音讯 id,2,发送人 id,3 发送人昵称,4 会话 id
        message_info = task.split("|")
        question = message_info[0]

        # 可判断关键词来执行指定工作,也能够间接调机器人 API 去获取答案并返回

        print(f'witchan question={question}=')
        answer = get_answer(question)
        print(f'witchan answer={answer}=')
        task += f'|{answer}'
        redis_cli.rpush(CALLBACK_QUEUE, task)


def get_answer(question):
    resp = requests.get(f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={question}")
    if resp.status_code != 200:
        return "道歉,我曾经打烊了~"
    return resp.json()["content"]


def main():
    while True:
        try:
            print(f"程序已启动")
            loop()
        except Exception as e:
            print(f"程序已解体,稍后重启: {e}")
            time.sleep(10)


if __name__ == '__main__':
    main()

实现钉钉机器人的主动回复音讯

持续批改 DingTalkRobot.dylib 库,在收到 redis 的回调音讯后,调用钉钉的钉钉的回复音讯办法。要害代码如下:

+ (void)replyMessageWithInfo:(NSString *)resp {

    // 0 内容,1 音讯 id,2,发送人 id,3 发送人昵称,4 会话 id,5 返回信息
    NSArray *messageInfo = [resp componentsSeparatedByString:@"|"];
    YYLog(@"messageInfo=%@=", messageInfo);
    if (messageInfo.count != 6) {return;}

    DTReplyMessageOption *messageOption = [NSClassFromString(@"DTReplyMessageOption") new];
    messageOption.isSendTranslateReply = NO;
    messageOption.replyType = 2;
    messageOption.replyAnswerId = 0;
    messageOption.mid = [messageInfo[1] longLongValue];
    messageOption.extension = nil;
    messageOption.atCustomRoleIds = nil;
    messageOption.atOpenIds = @{messageInfo[2]: [messageInfo[3] stringByAppendingString:@" "]};
    messageOption.replyMsg = [NSString stringWithFormat:@"@%@  %@", messageInfo[3], messageInfo[5]];
    messageOption.originMsg = [NSString stringWithFormat:@"> ###### \n> @%@ %@\n", [[self currentUserService] nickName], messageInfo[0]];
    messageOption.originMsgOwnerName = messageInfo[3];
    messageOption.cid = messageInfo[4];

    [[self messageService] sendReplyMessage:messageOption completion:nil];
}

后果

启动 redis 服务,启动注入动静库后的钉钉程序,运行 python 脚本,后果如下:

下图是基于该利用实现的 iOS 和安卓主动打包:

源码下载:链接: https://pan.baidu.com/s/1LdgM… 提取码: xw9i

End

浏览此文档的过程中遇到任何问题,请关注公众号【挪动端 Android 和 iOS 开发技术分享】或加 QQ 群【812546729

退出移动版