背景
原来公司使用的是某叮打卡,就是普通的定位打卡,之前已经从系统层做好了位置修改,配合自己写的APP做了注入任意位置,就在周五2022年12月30日突然发公告切换了打卡软件某力e,既然换了那就试试某叮的那一套对它有没有效果,结果很显然无效,如果有效就没有这一篇文章了。
一些猜想
因为之前分析过某叮的定位逻辑,这里大概描述一下。
它的定位逻辑是两个方向的,第一它通过SDK接口,申请了系统的GPS定位,调用了LocationManager
这个方法
1 2
requestLocationUpdates(@NonNull String provider, long minTime, float minDistance, @NonNull LocationListener listener, @Nullable Looper looper)
另一个方向就是基于基站和周边WiFi列表的定位+ip,这里叫LBS,这里就取2种逻辑中最快回调回来的,比如GPS快,就拿GPS的经纬度,再使用高德SDK的api获取坐标对应的地点名字,如果GPS不可用,或者无回调,或者LBS定位回调比GPS快,拿到坐标后,也通过SDK获取坐标对应的地点名字。以上就是它的总体逻辑。不要问怎么知道的[doge].
通过分析,某力和某叮都是用了高德SDK,那刚开始我直接修改系统GPS以为能成功,没想到没效果,而且requestLocationUpdates
是没有被调用的,那只能说它用的是LBS方式。
打开高德SDKdemo,发现了一个叫H5辅助定位,进入看看,且试用了一下。
原来还能这样啊。
再结合这个大佬的分析https://www.52pojie.cn/thread-1709943-1-1.html,定位重要代码是在`SDKWebViewFragment`中,大概能确定某力只用LBS方式。改系统的GPS数据是没办法的。
既然不吃系统数据,那就开始分析最新版本的情况吧,上面吾爱大佬的破解方式已经不适用最新版本了,他的文章也没有支出是那个版本,没有给出样本。所以能参考的就只有SDKWebViewFragment
中获取定位的方法,也就是web和原生native沟通的方法:
1
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
找到获取定位的方法。
我们是幸运的,方法还在,而且逻辑也不复杂。我们来大体分析下整个方法的代码吧。
我希望你有Android应用层开发经验,就算混淆的代码,也能看懂大概。不然瞎猜代码是很痛苦的,且会走弯路。
a方法是web和原生通信的方法,返回了一个BaseSDKResult对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
@Override public BaseSDKResult a (LocationGetRequest locationGetRequest, com.delicloud.app.jsbridge.main.c cVar) { if (com.delicloud.app.tools.utils .i.gd(this .mContentActivity)) { if (com.delicloud.app.tools.utils .m.n(this )) { AddressModel addressModel = (AddressModel) dl.a.be(this .mContentActivity, com.delicloud.app.commom.b.bBz); if (locationGetRequest.isCache() && addressModel != null && System.currentTimeMillis() - addressModel.getCache_time() <= 10000 ) { LocationGetResult locationGetResult = new LocationGetResult (); locationGetResult.setData(new LocationGetResult .LocationGetData(addressModel.getLatitude(), addressModel.getLongitude(), addressModel.getName(), addressModel.getAddress())); Log.e("cache" , com.delicloud.app.http.utils.c.aq(locationGetResult)); dl.a.a(this .mContentActivity, com.delicloud.app.commom.b.bBz, null ); return locationGetResult; } final boolean [] zArr = {true }; Observable.create(new ObservableOnSubscribe <Long>() { @Override public void subscribe (ObservableEmitter<Long> observableEmitter) throws Exception { if (SDKWebViewFragment.this .bLI == null ) { return ; } SDKWebViewFragment.this .bLI.a(new a .InterfaceC0166a() { @Override 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 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 <Long>() { @Override public void onComplete () { } @Override public void onSubscribe (Disposable disposable) { } @Override public void onNext (Long l2) { if (zArr[0 ]) { SDKWebViewFragment.this .a(com.delicloud.app.jsbridge.b.chq, new BaseSDKResult (JsSDKResultCode.GET_LOCATION_RESULT_FAIL)); } } @Override 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 ; } es.dmoral.toasty.b.bQ(this .mContentActivity, "当前系统定位开关未开启,无法定位" ).show(); return new BaseSDKResult (JsSDKResultCode.IBEACON_NEED_LOCATION_PERMISSION); }
①:gd这个判断,做了什么呢,进去看看。
1 2 3 4 5 6 7
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" ); } }
③再次检查定位权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
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() { @Override public void Za () { es.dmoral.toasty.b.bQ(Fragment.this .getActivity(), "权限拒绝后,将无法使用该功能" ).show(); } @Override public void Zb () { m.a(Fragment.this , m.cxo, 12 ); } }).show(fragment.getChildFragmentManager(), "权限申请" ); return false ; }
④根据一个key字符串,获取本地储存的数据,然后转Java bean对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
public static <T extends Serializable> 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,看你自己会那个。
1 2 3 4 5 6 7
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
public void a (String str, BaseSDKResult baseSDKResult) { if (this .ckD == null ) { return ; } Log.i("SDKWebViewFragment" , "call back registerMethod=" + str + ",result=" + com.delicloud.app.http.utils.c.aq(baseSDKResult)); if (this .ckF.containsKey(str)) { this .ckF.get(str).nm(com.delicloud.app.http.utils.c.aq(baseSDKResult)); } }
⑦进去aq看看,里面做了什么呢。
1 2 3 4 5 6 7 8 9 10
public static String aq (Object obj2) { gson = afV(); Gson gson2 = gson; if (gson2 != null ) { return gson2.toJson(obj2); } return null ; }
做开发的人基本上秒懂了,就是java bean转json吗,那么我们可以看看aq方法到底返回了什么。
上frida hook就行。
1 2 3 4 5 6 7
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; };
1 2 3 4 5 6 7 8 9 10 11
{ "code" :0 , "data" :{ "address" :"广东省广州市" , "latitude" :23. xxxx, "longitude" :113. xxxx, "name" :"xx大厦" }, "method" :"" , "msg" :"成功" }
打印的数据居然是这样的。
聪明的你应该知道怎么做了吧?
frida hook改位置
经过动态调试之后,我发现了aq的数据居然包含了位置信息,我当时就想到了破解方案。
替换大法!
我去替换里面的数据,看看效果如何。
说干就干。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
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); console .log ("判断地址::" + ret.indexOf ("data\":\{\"address" ) != -1 ); 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" + "}" ; if (ret.indexOf ("data\":\{\"address" ) != -1 ) { return addr; } return ret; }; });
执行脚本,下拉刷新看看效果。
我反手就点了,打卡成功。
看到这里,像做持久化hook的应该秒懂了。
我就不提供相关的成品了。
如何获取正确坐标
刚开始的时候我是去https://lbs.amap.com/tools/picker取坐标。
当我从这个网站取回来坐标后,并没有效果,显示的位置是目标坐标的4点钟方向再过一段距离。
后来问了其他有坐标处理经验的朋友阿肥,他告诉我地图坐标可能需要做标准换算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
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 ; 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 }; } 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 < 72.004 || lon > 137.8347 ) return true ; if (lat < 0.8293 || 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坐标
1
double [] wgs84Point = JXMapUtil.toWGS84Point(latitude, longitude);
这样的坐标喂给高德腾讯百度相关定位就OK了。
我这里提供一个apk方便坐标拾取。
如果使用,打卡拾取,点击你需要的位置,然后右下角点击√,返回的坐标就是wgs84标准的坐标了。
工具下载地址:
https://wwsk.lanzouy.com/iAFqH0jut6yj
MD5:d3c87fdd3d1982d29d485dc7baaab176
这样的坐标就是冇问题的啦!
总结
这个案例允许动态调试,没有root检查等等阻拦,是一个很好的实战例子。
在做这个逆向的事情的前提,我个人认为,你应该具备以下知识。
0:先学会开发!达到入门就OK。
1:Java基础扎实,有Android应用层开发的经验,能看懂SDK代码。
2:熟悉开发中使用到的第三方库,比如这里用到的gson,rxJava,高德定位SDK。
3:熟练使用jadx,apktool,AndroidKiller,Frida,Xposed等工具。
题外话,准备好AOSP系统代码,能修改系统且刷入手机,方便定位某些系统api,甚至定制接口。
夸张一点说十行代码搞定某定打卡(已实现了)(这只是其中一个案例)
我现在工作是搞Android TV launcher开发的,偶尔也会做点盒子,手机的业务,算是有一点点开发经验。
在熟悉开发的前提下,去逆向会顺利很多 。
–来自业余逆向菜鸡的总结。
我们在星球安全后厨等你~
- source:security-kitchen.com
评论