V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iofomo
V2EX  ›  Android

[Android] Binder 的 Oneway 拦截

  •  
  •   iofomo · 17 天前 · 843 次点击

    在某些虚拟化,免安装,打点,环境检测,拦截器等场景,针对Android系统服务接口的拦截是常用的技术方案。通常只是针对正向的接口调用,如果涉及被动的服务回调拦截,则实现起来就有些许麻烦。

    说明

    由于我们容器产品的特性,需要将应用完整的运行起来,所以必须要对各系统服务(超过100+系统服务)通信进行拦截过滤,修正和还原接口通信的数据。让系统和应用可以无感知运行,实现免安装运行的效果。

    整个方案基本上都聚焦在服务模块主动调用的拦截上,系统回调的拦截涉及较少,但随着功能的深入,越来越需要对服务回调的接口(Binder for Oneway)进行拦截。在这里将整个通用的拦截方案和实现过程分享出来,希望对大家有益。

    原理

    BinderOneway回调机制,即应用进程向系统服务注册回调(通常注册和取消注册成对出现),当服务端有相应时间时,可以直接回调给改Binder对象实例。

    如常见的AMS服务接口:

    // source code: /frameworks/base/core/java/android/app/IActivityManager.aidl
    interface IActivityManager {
    	// ...
      
    	Intent registerReceiver(IApplicationThread caller, 
                              String callerPackage, 
                              IIntentReceiver receiver,
                              IntentFilter filter, 
                              String requiredPermission,
                              int userId, 
                              int flags
                             );
      void unregisterReceiver(in IIntentReceiver receiver);
      
      // ...
    }
    

    我们的目标:

    1. 拦截AMSregisterReceiver方法,将参数receiver通过Proxy创建一个新的扩展类对象传递出去。
    2. 为了参数校验通过,所以对象的类名是合法的(如:android.content.IIntentReceiver
    3. 服务端实际拿到的是我们扩展的接口对象,因此当服务端,通过 Binder 数据还原成服务端的同名对象。
    4. 当服务端有事件回调时,则我们扩展的接口对象优先处理,然后再像原对象调用传递。
    5. 当应用注销回调时,同样需要将我们扩展的对象通知服务端解除。

    1.0 方案:源码导入

    由于通常系统接口类(如:IActivityManager.aidlIPackageManager.aidl等)均为隐藏类,因此很自然的想法是将系统的aidl源文件导入到工程中。

    配置好目录:

        sourceSets {
            main {
                aidl.srcDirs = ['src/main/aidl']
            }
        }
    

    编译后我们就可以连接该类,并进行继承扩展了,如:

    public class StubIntentReceiver extends IIntentReceiver.Stub {
        Object mOrigin;
    
        protected StubIntentReceiver(Object org) {
            this.mOrigin = org;
        }
    
        private static Method sMethod_performReceive;
        public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) throws RemoteException {
            // TODO something here ...
          
            if (null == sMethod_performReceive) {
                sMethod_performReceive = ReflectUtils.getDeclaredMethod(
                        mOrigin, "performReceive",
                        Intent.class, int.class, String.class, Bundle.class, boolean.class, boolean.class, int.class
                );
            }
            sMethod_performReceive.invoke(mOrigin, intent, resultCode, data, extras, ordered, sticky, sendingUser);
        }
    
    }
    

    对于IIntentReceiver.aidl的回调接口来说,这样就可以解决了,因为他满足了几个特性:

    1. 足够简单,就只有一个函数。
    2. 足够稳定,从9.0 ~ 14.0接口名和参数都一致。

    然而更多的接口并非如此,接口类函数不仅仅是多个,而且不同版本类方法各异,同函数参数也都不相同,这才是常态,所以我们自然的解决方案就是:flavor

    2.0 方案:Flavor

    既然每个版本可能不一致,那就编译多版本就可以解决了,如:

    这样确实能解决多版本系统接口变化的问题,但同时带来了新的问题:

    1. 多版本的编译,维护,加载运行导致工作量成倍增加,是个灾难。
    2. 通常接口中我们感兴趣的只是其中一部分,其他的接口则是直接放过。
    3. 很多系统接口参数又是继承于Parcelable的对象,而该对象又为隐藏类,因此又需要继续导入关联的类确保编译运行正常,导致越来越臃肿。
    4. 某些接口厂商还会在该类定制新的接口,无法做到默认兼容。

    3.0 方案:接口模板

    我们对于复杂的方案生来恐惧,越复杂越做不稳定,所以我们的目标:

    1. 无需多版本编译,一套代码适配所有版本。
    2. 仅需处理我们关心的接口,对于其他接口默认可放过。

    于是我们通过编译后的源码我们目标锁定在BinderonTransact函数,如:

    public interface IIntentReceiver extends android.os.IInterface
    {
      /** Local-side IPC implementation stub class. */
      public static abstract class Stub extends android.os.Binder implements android.content.IIntentReceiver
      {
        private static final java.lang.String DESCRIPTOR = "android.content.IIntentReceiver";
        /** Construct the stub at attach it to the interface. */
    
        @Override 
        public boolean onTransact(int code, 
                                  android.os.Parcel data,
                                  android.os.Parcel reply,
                                  int flags
                                 ) throws android.os.RemoteException
        {
          java.lang.String descriptor = DESCRIPTOR;
          switch (code)
          {
            case TRANSACTION_performReceive:
            {
                data.enforceInterface(DESCRIPTOR);
    						Intent _arg0;
                if (0 != data.readInt()) {
                    _arg0 = Intent.CREATOR.createFromParcel(data);
                } else {
                    _arg0 = null;
                }
    
                int _arg1 = data.readInt();
                String _arg2 = data.readString();
                Bundle _arg3;
                if (0 != data.readInt()) {
                    _arg3 = (Bundle)Bundle.CREATOR.createFromParcel(data);
                } else {
                    _arg3 = null;
                }
    
                boolean _arg4 = 0 != data.readInt();
                boolean _arg5 = 0 != data.readInt();
                int _arg6 = data.readInt();
    
              	// call function here !!!
                this.performReceive(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6);
    
                reply.writeNoException();
                return true;
            }
            default:
            {
              return super.onTransact(code, data, reply, flags);
            }
          }
        }
        
      }
    

    于是我们的方案:

    1. 定义目标接口类(如:IIntentReceiver.aidl),该接口无方法,仅保持名字一致,目的只是为了编译出IIntentReceiver.class类。
    2. 定义扩展类继承于接口代理类。
    3. 重载实现onTransact方法,仅处理感兴趣的codeaidl文件编译后函数对应的编号),其他的默认调用原对象方法。

    于是我们扩展实现类为:

    import android.content.IIntentReceiver;
    import android.content.Intent;
    import android.os.Bundle;
    import android.os.IBinder;
    import android.os.Parcel;
    import android.text.TextUtils;
    
    public class OnewayIIntentReceiver extends IIntentReceiver.Stub {
        private final Object mArgument;
        private static int TRANSACTION_performReceive = -1;
    
        public OnewayIIntentReceiver(Object org) {
            mArgument = org;
            if (TRANSACTION_performReceive < 0) {
                TRANSACTION_performReceive = ReflectUtils.getMethodCode(org, "TRANSACTION_performReceive");
            }
        }
    
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws android.os.RemoteException {
            if (TRANSACTION_performReceive == code) {
                data.enforceInterface(getInterfaceDescriptor());
                Intent _arg0;
                if (0 != data.readInt()) {
                    _arg0 = Intent.CREATOR.createFromParcel(data);
                } else {
                    _arg0 = null;
                }
    
                int _arg1 = data.readInt();
                String _arg2 = data.readString();
                Bundle _arg3;
                if (0 != data.readInt()) {
                    _arg3 = (Bundle)Bundle.CREATOR.createFromParcel(data);
                } else {
                    _arg3 = null;
                }
    
                boolean _arg4 = 0 != data.readInt();
                boolean _arg5 = 0 != data.readInt();
                int _arg6 = data.readInt();
    
                // do call origin
                Method method = ReflectUtils.getDeclaredMethod(
                        mArgument.mOrigin, "performReceive",
                        Intent.class, int.class, String.class, Bundle.class, boolean.class, boolean.class, int.class
                );
                method.invoke(mOrigin, _arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6);
    
                reply.writeNoException();
                return true;
            }
            return doTransact(code, data, reply, flags);
        }
      
        public boolean doTransact(int code, Parcel data, Parcel reply, int flags) {
            Method method = ReflectUtils.getDeclaredMethod(
                    mOrigin, "onTransact",
                    int.class, Parcel.class, Parcel.class, int.class
                );
            }
            try {
                return (Boolean) method.invoke(mOrigin, code, data, reply, flags);
            } catch (Throwable e) {
                Logger.e(e);
            }
            return false;
        }
    }
    

    至此,我们找到了相对简单,兼容性好的系统接口回调的拦截方案。

    如果该服务为Native实现,则需要参考我们的另一篇文章 ☞ 深入 Binder 拦截 ☜ 来解决。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1091 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 19:07 · PVG 03:07 · LAX 12:07 · JFK 15:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.