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