From 1fdc7c138db776b02bc751fd7a80c519ea3324d1 Mon Sep 17 00:00:00 2001 From: Ajay Panicker Date: Wed, 13 Apr 2016 14:11:57 -0700 Subject: [PATCH] Add protection against LE scanning abuse 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 | 28 ++++++++++++ src/com/android/bluetooth/gatt/GattService.java | 12 +++++- src/com/android/bluetooth/gatt/ScanClient.java | 2 + src/com/android/bluetooth/gatt/ScanManager.java | 54 ++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/com/android/bluetooth/gatt/AppScanStats.java b/src/com/android/bluetooth/gatt/AppScanStats.java index b9ada40c..e73f4151 100644 --- a/src/com/android/bluetooth/gatt/AppScanStats.java +++ b/src/com/android/bluetooth/gatt/AppScanStats.java @@ -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"); } diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java index 3051edbb..acc6350e 100644 --- a/src/com/android/bluetooth/gatt/GattService.java +++ b/src/com/android/bluetooth/gatt/GattService.java @@ -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); } diff --git a/src/com/android/bluetooth/gatt/ScanClient.java b/src/com/android/bluetooth/gatt/ScanClient.java index 64d3e1ff..b7bfd33f 100644 --- a/src/com/android/bluetooth/gatt/ScanClient.java +++ b/src/com/android/bluetooth/gatt/ScanClient.java @@ -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(); diff --git a/src/com/android/bluetooth/gatt/ScanManager.java b/src/com/android/bluetooth/gatt/ScanManager.java index 4ddbac0c..dca74c4b 100644 --- a/src/com/android/bluetooth/gatt/ScanManager.java +++ b/src/com/android/bluetooth/gatt/ScanManager.java @@ -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) { -- 2.11.0