/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.pm; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.os.Process; import android.os.ServiceManager; import android.util.ByteStringUtils; import android.util.EventLog; import android.util.Log; import com.android.server.pm.dex.DynamicCodeLogger; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Scheduled jobs related to logging of app dynamic code loading. The idle logging job runs daily * while idle and charging and calls {@link DynamicCodeLogger} to write dynamic code information * to the event log. The audit watching job scans the event log periodically while idle to find AVC * audit messages indicating use of dynamic native code and adds the information to * {@link DynamicCodeLogger}. * {@hide} */ public class DynamicCodeLoggingService extends JobService { private static final String TAG = DynamicCodeLoggingService.class.getName(); private static final boolean DEBUG = false; private static final int IDLE_LOGGING_JOB_ID = 2030028; private static final int AUDIT_WATCHING_JOB_ID = 203142925; private static final long IDLE_LOGGING_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1); private static final long AUDIT_WATCHING_PERIOD_MILLIS = TimeUnit.HOURS.toMillis(2); private static final int AUDIT_AVC = 1400; // Defined in linux/audit.h private static final String AVC_PREFIX = "type=" + AUDIT_AVC + " "; private static final Pattern EXECUTE_NATIVE_AUDIT_PATTERN = Pattern.compile(".*\\bavc: granted \\{ execute(?:_no_trans|) \\} .*" + "\\bpath=(?:\"([^\" ]*)\"|([0-9A-F]+)) .*" + "\\bscontext=u:r:untrusted_app(?:_25|_27)?:.*" + "\\btcontext=u:object_r:app_data_file:.*" + "\\btclass=file\\b.*"); private volatile boolean mIdleLoggingStopRequested = false; private volatile boolean mAuditWatchingStopRequested = false; /** * Schedule our jobs with the {@link JobScheduler}. */ public static void schedule(Context context) { ComponentName serviceName = new ComponentName( "android", DynamicCodeLoggingService.class.getName()); JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); js.schedule(new JobInfo.Builder(IDLE_LOGGING_JOB_ID, serviceName) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setPeriodic(IDLE_LOGGING_PERIOD_MILLIS) .build()); js.schedule(new JobInfo.Builder(AUDIT_WATCHING_JOB_ID, serviceName) .setRequiresDeviceIdle(true) .setRequiresBatteryNotLow(true) .setPeriodic(AUDIT_WATCHING_PERIOD_MILLIS) .build()); if (DEBUG) { Log.d(TAG, "Jobs scheduled"); } } @Override public boolean onStartJob(JobParameters params) { int jobId = params.getJobId(); if (DEBUG) { Log.d(TAG, "onStartJob " + jobId); } switch (jobId) { case IDLE_LOGGING_JOB_ID: mIdleLoggingStopRequested = false; new IdleLoggingThread(params).start(); return true; // Job is running on another thread case AUDIT_WATCHING_JOB_ID: mAuditWatchingStopRequested = false; new AuditWatchingThread(params).start(); return true; // Job is running on another thread default: // Shouldn't happen, but indicate nothing is running. return false; } } @Override public boolean onStopJob(JobParameters params) { int jobId = params.getJobId(); if (DEBUG) { Log.d(TAG, "onStopJob " + jobId); } switch (jobId) { case IDLE_LOGGING_JOB_ID: mIdleLoggingStopRequested = true; return true; // Requests job be re-scheduled. case AUDIT_WATCHING_JOB_ID: mAuditWatchingStopRequested = true; return true; // Requests job be re-scheduled. default: return false; } } private static DynamicCodeLogger getDynamicCodeLogger() { PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package"); return pm.getDexManager().getDynamicCodeLogger(); } private class IdleLoggingThread extends Thread { private final JobParameters mParams; IdleLoggingThread(JobParameters params) { super("DynamicCodeLoggingService_IdleLoggingJob"); mParams = params; } @Override public void run() { if (DEBUG) { Log.d(TAG, "Starting IdleLoggingJob run"); } DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger(); for (String packageName : dynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) { if (mIdleLoggingStopRequested) { Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request"); return; } dynamicCodeLogger.logDynamicCodeLoading(packageName); } jobFinished(mParams, /* reschedule */ false); if (DEBUG) { Log.d(TAG, "Finished IdleLoggingJob run"); } } } private class AuditWatchingThread extends Thread { private final JobParameters mParams; AuditWatchingThread(JobParameters params) { super("DynamicCodeLoggingService_AuditWatchingJob"); mParams = params; } @Override public void run() { if (DEBUG) { Log.d(TAG, "Starting AuditWatchingJob run"); } if (processAuditEvents()) { jobFinished(mParams, /* reschedule */ false); if (DEBUG) { Log.d(TAG, "Finished AuditWatchingJob run"); } } } private boolean processAuditEvents() { // Scan the event log for SELinux (avc) audit messages indicating when an // (untrusted) app has executed native code from an app data // file. Matches are recorded in DynamicCodeLogger. // // These messages come from the kernel audit system via logd. (Note that // some devices may not generate these messages at all, or the format may // be different, in which case nothing will be recorded.) // // The messages use the auditd tag and the uid of the app that executed // the code. // // A typical message might look like this: // type=1400 audit(0.0:521): avc: granted { execute } for comm="executable" // path="/data/data/com.dummy.app/executable" dev="sda13" ino=1655302 // scontext=u:r:untrusted_app_27:s0:c66,c257,c512,c768 // tcontext=u:object_r:app_data_file:s0:c66,c257,c512,c768 tclass=file // // The information we want is the uid and the path. (Note this may be // either a quoted string, as shown above, or a sequence of hex-encoded // bytes.) // // On each run we process all the matching events in the log. This may // mean re-processing events we have already seen, and in any case there // may be duplicate events for the same app+file. These are de-duplicated // by DynamicCodeLogger. // // Note that any app can write a message to the event log, including one // that looks exactly like an AVC audit message, so the information may // be spoofed by an app; in such a case the uid we see will be the app // that generated the spoof message. try { int[] tags = { EventLog.getTagCode("auditd") }; if (tags[0] == -1) { // auditd is not a registered tag on this system, so there can't be any messages // of interest. return true; } DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger(); List events = new ArrayList<>(); EventLog.readEvents(tags, events); for (int i = 0; i < events.size(); ++i) { if (mAuditWatchingStopRequested) { Log.w(TAG, "Stopping AuditWatchingJob run at scheduler request"); return false; } EventLog.Event event = events.get(i); // Discard clearly unrelated messages as quickly as we can. int uid = event.getUid(); if (!Process.isApplicationUid(uid)) { continue; } Object data = event.getData(); if (!(data instanceof String)) { continue; } String message = (String) data; if (!message.startsWith(AVC_PREFIX)) { continue; } // And then use a regular expression to verify it's one of the messages we're // interested in and to extract the path of the file being loaded. Matcher matcher = EXECUTE_NATIVE_AUDIT_PATTERN.matcher(message); if (!matcher.matches()) { continue; } String path = matcher.group(1); if (path == null) { // If the path contains spaces or various weird characters the kernel // hex-encodes the bytes; we need to undo that. path = unhex(matcher.group(2)); } dynamicCodeLogger.recordNative(uid, path); } return true; } catch (Exception e) { Log.e(TAG, "AuditWatchingJob failed", e); return true; } } } private static String unhex(String hexEncodedPath) { byte[] bytes = ByteStringUtils.fromHexToByteArray(hexEncodedPath); if (bytes == null || bytes.length == 0) { return ""; } return new String(bytes); } }