/* * 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.Process; import android.support.annotation.Keep; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; 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 javax.annotation.ParametersAreNonnullByDefault; 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.N_MR1; /** * Process-level condom * * Created by Oasis on 2017/4/17. */ @ParametersAreNonnullByDefault @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) { 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"); 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); } private static void validateProcessNames(final Application app, final String[] process_names) { final Thread thread = new Thread(new Runnable() { @Override public void run() { 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); 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 current_process_name, final CondomOptions options) { final int pos_colon = current_process_name.indexOf(':'); final String tag = pos_colon > 0 ? current_process_name.substring(pos_colon) : current_process_name; FULL_TAG = "Condom:" + tag; TAG = CondomCore.asLogTag(FULL_TAG); final CondomCore condom = new CondomCore(app, options); try { installCondomProcessActivityManager(condom); installCondomProcessPackageManager(condom); Log.d(TAG, "Global condom is installed in current process"); } catch (final Exception e) { 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 <= N_MR1) try { ActivityManagerNative_gDefault = ActivityManagerNative.getDeclaredField("gDefault"); } catch (final NoSuchFieldException ignored) {} // ActivityManagerNative.gDefault is no longer available on Android O. if (ActivityManagerNative_gDefault == null) { ActivityManagerNative_gDefault = ActivityManager.class.getDeclaredField("IActivityManagerSingleton"); } ActivityManagerNative_gDefault.setAccessible(true); final Class<?> Singleton = Class.forName("android.util.Singleton"); 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"); if (Proxy.isProxyClass(am.getClass()) && Proxy.getInvocationHandler(am) instanceof CondomProcessActivityManager) { Log.d(TAG, "CondomActivityManager is already installed in this process."); return; } 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); if (Proxy.isProxyClass(pm.getClass()) && Proxy.getInvocationHandler(pm) instanceof CondomProcessPackageManager) { Log.d(TAG, "CondomPackageManager is already installed in this process."); return; } final Object condom_pm = Proxy.newProxyInstance(condom.mBase.getClassLoader(), new Class[] {IPackageManager}, new CondomProcessPackageManager(condom, pm)); ActivityThread_sPackageManager.set(null, condom_pm); } private CondomProcess() {} /* ==================== */ @VisibleForTesting static class CondomProcessActivityManager extends CondomSystemService { private Object proceed(final Object proxy, final Method method, final Object[] args) throws Exception { final String method_name = method.getName(); final Intent intent; switch (method_name) { case "broadcastIntent": return mCondom.proceed(OutboundType.BROADCAST, (Intent) args[1], 0/* ActivityManager.BROADCAST_SUCCESS */, new CondomCore.WrappedValueProcedureThrows<Integer, Exception>() { @Override public Integer proceed() throws Exception { return (Integer) CondomProcessActivityManager.super.invoke(proxy, method, args); }}); case "bindService": intent = (Intent) args[2]; final Integer result = mCondom.proceed(OutboundType.BIND_SERVICE, intent, 0, new CondomCore.WrappedValueProcedureThrows<Integer, Exception>() { @Override public Integer proceed() throws Exception { return (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, new CondomCore.WrappedValueProcedureThrows<ComponentName, Exception>() { @Override public ComponentName proceed() throws Exception { return (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": final String name = (String) args[1]; if (! mCondom.shouldAllowProvider(mCondom.mBase, name, PackageManager.GET_UNINSTALLED_PACKAGES)) 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 IllegalAccessException, IllegalArgumentException, InvocationTargetException { 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 final CondomCore mCondom; } @VisibleForTesting static class CondomProcessPackageManager extends CondomSystemService { private Object proceed(final Object proxy, final Method method, final Object[] args) throws Exception { 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); //noinspection ResultOfMethodCallIgnored, since the result here may be the inner list if the original result is ParceledListSlice. final List<ResolveInfo> list = mCondom.proceedQuery(outbound_type, (Intent) args[0], new CondomCore.WrappedValueProcedureThrows<List<ResolveInfo>, Exception>() { @Override public List<ResolveInfo> proceed() throws Exception { return asList(result); }}); 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, new CondomCore.WrappedValueProcedureThrows<ResolveInfo, Exception>() { @Override public ResolveInfo proceed() throws Exception { if (! mCondom.mExcludeBackgroundServices) 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); 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 IllegalAccessException, IllegalArgumentException, InvocationTargetException { 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 final 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 IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (DEBUG) Log.d(TAG, mServiceTag + method.getName() + (args == null ? "" : Arrays.toString(args))); return method.invoke(mService, args); } 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; } static String FULL_TAG = "CondomProcess"; // Both will be replaced by compound tag in install(). static String TAG = "CondomProcess"; }