/* * Copyright (C) 2017 Oasis Feng. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * 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.oasisfeng.condom; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.os.Process; import android.support.annotation.CheckResult; import android.support.annotation.Keep; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import android.util.EventLog; import android.util.Log; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND; import static android.content.Context.ACTIVITY_SERVICE; import static android.content.pm.ApplicationInfo.FLAG_STOPPED; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1; import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; import static android.os.Build.VERSION_CODES.N; /** * The shared functionality for condom wrappers. * * Created by Oasis on 2017/4/21. */ @Keep @RestrictTo(RestrictTo.Scope.LIBRARY) class CondomCore { interface WrappedValueProcedure<R> extends WrappedValueProcedureThrows<R, RuntimeException> {} interface WrappedValueProcedureThrows<R, T extends Throwable> { R proceed() throws T; } static abstract class WrappedProcedure implements WrappedValueProcedure<Void> { abstract void run(); @Override public Void proceed() { run(); return null; } } @SuppressLint("CheckResult") void proceedBroadcast(final Intent intent, final CondomCore.WrappedProcedure procedure) { proceed(OutboundType.BROADCAST, intent, null, procedure); } @CheckResult <R, T extends Throwable> R proceed(final OutboundType type, final Intent intent, final @Nullable R negative_value, final CondomCore.WrappedValueProcedureThrows<R, T> procedure) throws T { final String target_pkg = getTargetPackage(intent); if (target_pkg != null) { if (mBase.getPackageName().equals(target_pkg)) return procedure.proceed(); // Self-targeting request is allowed unconditionally if (shouldBlockRequestTarget(type, intent, target_pkg)) return negative_value; } final int original_flags = adjustIntentFlags(type, intent); try { return procedure.proceed(); } finally { intent.setFlags(original_flags); } } @CheckResult <T extends Throwable> List<ResolveInfo> proceedQuery( final OutboundType type, final Intent intent, final CondomCore.WrappedValueProcedureThrows<List<ResolveInfo>, T> procedure) throws T { return proceed(type, intent, Collections.<ResolveInfo>emptyList(), new CondomCore.WrappedValueProcedureThrows<List<ResolveInfo>, T>() { @Override public List<ResolveInfo> proceed() throws T { final List<ResolveInfo> candidates = procedure.proceed(); if (mOutboundJudge != null && getTargetPackage(intent) == null) { // Package-targeted intent is already filtered by OutboundJudge in proceed(). final Iterator<ResolveInfo> iterator = candidates.iterator(); while (iterator.hasNext()) { final ResolveInfo candidate = iterator.next(); final String pkg = type == OutboundType.QUERY_SERVICES ? candidate.serviceInfo.packageName : (type == OutboundType.QUERY_RECEIVERS ? candidate.activityInfo.packageName : null); if (pkg != null && shouldBlockRequestTarget(type, intent, pkg)) // Dry-run is checked inside shouldBlockRequestTarget() iterator.remove(); // TODO: Not safe to assume the list returned from PackageManager is modifiable. } } return candidates; }}); } static String getTargetPackage(final Intent intent) { final ComponentName component = intent.getComponent(); return component != null ? component.getPackageName() : intent.getPackage(); } private boolean shouldBlockRequestTarget(final OutboundType type, final @Nullable Intent intent, final String target_pkg) { // Dry-run must be checked at the latest to ensure outbound judge is always called. return mOutboundJudge != null && ! mOutboundJudge.shouldAllow(type, intent, target_pkg) && ! mDryRun; } private int adjustIntentFlags(final OutboundType type, final Intent intent) { final int original_flags = intent.getFlags(); if (mDryRun) return original_flags; if (mExcludeBackgroundReceivers && (type == OutboundType.BROADCAST || type == OutboundType.QUERY_RECEIVERS)) intent.addFlags(SDK_INT >= N ? FLAG_RECEIVER_EXCLUDE_BACKGROUND : Intent.FLAG_RECEIVER_REGISTERED_ONLY); if (SDK_INT >= HONEYCOMB_MR1 && mExcludeStoppedPackages) intent.setFlags((intent.getFlags() & ~ Intent.FLAG_INCLUDE_STOPPED_PACKAGES) | Intent.FLAG_EXCLUDE_STOPPED_PACKAGES); return original_flags; } @Nullable ResolveInfo filterCandidates(final OutboundType type, final Intent original_intent, final @Nullable List<ResolveInfo> candidates, final String tag, final boolean remove) { if (candidates == null || candidates.isEmpty()) return null; final int my_uid = Process.myUid(); BackgroundUidFilter bg_uid_filter = null; ResolveInfo match = null; for (final Iterator<ResolveInfo> iterator = candidates.iterator(); iterator.hasNext(); match = null) { final ResolveInfo candidate = iterator.next(); final ApplicationInfo app_info = candidate.serviceInfo.applicationInfo; final int uid = app_info.uid; if (uid == my_uid) match = candidate; // Self UID is always allowed else if (mOutboundJudge == null || mOutboundJudge.shouldAllow(type, original_intent, app_info.packageName)) { if (mExcludeBackgroundServices) { if (bg_uid_filter == null) bg_uid_filter = new BackgroundUidFilter(); if (bg_uid_filter.isUidNotBackground(uid)) match = candidate; } else match = candidate; } if (match == null) log(tag, CondomEvent.FILTER_BG_SERVICE, app_info.packageName, original_intent.toString()); if (mDryRun) return candidate; // Always touch nothing and return the first candidate in dry-run mode. if (remove) { if (match == null) iterator.remove(); } else if (match != null) return match; } return null; } boolean shouldAllowProvider(final @Nullable ProviderInfo provider) { if (provider == null) return false; if (mBase.getPackageName().equals(provider.packageName)) return true; if (shouldBlockRequestTarget(OutboundType.CONTENT, null, provider.packageName)) return mDryRun; if (SDK_INT >= HONEYCOMB_MR1 && mExcludeStoppedPackages && (provider.applicationInfo.flags & FLAG_STOPPED) != 0) return mDryRun; return true; } boolean shouldAllowProvider(final Context context, final String name, final int flags) { return shouldAllowProvider(context.getPackageManager().resolveContentProvider(name, flags)); } enum CondomEvent { CONCERN, BIND_PASS, START_PASS, FILTER_BG_SERVICE } private void log(final String tag, final CondomEvent event, final String... args) { final Object[] event_args = new Object[2 + args.length]; event_args[0] = mBase.getPackageName(); event_args[1] = tag; // Package name and tag are shared parameters for all events. System.arraycopy(args, 0, event_args, 2, args.length); EventLog.writeEvent(EVENT_TAG + event.ordinal(), event_args); if (DEBUG) Log.d(asLogTag(tag), event.name() + " " + Arrays.toString(args)); } void logConcern(final String tag, final String label) { EventLog.writeEvent(EVENT_TAG + CondomEvent.CONCERN.ordinal(), mBase.getPackageName(), tag, label, getCaller()); if (DEBUG) Log.w(asLogTag(tag), label + " is invoked", new Throwable()); } void logIfOutboundPass(final String tag, final Intent intent, final @Nullable String target_pkg, final CondomEvent event) { if (target_pkg != null && ! mBase.getPackageName().equals(target_pkg)) log(tag, event, target_pkg, intent.toString()); } private static String getCaller() { final StackTraceElement[] stack = Thread.currentThread().getStackTrace(); if (stack.length <= 5) return "<bottom>"; final StackTraceElement caller = stack[5]; return caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber(); } static String buildLogTag(final String default_tag, final String prefix, final @Nullable String tag) { return tag == null || tag.isEmpty() ? default_tag : asLogTag(prefix + tag); } static String asLogTag(final String tag) { // Logging tag can be at most 23 characters. return tag.length() > 23 ? tag.substring(0, 23) : tag; } CondomCore(final Context base, final CondomOptions options) { mBase = base; DEBUG = (base.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; mExcludeBackgroundReceivers = options.mExcludeBackgroundReceivers; mExcludeBackgroundServices = options.mExcludeBackgroundServices; mOutboundJudge = options.mOutboundJudge; mDryRun = options.mDryRun; if (mDryRun) Log.w(TAG, "Start dry-run mode, no outbound requests will be blocked actually, despite later stated in log."); } final Context mBase; final boolean DEBUG; boolean mDryRun; @VisibleForTesting @Nullable OutboundJudge mOutboundJudge; boolean mExcludeStoppedPackages = true; boolean mExcludeBackgroundReceivers; boolean mExcludeBackgroundServices; private static final int EVENT_TAG = "Condom".hashCode(); private static final String TAG = "Condom"; /** * If set, the broadcast will never go to manifest receivers in background (cached * or not running) apps, regardless of whether that would be done by default. By * default they will receive broadcasts if the broadcast has specified an * explicit component or package name. * * @since API level 24 (Android N) */ @VisibleForTesting static final int FLAG_RECEIVER_EXCLUDE_BACKGROUND = 0x00800000; class BackgroundUidFilter { boolean isUidNotBackground(final int uid) { if (running_processes != null) { for (final ActivityManager.RunningAppProcessInfo running_process : running_processes) if (running_process.pid != 0 && running_process.importance < IMPORTANCE_BACKGROUND && running_process.uid == uid) return true; // Same UID does not guarantee same process. This is spared intentionally. } else if (running_services != null) { for (final ActivityManager.RunningServiceInfo running_service : running_services) if (running_service.pid != 0 && running_service.uid == uid) // Same UID does not guarantee same process. This is spared intentionally. return true; // Only running process is qualified, although getRunningServices() may not include all running app processes. } return false; } BackgroundUidFilter() { if (SDK_INT >= LOLLIPOP_MR1) { // getRunningAppProcesses() is limited on Android 5.1+. running_services = ((ActivityManager) mBase.getSystemService(ACTIVITY_SERVICE)).getRunningServices(32); // Too many services are never healthy, thus ignored intentionally. running_processes = null; } else { running_services = null; running_processes = ((ActivityManager) mBase.getSystemService(ACTIVITY_SERVICE)).getRunningAppProcesses(); } } private final @Nullable List<ActivityManager.RunningServiceInfo> running_services; private final @Nullable List<ActivityManager.RunningAppProcessInfo> running_processes; } }