OSDN Git Service

Add protection against LE scanning abuse
authorAjay Panicker <apanicke@google.com>
Wed, 13 Apr 2016 21:11:57 +0000 (14:11 -0700)
committerAjay Panicker <apanicke@google.com>
Fri, 6 May 2016 20:32:35 +0000 (20:32 +0000)
Added two checks to prevent abuse. The first check ensures that an
app doesn't scan too frequently in a certain time period. It is
allowed to scan again after its oldest scan exceedes said time
period. The second check ensures that an app doesn't scan for too
long. Upon starting a scan, this code waits a certain amount of time.
If the app is still scanning by that point, this code stops
the scan and forces the app to use opportunistic scanning instead.

Bug: 27357274
Change-Id: Ic99ac1f838e15ed99fe2fae643ef073d74b5c96b

src/com/android/bluetooth/gatt/AppScanStats.java
src/com/android/bluetooth/gatt/GattService.java
src/com/android/bluetooth/gatt/ScanClient.java
src/com/android/bluetooth/gatt/ScanManager.java

index b9ada40..e73f415 100644 (file)
@@ -42,6 +42,7 @@ import com.android.bluetooth.btservice.BluetoothProto;
         long duration;
         long timestamp;
         boolean opportunistic;
+        boolean timeout;
         boolean background;
         int results;
 
@@ -57,6 +58,12 @@ import com.android.bluetooth.btservice.BluetoothProto;
 
     static final int NUM_SCAN_DURATIONS_KEPT = 5;
 
+    // This constant defines the time window an app can scan multiple times.
+    // Any single app can scan up to |NUM_SCAN_DURATIONS_KEPT| times during
+    // this window. Once they reach this limit, they must wait until their
+    // earliest recorded scan exits this window.
+    static final long EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000;
+
     String appName;
     int scansStarted = 0;
     int scansStopped = 0;
@@ -134,6 +141,25 @@ import com.android.bluetooth.btservice.BluetoothProto;
         gattService.addScanEvent(scanEvent);
     }
 
+    synchronized void setScanTimeout() {
+        if (!isScanning)
+          return;
+
+        if (!lastScans.isEmpty()) {
+            LastScan curr = lastScans.get(lastScans.size() - 1);
+            curr.timeout = true;
+        }
+    }
+
+    synchronized boolean isScanningTooFrequently() {
+        if (lastScans.size() < NUM_SCAN_DURATIONS_KEPT) {
+            return false;
+        }
+
+        return (System.currentTimeMillis() - lastScans.get(0).timestamp) <
+            EXCESSIVE_SCANNING_PERIOD_MS;
+    }
+
     // This function truncates the app name for privacy reasons. Apps with
     // four part package names or more get truncated to three parts, and apps
     // with three part package names names get truncated to two. Apps with two
@@ -183,6 +209,7 @@ import com.android.bluetooth.btservice.BluetoothProto;
         if (isRegistered) sb.append(" (Registered)");
         if (lastScan.opportunistic) sb.append(" (Opportunistic)");
         if (lastScan.background) sb.append(" (Background)");
+        if (lastScan.timeout) sb.append(" (Forced-Opportunistic)");
         sb.append("\n");
 
         sb.append("  LE scans (started/stopped)         : " +
@@ -209,6 +236,7 @@ import com.android.bluetooth.btservice.BluetoothProto;
                 sb.append(scan.duration + "ms ");
                 if (scan.opportunistic) sb.append("Opp ");
                 if (scan.background) sb.append("Back ");
+                if (scan.timeout) sb.append("Forced ");
                 sb.append(scan.results + " results");
                 sb.append("\n");
             }
index 3051edb..acc6350 100644 (file)
@@ -66,6 +66,7 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 /**
  * Provides Bluetooth Gatt profile, as a service in
  * the Bluetooth application.
@@ -1300,7 +1301,16 @@ public class GattService extends ProfileService {
         } else {
             app = mClientMap.getAppScanStatsById(appIf);
         }
-        if (app != null) app.recordScanStart(settings);
+
+        if (app != null) {
+            if (app.isScanningTooFrequently() &&
+                checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED) != PERMISSION_GRANTED) {
+                Log.e(TAG, "App '" + app.appName + "' is scanning too frequently");
+                return;
+            }
+            scanClient.stats = app;
+            app.recordScanStart(settings);
+        }
 
         mScanManager.startScan(scanClient);
     }
index 64d3e1f..b7bfd33 100644 (file)
@@ -43,6 +43,8 @@ import java.util.UUID;
     // Pre-M apps are allowed to get scan results even if location is disabled
     boolean legacyForegroundApp;
 
+    AppScanStats stats = null;
+
     private static final ScanSettings DEFAULT_SCAN_SETTINGS = new ScanSettings.Builder()
             .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
 
index 4ddbac0..dca74c4 100644 (file)
@@ -64,6 +64,10 @@ public class ScanManager {
     private static final int MSG_START_BLE_SCAN = 0;
     private static final int MSG_STOP_BLE_SCAN = 1;
     private static final int MSG_FLUSH_BATCH_RESULTS = 2;
+    private static final int MSG_SCAN_TIMEOUT = 3;
+
+    // Maximum msec before scan gets downgraded to opportunistic
+    private static final int SCAN_TIMEOUT_MS = 5 * 60 * 1000;
 
     private static final String ACTION_REFRESH_BATCHED_SCAN =
             "com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN";
@@ -188,6 +192,9 @@ public class ScanManager {
                 case MSG_FLUSH_BATCH_RESULTS:
                     handleFlushBatchResults(client);
                     break;
+                case MSG_SCAN_TIMEOUT:
+                    mScanNative.regularScanTimeout();
+                    break;
                 default:
                     // Shouldn't happen.
                     Log.e(TAG, "received an unkown message : " + msg.what);
@@ -216,6 +223,14 @@ public class ScanManager {
                 mScanNative.startRegularScan(client);
                 if (!mScanNative.isOpportunisticScanClient(client)) {
                     mScanNative.configureRegularScanParams();
+
+                    if (!mScanNative.isFirstMatchScanClient(client)) {
+                        Message msg = mHandler.obtainMessage(MSG_SCAN_TIMEOUT);
+                        msg.obj = client;
+                        // Only one timeout message should exist at any time
+                        mHandler.removeMessages(SCAN_TIMEOUT_MS);
+                        mHandler.sendMessageDelayed(msg, SCAN_TIMEOUT_MS);
+                    }
                 }
             }
         }
@@ -225,6 +240,11 @@ public class ScanManager {
             if (client == null) return;
             if (mRegularScanClients.contains(client)) {
                 mScanNative.stopRegularScan(client);
+
+                if (mScanNative.numRegularScanClients() == 0) {
+                    mHandler.removeMessages(MSG_SCAN_TIMEOUT);
+                }
+
                 if (!mScanNative.isOpportunisticScanClient(client)) {
                     mScanNative.configureRegularScanParams();
                 }
@@ -484,6 +504,10 @@ public class ScanManager {
             return client.settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
         }
 
+        private boolean isFirstMatchScanClient(ScanClient client) {
+            return (client.settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
+        }
+
         private void resetBatchScan(ScanClient client) {
             int clientIf = client.clientIf;
             BatchScanParams batchScanParams = getBatchScanParams();
@@ -624,6 +648,36 @@ public class ScanManager {
             removeScanFilters(client.clientIf);
         }
 
+        void regularScanTimeout() {
+            for (ScanClient client : mRegularScanClients) {
+                if (!isOpportunisticScanClient(client) && !isFirstMatchScanClient(client)) {
+                    logd("clientIf set to scan opportunisticly: " + client.clientIf);
+                    setOpportunisticScanClient(client);
+                    client.stats.setScanTimeout();
+                }
+            }
+
+            // The scan should continue for background scans
+            configureRegularScanParams();
+            if (numRegularScanClients() == 0) {
+                logd("stop scan");
+                gattClientScanNative(false);
+            }
+        }
+
+        void setOpportunisticScanClient(ScanClient client) {
+            // TODO: Add constructor to ScanSettings.Builder
+            // that can copy values from an existing ScanSettings object
+            ScanSettings.Builder builder = new ScanSettings.Builder();
+            ScanSettings settings = client.settings;
+            builder.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC);
+            builder.setCallbackType(settings.getCallbackType());
+            builder.setScanResultType(settings.getScanResultType());
+            builder.setReportDelay(settings.getReportDelayMillis());
+            builder.setNumOfMatches(settings.getNumOfMatches());
+            client.settings = builder.build();
+        }
+
         // Find the scan client information
         ScanClient getClient(int clientIf) {
             for (ScanClient client : mRegularScanClients) {