【Android】深入Binder底层拦截

查看 170|回复 10
作者:iofomo   

基于底层动态拦截技术,实现对Android平台下应用进程Binder通信协议的动态分析和拦截。

☞ Github ☜  ☞ Gitee ☜
说明
Binder作为Android系统跨进程通信的核心机制。网上也有很多深度讲解该机制的文章,如:
  • Android跨进程通信详解Binder机制原理
  • Android系统核心机制Binder【系列】

    这些文章和系统源码可以很好帮助我们理解Binder的实现原理和设计理念,为拦截做准备。借助Binder拦截可以我们可以扩展出那些能力呢:
    [ol]
  • 虚拟化的能力,多年前就出现的应用免安装运行类产品如:VirtualApp/DroidPlugin/平行空间/双开大师/应用分身等。
  • 测试验证的能力,通常为Framework层功能开发。
  • 检测第三方SDK或模块系统服务调用访问情况(特别是敏感API调用)。
  • 逆向分析应用底层服务接口调用实现。
  • 第三方ROM扩展Framework服务。
    [/ol]
    现有方案
    一直以来实时分析和拦截进程的Binder通信是通过Java层的AIDL接口代{过}{滤}理来实现的。借助于Android系统Binder服务接口设计的规范,上层的接口均继承于IBinder。
    如一下为代{过}{滤}理目标对象的所有的接口API的方法:
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Proxy;
    private static void getInterface(Class cls, final HashSet> ss) {
        Class[] ii;
        do {
            ii = cls.getInterfaces();
            for (final Class i : ii) {
                if (ss.add(i)) {
                    getInterface(i, ss);
                }
            }
            cls = cls.getSuperclass();
        } while (cls != null);
    }
    private static Class[] getInterfaces(Class cls) {
        final HashSet> ss = new LinkedHashSet>();
        getInterface(cls, ss);
        if (0 [ss.size()]);
        }
        return null;
    }
    public static Object createProxy(Object org, InvocationHandler cb) {
        try {
            Class cls = org.getClass();
            Class[] cc = getInterfaces(cls);
            return Proxy.newProxyInstance(cls.getClassLoader(), cc, cb);
        } catch (Throwable e) {
            Logger.e(e);
        } finally {
            // TODO release fix proxy name
        }
        return null;
    }
    1、对于已经生成的Binder服务对象,在应用进程可参与实现逻辑之前就已经缓存了,我们需要找到并且进行替换(AMS、PMS、WMS等),如AMS在Android 8.0之后的缓存如下:
    // source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/app/ActivityManager.java
    package android.app;
    public class ActivityManager {
        public static IActivityManager getService() {
            return IActivityManagerSingleton.get();
        }
        private static final Singleton[I] IActivityManagerSingleton =
                new Singleton[I]() {
                    @Override
                    protected IActivityManager create() {
                        final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                        final IActivityManager am = IActivityManager.Stub.asInterface(b);
                        return am;
                    }
                };
    }
    因此我们需要找到并且替换它,如:
    Object obj;
    if (Build.VERSION.SDK_INT
    2、对于后续运行过程中才获取的Binder服务,则需要代{过}{滤}理ServiceManager,源码如下:
    // source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/os/ServiceManager.java
    package android.os;
    public final class ServiceManager {
        private static final String TAG = "ServiceManager";
        private static IServiceManager sServiceManager;
    }
    因此我们的代{过}{滤}理如下:
    Class cls = ReflectUtils.findClass("android.os.ServiceManager");
    Object org = ReflectUtils.getStaticFieldValue(cls, "sServiceManager");
    Object pxy = new createProxy(org);
    if (null != pxy) {
        ReflectUtils.setStaticFieldValue(getGlobalClass(), "sServiceManager", pxy);
    }
    这样每次在第一次访问该服务时,就会调用IServiceManager中的getService的方法,而该方法已经被我们代{过}{滤}理拦截,我们可以通过参数可以识别当前获取的是哪个服务,然后将获取的服务对象代{过}{滤}理后在继续返回即可。
    但是:这样的方案并不能拦截进程中所有的Binder服务。我们面临几大问题:
    [ol]

  • 首先,Android源码越来越庞大,了解所有的服务工作量很大,因此有哪些服务已经被缓存排查非常困难。

  • 其次,厂商越来越钟情于扩展自定义服务,这些服务不开源,识别和适配更加耗时。

  • 再次,有一部分服务只有native实现,并不能通过Java层的接口代{过}{滤}理进行拦截(如:Sensor/Audio/Video/Camera服务等)。
    // source code: http://aospxref.com/android-13.0.0_r3/xref/frameworks/av/camera/ICamera.cpp
    class BpCamera: public BpInterface[I]
    {
    public:
        explicit BpCamera(const sp[I]& impl)
            : BpInterface[I](impl)
        {
        }
        // start recording mode, must call setPreviewTarget first
        status_t startRecording()
        {
            ALOGV("startRecording");
            Parcel data, reply;
            data.writeInterfaceToken(ICamera::getInterfaceDescriptor());
            remote()->transact(START_RECORDING, data, &reply);
            return reply.readInt32();
        }
    }
    [/ol]
    新方案:基于底层拦截
    原理
    我们都知道Binder在应用进程运行原理如下图:


    topic-binderceptor.png (67.33 KB, 下载次数: 0)
    下载附件
    2023-12-7 19:04 上传

    不管是Java层还是native层的接口调用,最后都会通过ioctl函数访问共享内存空间,达到跨进程访问数据交换的目的。因此我们只要拦截ioctl函数,即可完成对所有Binder通信数据的拦截。底层拦截有以下优势:
    1)可以拦截所有的Binder通信。
    2)底层拦截稳定,高兼容性。从Android 4.x至Android 14,近10年的系统版本演进,涉及到Binder底层通信适配仅两次;一次是支持64位进程(当时需要同时兼容32位和64位进程访问Binder服务)。另一次是华为鸿蒙系统的诞生,华为ROM在Binder通信协议中增加了新的标识字段。
    要解决的问题
    如何拦截
    C/C++层的函数拦截,并不像Java层一样系统提供了较为稳定的代{过}{滤}理工具,在这里不是我们本期讨论的重点,可以直接采用网上开源的Hook框架:
  • https://github.com/bytedance/android-inline-hook
  • https://github.com/bytedance/bhook
  • https://github.com/asLody/whale

    如何过滤
    ioctl函数为系统底层设备访问函数,调用及其频繁,而Binder通信调用只是其中调用者之一,因此需要快速识别非Binder通信调用,不影响程序性能。
    函数定义:
    #include
    int ioctl(int fildes, unsigned long request, ...);
    request的参数定义:
    // source code: http://aospxref.com/android-14.0.0_r2/xref/bionic/libc/kernel/uapi/linux/android/binder.h
    #define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)
    #define BINDER_SET_IDLE_TIMEOUT _IOW('b', 3, __s64)
    #define BINDER_SET_MAX_THREADS _IOW('b', 5, __u32)
    #define BINDER_SET_IDLE_PRIORITY _IOW('b', 6, __s32)
    #define BINDER_SET_CONTEXT_MGR _IOW('b', 7, __s32)
    #define BINDER_THREAD_EXIT _IOW('b', 8, __s32)
    #define BINDER_VERSION _IOWR('b', 9, struct binder_version)
    #define BINDER_GET_NODE_DEBUG_INFO _IOWR('b', 11, struct binder_node_debug_info)
    #define BINDER_GET_NODE_INFO_FOR_REF _IOWR('b', 12, struct binder_node_info_for_ref)
    #define BINDER_SET_CONTEXT_MGR_EXT _IOW('b', 13, struct flat_binder_object)
    #define BINDER_FREEZE _IOW('b', 14, struct binder_freeze_info)
    #define BINDER_GET_FROZEN_INFO _IOWR('b', 15, struct binder_frozen_status_info)
    #define BINDER_ENABLE_ONEWAY_SPAM_DETECTION _IOW('b', 16, __u32)
    #define BINDER_GET_EXTENDED_ERROR _IOWR('b', 17, struct binder_extended_error)
    对应的源码:
    // source code: http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/IPCThreadState.cpp
    void IPCThreadState::threadDestructor(void *st) {
            ioctl(self->mProcess->mDriverFD, BINDER_THREAD_EXIT, 0);
    }
    status_t IPCThreadState::getProcessFreezeInfo(pid_t pid, uint32_t *sync_received, uint32_t *async_received) {
        return ioctl(self()->mProcess->mDriverFD, BINDER_GET_FROZEN_INFO, &info);
    }
    status_t IPCThreadState::freeze(pid_t pid, bool enable, uint32_t timeout_ms) {
        return ioctl(self()->mProcess->mDriverFD, BINDER_FREEZE, &info) mProcess->mDriverFD, BINDER_GET_EXTENDED_ERROR, &ee) mDriverFD, BINDER_WRITE_READ, &bwr);
    }
    快速过滤:
    static int ioctl_hook(int fd, int cmd, void* arg) {
        if (cmd != BINDER_WRITE_READ || !arg || g_ioctl_disabled) {
            return g_ioctl_func(fd, cmd, arg);
        }
    }
    如何解析
    目标源码:http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder
    重点解析发送(即BC_TRANSACTION和BC_REPLY)和接收(即BR_TRANSACTION和BR_REPLY)的类型数据。
    如何修改数据
    修改数据分为以下几种:
    1)修复调用时参数数据。
    2)修复调用后返回的结果数据。
    如果数据修复不改变当前数据的长度,只是内容的变化,则可以直接通过地址进行修改。否则需要创建新的内存进行修改后将新的数据地址设置到BINDER_WRITE_READ结构的buffer成员。此时处理好内存的释放问题。
    3)直接拦截本次调用。
    为了保障稳定性,不打断Binder的调用流程(通常这也是拦截和逆向方案保障稳定的最重要原则之一)。我们可以将目标函数code修改成父类处理的通用方法,然后通过修复调用的返回值即可完成拦截。
    方案实现
    数据解析
    Binder调用数据结构如下:


    1.png (51.25 KB, 下载次数: 0)
    下载附件
    2023-12-7 19:04 上传

    解析bwr
    bwr即binder_write_read,从源码中了解到ioctl的BINDER_WRITE_READ类型的arg数据结构为:
    struct binder_write_read {
      // 调用时传入的数据
             binder_size_t write_size;// call data
             binder_size_t write_consumed;// call data
             binder_uintptr_t write_buffer;// call data
            // 结果返回数据
             binder_size_t read_size;// recv data
             binder_size_t read_consumed;// recv data
             binder_uintptr_t read_buffer;// recv data
    };
    不管是传入还是返回的数据,都是一组BC命令或BR命令,也就是说一次调用上层会打包几个命令一起传递。因此我们需要通过循环来找到我们的命令。
    void binder_find_for_bc(struct binder_write_read& bwr) {
        binder_uintptr_t cmds = bwr.write_buffer;
        binder_uintptr_t end = cmds + (binder_uintptr_t)bwr.write_size;
        binder_txn_st* txn = NULL;
        while (0
    dump数据如下:
    write_buffer:0xb400007107d1d400, write_consumed:68, write_size:68
    00000000:  00 63 40 40 14 00 00 00  00 00 00 00 00 00 00 00  .c@@............
    00000010:  00 00 00 00 01 00 00 00  12 00 00 00 00 00 00 00  ................
    00000020:  00 00 00 00 54 00 00 00  00 00 00 00 00 00 00 00  ....T...........
    00000030:  00 00 00 00 00 4d 3a ac  70 00 00 b4 00 00 00 00  .....M:.p.......
    00000040:  00 00 00 00                                       ....
    BR_NOOP: 0x720c
    BR_TRANSACTION_COMPLETE: 0x7206
    BR_REPLY: 0
    解析txn
    txn即binder_transaction_data,Binder方法调用的方法参数信息定义如下:
    struct binder_transaction_data {
    union {
         __u32 handle;
         binder_uintptr_t ptr;
    } target;// 目标服务句柄,server端使用
    binder_uintptr_t cookie;// 缓存的Binder进行访问
    __u32 code;//方法编号
    __u32 flags;// 标识,如是否为 oneway
    __s32 sender_pid;
    __u32 sender_euid;
    binder_size_t data_size;// 数据长度
    binder_size_t offsets_size;// 若包含对象,则对象数据大小
    union {
         struct {
             binder_uintptr_t buffer;// Binder方法参数值地址
             binder_uintptr_t offsets;// Binder方法参数对象数据地址
         } ptr;
         __u8 buf[8];
    } data;
    };
    dumo数据如下:
    Trace   : target:       1   cookie:       0   code:      23   flags:   0x12(READ REPLY)
    Trace   :    pid:       0      uid:       0   size:     196    offs:8
    Trace   : 00000000:  00 00 00 80 ff ff ff ff  54 53 59 53 1c 00 00 00  ........TSYS....
    Trace   : 00000010:  61 00 6e 00 64 00 72 00  6f 00 69 00 64 00 2e 00  a.n.d.r.o.i.d...
    Trace   : 00000020:  61 00 70 00 70 00 2e 00  49 00 41 00 63 00 74 00  a.p.p...I.A.c.t.
    Trace   : 00000030:  69 00 76 00 69 00 74 00  79 00 4d 00 61 00 6e 00  i.v.i.t.y.M.a.n.
    Trace   : 00000040:  61 00 67 00 65 00 72 00  00 00 00 00 85 2a 62 73  a.g.e.r......*bs
    Trace   : 00000050:  13 01 00 00 00 38 dd 2a  71 00 00 b4 00 05 e9 31  .....8.*q......1
    Trace   : 00000060:  71 00 00 b4 01 00 00 0c  1a 00 00 00 63 00 6f 00  q...........c.o.
    Trace   : 00000070:  6d 00 2e 00 69 00 66 00  6d 00 61 00 2e 00 74 00  m...i.f.m.a...t.
    Trace   : 00000080:  72 00 61 00 6e 00 73 00  65 00 63 00 2e 00 63 00  r.a.n.s.e.c...c.
    Trace   : 00000090:  6f 00 6e 00 74 00 61 00  69 00 6e 00 65 00 72 00  o.n.t.a.i.n.e.r.
    Trace   : 000000a0:  00 00 00 00 08 00 00 00  73 00 65 00 74 00 74 00  ........s.e.t.t.
    Trace   : 000000b0:  69 00 6e 00 67 00 73 00  00 00 00 00 00 00 00 00  i.n.g.s.........
    Trace   : 000000c0:  01 00 00 00                                       ....
    Trace   : binder object offs:0x4c  type:0x73622a85  flags:0x113  ptr:0x2add3800  cookie:0x31e90500
    解析服务名
    Binder通信数据头如下,即可解析出目标服务名:
    void find_server_name(const binder_txn_st* txn) {
                    const int32_t* ptr = reinterpret_cast(txn->data.ptr.buffer);
              ++ ptr;// skip strict model
        if (29
    解析方法名
    Binder通信数据中标识该服务方法的参数是txn->code。AIDL定义类在编译后会为每个方法自动生成静态的方法。
    如定义的Binder接口方法为:
    interface IDemo {
      void test();
      void test2();
    }
    则编译后生成的类为:
    class IDemo$Stub {
       void test();
       void test2();
      static final int TRANSACTION_test = 1;
      static final int TRANSACTION_test2 = 2;
    }
    因此我们可以通过反射的方式,找到服务名对应的类所有静态成员变量,然后找到与code值相等的成员即为此方法。

    这里可能需要解决私有API的限制解除问题。

    // 可直接使用工程工具类
    TstClassPrinter.printStubByCodes("android.app.IActivityManager", 13, 16, 67);
    日志输出如下:


    2.png (106.57 KB, 下载次数: 0)
    下载附件
    2023-12-7 19:04 上传

    解析数据
    首先需要借助数据封装类Parcel。
    // souce code:
    // http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/include/binder/Parcel.h
    // http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/Parcel.cpp
    借助该类可以解析一些比较简单的数据,快速的找到目标内容。而对于比较复杂的数据,如参数值为Intent,该参数类型嵌套了多层的Parcelable成员,因此在native层通过Parcel来解析,兼容性比较差。因此我们选择通过回调到Java层来解析,修改后再格式化为native的buffer数据。这里需要处理好Java和native层的数据交换问题,以及回收。
    native层:
    // 创建
    jobject obtain(JNIEnv* env) {
        jclass jcls = env->FindClass("android/os/Parcel");
        jmethodID method = env->GetStaticMethodID(jcls, "obtain", "()Landroid/os/Parcel;");
        if (!method) return NULL;
        mParcelObj = env->CallStaticObjectMethod(jcls, method);
        if (!mParcelObj) return NULL;
        if (0 dataSize()) {
            method = env->GetMethodID(sParcelClass, "setDataPosition", "(I)V");
            if (method) {
                unmarshall(env, mUparcel->data(), mUparcel->dataSize());
                env->CallVoidMethod(mParcelObj, method, mUparcel->dataPosition());
            }
        }
        return mParcelObj;
    }
    // 回收
    void recycle(JNIEnv* env) {
        jclass jcls = env->FindClass("android/os/Parcel");
        jmethodID method = env->GetMethodID(jcls, "recycle", "()V");
        if (method) {
            env->CallVoidMethod(mParcelObj, method);
        }
        if (mParcelObj) {
            env->DeleteLocalRef(mParcelObj);
        }
        mParcelObj = NULL;
    }
    Java层:
    public static void clearHttpLink(Parcel p/*IN*/, Parcel q/*OUT*/) {
        try {
            Intent ii = Intent.CREATOR.createFromParcel(pp);
                    // TODO something ...
            // write new data
            q.appendFrom(p, p.dataPosition(), p.dataAvail());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    数据拦截
    Binder的数据解析和打印不会改变原数据内容,因此相对简单,如果要对数据进行修改,则相对复杂一些。修复的数据需要替换原数据,因此需要进行如下操作。
    1、数据替换。
    将txn中方法参数的数据指针指向新创建的数据区。
    int binder_replace_txn_for_br(binder_txn_st *txn, ParcelEx* reply, binder_size_t _pos) {
        size_t size = reply->ipcDataSize();
        uint8_t* repData = (uint8_t*)malloc(size + txn->offsets_size);
        memcpy(repData, reply->data(), size);
        if (0 offsets_size) {
            binder_replace_objects(txn, repData, _pos, ((int)size) - ((int)(txn->data_size)));
        }
        txn->data.ptr.buffer = reinterpret_cast(repData);
        txn->data_size = size;
        return 0;
    }
    2、修正对象指针。
    如果传入的参数包含Binder对象,如register方法的Observe。因此修复的数据可能导致偏移的地址前移或者后移,因此需要重新计算偏移,如:
    void replaceObjects(binder_txn_st *txn, uint8_t* objData, binder_size_t _pos, int _off) {
        binder_size_t* offs = reinterpret_cast(txn->data.ptr.offsets);
        unsigned count = txn->offsets_size / sizeof(binder_size_t);
        while (0 data.ptr.buffer + (int)(*offs), sizeof(binder_size_t))) {
                *offs += _off;
            }
            ++ offs;
        }
    }
    3、内存释放。
    需要保存原地址A和新的地址AA的映射关系到自定义的内存池中。
    当Binder通信命令出现BC_FREE_BUFFER和BR_FREE_BUFFER时,则通过该命令要释放的AA地址,然后从内存池找到与之对应A的地址,并设置回去让上层继续释放,完成内存使用的闭环。
    case BC_FREE_BUFFER:
    {
        uintptr_t* buffPtr = (uintptr_t *)cmd;
        uintptr_t ptr = MemPool::detach(*buffPtr);
        if (__UNLIKELY(0 != ptr)) {
            *buffPtr = ptr;// set origin buffer
        }
        cmd += sizeof(uintptr_t);// move to next command
    }   break;
    附:
    如果你有需要,可以直接使用我们已经封装好的SDK来实现相应的功能,该项目已经开源,可以直接使用,参考【集成文档】。

    数据, 方法

  • debug_cat   

    有些疑问,在集成文档中,[C++] 纯文本查看 复制代码参数过滤
    由于Binder通信日志繁多,无法甄别,因此我们需要过滤接口,比如只需要打印那些方法的参数中包含目标内容。如只打印那些Binder通信中包含字符串"com.demo"(Binder通信字符通常为uint16_t宽字符)内容的调用日志。
    由于Binder通信调用分为同步(target)和异步(oneway)两种方式,因此需要在对应的两个方法回调中处理,如:
    // target argument:com.demo
    static uint16_t g_debug_target[] = {'c','o','m','.','d','e','m','o'};
    static const uint32_t g_debug_target_length = 8;
    static bool doCheckService(binder_txn_st* txn, TBinderInfo& pInfo, TBinderTokenItem& token, bool oneway) {
        if ((txn->data_size data.ptr.buffer;
        for (int i=0, len = txn->data_size-sizeof(g_debug_target)-sizeof(int32_t); i  这个参数过滤的案例,应该怎么用。
    如:这个代码需要在哪里添加?
    在哪个模块中添加代码?
    需要引入那些头文件?
    添加后,如何测试,只能拦截当前App的调用得到log?
    还有GitHub上面的App模块中,有一个core.so,这个so是项目自带的,GitHub上面代码并不能编译得到core.so。
    iofomo
    OP
      


    debug_cat 发表于 2023-12-8 14:38
    有些疑问,在集成文档中,[mw_shl_code=cpp,true]参数过滤
    由于Binder通信日志繁多,无法甄别,因此我们需 ...

    抱歉core.so的模块还没有开源,但是提供了回调的接口
    见Binderception_debug.cpp中添加,你也可以参照BinderceptionDebug自定义类
    lukbinx   

    又是看不懂的文章
    walykyy   

    虽然看不懂,但是还是的支持一下,大佬发的文章值得学习
    q504860058   

    虽然看不懂,但是还是的支持一下,大佬发的文章值得学习
    1
    Recalled   

    感谢大佬分享
    虽然看不懂,但是还是的支持一下,大佬发的文章值得学习
    kira20220313   

    好好 学习
    wcy78619   

    这是论文吗?
    aonima   

    我是来膜拜大佬的,虽然看不懂,也不会用
    您需要登录后才可以回帖 登录 | 立即注册