1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.net.watchlist; 18 19 import static android.os.incremental.IncrementalManager.isIncrementalPath; 20 21 import android.annotation.Nullable; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageManager.NameNotFoundException; 27 import android.content.pm.UserInfo; 28 import android.os.Bundle; 29 import android.os.DropBoxManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.Message; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 import android.provider.Settings; 36 import android.text.TextUtils; 37 import android.util.Slog; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.ArrayUtils; 41 import com.android.internal.util.HexDump; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.security.NoSuchAlgorithmException; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.GregorianCalendar; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.concurrent.ConcurrentHashMap; 52 import java.util.concurrent.TimeUnit; 53 54 /** 55 * A Handler class for network watchlist logging on a background thread. 56 */ 57 class WatchlistLoggingHandler extends Handler { 58 59 private static final String TAG = WatchlistLoggingHandler.class.getSimpleName(); 60 private static final boolean DEBUG = NetworkWatchlistService.DEBUG; 61 62 @VisibleForTesting 63 static final int LOG_WATCHLIST_EVENT_MSG = 1; 64 @VisibleForTesting 65 static final int REPORT_RECORDS_IF_NECESSARY_MSG = 2; 66 @VisibleForTesting 67 static final int FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG = 3; 68 69 private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); 70 private static final String DROPBOX_TAG = "network_watchlist_report"; 71 72 private final Context mContext; 73 private final @Nullable DropBoxManager mDropBoxManager; 74 private final ContentResolver mResolver; 75 private final PackageManager mPm; 76 private final WatchlistReportDbHelper mDbHelper; 77 private final WatchlistConfig mConfig; 78 private final WatchlistSettings mSettings; 79 private int mPrimaryUserId = -1; 80 // A cache for uid and apk digest mapping. 81 // As uid won't be reused until reboot, it's safe to assume uid is unique per signature and app. 82 // TODO: Use more efficient data structure. 83 private final ConcurrentHashMap<Integer, byte[]> mCachedUidDigestMap = 84 new ConcurrentHashMap<>(); 85 86 private final FileHashCache mApkHashCache; 87 88 private interface WatchlistEventKeys { 89 String HOST = "host"; 90 String IP_ADDRESSES = "ipAddresses"; 91 String UID = "uid"; 92 String TIMESTAMP = "timestamp"; 93 } 94 WatchlistLoggingHandler(Context context, Looper looper)95 WatchlistLoggingHandler(Context context, Looper looper) { 96 super(looper); 97 mContext = context; 98 mPm = mContext.getPackageManager(); 99 mResolver = mContext.getContentResolver(); 100 mDbHelper = WatchlistReportDbHelper.getInstance(context); 101 mConfig = WatchlistConfig.getInstance(); 102 mSettings = WatchlistSettings.getInstance(); 103 mDropBoxManager = mContext.getSystemService(DropBoxManager.class); 104 mPrimaryUserId = getPrimaryUserId(); 105 if (context.getResources().getBoolean( 106 com.android.internal.R.bool.config_watchlistUseFileHashesCache)) { 107 mApkHashCache = new FileHashCache(this); 108 Slog.i(TAG, "Using file hashes cache."); 109 } else { 110 mApkHashCache = null; 111 } 112 } 113 114 @Override handleMessage(Message msg)115 public void handleMessage(Message msg) { 116 switch (msg.what) { 117 case LOG_WATCHLIST_EVENT_MSG: { 118 final Bundle data = msg.getData(); 119 handleNetworkEvent( 120 data.getString(WatchlistEventKeys.HOST), 121 data.getStringArray(WatchlistEventKeys.IP_ADDRESSES), 122 data.getInt(WatchlistEventKeys.UID), 123 data.getLong(WatchlistEventKeys.TIMESTAMP) 124 ); 125 break; 126 } 127 case REPORT_RECORDS_IF_NECESSARY_MSG: 128 tryAggregateRecords(getLastMidnightTime()); 129 break; 130 case FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG: 131 if (msg.obj instanceof Long) { 132 long lastRecordTime = (Long) msg.obj; 133 tryAggregateRecords(lastRecordTime); 134 } else { 135 Slog.e(TAG, "Msg.obj needs to be a Long object."); 136 } 137 break; 138 default: { 139 Slog.d(TAG, "WatchlistLoggingHandler received an unknown of message."); 140 break; 141 } 142 } 143 } 144 145 /** 146 * Get primary user id. 147 * @return Primary user id. -1 if primary user not found. 148 */ getPrimaryUserId()149 private int getPrimaryUserId() { 150 final UserInfo primaryUserInfo = ((UserManager) mContext.getSystemService( 151 Context.USER_SERVICE)).getPrimaryUser(); 152 if (primaryUserInfo != null) { 153 return primaryUserInfo.id; 154 } 155 return -1; 156 } 157 158 /** 159 * Return if a given package has testOnly is true. 160 */ isPackageTestOnly(int uid)161 private boolean isPackageTestOnly(int uid) { 162 final ApplicationInfo ai; 163 try { 164 final String[] packageNames = mPm.getPackagesForUid(uid); 165 if (packageNames == null || packageNames.length == 0) { 166 Slog.e(TAG, "Couldn't find package: " + Arrays.toString(packageNames)); 167 return false; 168 } 169 ai = mPm.getApplicationInfo(packageNames[0], 0); 170 } catch (NameNotFoundException e) { 171 // Should not happen. 172 return false; 173 } 174 return (ai.flags & ApplicationInfo.FLAG_TEST_ONLY) != 0; 175 } 176 177 /** 178 * Report network watchlist records if we collected enough data. 179 */ reportWatchlistIfNecessary()180 public void reportWatchlistIfNecessary() { 181 final Message msg = obtainMessage(REPORT_RECORDS_IF_NECESSARY_MSG); 182 sendMessage(msg); 183 } 184 forceReportWatchlistForTest(long lastReportTime)185 public void forceReportWatchlistForTest(long lastReportTime) { 186 final Message msg = obtainMessage(FORCE_REPORT_RECORDS_NOW_FOR_TEST_MSG); 187 msg.obj = lastReportTime; 188 sendMessage(msg); 189 } 190 191 /** 192 * Insert network traffic event to watchlist async queue processor. 193 */ asyncNetworkEvent(String host, String[] ipAddresses, int uid)194 public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) { 195 final Message msg = obtainMessage(LOG_WATCHLIST_EVENT_MSG); 196 final Bundle bundle = new Bundle(); 197 bundle.putString(WatchlistEventKeys.HOST, host); 198 bundle.putStringArray(WatchlistEventKeys.IP_ADDRESSES, ipAddresses); 199 bundle.putInt(WatchlistEventKeys.UID, uid); 200 bundle.putLong(WatchlistEventKeys.TIMESTAMP, System.currentTimeMillis()); 201 msg.setData(bundle); 202 sendMessage(msg); 203 } 204 handleNetworkEvent(String hostname, String[] ipAddresses, int uid, long timestamp)205 private void handleNetworkEvent(String hostname, String[] ipAddresses, 206 int uid, long timestamp) { 207 if (DEBUG) { 208 Slog.i(TAG, "handleNetworkEvent with host: " + hostname + ", uid: " + uid); 209 } 210 // Update primary user id if necessary 211 if (mPrimaryUserId == -1) { 212 mPrimaryUserId = getPrimaryUserId(); 213 } 214 215 // Only process primary user data 216 if (UserHandle.getUserId(uid) != mPrimaryUserId) { 217 if (DEBUG) { 218 Slog.i(TAG, "Do not log non-system user records"); 219 } 220 return; 221 } 222 final String cncDomain = searchAllSubDomainsInWatchlist(hostname); 223 if (cncDomain != null) { 224 insertRecord(uid, cncDomain, timestamp); 225 } else { 226 final String cncIp = searchIpInWatchlist(ipAddresses); 227 if (cncIp != null) { 228 insertRecord(uid, cncIp, timestamp); 229 } 230 } 231 } 232 insertRecord(int uid, String cncHost, long timestamp)233 private void insertRecord(int uid, String cncHost, long timestamp) { 234 if (DEBUG) { 235 Slog.i(TAG, "trying to insert record with host: " + cncHost + ", uid: " + uid); 236 } 237 if (!mConfig.isConfigSecure() && !isPackageTestOnly(uid)) { 238 // Skip package if config is not secure and package is not TestOnly app. 239 if (DEBUG) { 240 Slog.i(TAG, "uid: " + uid + " is not test only package"); 241 } 242 return; 243 } 244 final byte[] digest = getDigestFromUid(uid); 245 if (digest == null) { 246 return; 247 } 248 if (mDbHelper.insertNewRecord(digest, cncHost, timestamp)) { 249 Slog.w(TAG, "Unable to insert record for uid: " + uid); 250 } 251 } 252 shouldReportNetworkWatchlist(long lastRecordTime)253 private boolean shouldReportNetworkWatchlist(long lastRecordTime) { 254 final long lastReportTime = Settings.Global.getLong(mResolver, 255 Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, 0L); 256 if (lastRecordTime < lastReportTime) { 257 Slog.i(TAG, "Last report time is larger than current time, reset report"); 258 mDbHelper.cleanup(lastReportTime); 259 return false; 260 } 261 return lastRecordTime >= lastReportTime + ONE_DAY_MS; 262 } 263 tryAggregateRecords(long lastRecordTime)264 private void tryAggregateRecords(long lastRecordTime) { 265 long startTime = System.currentTimeMillis(); 266 try { 267 // Check if it's necessary to generate watchlist report now. 268 if (!shouldReportNetworkWatchlist(lastRecordTime)) { 269 Slog.i(TAG, "No need to aggregate record yet."); 270 return; 271 } 272 Slog.i(TAG, "Start aggregating watchlist records."); 273 if (mDropBoxManager != null && mDropBoxManager.isTagEnabled(DROPBOX_TAG)) { 274 Settings.Global.putLong(mResolver, 275 Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, 276 lastRecordTime); 277 final WatchlistReportDbHelper.AggregatedResult aggregatedResult = 278 mDbHelper.getAggregatedRecords(lastRecordTime); 279 if (aggregatedResult == null) { 280 Slog.i(TAG, "Cannot get result from database"); 281 return; 282 } 283 // Get all digests for watchlist report, it should include all installed 284 // application digests and previously recorded app digests. 285 final List<String> digestsForReport = getAllDigestsForReport(aggregatedResult); 286 final byte[] secretKey = mSettings.getPrivacySecretKey(); 287 final byte[] encodedResult = ReportEncoder.encodeWatchlistReport(mConfig, 288 secretKey, digestsForReport, aggregatedResult); 289 if (encodedResult != null) { 290 addEncodedReportToDropBox(encodedResult); 291 } 292 } else { 293 Slog.w(TAG, "Network Watchlist dropbox tag is not enabled"); 294 } 295 mDbHelper.cleanup(lastRecordTime); 296 } finally { 297 long endTime = System.currentTimeMillis(); 298 Slog.i(TAG, "Milliseconds spent on tryAggregateRecords(): " + (endTime - startTime)); 299 } 300 } 301 302 /** 303 * Get all digests for watchlist report. 304 * It should include: 305 * (1) All installed app digests. We need this because we need to ensure after DP we don't know 306 * if an app is really visited C&C site. 307 * (2) App digests that previously recorded in database. 308 */ 309 @VisibleForTesting getAllDigestsForReport(WatchlistReportDbHelper.AggregatedResult record)310 List<String> getAllDigestsForReport(WatchlistReportDbHelper.AggregatedResult record) { 311 // Step 1: Get all installed application digests. 312 final List<ApplicationInfo> apps = mContext.getPackageManager().getInstalledApplications( 313 PackageManager.MATCH_ALL); 314 final HashSet<String> result = new HashSet<>(apps.size() + record.appDigestCNCList.size()); 315 final int size = apps.size(); 316 for (int i = 0; i < size; i++) { 317 byte[] digest = getDigestFromUid(apps.get(i).uid); 318 if (digest != null) { 319 result.add(HexDump.toHexString(digest)); 320 } 321 } 322 // Step 2: Add all digests from records 323 result.addAll(record.appDigestCNCList.keySet()); 324 return new ArrayList<>(result); 325 } 326 addEncodedReportToDropBox(byte[] encodedReport)327 private void addEncodedReportToDropBox(byte[] encodedReport) { 328 mDropBoxManager.addData(DROPBOX_TAG, encodedReport, 0); 329 } 330 331 /** 332 * Get app digest from app uid. 333 * Return null if system cannot get digest from uid. 334 */ 335 @Nullable getDigestFromUid(int uid)336 private byte[] getDigestFromUid(int uid) { 337 return mCachedUidDigestMap.computeIfAbsent(uid, key -> { 338 final String[] packageNames = mPm.getPackagesForUid(key); 339 final int userId = UserHandle.getUserId(uid); 340 if (!ArrayUtils.isEmpty(packageNames)) { 341 for (String packageName : packageNames) { 342 try { 343 final String apkPath = mPm.getPackageInfoAsUser(packageName, 344 PackageManager.MATCH_DIRECT_BOOT_AWARE 345 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId) 346 .applicationInfo.publicSourceDir; 347 if (TextUtils.isEmpty(apkPath)) { 348 Slog.w(TAG, "Cannot find apkPath for " + packageName); 349 continue; 350 } 351 if (isIncrementalPath(apkPath)) { 352 // Do not scan incremental fs apk, as the whole APK may not yet 353 // be available, so we can't compute the hash of it. 354 Slog.i(TAG, "Skipping incremental path: " + packageName); 355 continue; 356 } 357 return mApkHashCache != null 358 ? mApkHashCache.getSha256Hash(new File(apkPath)) 359 : DigestUtils.getSha256Hash(new File(apkPath)); 360 } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) { 361 Slog.e(TAG, "Cannot get digest from uid: " + key 362 + ",pkg: " + packageName, e); 363 return null; 364 } 365 } 366 } 367 // Not able to find a package name for this uid, possibly the package is installed on 368 // another user. 369 return null; 370 }); 371 } 372 373 /** 374 * Search if any ip addresses are in watchlist. 375 * 376 * @param ipAddresses Ip address that you want to search in watchlist. 377 * @return Ip address that exists in watchlist, null if it does not match anything. 378 */ 379 @Nullable searchIpInWatchlist(String[] ipAddresses)380 private String searchIpInWatchlist(String[] ipAddresses) { 381 for (String ipAddress : ipAddresses) { 382 if (isIpInWatchlist(ipAddress)) { 383 return ipAddress; 384 } 385 } 386 return null; 387 } 388 389 /** Search if the ip is in watchlist */ isIpInWatchlist(String ipAddr)390 private boolean isIpInWatchlist(String ipAddr) { 391 if (ipAddr == null) { 392 return false; 393 } 394 return mConfig.containsIp(ipAddr); 395 } 396 397 /** Search if the host is in watchlist */ isHostInWatchlist(String host)398 private boolean isHostInWatchlist(String host) { 399 if (host == null) { 400 return false; 401 } 402 return mConfig.containsDomain(host); 403 } 404 405 /** 406 * Search if any sub-domain in host is in watchlist. 407 * 408 * @param host Host that we want to search. 409 * @return Domain that exists in watchlist, null if it does not match anything. 410 */ 411 @Nullable searchAllSubDomainsInWatchlist(String host)412 private String searchAllSubDomainsInWatchlist(String host) { 413 if (host == null) { 414 return null; 415 } 416 final String[] subDomains = getAllSubDomains(host); 417 for (String subDomain : subDomains) { 418 if (isHostInWatchlist(subDomain)) { 419 return subDomain; 420 } 421 } 422 return null; 423 } 424 425 /** Get all sub-domains in a host */ 426 @VisibleForTesting 427 @Nullable getAllSubDomains(String host)428 static String[] getAllSubDomains(String host) { 429 if (host == null) { 430 return null; 431 } 432 final ArrayList<String> subDomainList = new ArrayList<>(); 433 subDomainList.add(host); 434 int index = host.indexOf("."); 435 while (index != -1) { 436 host = host.substring(index + 1); 437 if (!TextUtils.isEmpty(host)) { 438 subDomainList.add(host); 439 } 440 index = host.indexOf("."); 441 } 442 return subDomainList.toArray(new String[0]); 443 } 444 getLastMidnightTime()445 static long getLastMidnightTime() { 446 return getMidnightTimestamp(0); 447 } 448 getMidnightTimestamp(int daysBefore)449 static long getMidnightTimestamp(int daysBefore) { 450 java.util.Calendar date = new GregorianCalendar(); 451 // reset hour, minutes, seconds and millis 452 date.set(java.util.Calendar.HOUR_OF_DAY, 0); 453 date.set(java.util.Calendar.MINUTE, 0); 454 date.set(java.util.Calendar.SECOND, 0); 455 date.set(java.util.Calendar.MILLISECOND, 0); 456 date.add(java.util.Calendar.DAY_OF_MONTH, -daysBefore); 457 return date.getTimeInMillis(); 458 } 459 } 460