package biz.bokhorst.xprivacy; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import android.content.Context; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.Parcel; import android.os.Process; import android.util.Log; import android.util.SparseArray; public class XBinder extends XHook { private Methods mMethod; private static long mToken = 0; private static Map<String, Boolean> mMapClassSystem = new HashMap<String, Boolean>(); private static Map<String, SparseArray<String>> mMapCodeName = new HashMap<String, SparseArray<String>>(); private static final int BITS_TOKEN = 16; private static final int FLAG_ALL = 0xFFFF; private static final int MASK_TOKEN = 0xFFFF; private static final int PING_TRANSACTION = ('_' << 24) | ('P' << 16) | ('N' << 8) | 'G'; private static final int DUMP_TRANSACTION = ('_' << 24) | ('D' << 16) | ('M' << 8) | 'P'; private static final int INTERFACE_TRANSACTION = ('_' << 24) | ('N' << 16) | ('T' << 8) | 'F'; private static final int TWEET_TRANSACTION = ('_' << 24) | ('T' << 16) | ('W' << 8) | 'T'; private static final int LIKE_TRANSACTION = ('_' << 24) | ('L' << 16) | ('I' << 8) | 'K'; private static final int SYSPROPS_TRANSACTION = ('_' << 24) | ('S' << 16) | ('P' << 8) | 'R'; // Service name should one-to-one correspond to the other lists // @formatter:off public static List<String> cServiceName = Arrays.asList(new String[] { "account", "activity", "clipboard", "connectivity", "content", "location", "telephony.registry", "telephony.msim.registry", "package", "iphonesubinfo", "iphonesubinfo_msim", "window", "wifi", "sip", "isms", "nfc", "appwidget", "bluetooth", "bluetooth_manager", "input", "sensorservice", "usb", "media.camera", "<noname>", "<noname>", "<noname>" }); // @formatter:on // @formatter:off public static List<String> cServiceDescriptor = Arrays.asList(new String[] { "android.accounts.IAccountManager", "android.app.IActivityManager", "android.content.IClipboard", "android.net.IConnectivityManager", "android.content.IContentService", "android.location.ILocationManager", "com.android.internal.telephony.ITelephonyRegistry", "com.android.internal.telephony.ITelephonyRegistryMSim", "android.content.pm.IPackageManager", "com.android.internal.telephony.IPhoneSubInfo", "com.android.internal.telephony.msim.IPhoneSubInfoMSim", "android.view.IWindowManager", "android.net.wifi.IWifiManager", "android.net.sip.ISipService", "com.android.internal.telephony.ISms", "android.nfc.INfcAdapter", "com.android.internal.appwidget.IAppWidgetService", "android.bluetooth.IBluetooth", "android.bluetooth.IBluetoothManager", "android.hardware.input.IInputManager", "android.gui.SensorServer", "android.hardware.usb.IUsbManager", "android.hardware.ICameraService", "android.app.IApplicationThread", "android.content.IContentProvider", "android.view.IWindowSession" }); // @formatter:on // @formatter:off public static List<String> cServiceOptional = Arrays.asList(new String[] { "<noname>", "iphonesubinfo", "iphonesubinfo_msim", "sip", "isms", "nfc", "bluetooth", "bluetooth_manager" }); // @formatter:on private XBinder(Methods method, String restrictionName) { super(restrictionName, method.name(), null); mMethod = method; } public String getClassName() { return (mMethod == Methods.transact ? "android.os.BinderProxy" : "android.os.Binder"); } public boolean isVisible() { return (mMethod != Methods.execTransact); } @Override public void setSecret(String secret) { super.setSecret(secret); mToken = (secret.hashCode() & MASK_TOKEN); } // @formatter:off // private boolean execTransact(int code, int dataObj, int replyObj, int flags) // public final boolean transact(int code, Parcel data, Parcel reply, int flags) // public native boolean transact(int code, Parcel data, Parcel reply, int flags) // frameworks/base/core/java/android/os/Binder.java // http://developer.android.com/reference/android/os/Binder.html // @formatter:on private enum Methods { execTransact, transact }; public static List<XHook> getInstances() { List<XHook> listHook = new ArrayList<XHook>(); listHook.add(new XBinder(Methods.execTransact, null)); // Binder listHook.add(new XBinder(Methods.transact, null)); // BinderProxy return listHook; } @Override protected void before(XParam param) throws Throwable { if (mMethod == Methods.execTransact) { // execTransact calls the overridden onTransact // Check for direct IPC checkIPC(param); } else if (mMethod == Methods.transact) { markIPC(param); } else Util.log(this, Log.WARN, "Unknown method=" + param.method.getName()); } @Override protected void after(XParam param) throws Throwable { // Do nothing } private void markIPC(XParam param) throws Throwable { // Allow management transactions int code = (Integer) param.args[0]; if (isManagementTransaction(code)) return; // Only for applications int uid = Binder.getCallingUid(); if (!PrivacyManager.isApplication(uid)) return; // Check interface name IBinder binder = (IBinder) param.thisObject; String descriptor = (binder == null ? null : binder.getInterfaceDescriptor()); if (!cServiceDescriptor.contains(descriptor)) return; // Search this object in call stack boolean ok = false; boolean found = false; StackTraceElement[] ste = Thread.currentThread().getStackTrace(); for (int i = 0; i < ste.length; i++) if (ste[i].getClassName().equals(param.thisObject.getClass().getName())) { found = true; // Check if caller class in user space String callerClassName = (i + 2 < ste.length ? ste[i + 2].getClassName() : null); if (callerClassName != null && !callerClassName.startsWith("java.lang.reflect.")) synchronized (mMapClassSystem) { if (!mMapClassSystem.containsKey(callerClassName)) try { ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class<?> clazz = Class.forName(callerClassName, false, loader); boolean boot = Context.class.getClassLoader().equals(clazz.getClassLoader()); mMapClassSystem.put(callerClassName, boot); } catch (ClassNotFoundException ignored) { mMapClassSystem.put(callerClassName, true); } ok = mMapClassSystem.get(callerClassName); } break; } // Conditionally mark if (ok) { int flags = (Integer) param.args[3]; if ((flags & ~FLAG_ALL) != 0) Util.log(this, Log.ERROR, "Unknown flags=" + Integer.toHexString(flags) + " descriptor=" + descriptor + " code=" + code + " uid=" + Binder.getCallingUid()); flags |= (mToken << BITS_TOKEN); param.args[3] = flags; } else { int level = (found ? Log.WARN : Log.ERROR); Util.log(this, level, "Unmarked descriptor=" + descriptor + " found=" + found + " code=" + code + " uid=" + Binder.getCallingUid()); Util.logStack(this, level, true); } } // Entry point from android_util_Binder.cpp's onTransact private void checkIPC(XParam param) throws Throwable { // Allow management transactions int code = (Integer) param.args[0]; if (isManagementTransaction(code)) return; // Only for applications int uid = Binder.getCallingUid(); if (!PrivacyManager.isApplication(uid)) return; // Check interface name IBinder binder = (IBinder) param.thisObject; String descriptor = (binder == null ? null : binder.getInterfaceDescriptor()); if (!cServiceDescriptor.contains(descriptor)) return; // Get token int flags = (Integer) param.args[3]; long token = (flags >> BITS_TOKEN) & MASK_TOKEN; flags &= FLAG_ALL; param.args[3] = flags; // Check token if (token != mToken) { String[] name = descriptor.split("\\."); String interfaceName = name[name.length - 1]; // Get transaction code name String codeName; synchronized (mMapCodeName) { if (!mMapCodeName.containsKey(descriptor)) { SparseArray<String> sa = new SparseArray<String>(); mMapCodeName.put(descriptor, sa); List<Class<?>> listClass = new ArrayList<Class<?>>(); if (param.thisObject.getClass().getSuperclass() != null) listClass.add(param.thisObject.getClass().getSuperclass()); try { listClass.add(Class.forName(descriptor)); } catch (ClassNotFoundException ignored) { } for (Class<?> clazz : listClass) for (Field field : clazz.getDeclaredFields()) try { if (field.getName().startsWith("TRANSACTION_") || field.getName().endsWith("_TRANSACTION")) { field.setAccessible(true); Integer txCode = (Integer) field.get(null); String txName = field.getName().replace("TRANSACTION_", "") .replace("_TRANSACTION", ""); sa.put(txCode, txName); } } catch (Throwable ignore) { } } codeName = mMapCodeName.get(descriptor).get(code); } if (codeName == null) { codeName = Integer.toString(code); Util.log(this, Log.WARN, "Unknown transaction=" + descriptor + ":" + code + " class=" + param.thisObject.getClass() + " uid=" + Binder.getCallingUid()); Util.logStack(this, Log.INFO); } Util.log(this, Log.INFO, "can restrict transaction=" + interfaceName + ":" + codeName + " flags=" + flags + " uid=" + uid + " my=" + Process.myUid()); if (isRestrictedExtra(uid, PrivacyManager.cIPC, "Binder", interfaceName + ":" + codeName)) { Util.log(this, Log.WARN, "Restricting " + interfaceName + ":" + codeName + " code=" + code); // Get reply parcel Parcel reply = null; try { // static protected final Parcel obtain(int obj) // frameworks/base/core/java/android/os/Parcel.java Method methodObtain = Parcel.class.getDeclaredMethod("obtain", Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? int.class : long.class); methodObtain.setAccessible(true); reply = (Parcel) methodObtain.invoke(null, param.args[2]); } catch (NoSuchMethodException ex) { Util.bug(this, ex); } // Block IPC if (reply == null) Util.log(this, Log.ERROR, "reply is null uid=" + uid); else { reply.setDataPosition(0); reply.writeException(new SecurityException("XPrivacy")); } param.setResult(true); } } } private static boolean isManagementTransaction(int code) { return (code == PING_TRANSACTION || code == DUMP_TRANSACTION || code == INTERFACE_TRANSACTION || code == TWEET_TRANSACTION || code == LIKE_TRANSACTION || code == SYSPROPS_TRANSACTION); } }