/* * 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.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.ReceiverCallNotAllowedException; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.os.Handler; import android.os.Process; import android.provider.Settings; import android.util.EventLog; import android.util.Log; import androidx.annotation.CheckResult; import androidx.annotation.Keep; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.oasisfeng.condom.ext.PackageManagerFactory; import com.oasisfeng.condom.util.Lazy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND; import static android.content.Context.ACTIVITY_SERVICE; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; 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; import static android.os.Build.VERSION_CODES.O; /** * The shared functionality for condom wrappers. * * Created by Oasis on 2017/4/21. */ @Keep @RestrictTo(RestrictTo.Scope.LIBRARY) @SuppressWarnings("TypeParameterHidesVisibleType") class CondomCore { interface WrappedValueProcedure<R> extends WrappedValueProcedureThrows<R, RuntimeException> {} interface WrappedValueProcedureThrows<R, T extends Throwable> { @Nullable R proceed() throws T; } static abstract class WrappedProcedure implements WrappedValueProcedure<Boolean> { abstract void run(); @Override public Boolean proceed() { run(); return null; } } ContentResolver getContentResolver() { return mContentResolver.get(); } PackageManager getPackageManager() { return mPackageManager.get(); } String getPackageName() { return mBase.getPackageName(); } void proceedBroadcast(final Context context, final Intent intent, final WrappedValueProcedure<Boolean> procedure, final @Nullable BroadcastReceiver resultReceiver) { if (proceed(OutboundType.BROADCAST, intent, Boolean.FALSE, procedure) == Boolean.FALSE && resultReceiver != null) resultReceiver.onReceive(new ReceiverRestrictedContext(context), intent); } @CheckResult <R, T extends Throwable> R proceed(final OutboundType type, final @Nullable Intent intent, final @Nullable R negative_value, final WrappedValueProcedureThrows<R, T> procedure) throws T { final String target_pkg = intent != null ? getTargetPackage(intent) : null; 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 = intent != null ? adjustIntentFlags(type, intent) : 0; try { return procedure.proceed(); } finally { if (intent != null) intent.setFlags(original_flags); } } @CheckResult <R, T extends Throwable> R proceed(final OutboundType type, final String target_pkg, final @Nullable R negative_value, final WrappedValueProcedureThrows<R, T> procedure) throws T { if (mBase.getPackageName().equals(target_pkg)) return procedure.proceed(); // Self-targeting request is allowed unconditionally if (shouldBlockRequestTarget(type, null, target_pkg)) return negative_value; return procedure.proceed(); } @CheckResult <T, E extends Throwable> List<T> proceedQuery(final OutboundType type, final @Nullable Intent intent, final WrappedValueProcedureThrows<List<T>, E> procedure, final Function<T, String> pkg_getter) throws E { return proceed(type, intent, Collections.emptyList(), () -> { final List<T> candidates = procedure.proceed(); if (candidates != null && mOutboundJudge != null && (intent == null || getTargetPackage(intent) == null)) { // Package-targeted intent is already filtered by OutboundJudge in proceed(). final Iterator<T> iterator = candidates.iterator(); while (iterator.hasNext()) { final T candidate = iterator.next(); final String pkg = pkg_getter.apply(candidate); 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; }); } interface Function<T, R> { R apply(T t); } 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; } @SuppressLint("WrongConstant") 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 true; // We know nothing about the provider, better allow than block. if (mBase.getPackageName().equals(provider.packageName)) return true; if (shouldBlockRequestTarget(OutboundType.CONTENT, null, provider.packageName)) return false; if (Settings.AUTHORITY.equals(provider.authority)) return true; // Always allow access to system settings, to avoid rare cases in the wild that the provider info of Settings provider is inaccurate. //noinspection SimplifiableIfStatement if (SDK_INT >= HONEYCOMB_MR1 && mExcludeStoppedPackages && (provider.applicationInfo.flags & (FLAG_SYSTEM | ApplicationInfo.FLAG_STOPPED)) == ApplicationInfo.FLAG_STOPPED) return mDryRun; return true; } boolean shouldAllowProvider(final Context context, final String name, final int flags) { return shouldAllowProvider(context.getPackageManager().resolveContentProvider(name, flags)); } Object getSystemService(final String name) { if (mKitManager != null) { final CondomKit.SystemServiceSupplier supplier = mKitManager.mSystemServiceSuppliers.get(name); if (supplier != null) return supplier.getSystemService(mBase, name); } return null; } boolean shouldSpoofPermission(final String permission) { return mKitManager != null && mKitManager.mSpoofPermissions.contains(permission); } Set<String> getSpoofPermissions() { return mKitManager != null ? mKitManager.mSpoofPermissions : Collections.emptySet(); } 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, final String tag) { mBase = base; DEBUG = (base.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; mExcludeBackgroundReceivers = options.mExcludeBackgroundReceivers; mExcludeBackgroundServices = SDK_INT < O && options.mExcludeBackgroundServices; mOutboundJudge = options.mOutboundJudge; mDryRun = options.mDryRun; final Lazy<PackageManager> lazy_pm = new Lazy<PackageManager>() { @Override protected PackageManager create() { return new CondomPackageManager(CondomCore.this, base.getPackageManager(), tag); }}; final PackageManagerFactory pm_factory = options.mPackageManagerFactory; mPackageManager = new Lazy<PackageManager>() { @Override protected PackageManager create() { return pm_factory != null ? pm_factory.getPackageManager(base, lazy_pm.get()) : lazy_pm.get(); }}; mContentResolver = new Lazy<ContentResolver>() { @Override protected ContentResolver create() { return new CondomContentResolver(CondomCore.this, base, base.getContentResolver()); }}; final List<CondomKit> kits = options.mKits == null ? null : new ArrayList<>(options.mKits); if (kits != null && ! kits.isEmpty()) { mKitManager = new CondomKitManager(); for (final CondomKit kit : kits) kit.onRegister(mKitManager); } else mKitManager = null; if (mDryRun) Log.w(tag, "Start dry-run mode, no outbound requests will be blocked actually, despite later stated in log."); } final Context mBase; // The real Context final boolean DEBUG; boolean mDryRun; @VisibleForTesting @Nullable OutboundJudge mOutboundJudge; boolean mExcludeStoppedPackages = true; boolean mExcludeBackgroundReceivers; boolean mExcludeBackgroundServices; private final Lazy<PackageManager> mPackageManager; private final Lazy<ContentResolver> mContentResolver; private final @Nullable CondomKitManager mKitManager; static final Function<ResolveInfo,String> SERVICE_PACKAGE_GETTER = resolve -> resolve.serviceInfo.packageName; static final Function<ResolveInfo,String> RECEIVER_PACKAGE_GETTER = resolve -> resolve.activityInfo.packageName; private static final int EVENT_TAG = "Condom".hashCode(); /** Mirror of the hidden Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND, since API level 24 (Android N) */ @RequiresApi(N) @VisibleForTesting static final int FLAG_RECEIVER_EXCLUDE_BACKGROUND = 0x00800000; static class CondomKitManager implements CondomKit.CondomKitRegistry { @Override public void addPermissionSpoof(final String permission) { mSpoofPermissions.add(permission); } @Override public void registerSystemService(final String name, final CondomKit.SystemServiceSupplier supplier) { mSystemServiceSuppliers.put(name, supplier); } final Map<String, CondomKit.SystemServiceSupplier> mSystemServiceSuppliers = new HashMap<>(); final Set<String> mSpoofPermissions = new HashSet<>(); } 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; // Fallback: Always treat as background app, since app with same UID will not reach here. } BackgroundUidFilter() { final ActivityManager am = (ActivityManager) mBase.getSystemService(ACTIVITY_SERVICE); if (am == null) { running_services = null; running_processes = null; } else if (SDK_INT >= LOLLIPOP_MR1) { // getRunningAppProcesses() is limited on Android 5.1+. running_services = am.getRunningServices(64); // Too many services are never healthy, thus ignored intentionally. running_processes = null; } else { running_services = null; running_processes = am.getRunningAppProcesses(); } } private final @Nullable List<ActivityManager.RunningServiceInfo> running_services; private final @Nullable List<ActivityManager.RunningAppProcessInfo> running_processes; } class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(final Context base) { super(base); } @Override public Intent registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) { return registerReceiver(receiver, filter, null, null); } @Override public Intent registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter, final @Nullable String broadcastPermission, final @Nullable Handler scheduler) { if (receiver == null) { // Allow retrieving current sticky broadcast; this is safe since we // aren't actually registering a receiver. return super.registerReceiver(null, filter, broadcastPermission, scheduler); } else { throw new ReceiverCallNotAllowedException( "BroadcastReceiver components are not allowed to register to receive intents"); } } @Override public boolean bindService(final Intent service, final ServiceConnection conn, final int flags) { throw new ReceiverCallNotAllowedException( "BroadcastReceiver components are not allowed to bind to services"); } } }