| /* |
| * 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<EventLog.Event> 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); |
| } |
| } |