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