概要

  • Android で GPS の位置偽装する方法
  • Technical な興味で試しただけ。悪用しないように。
  • ROM をビルドできる環境があれば簡単に位置偽装はできる。ま、ぐぐるとやってそうな人はいっぱい出てくるしね。
    • Pixel 3 が日本にも来そうだし最新の Android をビルドできる環境は整いそう
  • 4年前(2014/7月ぐらい)に試していたもの。最近別件で Galaxy Nexus を使った時に Ingress を試してみたらまだ使えたのでメモを残してみた。ただ滑らかには動かせない、ランダムに動かされてしまう。
  • 今どきのアプリではチェックされててまともには使えないと思われる。アプリによっては突然アカウントが BAN されるので注意。

試したアプリ

  • Ingress

    • まだ一応使えるけどまともに動けない。一定速度で動かすとランダムに動かされる。ランダム要素を入れて動かすと多少ましになるが。(というのに気付いたのはアプリのタイムスタンプから 2016/6/15 あたりだけど)
    • 4年前は滑らかに動けたので、何かしら対策はされてる。当時はどこにでも行けて楽しかった。すぐに BAN されたけどw
    • 昨日試したもの(2倍速再生)
      • ↑の右側の緑の◎が↓のサンプルアプリ
      • screenrecord は Android 4.4 からで、4.3 では使えない。。。ので、フリーなキャプチャアプリ(左側の赤い○)を使ったけど、かなり重くなった。。ので2倍速再生で。
      • ランダムに動かされてしまい、思ったようには動かせない。これでもランダム要素を入れて多少マシになってるはずなのだが。
  • ポケモン GO

    • Root 取ってる ROM だと起動できない
    • Magisk とかで回避できる?
  • Resources

    • 数分使っただけで ban された
    • 3 アカウント全て数分の使用で ban されたw
  • Google Maps

    • 特に問題なく使える
  • 他のは試したことない。。

  • どのアプリでも、位置情報に関連する 3G/LTE/Wi-Fi は Off にして Bluetooth で tethering しといた方がよさそう

詳細

Framework 側

  • ROM を Build できる環境を構築して frameworks/base/services/java/com/android/server/location/GpsLocationProvider.java に以下のパッチをあてる
    (Android 4.3.1 用のパッチ。当時のままで編集してないのでデバッグ文とかコメントとか適当だけど気にしないで^^; 公開するつもりなく書いてたものなので)
    • 内容は、
      • ServerThread で Unix domain socket で accept 待ちして、Java アプリから位置の差分を受ける
      • ReportPrevLocationThread で位置情報を定期的に reportLocation() する
        • 3 satellites が見つかるまでは位置情報は reort できないはずなので、一旦待つ
        • GPS が位置情報を report しなくても、1つ前の位置情報を定期的に reort する
        • その時に、アプリから受けてた差分を加算して report する。実際に加算しているのは reportLocation() の中。GPSからの report 時にも加算する様に。
    • パッチあてた後のソースは、こちら
diff --git a/services/java/com/android/server/location/GpsLocationProvider.java b/services/java/com/android/server/location/GpsLocationProvider.java
index 8c88cab..66d5815 100644
--- a/services/java/com/android/server/location/GpsLocationProvider.java
+++ b/services/java/com/android/server/location/GpsLocationProvider.java
@@ -82,6 +82,15 @@ import java.util.Date;
 import java.util.Map.Entry;
 import java.util.Properties;
 
+// Salt added {
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Random;
+// } Salt added
+
 /**
  * A GPS implementation of LocationProvider used by LocationManager.
  *
@@ -1000,6 +1009,12 @@ public class GpsLocationProvider implements LocationProviderInterface {
 
     private void startNavigating(boolean singleShot) {
         if (!mStarted) {
+            // Salt added {
+            Log.e("Salt", "GpsLocationProvider.java startNavigating()");
+            InitServer();
+            InitReportPrevLocation();
+            // } Salt added
+
             if (DEBUG) Log.d(TAG, "startNavigating, singleShot is " + singleShot);
             mTimeToFirstFix = 0;
             mLastFixTime = 0;
@@ -1066,6 +1081,11 @@ public class GpsLocationProvider implements LocationProviderInterface {
     private void stopNavigating() {
         if (DEBUG) Log.d(TAG, "stopNavigating");
         if (mStarted) {
+            // Salt added {
+            Log.e("Salt", "GpsLocationProvider.java stopNavigating()");
+            UninitServer();
+            UninitReportPrevLocation();
+            // } Salt added
             mStarted = false;
             mSingleShot = false;
             native_stop();
@@ -1091,6 +1111,128 @@ public class GpsLocationProvider implements LocationProviderInterface {
         return ((mEngineCapabilities & capability) != 0);
     }
 
+    // Salt added {
+    private final String SOCKET_NAME = "salt.modify.location";
+    private LocalServerSocket mLocalServerSocket;
+    private double mdLatFake;
+    private double mdLongFake;
+    private volatile boolean mbAbortServerThread;		// referred by 2 threads
+    private volatile boolean mbGotFakeLocation;			// referred by 2 threads
+    class ServerThread extends Thread {
+        @Override
+        public void run() {
+            while (!mbAbortServerThread) {
+                try {
+                    if (mLocalServerSocket == null) {
+                        Log.e("Salt", "GpsLocationProvider.java ServerThread mLocalServerSocket == null !!!");
+                        break;
+                    }
+
+                    LocalSocket ls = mLocalServerSocket.accept();
+                    // ls.setReuseAddress(true);
+                    // ls.setSoLinger(true, 0);
+
+                    final InputStream is = ls.getInputStream();
+                    byte [] buf = new byte[8];
+
+                    is.read(buf);
+                    mdLatFake  = ByteBuffer.wrap(buf).getDouble();
+
+                    is.read(buf);
+                    mdLongFake = ByteBuffer.wrap(buf).getDouble();
+
+                    ls.close();
+
+                    mbGotFakeLocation = true;
+                    Log.e("Salt", "GpsLocationProvider.java ServerThread got mdLatFake = " + mdLatFake + " / mdLongFake = " + mdLongFake);
+
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+
+            UninitServer();
+        }
+    }
+
+    private void InitServer() {
+        Log.e("Salt", "GpsLocationProvider.java InitServer()");
+        mbAbortServerThread = false;
+        mdLatFake  = 0;
+        mdLongFake = 0;
+        try {
+            if (mLocalServerSocket == null) {
+                mLocalServerSocket = new LocalServerSocket(SOCKET_NAME);
+                ServerThread st = new ServerThread();
+                st.start();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void UninitServer() {
+        Log.e("Salt", "GpsLocationProvider.java UninitServer()");
+        Log.e("Salt", "GpsLocationProvider.java UninitServer() skipped, because closing socket takes some time and it fails if open request comes while closing it.");
+        // mbAbortServerThread = true;
+        // try {
+        //     if (mLocalServerSocket != null) {
+        //         mLocalServerSocket.close();
+        //         mLocalServerSocket = null;
+        //     }
+        // } catch (IOException e) {
+        //     e.printStackTrace();
+        // }
+    }
+
+    private double mdLatPrev;		// = 35.33686;	// Tsujido station
+    private double mdLongPrev;		// = 139.44586;
+    private double mdAltitudePrev;	// = -14.0d;
+    private float mfSpeedPrev;		// = 0.06255f;
+    private float mfBearingPrev;	// = 164.0f;
+    private float mfAccuracyPrev;	// = 25.0f;
+    private int mnFlagsPrev;		// = 0x1f;	// LOCATION_HAS_LAT_LONG | LOCATION_HAS_ALTITUDE | LOCATION_HAS_SPEED | LOCATION_HAS_ACCURACY;
+    private volatile boolean mbGotSatellites;			// referred by 2 threads
+    private volatile boolean mbAbortReportPrevLocationThread;	// referred by 2 threads
+    private ReportPrevLocationThread mReportPrevLocationThread;
+    // private Random mRnd = new Random();
+    class ReportPrevLocationThread extends Thread {
+        @Override
+        public void run() {
+            do {
+                if (mbGotSatellites && mdLatPrev != 0) {	// reports fake location only after more than 3 satellites are found once and location is reported once
+                    if (mbGotFakeLocation) {
+                        mbGotFakeLocation = false;		// clear flag
+
+                        Log.e("Salt", "GpsLocationProvider.java ReportPrevLocationThread call reportLocation()");
+		        reportLocation(mnFlagsPrev, mdLatPrev, mdLongPrev, mdAltitudePrev, mfSpeedPrev, mfBearingPrev, mfAccuracyPrev, System.currentTimeMillis());
+                    }
+                }
+                try {
+                    Thread.sleep(1000);	// 900 + mRnd.nextInt(200));
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            } while (!mbAbortReportPrevLocationThread);
+        }
+    }
+
+    private void InitReportPrevLocation() {
+        Log.e("Salt", "GpsLocationProvider.java InitReportPrevLocation()");
+        mbAbortReportPrevLocationThread = false;
+        mbGotSatellites = false;
+        if (mReportPrevLocationThread == null) {
+            mReportPrevLocationThread = new ReportPrevLocationThread();
+            mReportPrevLocationThread.start();
+        }
+    }
+
+    private void UninitReportPrevLocation() {
+        mbAbortReportPrevLocationThread = true;
+        mReportPrevLocationThread = null;
+    }
+    // } Salt added
+
 
     /**
      * called from native code to update our position.
@@ -1101,6 +1243,23 @@ public class GpsLocationProvider implements LocationProviderInterface {
                 " timestamp: " + timestamp);
 
         synchronized (mLocation) {
+            // Salt added {
+            Log.e("Salt", "GpsLocationProvider.java reportLocation() flags:" + flags + " lat:" + latitude + " long:" + longitude + " altitude:" + altitude + " speed:" + speed + " bearing:" + bearing + " accuracy:" + accuracy + " timestamp:" + timestamp);
+
+            mnFlagsPrev    = flags;
+            mdLatPrev      = latitude;
+            mdLongPrev     = longitude;
+            mdAltitudePrev = altitude;
+            mfSpeedPrev    = speed;
+            mfBearingPrev  = bearing;
+            mfAccuracyPrev = accuracy;
+
+            latitude  += mdLatFake;
+            longitude += mdLongFake;
+
+            Log.e("Salt", "GpsLocationProvider.java reportLocation() modified lat: " + latitude + " long: " + longitude);
+            // } Salt added
+
             mLocationFlags = flags;
             if ((flags & LOCATION_HAS_LAT_LONG) == LOCATION_HAS_LAT_LONG) {
                 mLocation.setLatitude(latitude);
@@ -1246,6 +1405,17 @@ public class GpsLocationProvider implements LocationProviderInterface {
 
         int svCount = native_read_sv_status(mSvs, mSnrs, mSvElevations, mSvAzimuths, mSvMasks);
 
+        // Salt added {
+        if (svCount >= 3) {
+            mbGotSatellites = true;
+        }
+        else {
+            if (mbGotSatellites) {
+                return;		// Ignore if svCount is less than 3, because at least 3 satellites are needed to calculate location
+            }
+        }
+        // } Salt added
+
         synchronized (mListeners) {
             int size = mListeners.size();
             for (int i = 0; i < size; i++) {

Android アプリ側

  • socket に connect() して位置の差分を write() する
  • 全ソースは、こちら
    • これは GUI で操作するもの。上↑の動画の右側の緑の◎で操作する
    • 加速度センサーを使ってデバイスの傾きで操作する方が楽。だけど、ソースが汚すぎて公開できず。
private final String SOCKET_NAME = "salt.modify.location";

private void sendLocation() {
    try {
        mLocalSocket = new LocalSocket();
        LocalSocketAddress addr = new LocalSocketAddress(SOCKET_NAME);
        
        mLocalSocket.connect(addr);
        
        // lat
        ByteBuffer buffer = ByteBuffer.allocate(8);        // double = 64bits
        buffer.putDouble(mdLat);
        mLocalSocket.getOutputStream().write(buffer.array());
        
        // long
        buffer.clear();
        buffer.putDouble(mdLong);
        mLocalSocket.getOutputStream().write(buffer.array());
        
        mLocalSocket.close();
        
    } catch (IOException e) {
        // e.printStackTrace();
        Log.e(TAG, "sendLocation() couldn't find socket");
    }

}

Framework 側のその他の偽装

  • API で簡単に取得できる Build 番号や Android Version は、実存するものに変えておく

  • build/core/Makefile
    BUILD_ID は JLS36I になっていて、Official な ROM としてはリリースされてないはずなので、JWR66Y に変更

diff --git a/core/Makefile b/core/Makefile
index 55ab6a5..91e37d7 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -103,7 +103,8 @@ endif
BUILD_VERSION_TAGS := $(subst $(space),$(comma),$(sort $(BUILD_VERSION_TAGS)))

# A human-readable string that descibes this build in detail.
-build_desc := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT) $(PLATFORM_VERSION) $(BUILD_ID) $(BUILD_NUMBER) $(BUILD_VERSION_TAGS)
+# Salt changed from build_desc := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT) $(PLATFORM_VERSION) $(BUILD_ID) $(BUILD_NUMBER) $(BUILD_VERSION_TAGS)
+build_desc := $(BUILD_NUMBER)
$(INSTALLED_BUILD_PROP_TARGET): PRIVATE_BUILD_DESC := $(build_desc)

# The string used to uniquely identify this build;  used by the OTA server.
@@ -127,7 +128,8 @@ ifeq ($(TARGET_BUILD_VARIANT),user)
endif
else
# Non-user builds should show detailed build information
-  BUILD_DISPLAY_ID := $(build_desc)
+  # Salt changed from BUILD_DISPLAY_ID := $(build_desc)
+  BUILD_DISPLAY_ID := JWR66Y
endif

# Whether there is default locale set in PRODUCT_PROPERTY_OVERRIDES
  • build/core/version_defaults.mk
    Galaxy Nexus には 4.3.1 はないはずなので、4.3 に書き換え
diff --git a/core/version_defaults.mk b/core/version_defaults.mk
index 9c3b749..27a624b 100644
--- a/core/version_defaults.mk
+++ b/core/version_defaults.mk
@@ -41,7 +41,8 @@ ifeq "" "$(PLATFORM_VERSION)"
# which is the version that we reveal to the end user.
# Update this value when the platform version changes (rather
# than overriding it somewhere else).  Can be an arbitrary string.
-  PLATFORM_VERSION := 4.3.1
+  # Salt changed from PLATFORM_VERSION := 4.3.1
+  PLATFORM_VERSION := 4.3
endif

ifeq "" "$(PLATFORM_SDK_VERSION)"
@@ -94,5 +95,6 @@ ifeq "" "$(BUILD_NUMBER)"
# If no BUILD_NUMBER is set, create a useful "I am an engineering build
# from this date/time" value.  Make it start with a non-digit so that
# anyone trying to parse it as an integer will probably get "0".
-  BUILD_NUMBER := eng.$(USER).$(shell date +%Y%m%d.%H%M%S)
+  # Salt changed BUILD_NUMBER := eng.$(USER).$(shell date +%Y%m%d.%H%M%S)
+  BUILD_NUMBER := JWR66Y
endif
  • device/samsung/maguro/full_maguro.mk
    “AOSP on Maguro” という名前はまずいので “Galaxy Nexus” に変更
diff --git a/full_maguro.mk b/full_maguro.mk
index 810642e..7faf9f8 100644
--- a/full_maguro.mk
+++ b/full_maguro.mk
@@ -35,5 +35,6 @@ $(call inherit-product, device/samsung/maguro/device.mk)
PRODUCT_NAME := full_maguro
PRODUCT_DEVICE := maguro
PRODUCT_BRAND := Android
-PRODUCT_MODEL := AOSP on Maguro
+# Salt changed from PRODUCT_MODEL := AOSP on Maguro
+PRODUCT_MODEL := Galaxy Nexus
PRODUCT_RESTRICT_VENDOR_FILES := true
  • 他に気になるところ(未確認)
    • kernel version
      • kernel/Makefil の NAME, VERSION, PATCHLEVEL, SUBLEVEL あたりを書き換えればいいか?

その他

  • Unix domain socket の確認方法

    • Android の場合

      • cat /proc/net/unix で見る
      • toybox netstat -x で見る
      • busybox を入れて busybox netstat -x で見る
    • 普通の Linux の場合

      • netstat -x で UNIX domain socket
      • netstat -t で TCP socket
      • netstat -u で UDP socket
      • a で全部、l で listen してる port のみ
      • p で PID 表示
      • n でアドレスで表示

まとめ

  • 位置偽装そのものは簡単にできる。が、アプリ側のチェックがあって簡単には使えない。使えたとしても使ってはいけない。利用規約違反になる。