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