ebpf在Android安全上的应用:结合binder完成一个行为检测沙箱(下篇)
2024-4-25 17:45:53 Author: mp.weixin.qq.com(查看原文) 阅读量:12 收藏

作者坛账号:windy_ll

一、IPC简单介绍

IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。

Android在什么时候会有跨进程通信的需要?Android在请求系统服务的时候会有跨进程通信的需求,例如访问手机通讯录、获取定位等等行为,本文的目标即是实现一个简易的捕捉这些行为的沙箱


二、binder简单介绍

Binder是Android中的一种跨进程通信方式,可以理解为是IPC的一种具体实现方式


三、ServiceManager简单介绍

ServiceManager是Android中一个及其重要的系统服务,从它的名称上就可以知道,它是用于管理系统服务的

ServiceManagerinit进程启动

ServiceManager负责了以下的一些功能:服务的注册与查找、进程间通信、系统服务的启动与唤醒、提供系统服务的清单实例

binder驱动决定了底层的通信详情,那么ServiceManager则相当于导航,告诉具体的通信该怎么走,达到那里等


四、通信分析

4.1 客户端调用JAVA层分析

WifiManager类的getConnectInfo函数(该函数获取wifi信息)为例进行分析

ctrl+左键查看引用,可以发现该函数定义在android.net.wifi.WifiManager类中,如下图所示:

从上图可以看到,getConnectInfo函数具体代码只有一句throw new RuntimeException("Stub!");,这告诉我们这个函数是由rom中相同的类去替代执行,该函数在这被定义是编译所需要(PS:可以参考https://blog.csdn.net/ttkatrina/article/details/76180641),在android源码中的目录frameworks/base/wifi/java/amdroid/net/wifi下我们可以找到该类,然后找到该函数的具体实现,如下图所示:

可以发现该函数调用了IWifiManagergetConnectionInfo函数,在frameworks/base/wifi/java/amdroid/net/wifi目录下可以找到IWifiManager.aidl文件,该aidl中定义了getConnectionInfo函数,如下图所示:

这里需要引入一个概念 --- AIDLAIDL是android的一种接口语言,用于公开android服务的接口,以此来实现跨进程的函数调用。AIDL在编译时会生成两个类,即StubProxy两个类,Stub类是服务端抽象层的体现,Proxy是客户端获取的实例,android通过proxy-stub这种设计模式实现了IPC

下面写一个aidl文件然后生成相应的java代码来看看是怎么实现调用的,首先,我们在android studio中随便找一个项目,然后新建一个aidl文件,如下图所示:

然后Build->Make Probject即可生成,生成的路径位于build/generated/aidl_source_output_dir/debug/out/包名,如下图所示:

观察生成后的java文件可发现,Proxy类已经生成,在Proxy类中我们可以找到我们定义的函数,如下图所示:

具体分析一下该函数,首先通过obtain函数生成了一个Parcel实例,然后调用Parcelwrite系列函数进行写入,其实就是一个序列化的过程,然后调用了IBindertransact函数,跟踪分析一下该函数,在目录frameworks/base/core/java/android/os下可以找到该java文件,如下图所示:

可以发现,IBinder仅仅是一个接口,其中定义了transact方法,该方法有4个参数,第一个参数code在我们的远程调用中为函数编号,服务端接受到这个编号后,会去寻找Stub类中的静态变量,从而解析出是调用那个函数,第二个和第三个参数_data_reply为传入的参数和返回的值,都是经过序列化后的数据,最后一个参数flags为指示是否需要阻塞等待结果,0为阻塞等待,1为立即返回。

全局搜索一下,可以发现同目录下的BinderProxy类实现了该接口(PS:值得注意的是,同目录下面还存在一个Binder类,也实现了该接口,但Binder类是服务端的实现,而不是客户端的实现),如下图所示:

分析该函数,可以发现最后走向了transactNative函数,到此为止,进行IPC通信客户端java层已经分析完毕

4.2 客户端调用Native层分析

全局搜索一下transactNative函数,可以发现该函数在native层中注册信息,如下图所示:

跟踪一下android_os_BinderProxy_transact函数,可以发现该函数首先通过getBPNativeData(env, obj)->mObject.get()获取到了一个BpBinder对象,然后调用了BpBindertransact函数,如下图所示:

继续跟进下去,可以发现进入了IPCThreadStatetransact函数,如下图所示:

接着跟进,可以发现首先调用writeTransactionData函数,该函数作用为填充binder_transaction_data结构体,为发送到binder驱动做准备,然后调用waitForResponse等待返回,如下图所示:

跟进waitForResponse函数,可以发现该函数最重要的就是调用talkWithDriver函数,分析一下talkWithDriver函数,可以发现最终调用了ioctl,如下图所示:

到处为止,客户端native层分析完毕

4.3 内核层分析(binder驱动分析)

到此处,我们的ebpf程序就可以开始捕捉然后解析数据格式了

当用户层调用ioctl时,会进入内核态,进入binder_ioctl内核函数(ps:可在内核设备源码中的binder.c找到相应的描述符),分析一下binder_ioctl函数,可发现该函数主要作用为在两个进程之间首发数据,我们的通信数据ioctl命令是BINDER_WRITE_READ,当遇到该命令的时候,会调用binder_ioctl_write_read函数,如下图所示:

跟进binder_ioctl_write_read函数,可以发现,该函数首先将unsigned long类型的arg参数指向的地址的值读取到结构体binder_write_read中,说明当ioctl命令为BINDER_WRITE_READ时,传递进来的参数为指向结构的binder_write_read的指针,如下图所示:

到这里其实我们内核态的分析已经可以结束了,我们已经观察到了我们想要的数据了,即binder_write_read结构体,可以看一下该结构体的定义,如下所示:

 复制代码 隐藏代码
struct binder_write_read {
    binder_size_t write_size; /* 写内容的数据总大小 */
    binder_size_t write_consumed; /* 记录了从缓冲区读取写内容的大小 */
    binder_uintptr_t write_buffer; /* 写内容的数据的虚拟地址 */
    binder_size_t read_size; /* 读内容的数据总大小 */
    binder_size_t read_consumed; /* 记录了从缓冲区读取读内容的大小 */
    binder_uintptr_t read_buffer; /* 读内容的数据的虚拟地址 */
};

这个结构体是用来描述进程间通信过程中所传输的数据,我们读取从客户端发送到服务端的通信包只需要关注write_sizewrite_consumedwrite_buffer,其中,write_buffer指向的是一个数组,这个数组中就包含了binder_transaction_data结构体,这个结构体在native层writeTransactionData函数填充的,我们的目标通信包即是这个结构体,其中,write_bufferread_buffer指向的数据结构大致如下:

一般来说,驱动会一次性传递多条命令和地址的组合,而我们要在其中找到binder_transaction_data结构体对应的指令,在binder.h头文件中我们可以找到驱动定义的所有指令(https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/include/uapi/linux/android/binder.h),如下图所示:

可以发现,BC_TRANSACTIONBC_REPLY指令都对应着binder_transaction_data结构体参数,但我们只需要客户端发往驱动的数据包,所以我们只需要BC_TRANSACTION指令对应的参数即可

经过上面的分析,我们找到了我们需要的核心数据---binder_transaction_data结构体,现在来看一下该结构体的定义,定义如下:

 复制代码 隐藏代码
struct binder_transaction_data {
    union {
        __u32 handle;
        binder_uintptr_t ptr;
    } target; /* 该事务的目标对象 */
    binder_uintptr_t cookie; /* 只有当事务是由Binder驱动传递给用户空间时,cookie才有意思,它的值是处理该事务的Server位于C++层的本地Binder对象 */
    __u32 code; /* 方法编号 */
    __u32 flags;
    pid_t sender_pid;
    uid_t sender_euid;
    binder_size_t data_size; /* 数据长度 */
    binder_size_t offsets_size; /* 若包含对象,对象的数据大小 */
    union {
        struct {
            binder_uintptr_t buffer; /* 参数地址 */
            binder_uintptr_t offsets; /* 参数对象地址 */
        } ptr;
        __u8 buf[8];
    } data; /* 数据 */
};

有了该数据结构,我们就可以知道客户端调用服务端的函数的函数、参数等数据了


五、实现效果

PS:整套系统用于商业,就不做开源处理了,这里只给出核心结构体打印的截图,就不再发前端的截图了

读取手机通讯录(ps:这里读取出来的数据采用了Toast打印的,所以将捕捉到的Toast相应的通信包也一起打印了出来,下同):

获取地理位置:

获取wifi信息:


六、其他

上面其实我们只分析到了发送部分,反过来,其实我们也可以读取返回包甚至于修改返回包,就可用于对风控的对抗,例如在内核态中修改APP请求的设备标识信息(当然前提是app走系统提供的驱动通信),亦或者用于逆向的工作,例如过root检测等等。

这部分验证代码就暂不放出来了,感兴趣的可以自己实现一下

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知
开放注册、精华文章和周边活动等公告


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5Mjc3MDM2Mw==&mid=2651140600&idx=1&sn=f0161296c1a102ae251fb3346709d387&chksm=bd50a1ac8a2728ba58d95b4478fd1080a2e50a89ae57517592b5e82db5270120af11a5dc38b5&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh