Android 虚拟定位
侧边栏壁纸
  • 累计撰写 1 篇文章
  • 累计收到 9 条评论

Android 虚拟定位

admin
2023-12-29 / 5 评论 / 52 阅读 / 正在检测是否收录...

1. 前言

安卓虚拟定位技术已经出现很多年了,对于现在来说并不是什么新鲜技术。

前段时间的“大牛助手”因为涉及到大厂的利益被抓了,目前市面上也没有什么好的免费软件可以使用了。

想着自己写一个方案用于虚拟定位。

2. 背景

很多人可能认为虚拟定位的方式很简单,使用开发者选项中的虚拟定位即可,实际上并没有什么用,因为 app 不会使用简单的一种方式确定你的经纬度,而且这个方式也只是给开发者使用调试的。

开发者模式中提供了模拟位置的接口,能够自己开发一个用于模拟位置的app,只要在Manifest中声明权限"android.permission.ACCESS_MOCK_LOCATION"后,即可在开发者选项-选择模拟位置信息应用中选择这个app,具体任何模拟位置则由app中的实现决定。

3. 方案

在有Root的情况下,理论上虚拟定位能够对所有app生效。不讨论某些大厂的极端情况,一般来说,应用获取位置信息的来源有3个:

  • 移动网络
  • WIFI
  • GPS

使用的 Hook 技术可以用:

  • Xposed
  • Frida

通过 Xposed 或者 Frida 效果其实一致,通过对系统定位相关的 api 进行 Hook,
使其返回一个我们自定义的数据,或者使其失效,让需要定位的 APP 虚拟定位至我们想要的位置。

首先需要对 Android 定位的方式进行了解,目前市面上的打卡/定位 APP 肯定不只是通过简单的 GPS 位置来获取用户经纬度,如 GPS 定位、WIFI 定位、基站定位、AGPS 定位,而且通常会混合同时使用

那么我们就需要对 Android 哪些 api 可以进行位置确认进行分析。

4. 确定 Api

//基站定位相关
android.telephony.TelephonyManager
    getCellLocation()
    getAllCellInfo()
android.telephony.PhoneStateListener
    onCellLocationChanged(cellLocation)
    onCellInfoChanged()
//WIFI定位相关
android.net.wifi.WifiManager
    getScanResults()
    getWifiState()
    isWifiEnabled()
    getMacAddress()
    getSSID()
    getBSSID()
//获取网络状态
android.net.NetworkInfo
    isConnectedOrConnecting()
    isConnected()
    isAvailable()
//下面是关键的,要修改经纬度坐标的方法
android.location.LocationManager
    getLastLocation()
    getLastKnownLocation(string)
    getProviders()
    getBestProvider(criteria, boolean)
    requestLocationUpdates(...)
    requestSingleUpdate(...)

也就是我们需要对上方这些 api 进行 Hook,修改使其失效或者返回一个我们的期望数据。

5. 方案一:Xposed

对于 Xposed 的方案,我之前也写过好几篇文章(只不过服务器忘记续费丢完了),所以这里就不介绍 Xposed 的使用方式了。

提一下,目前在高版本上的方案,最佳的就是刷入 Magisk 之后载入 LSPosed,目前可以兼容到最新版本的 Android 14,而且可以指定生效的应用,不需要全局注入包名判断,并且可以实时生效不用重启。

XposedHelpers.findAndHookMethod("android.location.LocationManager", classLoader,
                "getGpsStatus", GpsStatus.class, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        if (!isOpen) return;
                        Log.d(TAG, "getGpsStatus");
                        GpsStatus gss = (GpsStatus) param.getResult();
                        if (gss == null)
                            return;

                        Class<?> clazz = GpsStatus.class;
                        Method m = null;
                        for (Method method : clazz.getDeclaredMethods()) {
                            if (method.getName().equals("setStatus")) {
                                if (method.getParameterTypes().length > 1) {
                                    m = method;
                                    break;
                                }
                            }
                        }
                        if (m == null)
                            return;

                        //access the private setStatus function of GpsStatus
                        m.setAccessible(true);

                        //make the apps belive GPS works fine now
                        int svCount = 5;
                        int[] prns = {1, 2, 3, 4, 5};
                        float[] snrs = {0, 0, 0, 0, 0};
                        float[] elevations = {0, 0, 0, 0, 0};
                        float[] azimuths = {0, 0, 0, 0, 0};
                        int ephemerisMask = 0x1f;
                        int almanacMask = 0x1f;

                        //5 satellites are fixed
                        int usedInFixMask = 0x1f;

                        XposedHelpers.callMethod(gss, "setStatus", svCount, prns, snrs, elevations, azimuths, ephemerisMask, almanacMask, usedInFixMask);
                        param.args[0] = gss;
                        param.setResult(gss);
                        try {
                            m.invoke(gss, svCount, prns, snrs, elevations, azimuths, ephemerisMask, almanacMask, usedInFixMask);
                            param.setResult(gss);
                        } catch (Exception e) {
                            XposedBridge.log(e);
                        }
                    }
                });

Hook 的代码比较多且都是类似的方式,这边就贴一个 api 的 Hook 演示说明,根据上方整理的 api list 自己依次实现就能完成。

6. 方案二:Frida

var Latitude = 30
var Longitude = 120

Java.perform(()=>{
    var telMng = Java.use("android.telephony.TelephonyManager")
    var ArrayList = Java.use("java.util.ArrayList")
    telMng.getCellLocation.implementation = function(){
        console.log("getCellLocation")
        return null
    }

    telMng.getNeighboringCellInfo.implementation = function(){
        console.log("getNeighboringCellInfo")
        return null
    }

    telMng.getAllCellInfo.implementation = function(){
        console.log("getAllCellInfo")
        return null
    }

    telMng.getPhoneCount.implementation = function(){
        console.log("getPhoneCount")
        return 1
    }

    var phoneStateListener = Java.use("android.telephony.PhoneStateListener")
    phoneStateListener.onCellLocationChanged.implementation = function(){
        console.log("onCellLocationChanged")
    }

    // phoneStateListener.onCellInfoChanged.implementation = function(){
    //     console.log("onCellInfoChanged")
    //     return this.onCellInfoChanged.apply(this,arguments)
    // }

    var WifiManager = Java.use("android.net.wifi.WifiManager")
    WifiManager.getScanResults.implementation = function(){
        console.log("getScanResults")
        return ArrayList.$new()
    }

    WifiManager.getWifiState.implementation = function(){
        console.log("getWifiState")
        return 1
    }

    WifiManager.isWifiEnabled.implementation = function(){
        console.log("isWifiEnabled")
        return true
    }


    var WifiInfo = Java.use("android.net.wifi.WifiInfo")
    WifiInfo.getMacAddress.implementation = function(){
        console.log("getMacAddress")
        return "00-00-00-00-00-00-00-00"
    }

    WifiInfo.getSSID.implementation = function(){
        console.log("getSSID")
        return null
    }

    WifiInfo.getBSSID.implementation = function(){
        console.log("getBSSID")
        return "00-00-00-00-00-00-00-00"
    }

    var NetworkInfo = Java.use("android.net.NetworkInfo")
    NetworkInfo.getTypeName.implementation = function(){
        console.log("getTypeName")
        return "WIFI"
    }

    NetworkInfo.isConnectedOrConnecting.implementation = function(){
        console.log("isConnectedOrConnecting")
        return true
    }

    NetworkInfo.isConnected.implementation = function(){
        console.log("isConnected")
        return true
    }

    NetworkInfo.isAvailable.implementation = function(){
        console.log("isAvailable")
        return true
    }

    var CellInfo = Java.use("android.telephony.CellInfo")
    CellInfo.isRegistered.implementation = function(){
        console.log("isRegistered")
        return true
    }

    var LocationManager = Java.use("android.location.LocationManager")
    var Location = Java.use("android.location.Location")
    LocationManager.getLastLocation.implementation = function(){
        console.log("getLastLocation")
        var location = Location.$new(LocationManager.GPS_PROVIDER.value)
        location.setLatitude(Latitude)
        location.setLongitude(Longitude)
        location.setAccuracy(100)
        location.setTime(new Date().getTime())
        return location
    }

    LocationManager.getLastKnownLocation.implementation = function(){
        console.log("getLastKnownLocation")
        var location = Location.$new(LocationManager.GPS_PROVIDER.value)
        location.setLatitude(Latitude)
        location.setLongitude(Longitude)
        location.setAccuracy(100)
        location.setTime(new Date().getTime())
        return location
    }    

    LocationManager.getProviders.overload('boolean').implementation = function(){
        console.log("getProviders1")
        var array = ArrayList.$new()
        array.add("gps")
        return array
    }

    LocationManager.getProviders.overload('android.location.Criteria', 'boolean').implementation = function(){
        console.log("getProviders2")
        var array = ArrayList.$new()
        array.add("gps")
        return array
    }

    LocationManager.getBestProvider.implementation = function(){
        console.log("getBestProvider")
        return "gps"
    }

    LocationManager.addGpsStatusListener.implementation = function(args){
        console.log("addGpsStatusListener")
        var ret = this.addGpsStatusListener(args)
        if(args != null){
            args.onGpsStatusChanged(1)
            args.onGpsStatusChanged(3)
        }
        return ret
    }

    LocationManager.addNmeaListener.overload('android.location.GpsStatus$NmeaListener').implementation = function(){
        console.log("addNmeaListener")
        return false
    }

    // LocationManager.getGpsStatus.implementation = function(){
    //     console.log("getGpsStatus")
    // }
    
})

这里可能没有 hook 所有的接口,有需要的可以自己补全,上方的这些对于企业微信打卡来说已经是完全够用的了。

7. 结束

本文的方案是在 Root 的情况下执行,但是如果想要在无 Root 的环境下完成也是可以的。

方案有非常多,比如简单的可以使用 VirtualRoot、sandBox 的方式,

也可以麻烦一点 objection 重新打包(注意重启签名绕过)、临时 Root、编译系统等。

另外本文所述方案只是用于学习虚拟定位的方式、学习 Hook 技术,并未提供破解定位的工具。

本文仅供学习使用,如果产生任何app封锁(封号)、法律问题,使用者自负
2

评论 (5)

取消
  1. 头像
    admin 作者
    MacOS · Google Chrome

    如果 frida hook 中提示有 class 找不到,但是实际上是有的,可以更新本地 Frida 的版本和手机上 Frida-Server 的版本。比如我是 Android 14,用的 16.1.0 版本,更新到 16.1.10 就可以正常 Hook app 中的 class 了

    回复
    1. 头像
      kiki_666
      Windows 10 · Google Chrome
      @ admin

      你好作者大大,我是一个正就读于某医学院的学生,想要hook某个第二课堂的软件,但是网易易盾保护,首先是检测到hook环境,无法直接利用xp框架,再者是反调试,无法直接frida,会被杀进程;修改app内部代码后回编译,发现签名值校验过不了,会提示“请到应用商店下载官方正版”。以上情况使我在对app进行脱壳处理后只能进行java层的分析,so层陷入了困难,而要想获取RSA公钥和iv值就必需进攻so层,特此请教您的hook方法。

      回复
      1. 头像
        jiyehoo
        iPhone · QQ Browser
        @ kiki_666

        思路要打开,调用 jni 的地方是 java/kotlin,那么把调用的地方 hook 了,replace 成你自己的实现。
        我记得2年前第二课堂软件的用易盾的入口方法叫 checkEnv(),简单的将其替换不执行即可。
        总而言之,对抗“反hook”的方式就是将执行“反hook”的地方给 hook 了。

        回复
    2. 头像
      kiki_666
      Windows 10 · Google Chrome
      @ admin

      我也在github上看见您留下的hook方法的教学方法,但是也正如您说的那样(服务器未续费,资源丢失),则实在是令人心痛。不只您是否愿意帮助,如能提供教学,本人愿和一同研究的计算机协会的朋友一同提供资金,作为感谢。

      回复
    3. 头像
      kiki_666
      Windows 10 · Google Chrome
      @ admin

      我现在在github上面留下了自己的研究仓库,用于提交到梦的阶段研究成果,如果您看了觉得我足够诚心,希望能够提供一个联系方式,或者是能够麻烦联系我:QQ2219911811,诚心请教

      回复