/* * 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.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Process; import android.util.Log; import androidx.annotation.Keep; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static android.content.pm.PackageManager.GET_ACTIVITIES; import static android.content.pm.PackageManager.GET_PROVIDERS; import static android.content.pm.PackageManager.GET_RECEIVERS; import static android.content.pm.PackageManager.GET_SERVICES; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.Q; /** * Process-level condom * * Created by Oasis on 2017/4/17. */ @Keep public class CondomProcess { /** * Install the condom protection for current process if it is not the default process. * * <p>This method must be called in {@link Application#onCreate()} to eliminate potential leakage. */ public static void installExceptDefaultProcess(final Application app) { installExceptDefaultProcess(app, new CondomOptions()); } /** * Install the condom protection for current process if it is not the default process. * * <p>This method must be called in {@link Application#onCreate()} to eliminate potential leakage. */ public static void installExceptDefaultProcess(final Application app, final CondomOptions options) { validateCondomOptions(options); final String current_process_name = getProcessName(app); if (current_process_name == null) return; final String default_process_name = app.getApplicationInfo().processName; if (! current_process_name.equals(default_process_name)) install(app, current_process_name, options); } /** * Install the condom protection for current process if its process name matches. This method should be called in {@link Application#onCreate()}. * * @param process_names list of processes where Condom process should NOT be installed, in the form exactly as defined * by <code>"android:process"</code> attribute of components in <code>AndroidManifest.xml</code>. * <b>BEWARE: Default process must be explicitly listed here if it is expected to be excluded.</b> */ public static void installExcept(final Application app, final CondomOptions options, final String... process_names) { if (process_names.length == 0) throw new IllegalArgumentException("At lease one process name must be specified"); validateCondomOptions(options); final String current_process_name = getProcessName(app); if (current_process_name == null) return; for (final String process_name : process_names) if (! current_process_name.equals(getFullProcessName(app, process_name))) { install(app, current_process_name, options); return; } if ((app.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) validateProcessNames(app, process_names); } /** * Install the condom protection for current process. This method should generally never be called in the main process of app. * * <p>It is suggested to use {@link android.content.ContentProvider ContentProvider} with "android:process", to install it in specified process. */ public static void installInCurrentProcess(final Application app, final String tag, final CondomOptions options) { install(app, tag, options); } private static void validateCondomOptions(final CondomOptions options) { if (options.mKits != null && ! options.mKits.isEmpty()) throw new IllegalArgumentException("CondomKit is not yet compatible with CondomProcess. " + "If you really need this, please submit a feature request on GitHub issue tracker, with the use case."); } private static void validateProcessNames(final Application app, final String[] process_names) { final Thread thread = new Thread(() -> doValidateProcessNames(app, process_names)); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } private static void doValidateProcessNames(final Application app, final String[] process_names) { try { final PackageInfo info = app.getPackageManager().getPackageInfo(app.getPackageName(), GET_ACTIVITIES | GET_SERVICES | GET_RECEIVERS | GET_PROVIDERS); final Set<String> defined_process_names = new HashSet<>(); if (info.activities != null) for (final ActivityInfo activity : info.activities) defined_process_names.add(activity.processName); if (info.services != null) for (final ServiceInfo service : info.services) defined_process_names.add(service.processName); if (info.receivers != null) for (final ActivityInfo receiver : info.receivers) defined_process_names.add(receiver.processName); if (info.providers != null) for (final ProviderInfo provider : info.providers) defined_process_names.add(provider.processName); for (final String process_name : process_names) if (! defined_process_names.contains(getFullProcessName(app, process_name))) throw new IllegalArgumentException("Process name \"" + process_name + "\" is not used by any component in AndroidManifest.xml"); } catch (final PackageManager.NameNotFoundException ignored) {} // Should never happen } private static String getFullProcessName(final Context context, final String process_name) { return process_name.length() > 0 && process_name.charAt(0) == ':' ? context.getPackageName() + process_name : process_name; } private static @Nullable String getProcessName(final Context context) { final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (am == null) return null; final List<ActivityManager.RunningAppProcessInfo> processes; try { processes = am.getRunningAppProcesses(); } catch (final SecurityException e) { return null; } // Isolated process not allowed to call getRunningAppProcesses final int pid = Process.myPid(); if (processes != null) for (final ActivityManager.RunningAppProcessInfo process : processes) if (process.pid == pid) return process.processName; Log.e(TAG, "Error querying the name of current process."); return null; } private static void install(final Application app, final String process_name_or_tag, final CondomOptions options) { final int pos_colon = process_name_or_tag.indexOf(':'); final String tag = pos_colon > 0 ? process_name_or_tag.substring(pos_colon + 1) : process_name_or_tag; FULL_TAG = "Condom:" + tag; TAG = CondomCore.asLogTag(FULL_TAG); final CondomCore condom = new CondomCore(app, options, TAG); try { installCondomProcessActivityManager(condom); installCondomProcessPackageManager(condom); Log.d(TAG, "Global condom is installed in current process"); } catch (final Exception e) { condom.logConcern(TAG_INCOMPATIBILITY, e.getMessage()); Log.e(TAG, "Error installing global condom in current process", e); } } @SuppressLint("PrivateApi") private static void installCondomProcessActivityManager(final CondomCore condom) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { final Class<?> ActivityManagerNative = Class.forName("android.app.ActivityManagerNative"); Field ActivityManagerNative_gDefault = null; if (SDK_INT < O) try { ActivityManagerNative_gDefault = ActivityManagerNative.getDeclaredField("gDefault"); } catch (final NoSuchFieldException ignored) {} // ActivityManagerNative.gDefault is no longer available on Android O. if (ActivityManagerNative_gDefault == null) { //noinspection JavaReflectionMemberAccess ActivityManagerNative_gDefault = ActivityManager.class.getDeclaredField("IActivityManagerSingleton"); } ActivityManagerNative_gDefault.setAccessible(true); final Class<?> Singleton = Class.forName("android.util.Singleton"); @SuppressLint("DiscouragedPrivateApi") final Method Singleton_get = Singleton.getDeclaredMethod("get"); Singleton_get.setAccessible(true); final Field Singleton_mInstance = Singleton.getDeclaredField("mInstance"); Singleton_mInstance.setAccessible(true); final Class<?> IActivityManager = Class.forName("android.app.IActivityManager"); final Object/* Singleton */singleton = ActivityManagerNative_gDefault.get(null); if (singleton == null) throw new IllegalStateException("ActivityManagerNative.gDefault is null"); final Object/* IActivityManager */am = Singleton_get.invoke(singleton); if (am == null) throw new IllegalStateException("ActivityManagerNative.gDefault.get() returns null"); final InvocationHandler handler; if (Proxy.isProxyClass(am.getClass()) && (handler = Proxy.getInvocationHandler(am)) instanceof CondomProcessActivityManager) { Log.w(TAG, "CondomActivityManager was already installed in this process."); ((CondomProcessActivityManager) handler).mCondom = condom; } else { final Object condom_am = Proxy.newProxyInstance(condom.mBase.getClassLoader(), new Class[] { IActivityManager }, new CondomProcessActivityManager(condom, am)); Singleton_mInstance.set(singleton, condom_am); } } @SuppressLint("PrivateApi") private static void installCondomProcessPackageManager(final CondomCore condom) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { final Class<?> ActivityThread = Class.forName("android.app.ActivityThread"); final Field ActivityThread_sPackageManager = ActivityThread.getDeclaredField("sPackageManager"); ActivityThread_sPackageManager.setAccessible(true); final Class<?> IPackageManager = Class.forName("android.content.pm.IPackageManager"); final Object pm = ActivityThread_sPackageManager.get(null); final InvocationHandler handler; if (Proxy.isProxyClass(pm.getClass()) && (handler = Proxy.getInvocationHandler(pm)) instanceof CondomProcessPackageManager) { Log.w(TAG, "CondomPackageManager was already installed in this process."); ((CondomProcessPackageManager) handler).mCondom = condom; } else { final Object condom_pm = Proxy.newProxyInstance(condom.mBase.getClassLoader(), new Class[] { IPackageManager }, new CondomProcessPackageManager(condom, pm)); ActivityThread_sPackageManager.set(null, condom_pm); } } private CondomProcess() {} private static final String TAG_INCOMPATIBILITY = "Incompatibility"; /* ==================== */ @VisibleForTesting static class CondomProcessActivityManager extends CondomSystemService { private Object proceed(final Object proxy, final Method method, final Object[] args) throws Throwable { final String method_name = method.getName(); final Intent intent; final int result; switch (method_name) { case "broadcastIntent": // int broadcastIntent(IApplicationThread caller, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle map, String/[23+] String[] requiredPermissions, [18+ int appOp], [23+ Bundle options], boolean serialized, boolean sticky, [16+ int userId]); result = mCondom.proceed(OutboundType.BROADCAST, (Intent) args[1], Integer.MIN_VALUE, () -> (Integer) CondomProcessActivityManager.super.invoke(proxy, method, args)); final Object result_receiver = args[3]; if (result != Integer.MIN_VALUE) return result; if (result_receiver == null) return 0/* ActivityManager.BROADCAST_SUCCESS */; // Invoke the result receiver as if the ordered broadcast has been sent to no one, if the broadcast is blocked by condom. final Method IIntentReceiver_performReceive = result_receiver.getClass().getMethod("performReceive", SDK_INT >= JELLY_BEAN_MR1 // void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, [17+ int sendingUser]) ? new Class[] { Intent.class, int.class, String.class, Bundle.class, boolean.class, boolean.class, int.class} : new Class[] { Intent.class, int.class, String.class, Bundle.class, boolean.class, boolean.class }); IIntentReceiver_performReceive.invoke(result_receiver, SDK_INT >= JELLY_BEAN_MR1 ? new Object[] { args[1], args[4], args[5], args[6], args[args.length - 3], args[args.length - 2], args[args.length - 1] } : new Object[] { args[1], args[4], args[5], args[6], args[8], args[9] }); return 0/* ActivityManager.BROADCAST_SUCCESS */; case "bindService": // Android P- case "bindIsolatedService": // Android Q+ intent = (Intent) args[2]; result = mCondom.proceed(OutboundType.BIND_SERVICE, intent, 0, () -> (Integer) CondomProcessActivityManager.super.invoke(proxy, method, args)); // Result: 0 - no match, >0 - succeed, <0 - SecurityException. if (result > 0) mCondom.logIfOutboundPass(FULL_TAG, intent, CondomCore.getTargetPackage(intent), CondomCore.CondomEvent.BIND_PASS); return result; case "startService": intent = (Intent) args[1]; final ComponentName component = mCondom.proceed(OutboundType.START_SERVICE, intent, null, () -> (ComponentName) CondomProcessActivityManager.super.invoke(proxy, method, args)); if (component != null) mCondom.logIfOutboundPass(FULL_TAG, intent, component.getPackageName(), CondomCore.CondomEvent.START_PASS); return component; case "getContentProvider": // (ApplicationThread, [Q+ String opPackageName], String authority, int userId, boolean stable) final String name = (String) args[SDK_INT >= Q ? 2 : 1]; if (! mCondom.shouldAllowProvider(mCondom.mBase, name, PackageManager.MATCH_ALL)) // MATCH_ALL as special hint to ask the hooked IPackageManager.resolveContentProvider() to bypass. return null; // Actually blocked by IPackageManager.resolveContentProvider() which is called in shouldAllowProvider() above. break; } return super.invoke(proxy, method, args); } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { try { return proceed(proxy, method, args); } catch (final Exception e) { if (DEBUG) Log.e(TAG, "Error proceeding " + method, e); } return super.invoke(proxy, method, args); } CondomProcessActivityManager(final CondomCore condom, final Object am) { super (am, "IActivityManager.", condom.DEBUG); mCondom = condom; } private CondomCore mCondom; } @VisibleForTesting static class CondomProcessPackageManager extends CondomSystemService { private Object proceed(final Object proxy, final Method method, final Object[] args) throws Throwable { final String method_name = method.getName(); OutboundType outbound_type = null; switch (method_name) { case "queryIntentServices": outbound_type = OutboundType.QUERY_SERVICES; if (IPackageManager_queryIntentServices == null) IPackageManager_queryIntentServices = method; if (args[0] == DUMMY_INTENT) return null; // Short-circuit for capturing IPackageManager_queryIntentServices. case "queryIntentReceivers": if (outbound_type == null) outbound_type = OutboundType.QUERY_RECEIVERS; final Object result = super.invoke(proxy, method, args); final List<ResolveInfo> list = mCondom.proceedQuery(outbound_type, (Intent) args[0], () -> asList(result), outbound_type == OutboundType.QUERY_SERVICES ? CondomCore.SERVICE_PACKAGE_GETTER : CondomCore.RECEIVER_PACKAGE_GETTER); // Both "queryIntentServices" and "queryIntentReceivers" reach here. if (list.isEmpty()) asList(result).clear(); // In case Collections.emptyList() is returned due to targeted query being rejected by outbound judge. return result; case "resolveService": // Intent flags could only filter background receivers, we have to deal with services by ourselves. final Intent intent = (Intent) args[0]; final int original_intent_flags = intent.getFlags(); return mCondom.proceed(OutboundType.QUERY_SERVICES, intent, null, () -> { if (! mCondom.mExcludeBackgroundServices && mCondom.mOutboundJudge == null) return (ResolveInfo) CondomProcessPackageManager.super.invoke(proxy, method, args); if (IPackageManager_queryIntentServices == null) { mCondom.mBase.getPackageManager().queryIntentServices(DUMMY_INTENT, 0); if (IPackageManager_queryIntentServices == null) throw new IllegalStateException("Failed to capture IPackageManager.queryIntentServices()"); } final List<ResolveInfo> candidates = asList(CondomProcessPackageManager.super.invoke(proxy, IPackageManager_queryIntentServices, args)); return mCondom.filterCandidates(OutboundType.QUERY_SERVICES, intent.setFlags(original_intent_flags), candidates, FULL_TAG, false); }); case "resolveContentProvider": final ProviderInfo provider = (ProviderInfo) super.invoke(proxy, method, args); final int flags = (int) args[1]; if ((flags & PackageManager.MATCH_ALL) != 0) return provider; // MATCH_ALL will be used by the hooked IActivityManager.getContentProvider(). return mCondom.shouldAllowProvider(provider) ? provider : null; case "getInstalledApplications": case "getInstalledPackages": mCondom.logConcern(FULL_TAG, "IPackageManager." + method_name); break; } return super.invoke(proxy, method, args); } final Intent DUMMY_INTENT = new Intent(); @SuppressWarnings("unchecked") private <T> List<T> asList(final Object list) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { if (list instanceof List) return (List<T>) list; final Class<?> clazz = list.getClass(); if (! "android.content.pm.ParceledListSlice".equals(clazz.getName())) throw new IllegalArgumentException("Neither List nor ParceledListSlice: " + clazz); if (ParceledListSlice_getList == null) ParceledListSlice_getList = clazz.getMethod("getList"); return (List<T>) ParceledListSlice_getList.invoke(list); } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { try { return proceed(proxy, method, args); } catch (final Exception e) { if (DEBUG) Log.e(TAG, "Error proceeding " + method, e); } return super.invoke(proxy, method, args); } CondomProcessPackageManager(final CondomCore condom, final Object pm) { super(pm, "IPackageManager.", condom.DEBUG); mCondom = condom; } @VisibleForTesting CondomCore mCondom; private Method IPackageManager_queryIntentServices; private Method ParceledListSlice_getList; } private static class CondomSystemService implements InvocationHandler { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { if (DEBUG) Log.d(TAG, mServiceTag + method.getName() + (args == null ? "" : Arrays.deepToString(args))); try { return method.invoke(mService, args); } catch (final InvocationTargetException e) { throw e.getTargetException(); } } CondomSystemService(final Object am, final String tag, final boolean debuggable) { mService = am; mServiceTag = tag; DEBUG = debuggable; } private final Object mService; private final String mServiceTag; final boolean DEBUG; } private static String FULL_TAG = "CondomProcess"; // Both will be replaced by compound tag in install(). private static String TAG = "CondomProcess"; }