阅读此文档的过程中遇到任何问题,请关注公众号【移动端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/10976
处理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/1LdgMmAJrPH6DaZLoEV4kYA 提取码: xw9i