Binder中的SEAndroid控制
前言
Binder 也即Android独有的进程间通信方式,在 Android 系统中无处不在。区别于共享内存、socket、管道等其他进程间通信的手段,Binder 的实现较为独特。从本质上讲,Binder 是借由对/dev/binder 的一系列操作实现的,在内核中作为驱动存在,当然其权限控制可以正常借由Linux的安全模块实现。同时,由于所有系统服务都需要在ServiceManager类中进行注册,考虑ServiceManager总揽Binder的支配地位,又要对其中的权限控制进行特殊设计。
一、SELinux概述
首先复习一下SELinux的基本概念。SELinux 即Security-Enhanced Linux,搭建在Linux Security Module(LSM)基础上,目前已经成为最受欢迎,使用最广泛的安全方案。SELinux 是典型的Mandatory Access Controls(MAC)实现,对系统中每个对象都生成一个安全上下文(Security Context),每一个对象访问系统的资源都要进行安全上下文审查。审查的规则包括类型强制检测(type enforcement),多层安全审查(Multi-Level Security),以及基于角色的访问控制(RBAC: Role Based Access Control)。
此外,上文提到的LSM发布于2002年,是Linux 内核中一个轻量级、通用的访问控制框架,将许多不同的访问控制模型实现为可加载的内核模块。上述的MAC实现,包括RBAC(即SEAndroid中的domain)和type enforcement均是基于LSM框架的接口进行开发。
SELinux访问流程如下:
可以看到LSM在其中起到支配性作用。
二、binder相关权限
到kernel代码中追根溯源,在如下位置中可以看到针对binder,SELinux的安全权限是如何定义的。在common/security/selinux/include/classmap.h中可以找到如下代码:
其中权限解释如下:
(1)impersonate权限代表进程是否可以模拟另一个进程与其他进程通信;
(2)call代表Client与Server通信的权限;
(3)set_context_mgr代表是否可作为ContextManager管理binder,这一权限仅供servicemanager和hwservicemanager使用;
(4)transfer就是传输binder对象的权限。
实际使用中,transfer通常和需要和call搭配使用,但call权限则可以单独存在。此外,在旧版本的Android中,所有的app都属于unconfined_domain,因此具有call、set_context_mgr、transfer三个属性,即允许所有app之间可以任意通信,这样的设计也许是早期的无奈之举,但显然是不合理的。如今unconfined_domain的定义连带这种做法已经被废弃,开发者需要根据功能设计,单独添加适用的权限。
说到binder,就不得不提ServiceManager。这里引用一张图片辅助理解ServiceManager的特殊地位:
那么ServiceManager相关的SELinux权限是如何添加的呢?在system/sepolicy/public/servicemanager.te中可以找到如下代码:
注释表明,ServiceManager只作为 Binder 上下文管理器存在。因此,它只接收和传输由其他domain创建的其他引用,而从不传递自己的引用或启动 Binder IPC。这对应11行内容,即:允许 ServiceManager 进程向 Binder 驱动设置 ContextManager 。12行开始,添加了其向一系列domain包括init、vendor_init、hwservicemanager、vndservicemanager传输binder对象的权限。20行和22行同样体现了其作为上下文管理器的作用。最后添加了用于debug的权限。
三、设置 ContextManager 过程中的权限控制
ServiceManager这一系列权限中的最关键的是11行的set_context_mgr。ContextManager除了申请权限之外, 还负责相关代码功能的实现,这些功能与权限控制也紧密相关,下面对此进行详细介绍。
首先需要观察ServiceManager的启动过程。相关代码的位置在frameworks/native/cmds/servicemanager/中:
其中主要功能存在于Access.cpp、ServiceManager.cpp以及main.cpp中。其中main.cpp中由ServiceManager的入口:
经过多个版本迭代,main.c变为了main.cpp,转而使用面向对象的编程思想,令代码可读性大大增加,不过其基本功能仍维持不变。
(1)打开binder设备文件;
(2)设置自己为前文所述的ContextManager;
(3)进入循环,作为service等待client的请求。
其中关键步骤是:
126行 sp<ProcessState> ps = ProcessState::initWithDriver(driver);
136行 ps->becomeContextManager();
从字面理解,要了解权限控制,显然需要从becomeContextManager()函数入手。这一函数的定义在frameworks/native/libs/binder/ProcessState.cpp中:
函数首先初始化了一个struct:flat_binder_object(它的定义位于bionic/libc/kernel/uapi/linux/android/binder.h),其中的flag设置为FLAT_BINDER_FLAG_TXN_SECURITY_CTX,用于标记其属于ContextManager。
随后调用了ioctl,与驱动之间同步了信息。这些步骤虽然也是必须的,但似乎发生在权限控制之后。
回到main.cpp,果然发现在124行别有洞天:
这里使用Access初始化了manager。进入到Access.cpp中,可以看到这个类中封装了一系列权限控制的过程。而这个文件在老版本Android中并不存在。从它的初始化过程开始观察:
构造函数首先初始化了一个名为cb的selinux_callback的union对象。这个union定义在external/selinux/libselinux/include/selinux/selinux.h中,包含了一系列的函数指针,用于指定类似printf样式的格式和参数,同时也有专门指明消息类型的类型代码。之后将cb中的func_audit指定为静态函数auditCallback,该函数实际上只是封装了printf。随后调用selinux_set_callback,从cpp再到c中,指定callback pointer为对应函数指针。接下来的步骤也是类似的。最后检查了SELinux是否处于打开状态,以及线程是否存在。看来,使用cpp封装曾经的c代码的过程还是非常曲折的。
此外,我们还可以看到,初始化过程本身做的事情非常有限,仅仅是注册了一系列回调,且是跟输出log有关的回调,并没有实际功能。不过,初始化之后,Access.cpp中的一系列功能函数都可以通过面向对象的方式访问到。这些功能才是关键。我们集中分析一下被调用较多的getCallingContext()和canFind()函数:
getCallingContext()在frameworks/native/cmds/servicemanager/ServiceManager.cpp中被调用了12次。该函数返回一个CallingContext结构体,其中存储了pid、uid和sid。换句话说,这个函数返回的是当前线程的信息。再回过头观察getCallingContext()在ServiceManager中被调用的情况,发现其目的往往是获取函数返回的CallingContext结构体的引用,再将其传递给其他函数使用。而进一步观察这些接收结构体引用作为参数的函数,正是在Access.cpp中的其他函数,例如canFind(),canAdd(),canList(),actionAllowed()等。深究这些函数的实现,实际上都是进一步调用了external/selinux/libselinux/src/checkAccess.c中 selinux_check_access()函数。这个函数是SELinux提供给用户空间的用户计算策略之一,另一个是avc_has_perm()函数。
事实上,在原本的kernel代码中,selinux_check_access是一个c文件,内容丰富却又简单,几十行代码中甚至包含了一部分main.cpp中的功能。显然,为了让SELinux成为了SEAndroid,Google基于kernel的这部分代码做了拆分和封装,并进行了极大的扩充。
四、分析Access类的实现
Access.cpp中功能函数的实现如下:
可以看到,canFind()和canAdd()都调用了actionAllowedFromLookup()函数。事实上,这个函数首先调用了selabel_lookup(),字面上理解这个函数是进行了某种“查找”操作,随后调用actionAllowed()函数,间接调用了selinux_check_access()。这一过程在老版本的Android中,由property_service.c中的check_mac_perms()函数实现,当初是则直接调用selabel_lookup()和selinux_check_access()。如今重新进行封装和扩展后,接收了操作符perm作为参数,允许更为灵活地使用这两个C语言的函数;相比之下,原先的check_mac_perms()则直接指定perm为“set”,功能较为单一。至于canList()函数,则是直接调用了actionAllowed()。
回头去看selabel_lookup()的“查找”。这里不赘述其实现细节,简单而言,这个函数的功能是获得增加或者修改名称为name(第三个参数)的属性所需要的安全上下文tctx(第二个参数)。
接下来,就是调用了“主角”selinux_check_access()函数。其在Access.cpp中的调用是这样的:
函数的原型是int selinux_check_access(const char * scon, const char * tcon, const char *class, const char *perm, void *aux),需要输入的为主体、客体、客体类、权限集的字符串表示。这个函数目的是:检查请求增加或者修改属性的进程的安全上下文sctx是否有权限对安全上下文为tctx的属性进行操作。在调用它的actionAllowedFromLookup()中,selabel_lookup()的第二个参数和第三个参数,分别通过actionAllowed()传递到selinux_check_access()的第二个和第五个参数,这也与函数的功能相互印证。至于这两个函数本身的实现,基本上就是将规定权限的文本解析出来,之后按照函数功能或perm标识,去适用于不同的逻辑,返回需要的结果。此外,原先在init进程启动的时候初始化的全局变量sehandle_prop,在新的设计中也由getSehandle()封装,增加了一些判空的操作,并适用于vendor解耦后的新架构,更为稳妥地取到handle值。这个handle值也是SEAndroid for binder中一个重要的机制,在后文中会详细说明。
五、回看ServiceManager的权限管理
经过对SELinux一系列的讨论后,我们终于又可以回头来看binder相关的实现。这是因为随着我们对SEAndroid的深入理解,发现我们漏掉了一个重要部分:在frameworks/native/cmds/servicemanager/ServiceManager.cpp中,入口处调用addService()函数,增加了名为manager的服务。addService()这个函数的实现如下:
毫不夸张地说,做过Android开发的同志们99%都使用过这个函数(尤其是Java层的)。在了解了Access这个类的信息后,再看到这个函数的实现会感觉异常亲切。我们现在不光需要对权限控制的细节有所了解,还需要知道注册“manager”服务的时候,会有什么不一样的事情发生。需要说明的是,这里讨论范围仅限native代码。
函数首先使用Access类中的getCallingContext()函数获取当前的线程信息,这一点在前文当中已经说明。随后调用system/core/libcutils/multiuser.cpp中的multiuser_get_app_id()函数,将返回结果与AID_APP也就是10000比较,判断该Uid是否超过,若超过则返回错误状态。之后调用了Access.cpp中的canAdd()函数,前面说过,这个函数是查找当前进程是否有对名为name的服务进行“添加”也即Add操作的权限,换句话说,判断SELinux是否允许其添加该服务。如果没有相关权限,则会返回错误状态。随后是判断参数binder非空以及判断服务名是否合法,这里不再赘述。接下来对binder进行linkToDeath操作,覆写同名旧服务,最后注册了回调。可见,经过封装后的代码结构十分清晰,addService()函数中也仅仅用了一个mAccess->canAdd(ctx, name)就判断了是否可以添加服务。整个过程中似乎没有体现出“manager”服务的特殊性,但这其实也符合程序设计的思想。相比于老版Android中,在每次创建binder的时候,都还要去检查当前进程是否具有注册 ContextManager 的 SEAndroid 安全权限,新版的实现显然要更加合理。
再次回到调用addService()函数的main.cpp中的main()函数入口可以看到,注册manager服务后,函数调用了frameworks/native/libs/binder/IPCThreadState.cpp中的setTheContextObject()。这个函数仅仅是为了将ServiceManager传入为IPCThreadState的成员变量,从而令manager在executeCommand()中以the_context_object的身份执行TRANSACTION操作,即调用transact()函数。有一定英语水平的同志看到the_context_object这个成员变量的名字,会从“the XXX”这样的结构中感受到一股王霸之气。最后,正如前文中所讲的,Service线程在进入无限循环之前,用binder driver初始化而来的ProcessState调用了它本身的becomeContextManager()函数,利用ioctl的系统调用同步了信息。
前面我们已经由ServiceManager进入到了ProcessState和IPCThreadState。熟悉binder的同学会知道,IPCThreadState间接受到ProcessState单例模式控制,它们是实现binder一系列功能的核心。因此在其中必然涉及了权限管理的问题。不过这个过程隐藏的比较深,大致步骤是这样的:
首先需要binder通信的进程会维护一个ProcessState的对象,并由ProcessState打开binder驱动,建立线程池,让其进程里面所有的线程都能通过binder进行通信。同时,每个线程都有一个IPCThreadState实例登记在Linux线程的上下文附属数据中,主要负责Binder的读取,写入和请求处理框架。IPCThreadState在构造的时候获取进程的ProcessState并记录在自己的成员变量mProcess中,通过mProcess可以获得Binder的句柄。前文提到在IPCThreadState中有个transact()函数,这个函数在frameworks/native/libs/binder/BpBinder.cpp中被BpBinder::transact()调用。而BpBinder::transact()是覆盖了IBinder中的虚函数transact()。因此,在日常使用Binder的过程中,通常使用IBinder初始化BpInterface,再通过其中的remote()函数获取mRemote的成员变量也即IBinder对象,再去调用其transact()函数。看来,想要得知权限管理是如何进行的,我们就要去IPCThreadState::transact()中查看函数是如何实现的。
可见,这个函数中包含了大量判断是否需要response以及是否是单向通信的逻辑,而真正关键的在于writeTransactionData()这个函数,这也是IPCThreadState类中的成员函数。这个函数的功能十分简单,那就是用传入其中的标识binder传输的一系列参数,初始化binder_transaction_data,再调用mOut.write(&tr, sizeof(tr)),也就是frameworks/native/libs/binder/Parcel.cpp中的Parcel::write()向指定的地址写入数据。至此,client与service通信的过程还没有结束,还需要service读取client所写内容。首先,service在线程池中循环监听client的binder请求数据,调用getAndExecuteCommand()函数,从而调用其中的talkwithDriver()方法。在talkWithDriver()中进行了系统调用ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) 。这个系统调用的功能是对/dev/binder 进行先write再read操作,这个系统调用是阻塞式的。读写缓冲区就是bwr变量,这是一个binder_write_read结构体的引用。其结构如下:
write_size是写入的缓冲区大小,缓存区指针存放在 write_buffer,在这里等于mOut。read_size是读取缓冲区大小,缓冲区指针存放在read_buffer,在这里等于mIn。
在getAndExecuteCommand()函数的最后,调用了executeCommand()。这个函数在前文中已经提到过,其中涉及了ServiceManager传递数据的过程。当此次请求是一个客户端请求时,会走到BR_TRANSACTION的case中。上面的代码我们可以看到这么一句sp<BBinder> b((BBinder*)tr.cookie) ,这个是我们从binder驱动中读出的值。最后会调用onTransact()函数,这是一个纯虚函数,每一个Service需要自行实现它。这样,client发送的信息中包含的操作码,就可以被service根据实际情况处理成不同的实现。
七、binder传递过程中的SEAndroid权限检查
在明确了binder的通信过程后,我们需要了解执行SEAndroid权限检查位于哪一个步骤中。经过查阅资料发现,这部分内容体现在binder_transaction_data结构体当中,正好处在向binder驱动写入数据时。
函数 writeTransactionData()将数据组装成为 binder_transaction_data 结构体对象,然后将 cmd 和这个结构体对象 tr 都写入 IPCThreadState 的属性 mOut 中。每一个包含在通信数据里面的 Binder 对象都是用一个 flat_binder_object 结构体来描述。结构体 flat_binder_object 的结构如下:
在老版本的Android中,flat_binder_object的第一个成员变量不是binder_object_header,而是type,如今type被封装到了binder_object_header里面。但基本的逻辑仍然保持不变,当它的值等于 BINDER_TYPE_BINDER、BINDER_TYPE_WEAK_BINDER、BINDER_TYPE_HANDLE 和 BINDER_TYPE_WEAK_HANDLE 的时候,都表示它描述的是一个 Binder 对象。其中,前两者表示结构体 flat_binder_object 描述的是一个 Binder 实体对象,而后两者表示结构体 flat_binder_object 描述的是一个 Binder 引用对象。在 Binder 驱动中,Binder 实体对象使用结构体 binder_node 来描述,而 Binder 引用对象使用结构体 binder_ref 来描述。无论是哪一种情况,都需要通过调用函数security_binder_transfer_binder 来检查源进程 proc 是否具有向目标进程 target_proc 传递 Binder 对象的权限。
首先,Linux kernel中维护了一个security_hook_list类型的数组,security_hook_list的定义如下图所示,除了常规的hlist_node/hlist_head(用以支持hash list)之外,还包括一个union类型的securty_list_options,它即为对应的的SELinux操作的函数指针(hook)。
其次,这个数据结构是怎么与binder传输的检查关联起来的呢?这就要看一下security_hook_list数组的初始化了:
通过上述操作,可以看出,经过宏展开之后,变成了如下的数据结构:
static struct security_hook_list selinux_hooks[] __lsm_ro_after_init = {
...
{
.head = &security_hook_heads.binder_transfer_binder,
.hook = {
.binder_transfer_binder = selinux_binder_transfer_binder
}
},
...
}
然后,linux kernel的security.c中,会通过security_add_hooks将此security_hook_list节点添加到security_hook_heads.binder_transfer_binder 这个头节点中。
回到security_binder_transfer_binder,其实现为遍历security_hook_heads.binder_transfter_binder这个hash list头节点,并依次调用其hook方法,由于这里binder_transter_binder的hook列表里面目前仅有一个selinux_binder_transfter_binder,因此security_binder_transfter_binder会退化为调用selinux_binder_transfter_binder。
这里通过hash list以及数组列表的形式,实现了security_binder_transfter_binder可以依次经过多种安全检查,这些安全检查均可以挂载到security_hook_heads.binder_tranfter_binder这个头节点中。仅当所有安全检查都通过之后,本函数调用才会允许成功调用。基于此可以推断出,厂商可以扩展各种类型的binder传输检查,若想增加安全检查,可以通过往security_hook_heads.binder_tranfter_binder添加客制化的hook方法即可。
最后,在调用了hook根据源进程和目标进程获取了安全上下文之后,就可以调用 LSM 模块提供的函数 avc_has_perm 来检查源进程是否具有向目标进程传递 Binder 对象的 SEAndroid 安全权限了,也就是类型为 SECCLASS_BINDER 的 BINDER__TRANSFER 权限。这就使得binder调用与SELinux 的安全检查自上而下关联了起来。
八、总结
binder是Android中的进程间通信机制,因此SELinux在Android中应用后,势必要建立一套完整而又安全的权限控制机制。简而言之,binder的实现过程是系统服务到Service Manager中进行注册,之后借由实例化IBinder对象,以系统调用的方式操作dev/binder节点,进行通信。因此,在binder领域,ServiceManager具有最高的权限,可以检查系统服务是否被允许注册;而在通信过程中,权限的检查发生在kernel层,是由hook提供了增加/查找权限等功能,并维护了一些数据结构临时保存了权限信息,避免频繁地重复进行文本的解析和访问。
Linux kernel的security设计提供了一套安全检查的机制,SELinux是其中的一种安全检查的机制,除了SELinux之外,我们也可以对linux的security机制进行扩展(比如AppArmor、Yama、Smack等)。同理,SELinux中支持的权限检查在设计上也考虑的足够弹性,Android对其进行了扩展支持了binder调用的安全检查,除了binder调用检查之外,还可以扩展为更多的安全检查,比如某些设备提供了更加客制化进程、处理器间、私有资源的访问,基于安全的考虑,可以对这些访问添加和扩展更多的限制机制,从而能够更加安全的保护用户的设备。
参考资料:
1、https://blog.csdn.net/Luoshengyang/article/details/38326729
2、https://cs.android.com/
3、https://android.googlesource.com/
4、https://blog.51cto.com/u_15375308/4010106
5、https://docs.huihoo.com/doxygen/linux/kernel/3.7/index.html
6、https://www.systutorials.com/docs/linux/man/
参考代码链接:
1、https://github.com/torvalds/linux/blob/master/security/SELinux/include/classmap.h
2、https://android.googlesource.com/platform/frameworks/native/+/master/cmds/servicemanager/main.cpp
3、https://android.googlesource.com/platform/frameworks/native/+/refs/heads/master/cmds/servicemanager/
4、https://android.googlesource.com/platform/frameworks/native/+/refs/heads/master/cmds/servicemanager/main.cpp
5、https://android.googlesource.com/platform/frameworks/native/+/refs/heads/master/libs/binder/ProcessState.cpp
6、https://mirrors.edge.kernel.org/pub/