debug_cat 发表于 2023-1-2 12:23

Android逆向破解某力定位打卡

本帖最后由 莫问刀 于 2023-2-11 15:58 编辑

## 背景

原来公司使用的是某叮打卡,就是普通的定位打卡,之前已经从系统层做好了位置修改,配合自己写的APP做了注入任意位置,就在周五2022年12月30日突然发公告切换了打卡软件某力e,既然换了那就试试某叮的那一套对它有没有效果,结果很显然无效,如果有效就没有这一篇文章了。



#### 一些猜想

因为之前分析过某叮的定位逻辑,这里大概描述一下。

它的定位逻辑是两个方向的,第一它通过SDK接口,申请了系统的GPS定位,调用了`LocationManager`这个方法

```java
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获取坐标对应的地点名字。以上就是它的总体逻辑。不要问怎么知道的.



通过分析,某力和某叮都是用了高德SDK,那刚开始我直接修改系统GPS以为能成功,没想到没效果,而且`requestLocationUpdates`是没有被调用的,那只能说它用的是LBS方式。

打开高德SDKdemo,发现了一个叫H5辅助定位,进入看看,且试用了一下。




原来还能这样啊。

再结合这个大佬的分析https://www.52pojie.cn/thread-1709943-1-1.html,定位重要代码是在`SDKWebViewFragment`中,大概能确定某力只用LBS方式。改系统的GPS数据是没办法的。

既然不吃系统数据,那就开始分析最新版本的情况吧,上面吾爱大佬的破解方式已经不适用最新版本了,他的文章也没有支出是那个版本,没有给出样本。所以能参考的就只有`SDKWebViewFragment`中获取定位的方法,也就是web和原生native沟通的方法:

```java
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对象。

```java
    @Override // 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() <= 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};
                //看到这里,做开发的小伙伴就秒懂了。
                      //rxJava,创建订阅,子线程走某事,成功后切换主线下拿到结果。
                //也就是⑥位置是结果,我们关注结果就行了。
                Observable.create(new ObservableOnSubscribe<Long>() { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.7
                  @Override // io.reactivex.ObservableOnSubscribe
                  public void subscribe(ObservableEmitter<Long> 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 = 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 = 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>() { // 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) {
                            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) {
                            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这个判断,做了什么呢,进去看看。

```java
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");
    }
}
```

③再次检查定位权限

```java
    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 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,看你自己会那个。

```javascript
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

```java
    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<String, c> 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看看,里面做了什么呢。

```java
    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就行。

```java
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;
};
```

```java
{
    "code":0,
    "data":{
      "address":"广东省广州市",
      "latitude":23.xxxx,
      "longitude":113.xxxx,
      "name":"xx大厦"
    },
    "method":"",
    "msg":"成功"
}
```

打印的数据居然是这样的。

聪明的你应该知道怎么做了吧?



#### frida hook改位置

经过动态调试之后,我发现了aq的数据居然包含了位置信息,我当时就想到了破解方案。

替换大法!

我去替换里面的数据,看看效果如何。

说干就干。

```javascript
//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;
    };
   
});
```

执行脚本,下拉刷新看看效果。



我反手就点了,打卡成功。



看到这里,像做持久化hook的应该秒懂了。

我就不提供相关的成品了。



#### 如何获取正确坐标

刚开始的时候我是去https://lbs.amap.com/tools/picker取坐标。

当我从这个网站取回来坐标后,并没有效果,显示的位置是目标坐标的4点钟方向再过一段距离。

后来问了其他有坐标处理经验的朋友阿肥,他告诉我地图坐标可能需要做标准换算。

```java
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;



    //____________________________坐标转化_____________________________________________________

    /**
   * 转化为火星坐标系
   * @Param latitude
   * @param longitude
   * @return
   */
    public static double[] toGCJ02Point(double latitude, double longitude) {
      double[] dev = calDev(latitude, longitude);
      double retLat = latitude + dev;
      double retLon = longitude + dev;
      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;
      double retLon = longitude - dev;
      dev = calDev(retLat, retLon);
      retLat = latitude - dev;
      retLon = longitude - dev;
      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坐标

```java
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开发的,偶尔也会做点盒子,手机的业务,算是有一点点开发经验。

在熟悉开发的前提下,去逆向会顺利很多 。

--来自业余逆向菜鸡的总结。

luxingyu329 发表于 2023-1-3 15:42

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

debug_cat 发表于 2023-1-5 15:15

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

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

debug_cat 发表于 2023-1-2 12:40

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

:lol那就达到目的了

debug_cat 发表于 2023-1-2 12:34

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

{:1_918:}捕获一枚大佬~

影子恋人 发表于 2023-1-2 12:29

向大佬低头

a2415868 发表于 2023-1-2 12:36

认真看完了,感觉学到了很多!

WLK970606 发表于 2023-1-2 13:33

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

影子恋人 发表于 2023-1-2 13:47

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

谁是大佬

debug_cat 发表于 2023-1-2 13:52

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

{:1_918:}{:1_918:}{:1_918:}不会吧不会吧,这么巧吗,我在越秀区上班哦

matocool 发表于 2023-1-2 13:57

向大佬低头

52pojie44521 发表于 2023-1-2 13:58

给力啊,大神
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: Android逆向破解某力定位打卡