• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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