背景
原来公司使用的是某叮打卡,就是普通的定位打卡,之前已经从系统层做好了位置修改,配合自己写的APP做了注入任意位置,就在周五2022年12月30日突然发公告切换了打卡软件某力e,既然换了那就试试某叮的那一套对它有没有效果,结果很显然无效,如果有效就没有这一篇文章了。
一些猜想
因为之前分析过某叮的定位逻辑,这里大概描述一下。
它的定位逻辑是两个方向的,第一它通过SDK接口,申请了系统的GPS定位,调用了LocationManager
这个方法
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沟通的方法:
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对象。
@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这个判断,做了什么呢,进去看看。
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() {
@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对象。
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 {
String string = getString(context, str);
if (TextUtils.isEmpty(string)) {
return null;
}
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) {
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看看,里面做了什么呢。
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就行。
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的数据居然包含了位置信息,我当时就想到了破解方案。
替换大法!
我去替换里面的数据,看看效果如何。
说干就干。
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点钟方向再过一段距离。
后来问了其他有坐标处理经验的朋友阿肥,他告诉我地图坐标可能需要做标准换算。
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坐标
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,甚至定制接口。
夸张一点说十行代码搞定某定打卡(已实现了https://www.bilibili.com/video/BV1aK411Z7oQ)(这只是其中一个案例)
这一篇是通过hook不修改apk做的打卡,下一篇是改apk,直接插入需要的经纬度直接打卡。
个人博客地址:http://www.debuglive.cn/article/1059234217549889536
我现在工作是搞Android TV launcher开发的,偶尔也会做点盒子,手机的业务,算是有一点点开发经验。
在熟悉开发的前提下,去逆向会顺利很多 。
--来自业余逆向菜鸡的总结。