iOS钉钉远程打卡助手(支持越狱和非越狱)
前言:本文主要讲述使用hook方式实现钉钉远程打卡功能,涉及到tweak相关知识,如果你不想了解具体实现细节可直接到我的Github地址参考安装(包含越狱和非越狱两种方法)
你是不是像小编一样每个月靠着固定薪水维持家庭开支,而且还要经过层层“剥离”...... 一旦迟到扣工资是小事,是不是全勤奖升职加薪的机会就泡汤了?是不是每天早上都想懒会床(嗯...让我再睡会...),不想上班...本文就讲述如何拥有一个“免死金牌” 😆
迟到目前越来越多的企业考勤都从传统都指纹打卡转移到了使用钉钉或者企业微信这类的APP进行考勤,主要是定位和Wi-Fi考勤两种方式。APP考勤这块企业使用钉钉的比较多(我瞎蒙的😄),因为钉钉主要是面向企业服务的而不是员工(替我们心疼几秒...😫),所以本文先解决钉钉的群众问题,企业微信将安排在下一期。
钉钉卡项目完整代码,已托管到Github。如果喜欢,欢迎Star
思路
hook一个APP最难的不是代码,往往是分析出合适的切入点。
- 想要实现hook定位打卡,最简单最直接的就是直接hook APP的定位功能,这也就是要实现虚拟定位。
- 想要实现Wi-Fi打卡,就必须要hook APP的Wi-Fi获取方法,那么就需要找到获取Wi-Fi的方法。
那么也就是说我们需要实现虚拟定位和hook Wi-Fi获取的方法?如果这么做就相对比较麻烦了,因为我们至少要找到两个切入点。那么应该要这么做比较合适呢?其实只要你细心就能发现打卡页面以及外面的工作页面全部都是H5的页面(其实钉钉使用了自家的Weex),这个反汇编后也能印证。
h5
- 既然是H5页面,那么很有可能用到JS调用原生功能来获取Wi-Fi和定位信息。(使用过Weex的同学应该知道,其实是原生封装好功能模块然后暴露出一个Module给Weex使用,然后用WXSDKEngine去register一下Module,以此增强Weex的功能)
-
既然是需要交互,那么直接在Hopper或者IDA检索就能发现切入点
切入点
小伙伴们该说了,首先我不一定知道这个是H5页面,其次我也不知道啥原生和JS交互,臣妾做不到啊,有没有更直观简单的找到切入点的方法呢?
...
其实上面主要从静态分析来考虑,我们可以换一个角度来思考,既然考勤需要获取定位,那么肯定用到了locationManager:didUpdateLocations:代理方法,所以使用hopper或者IDA检索一波:
locationManager:didUpdateLocations:
什么?那么多...是哪一个呢?这时候动态分析就派上用场了,下面介绍全部基于lldb调试
1.进入lldb之后,打断点:
(lldb) br s -n "-[AMapLocationManager locationManager:didUpdateLocations:]"
2.点击考勤打卡,这时候观察终端
Process 1735 stopped
* thread #40, name = 'com.autonavi.AMapLocationThread', stop reason = breakpoint 1.1
frame #0: 0x00000001004900dc DingTalk`-[AMapLocationManager locationManager:didUpdateLocations:]
DingTalk`-[AMapLocationManager locationManager:didUpdateLocations:]:
-> 0x1004900dc <+0>: sub sp, sp, #0x120 ; =0x120
0x1004900e0 <+4>: stp d15, d14, [sp, #0x80]
0x1004900e4 <+8>: stp d13, d12, [sp, #0x90]
0x1004900e8 <+12>: stp d11, d10, [sp, #0xa0]
Target 0: (DingTalk) stopped.
由此可见已经进入断点,进而印证我们的猜测,考勤定位使用的AMapLocationManager这个类,定位回调的就是下面这个方法(注:hook此方法可实现虚拟定位打卡功能):
-[AMapLocationManager locationManager:didUpdateLocations:]
3.确定了使用AMapLocationManager这个类后,接下来直接用Hopper或者IDA检索AMapLocationManager:
AMapLocationManager
4.上面框框的方法是不是很熟悉😁,猜测这个就是调用定位的入口代码,同样打个断点验证一下:
(lldb) br s -n "-[AMapLocationManager startUpdatingLocation]"
5.点击考勤打卡,观察终端,如期的到达断点
Process 1748 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001004ec3ec DingTalk`-[AMapLocationManager startUpdatingLocation]
DingTalk`-[AMapLocationManager startUpdatingLocation]:
-> 0x1004ec3ec <+0>: stp x22, x21, [sp, #-0x30]!
0x1004ec3f0 <+4>: stp x20, x19, [sp, #0x10]
0x1004ec3f4 <+8>: stp x29, x30, [sp, #0x20]
0x1004ec3f8 <+12>: add x29, sp, #0x20 ; =0x20
Target 0: (DingTalk) stopped.
6.然后就很简单了,打印一下调用栈,就能找到切入点了:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001004ec3ec DingTalk`-[AMapLocationManager startUpdatingLocation]
frame #1: 0x0000000101bd8558 DingTalk`-[DTALocationManager dt_startUpdatingLocation] + 200
frame #2: 0x00000001029c2e90 DingTalk`-[LAPLocationInfo start:to:] + 1424
frame #3: 0x00000001029d5eec DingTalk`___lldb_unnamed_symbol76963$$DingTalk + 52
frame #4: 0x00000001029d5964 DingTalk`-[LAPluginInstanceCollector handleJavaScriptRequest:callback:] + 2076
frame #5: 0x00000001052d472c DingTalkHelper.dylib`_logos_method$_ungrouped$LAPluginInstanceCollector$handleJavaScriptRequest$callback$(LAPluginInstanceCollector*, objc_selector*, objc_object*, void (objc_object*) block_pointer) + 128
frame #6: 0x00000001029b63b4 DingTalk`-[LAWVPluginInstanceCollector registerInstanceCollector]_block + 152
frame #7: 0x00000001029b02b8 DingTalk`-[LAWebViewJavascriptBridge _handleQueueString:] + 1100
frame #8: 0x00000001029afccc DingTalk`-[LAWebViewJavascriptBridge _flushMessageQueue] + 308
frame #9: 0x00000001029b08e0 DingTalk`-[LAWebViewJavascriptBridge webView:shouldStartLoadWithRequest:navigationType:] + 304
frame #10: 0x00000001029cc748 DingTalk`-[LAWebView callback_webViewShouldStartLoadWithRequest:navigationType:] + 172
frame #11: 0x00000001029e1a7c DingTalk`-[LAWebViewProgressEstimater webView:shouldStartLoadWithRequest:navigationType:] + 288
frame #12: 0x0000000187c131c8 UIKit`-[UIWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 296
frame #13: 0x0000000182890ae0 CoreFoundation`__invoking___ + 144
frame #14: 0x0000000182788548 CoreFoundation`-[NSInvocation invoke] + 284
frame #15: 0x000000018278ce70 CoreFoundation`-[NSInvocation invokeWithTarget:] + 60
frame #16: 0x00000001876d821c WebKitLegacy`-[_WebSafeForwarder forwardInvocation:] + 156
frame #17: 0x000000018288eaa4 CoreFoundation`___forwarding___ + 408
frame #18: 0x000000018278cd1c CoreFoundation`_CF_forwarding_prep_0 + 92
frame #19: 0x0000000182890ae0 CoreFoundation`__invoking___ + 144
frame #20: 0x0000000182788548 CoreFoundation`-[NSInvocation invoke] + 284
frame #21: 0x00000001867823d8 WebCore`HandleDelegateSource(void*) + 108
frame #22: 0x0000000182841124 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #23: 0x0000000182840bb8 CoreFoundation`__CFRunLoopDoSources0 + 540
frame #24: 0x000000018283e8b8 CoreFoundation`__CFRunLoopRun + 724
frame #25: 0x0000000182768d10 CoreFoundation`CFRunLoopRunSpecific + 384
frame #26: 0x0000000184050088 GraphicsServices`GSEventRunModal + 180
frame #27: 0x0000000187a3df70 UIKit`UIApplicationMain + 204
frame #28: 0x00000001000e4548 DingTalk`___lldb_unnamed_symbol1$$DingTalk + 88
frame #29: 0x00000001823068b8 libdyld.dylib`start + 4
7.下面就是我们hook的切入方法了:
-[LAPluginInstanceCollector handleJavaScriptRequest:callback:]
代码实现:
历经“千辛万苦”,终于可以写代码了。handleJavaScriptRequest:callback:方法传递进来两个参数,第一个参数是JS的请求对象(JS传递过来的对象),第二个是callback就是钉钉根据JS的具体请求做具体处理后对JS的一个结果回调(callback JS Method)。
那么我们就可以利用“中间人攻击”原理hook这个方法。首先拦截到JS的请求,分析是否是获取定位经纬度或者Wi-Fi信息,如果是我们就自己构造一个callback传递给原生方法,这样原生方法获取经纬度或者Wi-Fi信息后就会对我们自己的callback进行回调,然后我们对回调传递的内容进行修改最后将修改后的数据回传给JS即可实现HOOK打卡功能。
利用这个原理,我们能做的远远不仅如此...
action为"getInterface"时是请求获取Wi-Fi信息,我们只需修改macIp、ssid即可。action为"start"时是获取定位信息,只需修改accuracy、latitude、longitude即可,具体代码如下:
%hook LAPluginInstanceCollector
- (void)handleJavaScriptRequest:(id)arg2 callback:(void(^)(id dic))arg3{
if(![LLPunchManager shared].punchConfig.isOpenPunchHelper){
%orig;
} else if([arg2[@"action"] isEqualToString:@"getInterface"]){
id callback = ^(id dic){
NSDictionary *retDic = @{
@"errorCode" : @"0",
@"errorMessage": @"",
@"keep": @"0",
@"result": @{
@"macIp": [LLPunchManager shared].punchConfig.wifiMacIp,
@"ssid": [LLPunchManager shared].punchConfig.wifiName
}
};
arg3(![LLPunchManager shared].punchConfig.isLocationPunchMode ? retDic : dic);
};
%orig(arg2,callback);
} else if([arg2[@"action"] isEqualToString:@"start"]){
id callback = ^(id dic){
NSDictionary *retDic = @{
@"errorCode" : @"0",
@"errorMessage": @"",
@"keep": @"1",
@"result": @{
@"aMapCode": @"0",
@"accuracy": [LLPunchManager shared].punchConfig.accuracy,
@"latitude": [LLPunchManager shared].punchConfig.latitude,
@"longitude": [LLPunchManager shared].punchConfig.longitude,
@"netType": @"",
@"operatorType": @"unknown",
@"resultCode": @"0",
@"resultMessage": @""
}
};
arg3([LLPunchManager shared].punchConfig.isLocationPunchMode ? retDic : dic);
};
%orig(arg2,callback);
} else {
%orig;
}
}
%end
注:代码略去了相应的对原生模块的回调,因为只有在分析时有必要。
解决钉钉弹出非法客户端问题
越狱手机直接使用插件不存在这个问题,只有自己修改钉钉bundleId时会出现这个问题,原因很明显,就是钉钉运行时对bundleId进行了检测,解决办法如下:
%hook DTInfoPlist
+ (NSString *)getAppBundleId{
return @"com.laiwang.DingTalk";
}
%end
总结
看到这里,你肯定发现整个篇幅都在讲述分析过程,具体编码被我一笔带过了,原因正如我开始讲述的那样,逆向最难的是发现hook切入点,真正花时间的也是分析过程。一旦找到合适的切入点,可能实现功能仅仅只需要几行代码即可。
项目完整代码,已托管到Github。如果喜欢,欢迎Star
号外
最近有好多小伙伴问我是怎么打包签名的,这里推荐一个我自用的python签名打包脚本,支持签名Watch和Plugins。
签名脚本完整代码,已托管到Github。