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