Android逆向破解某力定位打卡

查看 193|回复 11
作者:莫问刀   
背景
原来公司使用的是某叮打卡,就是普通的定位打卡,之前已经从系统层做好了位置修改,配合自己写的APP做了注入任意位置,就在周五2022年12月30日突然发公告切换了打卡软件某力e,既然换了那就试试某叮的那一套对它有没有效果,结果很显然无效,如果有效就没有这一篇文章了。
一些猜想
因为之前分析过某叮的定位逻辑,这里大概描述一下。
它的定位逻辑是两个方向的,第一它通过SDK接口,申请了系统的GPS定位,调用了LocationManager这个方法
requestLocationUpdates(@NonNull String provider, long minTime, float minDistance,
        @NonNull LocationListener listener, home.php?mod=space&uid=1043391 Looper looper)
另一个方向就是基于基站和周边WiFi列表的定位+ip,这里叫LBS,这里就取2种逻辑中最快回调回来的,比如GPS快,就拿GPS的经纬度,再使用高德SDK的api获取坐标对应的地点名字,如果GPS不可用,或者无回调,或者LBS定位回调比GPS快,拿到坐标后,也通过SDK获取坐标对应的地点名字。以上就是它的总体逻辑。不要问怎么知道的[doge].
通过分析,某力和某叮都是用了高德SDK,那刚开始我直接修改系统GPS以为能成功,没想到没效果,而且requestLocationUpdates是没有被调用的,那只能说它用的是LBS方式。
打开高德SDKdemo,发现了一个叫H5辅助定位,进入看看,且试用了一下。


Xnip2023-01-01_18-53-00.png (481.14 KB, 下载次数: 0)
下载附件
2023-1-2 12:20 上传

原来还能这样啊。
再结合这个大佬的分析https://www.52pojie.cn/thread-1709943-1-1.html,定位重要代码是在SDKWebViewFragment中,大概能确定某力只用LBS方式。改系统的GPS数据是没办法的。
既然不吃系统数据,那就开始分析最新版本的情况吧,上面吾爱大佬的破解方式已经不适用最新版本了,他的文章也没有支出是那个版本,没有给出样本。所以能参考的就只有SDKWebViewFragment中获取定位的方法,也就是web和原生native沟通的方法:
public BaseSDKResult a(LocationGetRequest locationGetRequest, com.delicloud.app.jsbridge.main.c cVar)
开始分析定位重要位置
某力版本android:versionName="2.5.9"
样本地址:https://wwsk.lanzouy.com/iikQR0judryj,MD5:76f852dc4108e05cdb6f105df26c32a5
反编译工具:jadx
我们把样本拖进去jadx中,搜索SDKWebViewFragment,找到之后打开它。
根据上面吾爱大佬的文章,参考是否还存在这个方法,根据开发经验,一般都不会乱改web和原生通信方法。
继续搜索BaseSDKResult a(LocationGetRequest locationGetRequest找到获取定位的方法。


Xnip2023-01-01_19-11-00.png (454.67 KB, 下载次数: 0)
下载附件
2023-1-2 12:20 上传

我们是幸运的,方法还在,而且逻辑也不复杂。我们来大体分析下整个方法的代码吧。

我希望你有Android应用层开发经验,就算混淆的代码,也能看懂大概。不然瞎猜代码是很痛苦的,且会走弯路。

a方法是web和原生通信的方法,返回了一个BaseSDKResult对象。
    home.php?mod=space&uid=1892347 // com.delicloud.app.jsbridge.b
    public BaseSDKResult a(LocationGetRequest locationGetRequest, com.delicloud.app.jsbridge.main.c cVar) {
        //第一个判断①gd,通过传入context获取了系统服务LocationManager,
        //然后isProviderEnabled(GeocodeSearch.GPS)翻译:GPS服务能用吗,
        //locationManager.isProviderEnabled("network")网络定位能用吗,
        //如果其中一个可以用,那么就进入里面代码块,如果都不能,就进入②toast一个提示语。
        //显然,这个APP在进入首页的时候会问你要定位权限,你一定要给,有了定位
        //这里的判断就一定会成立。
        if (com.delicloud.app.tools.utils.i.gd(this.mContentActivity)) {
            //③n方法代码判断
            //这里就是再次检查权限,如果没有弹窗提示要权限,
            //如果没有权限,就没有下文了,我们会给APP权限的,这里判断就可以默认为true了
            if (com.delicloud.app.tools.utils.m.n(this)) {
                //④be方法,从本地sp文件中根据某个key取出数据,然后强转为AddressModel对象。
                AddressModel addressModel = (AddressModel) dl.a.be(this.mContentActivity, com.delicloud.app.commom.b.bBz);
                //请求的时候如果需要缓存,且缓存的addressModel不是空的
                //且当前时间和缓存的时间小于10000ms(10秒),就使用缓存。
                //经过我的动态调试,addressModel是null,也就是这里的判断不成立。
                //⑤,看我给出的动态调试代码。
                if (locationGetRequest.isCache() && addressModel != null && System.currentTimeMillis() - addressModel.getCache_time() () { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.7
                    @Override // io.reactivex.ObservableOnSubscribe
                    public void subscribe(ObservableEmitter observableEmitter) throws Exception {
                        if (SDKWebViewFragment.this.bLI == null) {
                            return;
                        }
                        SDKWebViewFragment.this.bLI.a(new a.InterfaceC0166a() { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.7.1
                            @Override // com.delicloud.app.tools.utils.a.InterfaceC0166a
                            public void a(double d2, double d3, String str, String str2, String str3, String str4) {
                                zArr[0] = false;
                                LocationGetResult locationGetResult2 = new LocationGetResult();
                                locationGetResult2.setData(new LocationGetResult.LocationGetData(Double.valueOf(d2), Double.valueOf(d3), str, str2));
                                Log.i(SocializeConstants.KEY_LOCATION, com.delicloud.app.http.utils.c.aq(locationGetResult2));
                                SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.chq, locationGetResult2);
                            }
                            @Override // com.delicloud.app.tools.utils.a.InterfaceC0166a
                            public void ZP() {
                                zArr[0] = false;
                                SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.chq, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                            }
                        });
                    }
                }).timeout(60L, TimeUnit.SECONDS).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer() { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.6
                    @Override // io.reactivex.Observer
                    public void onComplete() {
                    }
                    @Override // io.reactivex.Observer
                    public void onSubscribe(Disposable disposable) {
                    }
                    @Override // io.reactivex.Observer
                    /* renamed from: e */
                    //⑥这里是定位返回成功后
                    public void onNext(Long l2) {
                        //看到这个判断我觉得很奇怪,为什么要这样呢,到底是否成立呢。
                        //看到申请定位前有那么一句final boolean[] zArr = {true};
                        //这又是何苦,上面标记了true,那判断就一定成立。
                        //最后我们调用了a方法。文章往下拉,看a的实现。⑥
                        if (zArr[0]) {
                            SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.chq, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                        }
                    }
                    @Override // io.reactivex.Observer
                    //这里是rxJava流程遇到错误回调
                    public void onError(Throwable th) {
                        if (zArr[0]) {
                            SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.chq, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                        }
                    }
                });
            }
            return null;
        }
        //如果GPS和定位不能用,就toast
        es.dmoral.toasty.b.bQ(this.mContentActivity, "当前系统定位开关未开启,无法定位").show();
        return new BaseSDKResult(JsSDKResultCode.IBEACON_NEED_LOCATION_PERMISSION);
    }
①:gd这个判断,做了什么呢,进去看看。
public class i {
    //获取定位服务,判断是否可用,如果我们授权了肯定可以用
    public static boolean gd(Context context) {
        LocationManager locationManager = (LocationManager) context.getSystemService(SocializeConstants.KEY_LOCATION);
        return locationManager.isProviderEnabled(GeocodeSearch.GPS) || locationManager.isProviderEnabled("network");
    }
}
③再次检查定位权限
    public static boolean n(final Fragment fragment) {
        if (c(fragment.getContext(), cxo)) {
            return true;
        }
        if (com.delicloud.app.commom.b.bAc) {
            return false;
        }
        com.delicloud.app.commom.b.bAc = true;
        com.delicloud.app.deiui.feedback.dialog.b.bVs.d(fragment.getActivity(), "得力e+申请访问精准定位权限", "用于极速打卡、考勤签到打卡、天气服务等功能。拒绝或取消授权不影响其他服务", "去开启", "取消", true, new b.a() { // from class: com.delicloud.app.tools.utils.m.6
            @Override // com.delicloud.app.deiui.feedback.dialog.b.a
            public void Za() {
                es.dmoral.toasty.b.bQ(Fragment.this.getActivity(), "权限拒绝后,将无法使用该功能").show();
            }
            @Override // com.delicloud.app.deiui.feedback.dialog.b.a
            public void Zb() {
                m.a(Fragment.this, m.cxo, 12);
            }
        }).show(fragment.getChildFragmentManager(), "权限申请");
        return false;
    }
④根据一个key字符串,获取本地储存的数据,然后转Java bean对象。
    public static  T be(Context context, String str) {
        try {
            return (T) bh(context, str);
        } catch (Exception e2) {
            e2.printStackTrace();
            return null;
        }
    }
        private static Object bh(Context context, String str) throws IOException, ClassNotFoundException {
        //从sp中取出数据
        String string = getString(context, str);
        if (TextUtils.isEmpty(string)) {
            return null;
        }
        //这里经过base64解码,也就是我们可以根据str这个key去sp中找到里面的数据
        //然后base64解码就可以看到存储内容了,有兴趣可以hook得到str,看看sp的数据哦
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.decode(string.getBytes(), 0));
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object readObject = objectInputStream.readObject();
        byteArrayInputStream.close();
        objectInputStream.close();
        return readObject;
    }
⑤AddressModel addressModel = (AddressModel) dl.a.be(this.mContentActivity, com.delicloud.app.commom.b.bBz);中be方法frida hook代码,这里也可以用Xposed的hook,看你自己会那个。
let a = Java.use("dl.a");
a["be"].implementation = function (context, str) {
    console.log('be is called' + ', ' + 'context: ' + context + ', ' + 'str: ' + str);
    let ret = this.be(context, str);
    console.log('be ret value is ' + ret);
    return ret;
};
经过我的调试,返回值是null,也就是没缓存,取缓存条件不成立,继续往下走。
⑥onNext流程调用了a
    public void a(String str, BaseSDKResult baseSDKResult) {
        //这里可以忽略,这东西不是null,原因,fragment创建的时候,获取了webview组件
        //ckD就是BridgeWebView实例,不可能,
        //这里判断是防止fragment退出之后清理了BridgeWebView,rxJava还继续回调引发
        //空指针异常,要判断下,正常情况下可以理解往下走
        //以下就是ckD赋值代码
        //public View onCreateView(LayoutInflater layoutInflater, \
        //@Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
        //View onCreateView = super.onCreateView(layoutInflater, viewGroup, bundle);
        //this.ckD = (BridgeWebView) onCreateView.findViewById(R.id.fragment_web_view);
        //return onCreateView;
        //}
        if (this.ckD == null) {
            return;
        }
        //这个开发真是个大聪明,居然直接把重要数据log打印出来,我真的谢谢你。
        //在开发的时候调试版本可以打log,辅助开发,到了生产环境一定要把log关掉
        //通常我们都是使用一个log工具做代{过}{滤}理,统一打印log处理
        //在发布最终版本的时候关闭log工具开关,同时通过混淆把Log.i等代码移除
        //大家可以过滤看看,这里打印了什么,破解的方案就在这里了
        Log.i("SDKWebViewFragment", "call back registerMethod=" + str + ",result=" + com.delicloud.app.http.utils.c.aq(baseSDKResult));
        //ckF是private HashMap ckF = new HashMap();一个Map
        //这里的判断是:Map中是否包含了str这个(变量里面的值)键值对,如果有,就取出str对应的
        //value,这个value就是c对象,c对象是:
        //public interface c {
            //    void nm(String str);
        //}
        //用人话说就是:取出str对应的c接口的具体实现对象然后调用nm方法
        if (this.ckF.containsKey(str)) {
            //这里调用nm之前,aq方法处理了baseSDKResult(里面就是定位结果)⑦
            this.ckF.get(str).nm(com.delicloud.app.http.utils.c.aq(baseSDKResult));
        }
    }
⑦进去aq看看,里面做了什么呢。
    public static String aq(Object obj2) {
        gson = afV();
        Gson gson2 = gson;
        //gson是一个处理json的工具,这里使用了gson的api,把java bean转成json字符串
        if (gson2 != null) {
            //把传递进来的java bean转json字符串
            return gson2.toJson(obj2);
        }
        return null;
    }
做开发的人基本上秒懂了,就是java bean转json吗,那么我们可以看看aq方法到底返回了什么。
上frida hook就行。
let c = Java.use("com.delicloud.app.http.utils.c");
c["aq"].implementation = function (obj2) {
    console.log('aq is called' + ', ' + 'obj2: ' + obj2);
    let ret = this.aq(obj2);
    console.log('aq ret value is ' + ret);
    return ret;
};
{
    "code":0,
    "data":{
        "address":"广东省广州市",
        "latitude":23.xxxx,
        "longitude":113.xxxx,
        "name":"xx大厦"
    },
    "method":"",
    "msg":"成功"
}
打印的数据居然是这样的。
聪明的你应该知道怎么做了吧?
frida hook改位置
经过动态调试之后,我发现了aq的数据居然包含了位置信息,我当时就想到了破解方案。
替换大法!
我去替换里面的数据,看看效果如何。
说干就干。
//com.delicloud.app.http.utils.c
Java.perform(function() {
    var com_delicloud_app_http_utils_c_clz = Java.use('com.delicloud.app.http.utils.c');
    var com_delicloud_app_http_utils_c_clz_method_aq_4105 = com_delicloud_app_http_utils_c_clz.aq.overload('java.lang.Object');
    com_delicloud_app_http_utils_c_clz_method_aq_4105.implementation = function(v0) {
        var ret = com_delicloud_app_http_utils_c_clz_method_aq_4105.call(com_delicloud_app_http_utils_c_clz, v0);
        console.log("json:", ret);
        //data":{"address
        console.log("判断地址::" + ret.indexOf("data\":\{\"address") != -1 );
        //代码中必须把json转义,不转义语法错误。
        var addr = "{\n" +
            "    \"code\":0,\n" +
            "    \"data\":{\n" +
            "        \"address\":\"目标地址名称,自己替换经纬度,后面给工具获取\",\n" +
            "        \"latitude\":23.2222,\n" +
            "        \"longitude\":113.22222,\n" +
            "        \"name\":\"某大厦\"\n" +
            "    },\n" +
            "    \"method\":\"\",\n" +
            "    \"msg\":\"成功\"\n" +
            "}";
        //如果返回的数据包含了这样的字符串,就直接替换我们目的地,其他
        //java bean转json的就正常返回就行。
        if (ret.indexOf("data\":\{\"address") != -1) {
            return addr;
        }
        return ret;
    };
});
执行脚本,下拉刷新看看效果。


Xnip2023-01-01_20-39-53.png (408.5 KB, 下载次数: 0)
下载附件
2023-1-2 12:19 上传

我反手就点了,打卡成功。
看到这里,像做持久化hook的应该秒懂了。
我就不提供相关的成品了。
如何获取正确坐标
刚开始的时候我是去https://lbs.amap.com/tools/picker取坐标。
当我从这个网站取回来坐标后,并没有效果,显示的位置是目标坐标的4点钟方向再过一段距离。
后来问了其他有坐标处理经验的朋友阿肥,他告诉我地图坐标可能需要做标准换算。
public class JXMapUtil {
    private static final String PN_GAODE_MAP = "com.autonavi.minimap";   // 高德地图包名
    private static final String PN_BAIDU_MAP = "com.baidu.BaiduMap";     // 百度地图包名
    private static final String PN_TENCENT_MAP = "com.tencent.map";      // 腾讯地图包名
    private static final double a = 6378245.0;
    private static final double pi = 3.1415926535897932384626;
    private static final double ee = 0.00669342162296594323;
    //____________________________坐标转化_____________________________________________________
    /**
     * 转化为火星坐标系
     * home.php?mod=space&uid=952169 latitude
     * @param longitude
     * @return
     */
    public static double[] toGCJ02Point(double latitude, double longitude) {
        double[] dev = calDev(latitude, longitude);
        double retLat = latitude + dev[0];
        double retLon = longitude + dev[1];
        return new double[] { retLat, retLon };
    }
    /**
     * 火星坐标系 转化为 WGS84(国际坐标系
     * @param latitude
     * @param longitude
     * @return
     */
    public static double[] toWGS84Point(double latitude, double longitude) {
        double[] dev = calDev(latitude, longitude);
        double retLat = latitude - dev[0];
        double retLon = longitude - dev[1];
        dev = calDev(retLat, retLon);
        retLat = latitude - dev[0];
        retLon = longitude - dev[1];
        return new double[] { retLat, retLon };
    }
    private static double[] calDev(double wgLat, double wgLon) {
        if (isOutOfChina(wgLat, wgLon)) {
            return new double[] { 0, 0 };
        }
        double dLat = calLat(wgLon - 105.0, wgLat - 35.0);
        double dLon = calLon(wgLon - 105.0, wgLat - 35.0);
        double radLat = wgLat / 180.0 * pi;
        double magic = Math.sin(radLat);
        magic = 1 - ee * magic * magic;
        double sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
        return new double[] { dLat, dLon };
    }
    private static boolean isOutOfChina(double lat, double lon) {
        if (lon  137.8347)
            return true;
        if (lat  55.8271)
            return true;
        return false;
    }
    private static double calLat(double x, double y) {
        double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
        return ret;
    }
    private static double calLon(double x, double y) {
        double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
        return ret;
    }
}
也就是你从网站拾取的坐标需要转wgs84坐标
double[] wgs84Point = JXMapUtil.toWGS84Point(latitude, longitude);
这样的坐标喂给高德腾讯百度相关定位就OK了。
我这里提供一个apk方便坐标拾取。
如果使用,打卡拾取,点击你需要的位置,然后右下角点击√,返回的坐标就是wgs84标准的坐标了。
工具下载地址:
https://wwsk.lanzouy.com/iAFqH0jut6yj
MD5:d3c87fdd3d1982d29d485dc7baaab176


device-2023-01-01-211334.gif (1.59 MB, 下载次数: 0)
下载附件
2023-1-2 12:19 上传

这样的坐标就是冇问题的啦!
总结
这个案例允许动态调试,没有root检查等等阻拦,是一个很好的实战例子。
在做这个逆向的事情的前提,我个人认为,你应该具备以下知识。
0:先学会开发!达到入门就OK。
1:Java基础扎实,有Android应用层开发的经验,能看懂SDK代码。
2:熟悉开发中使用到的第三方库,比如这里用到的gson,rxJava,高德定位SDK。
3:熟练使用jadx,apktool,AndroidKiller,Frida,Xposed等工具。
题外话,准备好AOSP系统代码,能修改系统且刷入手机,方便定位某些系统api,甚至定制接口。
夸张一点说十行代码搞定某定打卡(已实现了https://www.bilibili.com/video/BV1aK411Z7oQ)(这只是其中一个案例)
这一篇是通过hook不修改apk做的打卡,下一篇是改apk,直接插入需要的经纬度直接打卡。
个人博客地址:http://www.debuglive.cn/article/1059234217549889536
我现在工作是搞Android TV launcher开发的,偶尔也会做点盒子,手机的业务,算是有一点点开发经验。
在熟悉开发的前提下,去逆向会顺利很多 。
--来自业余逆向菜鸡的总结。

坐标, 方法

莫问刀
OP
  


林伊轩 发表于 2023-1-5 15:08
这...太牛了.
修改aosp代码刷入到自己手机就很有难度吧

整体来说是琐碎的事情很多,繁琐,其中一步错误,后面就卡主。
总体来说,就是创建Ubuntu300gb空间,下载手机对应有驱动的版本的源码分支,导入ubt中,配置各种需要的编译lib,然后开始编译,过程中遇到错误就解决,最后手机解开bl锁,Google亲儿子的手机和AOSP有对应关系,只要你驱动下对,刷机是不会出问题的。这样刷的系统才是真的原生系统,原生到代码都是你可以一行一行修改的。这是我的公众号地址https://mp.weixin.qq.com/mp/appm ... d=1#wechat_redirect里面有我编译系统刷机改机的笔记
luxingyu329   

我觉得研究这个一定要有一段路要走,支持楼主
影子恋人   

向大佬低头
莫问刀
OP
  


影子恋人 发表于 2023-1-2 12:29
向大佬低头

捕获一枚大佬~
a2415868   

认真看完了,感觉学到了很多!
莫问刀
OP
  


a2415868 发表于 2023-1-2 12:36
认真看完了,感觉学到了很多!

那就达到目的了
WLK970606   

卧槽楼主,你确定不是和我一个公司的?我们已经也是钉钉,上周五换了得力e+..............
影子恋人   


莫问刀 发表于 2023-1-2 12:34
捕获一枚大佬~

谁是大佬
莫问刀
OP
  


WLK970606 发表于 2023-1-2 13:33
卧槽楼主,你确定不是和我一个公司的?我们已经也是钉钉,上周五换了得力e+..............

不会吧不会吧,这么巧吗,我在越秀区上班哦
您需要登录后才可以回帖 登录 | 立即注册

返回顶部