iOS钉钉远程打卡助手(支持越狱和非越狱)


#1

前言:本文主要讲述使用hook方式实现钉钉远程打卡功能,涉及到tweak相关知识,如果你不想了解具体实现细节可直接到我的Github地址参考安装(包含越狱和非越狱两种方法)

  你是不是像小编一样每个月靠着固定薪水维持家庭开支,而且还要经过层层“剥离”… 一旦迟到扣工资是小事,是不是全勤奖升职加薪的机会就泡汤了?是不是每天早上都想懒会床(嗯…让我再睡会…),不想上班…本文就讲述如何拥有一个“免死金牌” :laughing:

  目前越来越多的企业考勤都从传统都指纹打卡转移到了使用钉钉或者企业微信这类的APP进行考勤,主要是定位和Wi-Fi考勤两种方式。APP考勤这块企业使用钉钉的比较多(我瞎蒙的:smile:),因为钉钉主要是面向企业服务的而不是员工(替我们心疼几秒…:tired_face:),所以本文先解决钉钉的群众问题,企业微信将安排在下一期。

钉钉卡

项目完整代码,已托管到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


#2

好,终于可以不用上班了


#3

我试过了, 为啥还是打不了卡


#4

哥们,你好,能加一下你的微信吗?我的微信号是phixxlon


#5

在钉钉设置里面配置好了吗?


#6

都配置了, 重新启动了也


#7

配置的是WiFi还是定位?
注意一下设置里面有个打卡模式切换,如果你仅配置了定位打卡请打开开关,
如果配置的是Wi-Fi模式,请关闭那个开关。


#8

我所有都配置 都打开了…


#9

很棒的分享,参考下这个https://github.com/gengjf/DingTalkDylib,弄个完整版6到飞起。


#10

我现在就想学习一个可以使用的, 如果一个插件不可以正常使用, 那么我感觉是失败的


#11

这个插件在大部分机型上都是测试过可用的。
Wi-Fi打卡是最准确的,定位打卡经纬度偏差一点点就可能是外勤了。
如果显示外勤打卡请在打卡页面点击考勤范围,查看一下你当前距离考勤范围有多远…然后手动调整一下经纬度(一般是不需要的)。
那么如何查询考勤范围的经纬度呢?直接进入经纬度查询,搜索一下你的考勤点,在配置页面输入Google地图经纬度即可(注意考勤点一定要在考勤范围内)。
另外能否发一下你的配置截图?


#12

在打卡的时候人脸识别也不会出现了?


#13

大神, mac版的 qq 支持红包了, 我看了下关键词是wallet, 没弄明白😂, mac 自动抢


#14

但是我在使用的时候会弹出一个提示框, 然后就是用不了


#15

使用impactor安装ipa的吗


#16

WechatIMG83

这个是内部的API iOS最新系统不支持么?


#17

我都搞了一天了,也是老出现这个问题。


#18

很好很强大,要是定位那个功能能够集成高德地图,默认打开当前所在城市,可以搜索地址,并可以在地图上选择地址的话,那就更棒了,给LZ点赞


#20

这个问题只有在iOS11出现其他版本没事不知道是什么原因


#21

定位的问题应该是我用的是“签到”,而不是“考勤打卡”,有没有针对签到功能定位啊,请参考前面评论中的代码