首页 > 代码库 > SEAndroid安全机制对Android属性访问的保护分析
SEAndroid安全机制对Android属性访问的保护分析
Android系统通过属性暴露设备和运行时信息,并且可以通过设置属性来控制系统行为。因此,属性也像文件一样,是一种需要保护的资源。在启用SEAndroid之前,敏感属性只能被预先设定的进程进行设置。启用SEAndroid之后,敏感属性会进一步被SEAndroid安全策略保护。这样就可以更有效地保护系统属性了。在本文中,我们就详细分析SEAndroid安全机制对Android属性设置保护提供的支持。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
在分析SEAndroid安全机制对Android属性保护的支持之前,我们首先要了解Android属性的实现框架,如图1所示:
图1 Android属性的实现框架
Android系统的所有属性都是保存在内存中。这块属性内存区是通过将文件/dev/__properties__映射到内存中创建的,它由头部区域和属性值区域两部分内容组成。
属性区头部通过结构体prop_area描述,如下所示:
struct prop_area { unsigned volatile count; unsigned volatile serial; unsigned magic; unsigned version; unsigned reserved[4]; unsigned toc[1]; };这个结构体定义在文件bionic/libc/include/sys/_system_properties.h中。
结构体prop_area的各个成员变量的含义如下所示:
count: 属性内在区当前包含的属性个数。
serial: 增加或者修改属性时用到的同步变量。
magic: 属性内存区魔数,用来识别属性内存区。
version: 属性内在区版本号,用来识别属性内存区。
reserved: 保留字段。
toc: 属性内容索引表。
属性值区域由一系列的属性组成,每一个属性都通过结构体prop_info描述,如下所示:
struct prop_info { char name[PROP_NAME_MAX]; unsigned volatile serial; char value[PROP_VALUE_MAX]; };这个结构体定义在文件bionic/libc/include/sys/_system_properties.h中。
结构体prop_info的各个成员变量的含义如下所示:
name: 属性名称,长度最大为32个字节。
serial: 修改属性值时用到的同步变量,同时也用来描述属性值的长度。
value: 属性值,长度最大为92个字节。
属性内存区域最多可以容纳的属性个数为247个,并且每一个属性在头部的索引表都对应有一个类型为unsigned的索引项。每一个属性占用的字节数为32 + 4 + 92 = 128,每一个属性索引项占用的字节数是4个字节,因此属性内存区头部,即结构体prop_area,总区占用的字节数是4 + 4 + 4 + 4 + 4 * 4 + 4 * 247 = 1020。紧接着属性内存区头部的属性值区域。考虑到对齐因素,属性值区域从属性区域的1024个字节偏移开始。因此,整个属性区域占用的内存大小是1024 + 247 * 128 = 32768个字节,刚好就是32Kb。
在结构体prop_area中,magic和version是两个用来识别属性内存区域的字段,它们的值分别被设置为PROP_AREA_MAGIC和PROP_AREA_VERSION。PROP_AREA_MAGIC和PROP_AREA_VERSION是两个宏,它们的值如下所示:
#define PROP_AREA_MAGIC 0x504f5250 #define PROP_AREA_VERSION 0x45434f76这两个宏定义在文件bionic/libc/include/sys/_system_properties.h中。
在结构体prop_area中,toc是table of content的简称,描述的是属性索引表。每一个属性在这个索引表里面都对应有一个索引项。每一个索引项都使用一个无符号数来表示。其中,最高一个字节描述的是对应的属性的名称的长度,低三个字节描述的是对应的属性相对于属性内存区域开头的偏移。通过宏TOC_NAME_LEN和TOC_TO_INFO可以获得一个索引项对应的属性的名称的长度和内存位置,如下所示:
#define TOC_NAME_LEN(toc) ((toc) >> 24) #define TOC_TO_INFO(area, toc) ((prop_info*) (((char*) area) + ((toc) & 0xFFFFFF)))这两个宏定义在文件bionic/libc/include/sys/_system_properties.h中。
其中,toc描述的是一个索引项,而area描述的是属性内存区域头部。也就是说,给出一个结构体prop_area,以及一个索引项n,我们就可以找到一个对应的prop_info结构体。这个功能可以通过调用函数__system_property_find_nth来实现。
函数__system_property_find_nth的实现如下所示:
const prop_info *__system_property_find_nth(unsigned n) { prop_area *pa = __system_property_area__; if(n >= pa->count) { return 0; } else { return TOC_TO_INFO(pa, pa->toc[n]); } }这个函数定义在文件libc/bionic/system_properties.c中。
在函数__system_property_find_nth中,__system_property_area__指向的就是一个prop_area结构体,而参数n描述的就是一个属性索引项。只有在参数n的值小于__system_property_area__描述的属性区域当前包含的属性个数的前提下,使用宏TOC_TO_INFO获得的prop_info结构体才是正确的。
此外,给出一个结构体prop_area,以及一个属性名称,我们也可以找到一个对应的的prop_info结构体。这个功能可以通过调用函数__system_property_find来实现。
函数__system_property_find的实现如下所示:
const prop_info *__system_property_find(const char *name) { prop_area *pa = __system_property_area__; unsigned count = pa->count; unsigned *toc = pa->toc; unsigned len = strlen(name); prop_info *pi; while(count--) { unsigned entry = *toc++; if(TOC_NAME_LEN(entry) != len) continue; pi = TOC_TO_INFO(pa, entry); if(memcmp(name, pi->name, len)) continue; return pi; } return 0; }这个函数定义在文件libc/bionic/system_properties.c中。
函数__system_property_find通过遍历__system_property_area__描述的属性区域头部中的索引表,找到每一个属性的名称,并且拿来与参数name描述的属性名称比较,直到找到一个相等的值为止,这样就意味着找到了对应的prop_info结构体。
在结构体prop_area中,serial用来同步属性的增加或者修改。每一次属性修改或者增辊它的值都会增加1,如下面的代码片段所示:
pa = __system_property_area__; /*update prop info or add prop info*/ pa->serial++; __futex_wake(&pa->serial, INT32_MAX);serial的值增加1之后,还需要调用系统接口__futex_wake来唤醒处于睡眠状态的并且是正在监听serial值变化的最多INT32_MAX个进程。进程可以通过下面的代码片段来监听serial值变化:
prop_area *pa = __system_property_area__; n = pa->serial; do { __futex_wait(&pa->serial, n, 0); } while(n == pa->serial);上面的代码片段首先是获得serial原来的值n,然后调用系统接口__futex_wait等待serial的值发生变化。如果没有发生变化,那么进程就会进入睡眠状态,直到第三个参数指定的超时时间到来为止。当进程从系统接口__futex_wait返回时,需要检查serial原来的值n是否与当前的值相等。如果相等,那么就说明调用系统接口__futex_wait是超时返回。这时候就需要再次调用__futex_wait等待serial的值发生变化。这与Linux内核的顺序锁(seqlock)实现是类似的。
在结构体prop_info中,serial的最高字节用来描述对应的属性的值的长度,低三个字用来同步属性值的读写,并且最低的一位是用来描述属性是否正在修改中。我们可以通过宏SERIAL_VALUE_LEN和SERIAL_DIRTY来获得对应的属性的值的长度,以及检查属性值是否正在修改中,如下所示:
#define SERIAL_VALUE_LEN(serial) ((serial) >> 24) #define SERIAL_DIRTY(serial) ((serial) & 1)这两个宏定义在文件bionic/libc/include/sys/_system_properties.h中。
修改属性值之前,首先是要将对应的prop_info结构体的serial值的最低一位设置为1。修改属性值之后,需要将对应的prop_info结构体的serial值加1,同时也会将其最低一位恢复为0。这个功能可以通过调用函数update_prop_info来实现,如下所示:
static void update_prop_info(prop_info *pi, const char *value, unsigned len) { pi->serial = pi->serial | 1; memcpy(pi->value, value, len + 1); pi->serial = (len << 24) | ((pi->serial + 1) & 0xffffff); __futex_wake(&pi->serial, INT32_MAX); }这个函数定义在文件system/core/init/property_service.c中。
在函数update_prop_info中,参数pi描述的是要修改的属性,参数value和len描述的是修改后的值及其长度。修改完成之后,需要调用系统接口__futex_wake唤醒正在监听serial值变化的最多INT32_MAX个进程。
相应地,一个进程在读取属性值中,需要检查是否有另外一个进程正在修改该属性的值。如果有的话,就需要等待。这个功能通过调用函数__system_property_read来实现,如下所示:
int __system_property_read(const prop_info *pi, char *name, char *value) { unsigned serial, len; for(;;) { serial = pi->serial; while(SERIAL_DIRTY(serial)) { __futex_wait((volatile void *)&pi->serial, serial, 0); serial = pi->serial; } len = SERIAL_VALUE_LEN(serial); memcpy(value, pi->value, len + 1); if(serial == pi->serial) { if(name != 0) { strcpy(name, pi->name); } return len; } } }这个函数定义在文件bionic/libc/bionic/system_properties.c中。
在函数__system_property_read中,参数pi表示要读取的属性,参数name和value用来保存读取出来的属性名称和属性值。
在for循环内部的while循环,是用来确保结构体pi的serial的最低一位不等于1,也就是确定没有其它进程正在修改它值中,这是通过调用系统接口__futex_wait来实现的。确定没有其它进程正在修改结构体pi之后,就可以将它的属性名称和属性值读取出来。首先是读出属性值,接着再读出属性名称。然而,在读取属性值的时候,有可能其它进程又对该属性进行了修改,这时候就需要检查之前获得的serai值与当前的serial值是否相等。如果不相等的话,那么就说明有其它进程对正在读取的属性进行了修改,因此就需要重新执行for循环进行重新读取。
从这里就可以看出,结构体prop_area和结构体prop_info的成员变量serial的作用是类似的,不同的是前者用来同步整个属性内存区域属性的增加或者修改,而后者仅公是用来同步某一个属性的读写。
理解了属性内存区域的结构之后,我们再来看属性内存区域的创建和初始化。属性内存区域是由init进程在启动的过程中创建和初始化。创建和初始化完成之后,其它进程可以将这块属性内存区域以只读的方式映射到自己的地址空间去,这样其它进程就可以直接从自己的地址空间读出属性值。另一方面,如果其它进程需要增加或者修改属性的值,那么就必须要通过init进程来进行。Init进程在启动的时候,会创建一个属性管理服务。这个属性管理服务会创建一个Server端Socket,用来接收其它进程发送过来的增加或者修改属性的请求。
在init进程的入口函数main中,与属性相关的逻辑如下所示:
int main(int argc, char **argv) { ...... property_init(); ...... if (!is_charger) property_load_boot_defaults(); ...... queue_builtin_action(property_service_init_action, "property_service_init"); ...... for(;;) { ...... execute_one_command(); ...... if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } ...... nr = poll(ufds, fd_count, timeout); ...... for (i = 0; i < fd_count; i++) { if (ufds[i].revents == POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); ...... } } } return 0; }
这个函数定义在文件system/core/init/init.c中。
在main函数中,首先调用函数property_init来创建属性内存区域。接着再判断当前是否处于充电模式,即变量is_charger的值是否等于true。如果不等于的话,那么就说明系统是处于正常启动的过程中,这时候就会调用函数property_load_boot_defaults初始化一些默认的属性。接下来是将函数property_service_init_action加入到内部的一个命令队列中去,以便在接下来的for循环中可以通过函数execute_one_command来执行。函数property_service_init_action的作用是启动属性管理服务。
在main函数最后的for循环中,除了调用函数execute_one_command执行命令队列中的命令之外,还做其它事情。其中的一件事情就是监听其它进程通过Socket发送过来的增加或者修改属性的请求。用来接收请求的Server端Socket是在函数property_service_init_action的调用过程中创建的,并且可以通过函数get_property_set_fd来获得它的文件描述符。
有了这个Server端Socket的文件描述符之后,就可以通过函数poll来监听其它进程发送过来的请求了。也就是说,一旦其它进程发送请求过来,那么Init进程就会从函数poll返回,并且获得一个类型为POLLIN的事件。如果这个POLLIN事件对应的文件描述符就是用来接收增加或者修改属性请求的Server端Socket的文件描述符,那么就调用另外一个函数handle_property_set_fd来处理请求。
接下来我们就分析上面描述的函数property_init、property_load_boot_defaults、property_service_init_action和get_property_set_fd的实现。等到后面分析属性的增加或者修改时,再分析函数handle_property_set_fd的实现。
函数property_init的实现如下所示:
void property_init(void) { init_property_area(); }这个函数定义在文件system/core/init/property_service.c中。
函数property_init的实现很简单,它通过调用另外一个函数init_property_area来创建和初始化一块属性内存区域。
函数init_property_area的实现如下所示:
/* (8 header words + 247 toc words) = 1020 bytes */ /* 1024 bytes header and toc + 247 prop_infos @ 128 bytes = 32640 bytes */ #define PA_COUNT_MAX 247 #define PA_INFO_START 1024 #define PA_SIZE 32768 static workspace pa_workspace; static prop_info *pa_info_array; extern prop_area *__system_property_area__; static int init_property_area(void) { prop_area *pa; if(pa_info_array) return -1; if(init_workspace(&pa_workspace, PA_SIZE)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START); pa = pa_workspace.data; memset(pa, 0, PA_SIZE); pa->magic = PROP_AREA_MAGIC; pa->version = PROP_AREA_VERSION; /* plug into the lib property services */ __system_property_area__ = pa; property_area_inited = 1; return 0; }这个函数定义在文件system/core/init/property_service.c中。
函数init_property_area首先是检查全局变量pa_info_array的值。如果不等于NULL,那么就说明属性内存区域已经创建过了,因此就直接返回。如果等于NULL,接下来就调用函数init_workspace来创建一块大小等于PA_SIZE的属性内存区域,并且将创建出来的属性内存区域的地址保存在全局变量pa_workspace所指向的一个workspace结构体的成员变量data中。
全局变量pa_info_array指向的是属性值区域,它位于属性内存区域开始偏移PA_INFO_START的位置上,因此,将全局变量pa_workspace所指向的一个workspace结构体的成员变量data的值再加上PA_INFO_START的值,就可以得到属性值区域的开始位置。以后就可以从这个位置开始增加属性。
函数init_property_area再接下来将属性内存区域的头部转化为一个prop_area结构体pa,并且初始化好它的成员变量magic和version,
函数init_property_area最后已经创建和初始好的属性内存区域保存在全局变量__system_property_area__中,并且将另外一个全局变量property_area_inited的值设置为1,以表示属性内存区域已经初始化完毕。
接下来我们再分析函数init_workspace创建属性内存区域的过程,如下所示:
static int init_workspace(workspace *w, size_t size) { void *data; int fd; /* dev is a tmpfs that we can use to carve a shared workspace * out of, so let‘s do that... */ fd = open(PROP_FILENAME, O_RDWR | O_CREAT | O_NOFOLLOW, 0644); if (fd < 0) return -1; if (ftruncate(fd, size) < 0) goto out; data = http://www.mamicode.com/mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);> 这个函数定义在文件system/core/init/property_service.c中。从函数init_workspace的实现我们就可以看出属性内存区域是如何创建的。首先是调用函数open以读写方式打开一个名称为PROP_FILENAME的文件, 接着再调用函数ftruncate将该文件的大小设置为参数size指定的值,最后再调用函数mmap以共享的方式将前面打开的文件映射到内存中来。映射成功后,就可以得一块大小为size的属性内存区域了。
PROP_FILENAM是一个定义在文件bionic/libc/include/sys/_system_properties.h中的宏,如下所示:
#define PROP_FILENAME "/dev/__properties__"注意,由于上面打开文件/dev/__properties__之后,需要调用函数ftruncate设置它的大小,因此,这时候/dev/__properties__文件是以读写方式打开的。打开得到的文件描述符需要保存在参数w指向的一个workspace结构体中,以便以后可以对它进行关闭。由于以后我们要求不能通过文件描述符来直接向属性内存区域写入数据,因此,函数init_workspace接下来需要将前面打开的文件/dev/__properties__关闭,然后再重新以只读方式打开。这样得到的文件描述符就不可以用来写入数据了。
回到init进程的入口函数main中,接下来我们再分析函数property_load_boot_defaults的实现。
函数property_load_boot_defaults的实现如下所示:
void property_load_boot_defaults(void) { load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT); }这个函数定义在文件system/core/init/property_service.c中。函数property_load_boot_defaults通过调用另外一个函数load_properties_from_file将定义在文件PROP_PATH_RAMDISK_DEFAULT里面的属性加载前面创建的属性内存区域中。
PROP_PATH_RAMDISK_DEFAULT是一个宏,定义在文件bionic/libc/include/sys/_system_properties.h中,如下所示:
#define PROP_PATH_RAMDISK_DEFAULT "/default.prop"再回到init进程的入口函数main中,接下来我们再分析函数property_service_init_action的实现。函数property_service_init_action的实现如下所示:
static int property_service_init_action(int nargs, char **args) { /* read any property files on system or data and * fire up the property service. This must happen * after the ro.foo properties are set above so * that /data/local.prop cannot interfere with them. */ start_property_service(); return 0; }这个函数定义在文件system/core/init/init.c中。
函数property_service_init_action的实现很简单,它通过调用另外一个函数start_property_service来启动属性管理服务。
函数start_property_service的实现如下所示:
void start_property_service(void) { int fd; load_properties_from_file(PROP_PATH_SYSTEM_BUILD); load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT); load_override_properties(); /* Read persistent properties after all default values have been loaded. */ load_persistent_properties(); fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); listen(fd, 8); property_set_fd = fd; }这个函数定义在文件system/core/init/property_service.c中。函数start_property_service首先是调用函数load_properties_from_file将定义在文件PROP_PATH_SYSTEM_BUILD和PROP_PATH_SYSTEM_DEFAULT中的属性加载到前面创建的属性内存区域中。接着又调用函数load_override_properties判断当前是否是以debug模式启动。如果是的话,那就再将定义在文件PROP_PATH_LOCAL_OVERRIDE中的属性加载到前面创建的属性内存区域中来。再接下来还会调用函数load_persistent_properties将保存在目录PERSISTENT_PROPERTY_DIR中的持久属性加载到前面创建的属性内存区域中来。
上面提到的PROP_PATH_SYSTEM_BUILD、PROP_PATH_SYSTEM_DEFAULT和PROP_PATH_LOCAL_OVERRIDE和PERSISTENT_PROPERTY_DIR均是定义在文件bionic/libc/include/sys/_system_properties.h中的宏,如下所示:
#define PROP_PATH_SYSTEM_BUILD "/system/build.prop" #define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop" #define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"另外一个宏PERSISTENT_PROPERTY_DIR定义在文件ystem/core/initprop/erty_service.c中。函数start_property_service加载完成保存在指定文件中的属性之后,就调用函数create_socket创建了一个名称为PROP_SERVICE_NAME的Socket,并且调用函数listen来监听其它进程发送过的增加或者修改属性请求。
PROP_SERVICE_NAME是一个定义在文件bionic/libc/include/sys/_system_properties.h中的宏,如下所示:
#define PROP_SERVICE_NAME "property_service"最后,函数start_property_service将前面创建Socket获得的文件描述符保存在全局变量property_set_fd中,以便init进程的main函数可以通过函数get_property_set_fd获取。
函数get_property_set_fd的实现如下所示:
int get_property_set_fd() { return property_set_fd; }这个函数定义在文件system/core/init/property_service.c中。这样,我们就分析完成了Init进程创建和初始化属性内存区域以及创建属性管理服务的过程。前面我们提到,其它进程也需要将Init进程创建的属性内存区域映射到自己的进程地址访问来,以便可以对属性进行直接的读取。接下来我们继续分析其它进程是如何将Init进程创建的属性内存区域映射到自己的进程地址空间的。
我们以Android应用程序进程为例来说明属性内存区域映射到非Init进程地址空间的过程。从Android应用程序进程启动过程的源代码分析前面一文可以知道,Android应用程序进程是由Zygote进程fork出来的。Zygote进程是作为Init进程的一个服务启动的,启动脚本如下所示:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server class main socket zygote stream 660 root system onrestart write /sys/android_power/request_state wake onrestart write /sys/power/state on onrestart restart media onrestart restart netd这段启动脚本定义在文件system/core/rootdir/init.rc中。在Init进程中,所有的服务都是通过函数service_start启动的,它的定义如下所示:
void service_start(struct service *svc, const char *dynamic_args) { ...... char *scon = NULL; int rc; if (is_selinux_enabled() > 0) { char *mycon = NULL, *fcon = NULL; ...... rc = getcon(&mycon); ...... rc = getfilecon(svc->args[0], &fcon); ...... rc = security_compute_create(mycon, fcon, string_to_security_class("process"), &scon); ...... } ...... pid = fork(); if (pid == 0) { struct socketinfo *si; ...... int fd, sz; ...... if (properties_inited()) { get_property_workspace(&fd, &sz); sprintf(tmp, "%d,%d", dup(fd), sz); add_environment("ANDROID_PROPERTY_WORKSPACE", tmp); } ...... setsockcreatecon(scon); for (si = svc->sockets; si; si = si->next) { int socket_type = ( !strcmp(si->type, "stream") ? SOCK_STREAM : (!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET)); int s = create_socket(si->name, socket_type, si->perm, si->uid, si->gid); if (s >= 0) { publish_socket(si->name, s); } } ...... if (!dynamic_args) { if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) { ERROR("cannot execve(‘%s‘): %s\n", svc->args[0], strerror(errno)); } } else { ...... execve(svc->args[0], (char**) arg_ptrs, (char**) ENV); } ...... } ...... }这个函数定义在文件system/core/init/init.c中。参数svc描述的是要启动的服务的信息,参数dynamic_args描述的是服务启动参数。
Init进程启动的服务都是运行在一个独立的进程里面的,所以这里需要调用fork函数。在fork进程前面,与SEAndroid相关的逻辑如下所示:
1. 在创建进程之前,调用函数is_selinux_enabled检查系统是否开启了SEAndroid。如果开启了,那么就继续调用函数getcon来获得当前进程(即init进程)的安全上下文mycon,以及调用函数getfilecon获得服务的可执行文件(保存在参数args[0])的安全上下文fcon。有了这两个安全上下文之后,就可以调用另外一个函数security_compute_create到内核的SELinux模块去查询获得即将要创建的进程的安全上下文scon。
2. 在创建进程之后,调用函数properties_inited检查Init进程是否已经创建和初始化好属性内存区域了。如果已经创建和初始化好,那么就继续调用函数get_property_workspace来获得用来描述属性内存区域的文件描述符fd以及属性内存区域的大小sz。我们在前面提到,属性内存区域是通过将文件/dev/__properties__映射到内存中创建的。因此,这里得到的文件描述符fd实际上指向的就是文件/dev/__properties__。再接下来将属性内存区域的文件描述符以及大小拼接成一个字符串,并且将得到的字符串作为环境变量ANDROID_PROPERTY_WORKSPACE的值。
3. 调用函数setsockcreatecon设置新创建的Socket的安全上下文,这是因为接下来要为Zygote进程创建一个名称zygote的Socket。这个Socket是在Init进程的启动脚本init.rc中配置要创建的,并且是用来接收ActivityManagerService发送过来的应用程序进程创建请求的。这一步说明在SEAndroid中,除了进程、文件、属性,Socket也是有安全上下文的。通过给Socket设置安全上下文,我们就可以通过SEAndroid安全策略来设置什么样的进程可以连接什么样的Socket。例如,我们可以通过SEAndroid安全策略限定只有ActivityManagerService所运行在的System Server进程才会权限连接名称为zygote的Socket。这样就可以使得只有有权限的进程,才可以请求Zygote进程创建应用程序进程。
4. 调用函数execve在新创建的进程中加载参数args[0]所描述的要执行文件,实际上就是/system/bin/app_process文件。从前面SEAndroid安全机制中的进程安全上下文关联分析一文可以知道,加载了/system/bin/app_process文件之后,新进程的安全上下文的domain就会被设置为zygote了。这样就可以与init进程的安全上下文区分开来。
应用程序进程都是通过Zygote进程fork出来的,这意味着应用程序进程会继续父进程zygote的环境变量,也就是会继续前面第2步创建的环境变量ANDROID_PROPERTY_WORKSPACE。有了这个环境变量之后,就可以得到用来描述在Init进程创建的属性内存区域的文件描述符以及大小了。
在Android系统中,所有进程基本上都会使用到C库,应用程序进程也不例外。Android系统使用的C库为bionic,它定义了一个函数 __libc_preinit,是在C库为bioni动态链接的时候调用的,如下所示:
// We flag the __libc_preinit function as a constructor to ensure // that its address is listed in libc.so‘s .init_array section. // This ensures that the function is called by the dynamic linker // as soon as the shared library is loaded. __attribute__((constructor)) static void __libc_preinit() { // Read the kernel argument block pointer from TLS. void* tls = const_cast<void*>(__get_tls()); KernelArgumentBlock** args_slot = &reinterpret_cast<KernelArgumentBlock**>(tls)[TLS_SLOT_BIONIC_PREINIT]; KernelArgumentBlock* args = *args_slot; // Clear the slot so no other initializer sees its value. // __libc_init_common() will change the TLS area so the old one won‘t be accessible anyway. *args_slot = NULL; __libc_init_common(*args); // Hooks for the debug malloc and pthread libraries to let them know that we‘re starting up. pthread_debug_init(); malloc_debug_init(); }这个函数定义在文件bionic/libc/bionic/libc_init_dynamic.cpp中。函数__libc_preinit前面的关键字__attribute__((constructor))是用来告诉编译器gcc和动态链接器linker,在加载bionic库的时候,要执行后面定义的函数。
函数__libc_preinit首先是从线程本地存储区域中获得本进程的启动参数args,接着再调用函数__libc_init_common来初始化C库。初始化C库之后,函数__libc_preinit再调用另外两个函数pthread_debug_init和malloc_debug_init来通知模块pthread和malloc,C库已经初始化好了。
函数__libc_init_common的实现如下所示:
void __libc_init_common(KernelArgumentBlock& args) { ...... __system_properties_init(); // Requires ‘environ‘. }这个函数定义在文件bionic/libc/bionic/libc_init_common.cpp中。函数__libc_init_common用来初始化C库,其中的一个工作就是调用函数__system_properties_init来初始化属性内存区域。
函数__system_properties_init的定义如下所示:
static unsigned dummy_props = 0; prop_area *__system_property_area__ = (void*) &dummy_props; int __system_properties_init(void) { bool fromFile = true; int result = -1; if(__system_property_area__ != ((void*) &dummy_props)) { return 0; } int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW); if ((fd < 0) && (errno == ENOENT)) { ...... fd = get_fd_from_env(); fromFile = false; } prop_area *pa = mmap(NULL, fd_stat.st_size, PROT_READ, MAP_SHARED, fd, 0); ...... if((pa->magic != PROP_AREA_MAGIC) || (pa->version != PROP_AREA_VERSION)) { munmap(pa, fd_stat.st_size); goto cleanup; } __system_property_area__ = pa; result = 0; cleanup: if (fromFile) { close(fd); } return result; }这个函数定义在文件bionic/libc/bionic/system_properties.c中。在文件system_properties.c中,定义有一个全局变量__system_property_area__,它的初始值被设置为一个全局变量dummy_props的地址,用来表明在本进程中,属性内存区域还没有初始过。一旦本进程的属性内存区域初始化之后,全局变量__system_property_area__就会指向这块属性内存区域。
了解了全局变量__system_property_area__的作用之后,我们就开始分析函数__system_properties_init初始化属性内存区域的过程。
首先是调用函数open以只读方式打开文件PROP_FILENAME。从前面的分析可以知道,PROP_FILENAME是一个宏,它的值等于/dev/__properties__,init进程也是通过打开它来创建属性内存区域的。如果打开文件PROP_FILENAME失败,那么就通过调用函数get_fd_from_env来获得指向属性内存区域的文件描述符。
函数get_fd_from_env的实现如下所示:
static int get_fd_from_env(void) { char *env = getenv("ANDROID_PROPERTY_WORKSPACE"); if (!env) { return -1; } return atoi(env); }这个函数定义在文件bionic/libc/bionic/system_properties.c中。函数get_fd_from_env是通过读取环境变量ANDROID_PROPERTY_WORKSPACE来获得指向属性内在区域的文件描述符的。这个环境变量刚好就是在Zygote进程创建的时候设置的。也就是说,应用程序进程可以通过父进程Zygote设置的环境变量ANDROID_PROPERTY_WORKSPACE来获得指向属性内存区域的文件描述符。
回到函数__system_properties_init中,获得了指向属性内存区域的文件描述符之后,接下来就调用函数mmap以只读共享的方式将该文件描述符指向的文件/dev/__properties__映射到本进程来。我们注意到,init进程也是通过共享的方式来映射文件/dev/__properties__的。正是由于如此,init进程和其它进程才可以做到共享属性内存区域。但是,属性内存区域在init进程中可读可写的,而在其它进程中只可以读。
获得了属性内存区域之后,就会验证它头部的魔数和版本号是否正确。正确的话,就说明得到的是一块有效的属性内存区域,因此,就可以把它的地址保存在全局变量__system_property_area__中。
这样,我们就以应用程序进程为例,分析了其它进程初始化属性内存区域的过程。重点就是通过共享方式将/dev/__properties__映射到自己的进程地址空间来。以实现共享在init进程创建的属性内存区域。
接下来我们再来分析属性的读取。C库提供了一个函数__system_property_get,用来读取Android系统中的属性,它的定义如下所示:
int __system_property_get(const char *name, char *value) { const prop_info *pi = __system_property_find(name); if(pi != 0) { return __system_property_read(pi, 0, value); } else { value[0] = 0; return 0; } }这个函数定义在文件bionic/libc/bionic/system_properties.c中。参数name用来指定要读取的属性的名称,参数value用来保存读取出来的属性的值。
函数__system_property_get首先是调用函数__system_property_find在前面已经初始化好的属性内存区域__system_property_area__中找到与参数name对应的一个结构体prop_info,接着再调用函数__system_property_read来读保存在该结构体中的属性的值。
函数__system_property_find和__system_property_read在前面都已经分析过了,这里就不再详述。从函数__system_property_get的实现就可以看出,读取Android系统的属性是没有权限控制的,并且可以直接从本进程的地址空间读取。然而,属性的增加或者修改就没有那么简单了,接下来我们就进行详细分析,这个过程也是本文的主题,前面的内容都是铺垫。
C库除了提供函数__system_property_get来读取属性之外,还提供了另外一个函数__system_property_set来增加或者修改属性,它的定义如下所示:
int __system_property_set(const char *key, const char *value) { int err; prop_msg msg; if(key == 0) return -1; if(value =http://www.mamicode.com/= 0) value = "";> 这个函数定义在文件bionic/libc/bionic/system_properties.c中。参数key描述的是要增加或者修改的属性的名称,参数value描述的是要增加或者要修改的属性的值。
函数__system_property_get首先是将要增加或者修改的属性的相关信息封装在一个类型为PROP_MSG_SETPROP的消息中,接着再调用函数send_prop_msg将它发送给init进程。
函数send_prop_msg的实现如下所示:
static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME; static int send_prop_msg(prop_msg *msg) { struct pollfd pollfds[1]; ...... struct sockaddr_un addr; ...... int s; ...... s = socket(AF_LOCAL, SOCK_STREAM, 0); ...... memset(&addr, 0, sizeof(addr)); namelen = strlen(property_service_socket); strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path); addr.sun_family = AF_LOCAL; alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1; if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) { close(s); return result; } r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0)); if(r == sizeof(prop_msg)) { ...... pollfds[0].fd = s; pollfds[0].events = 0; r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */)); if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) { result = 0; } else { ...... result = 0; } } close(s); return result; }这个函数定义在文件bionic/libc/bionic/system_properties.c中。在文件ystem_properties.c中,定义有一个全局变量property_service_socket,它的值等于"/dev/socket/" PROP_SERVICE_NAME。从前面分析属性管理服务的启动过程可以知道,PROP_SERVICE_NAME是一个宏,它的值等于“property_service”,是属性管理服务正在监听的一个Socket的名称。
清楚上面的信息之后,函数send_prop_msg的执行过程就简单了,如下所示:
1. 调用函数socket创建一个本地Socket,并且将它要连接的对端Socket的地址设置为/dev/socket/property_service。
2. 调用函数connect请求与对端Socket进行连接。
3. 连接成功后,调用函数send向对端发送参数msg描述的消息。
4. 发送成功后,调用函数poll等待对端关闭连接。
一旦对端Socket关闭了连接,就说明增加或者修改属性成功了。
前面在分析init进程的入口函数main时提到,一旦有进程通过文件/dev/socket/property_service描述的Socket发送连接请求时,函数handle_property_set_fd就会被调用,它的实现如下所示:
static int property_set_fd = -1; void handle_property_set_fd() { prop_msg msg; int s; int r; int res; struct ucred cr; struct sockaddr_un addr; socklen_t addr_size = sizeof(addr); socklen_t cr_size = sizeof(cr); char * source_ctx = NULL; if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) { return; } /* Check socket options here */ if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) { close(s); ERROR("Unable to receive socket options\n"); return; } r = TEMP_FAILURE_RETRY(recv(s, &msg, sizeof(msg), 0)); if(r != sizeof(prop_msg)) { ERROR("sys_prop: mis-match msg size received: %d expected: %d errno: %d\n", r, sizeof(prop_msg), errno); close(s); return; } switch(msg.cmd) { case PROP_MSG_SETPROP: msg.name[PROP_NAME_MAX-1] = 0; msg.value[PROP_VALUE_MAX-1] = 0; getpeercon(s, &source_ctx); if(memcmp(msg.name,"ctl.",4) == 0) { // Keep the old close-socket-early behavior when handling // ctl.* properties. close(s); if (check_control_perms(msg.value, cr.uid, cr.gid, source_ctx)) { handle_control_message((char*) msg.name + 4, (char*) msg.value); } else { ERROR("sys_prop: Unable to %s service ctl [%s] uid:%d gid:%d pid:%d\n", msg.name + 4, msg.value, cr.uid, cr.gid, cr.pid); } } else { if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) { property_set((char*) msg.name, (char*) msg.value); } else { ERROR("sys_prop: permission denied uid:%d name:%s\n", cr.uid, msg.name); } // Note: bionic‘s property client code assumes that the // property server will not close the socket until *AFTER* // the property is written to memory. close(s); } freecon(source_ctx); break; default: close(s); break; } }这个函数定义在system/core/init/property_service.c中。函数handle_property_set_fd的执行过程如下所示:
1. 调用函数accept接受其它进程发送过来的连接请求。
2. 接受了请求后,调用函数getsocketopt获得发送端进程的信息,例如uid和guid。
2. 调用函数recv获取请求的消息内容,并且保存在一个类型为prop_msg的结构体msg中。
3. 目前属性管理服务只处理类型为PROP_MSG_SETPROP的消息,对应的就是增加或者修改属性的请求,按照下面的步骤进行处理。
4. 调用函数getpeercon获得发送端Socket的安全上下文source_ctx,一般就是等于发送端进程的安全上下文了。
5. 如果发送过来的消息请求处理的属性的名称以“ctl.”开头,那么就说明要处理的是一个控制消息。这时候先调用check_control_perms检查发送端进程是否具有相应的权限。如果有的话,再调用函数handle_control_message来处理该消息。
6. 如果发送过来的消息请求处理的属性的名称不是以“ctl.”开头,那么就说明要处理的是一个普通消息。这时候先调用check_perms检查发送端进程是否具有相应的权限。如果有的话,再调用函数property_set来处理该消息。
我们以普通消息的处理过程为例,来说明SEAndroid是如何保护系统的属性的。
函数check_perms的实现如下所示:
static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) { int i; unsigned int app_id; if(!strncmp(name, "ro.", 3)) name +=3; if (uid == 0) return check_mac_perms(name, sctx); app_id = multiuser_get_app_id(uid); if (app_id == AID_BLUETOOTH) { uid = app_id; } for (i = 0; property_perms[i].prefix; i++) { if (strncmp(property_perms[i].prefix, name, strlen(property_perms[i].prefix)) == 0) { if ((uid && property_perms[i].uid == uid) || (gid && property_perms[i].gid == gid)) { return check_mac_perms(name, sctx); } } } return 0; }这个函数定义在system/core/init/property_service.c中。参数name描述的是要增加或者修改的属性的名称,参数uid、gid和sctc分别描述要增加或者修改属性的进程的uid、gid和安全上下文。
函数check_perms的作用是检查请求增加或者修改属性的进程是否具有应的权限,其中就包括基于UID/GID的权限检查和基于SEAndroid的权限检查,它的检查过程如下所示:
1. 如果请求的进程的uid等于0,即这是一个root用户进程,那么就略过基于ID/GID的权限检查,而直接调用函数check_mac_perms进行SEAndroid权限检查。
2. 如果请求的进程的uid不等于0,就那么就先调用multiuser_get_app_id检查请求的进程是否是一个应用程序进程。如果是应用程序进程的话,使用其App ID进行安全检查。在单用户环境中,应用程序进程的UID即为应用程序的App ID,但是在多用户环境中,两者是不相等的。
3. 遍历数组property_perms。检查名称为name的属性是否在它的配置当中。如果在它的配置之中,一方面是要求请求进程具有与配置一样的UID/GID,另一方面是要求请求进程的安全上下文符合SEAndroid安全策略定义的规则。如果不在它的配置当中,那么就拒绝对名称为name的属性进行增加或者修改。
数组property_perms实际上是定义了一个白名单,用来规则什么样的进程可以增加或者修改什么样的属性,它定义在文件system/core/init/property_service.c中,如下所示:
/* White list of permissions for setting property services. */ struct { const char *prefix; unsigned int uid; unsigned int gid; } property_perms[] = { { "net.rmnet0.", AID_RADIO, 0 }, { "net.gprs.", AID_RADIO, 0 }, { "net.ppp", AID_RADIO, 0 }, { "net.qmi", AID_RADIO, 0 }, { "net.lte", AID_RADIO, 0 }, { "net.cdma", AID_RADIO, 0 }, { "ril.", AID_RADIO, 0 }, { "gsm.", AID_RADIO, 0 }, { "persist.radio", AID_RADIO, 0 }, { "net.dns", AID_RADIO, 0 }, { "sys.usb.config", AID_RADIO, 0 }, { "net.", AID_SYSTEM, 0 }, { "dev.", AID_SYSTEM, 0 }, { "runtime.", AID_SYSTEM, 0 }, { "hw.", AID_SYSTEM, 0 }, { "sys.", AID_SYSTEM, 0 }, { "service.", AID_SYSTEM, 0 }, { "wlan.", AID_SYSTEM, 0 }, { "bluetooth.", AID_BLUETOOTH, 0 }, { "dhcp.", AID_SYSTEM, 0 }, { "dhcp.", AID_DHCP, 0 }, { "debug.", AID_SYSTEM, 0 }, { "debug.", AID_SHELL, 0 }, { "log.", AID_SHELL, 0 }, { "service.adb.root", AID_SHELL, 0 }, { "service.adb.tcp.port", AID_SHELL, 0 }, { "persist.sys.", AID_SYSTEM, 0 }, { "persist.service.", AID_SYSTEM, 0 }, { "persist.security.", AID_SYSTEM, 0 }, { "persist.service.bdroid.", AID_BLUETOOTH, 0 }, { "selinux." , AID_SYSTEM, 0 }, { NULL, 0, 0 } };例如,以“net.”开头的属性只有系统用户进程(AID_SYSTEM)才可以增加或者修改。一旦通过了基于UID/GID的白名单检查之后, 接下来就调用函数check_mac_perms进行SEAndroid安全检查,它的实现如下所示:
static int check_mac_perms(const char *name, char *sctx) { if (is_selinux_enabled() <= 0) return 1; char *tctx = NULL; const char *class = "property_service"; const char *perm = "set"; int result = 0; if (!sctx) goto err; if (!sehandle_prop) goto err; if (selabel_lookup(sehandle_prop, &tctx, name, 1) != 0) goto err; if (selinux_check_access(sctx, tctx, class, perm, name) == 0) result = 1; freecon(tctx); err: return result; }这个函数定义在system/core/init/property_service.c中。参数name描述的是要进行增加或者修改的属性的名称,参数sctx描述的是请求增加或者修改属性的进程的安全上下文。
只有在启用了SEAndroid的情况下,函数check_mac_perms才会进行SEAndroid安全检查。sehandle_prop是一个全局变量,用来描述我们在前面SEAndroid安全机制框架分析一文分析的文件external/sepolicy/property_contexts。这个文件定义了具有什么样的安全上下文的进程能够增加或者修改什么样的属性。
函数首先调用函数selabel_lookup获得增加或者修改名称为name的属性所需要的安全上下文tctx,接着再调用函数selinux_check_access检查请求增加或者修改属性的进程的安全上下文sctx是否有权限对安全上下文为tctx的属性进行操作。
全局变量sehandle_prop是在init进程启动的时候初始化的,如下所示:
static int selinux_enabled = 1; int main(int argc, char **argv) { ...... if (selinux_enabled) { if (selinux_android_load_policy() < 0) { selinux_enabled = 0; INFO("SELinux: Disabled due to failed policy load\n"); } else { selinux_init_all_handles(); } } else { INFO("SELinux: Disabled by command line option\n"); } ...... }这个函数定义在文件system/core/init/init.c中。在启用SEAndroid的情况下,init进程首先调用函数selinux_android_load_policy加载SEAndroid安全策略到内核中的SELinux模块中,接着再调用函数selinux_init_all_handles来加载描述文件和属性安全上下文的文件。
函数selinux_init_all_handles的定义如下所示:
void selinux_init_all_handles(void) { sehandle = selinux_android_file_context_handle(); sehandle_prop = selinux_android_prop_context_handle(); }这个函数定义在文件system/core/init/init.c中。函数selinux_android_file_context_handle由libseliux提供,用来打开我们在前面SEAndroid安全机制框架分析一文分析的文件external/sepolicy/file_contexts。因为init进程在启动的过程中,需要创建一些文件。因此,它需要打开文件external/sepolicy/file_contexts,来获得新创建的文件的安全上下文,并且进行相应的设置。设置文件安全上下文的过程可以参考前面SEAndroid安全机制中的文件安全上下文关联分析一文。
下面我们主要分析一下函数selinux_android_prop_context_handle的实现,如下所示:
static const struct selinux_opt seopts_prop[] = { { SELABEL_OPT_PATH, "/data/security/property_contexts" }, { SELABEL_OPT_PATH, "/property_contexts" }, { 0, NULL } }; struct selabel_handle* selinux_android_prop_context_handle(void) { int i = 0; struct selabel_handle* sehandle = NULL; while ((sehandle == NULL) && seopts_prop[i].value) { sehandle = selabel_open(SELABEL_CTX_ANDROID_PROP, &seopts_prop[i], 1); i++; } if (!sehandle) { ERROR("SELinux: Could not load property_contexts: %s\n", strerror(errno)); return NULL; } INFO("SELinux: Loaded property contexts from %s\n", seopts_prop[i - 1].value); return sehandle; }这个函数定义在文件system/core/init/init.c中。函数selinux_android_prop_context_handle依次检查设备上的/data/security/property_contexts和/property_contexts文件。如果其中一个存在,就调用selinux库提供的函数selabel_open对它进行打开并且解析,以便以后可以调用函数selabel_lookup进行检索。
关于函数selabel_open、selabel_lookup,以及前面提到的selinux_check_access,我们就不进行分析了,它们无非就是执行一些文本分析和比较的工作。
这样,我们就分析完成属性增加或者修改所要进行的UID/GID和SEAndroid安全检查了。回到前面分析的函数handle_property_set_fd中,一旦通过了安全检查,接下来就会调用函数property_set对请求的属性进行增加或者修改。
函数property_set的实现如下所示:
int property_set(const char *name, const char *value) { prop_area *pa; prop_info *pi; ...... pi = (prop_info*) __system_property_find(name); if(pi != 0) { /* ro.* properties may NEVER be modified once set */ if(!strncmp(name, "ro.", 3)) return -1; pa = __system_property_area__; update_prop_info(pi, value, valuelen); pa->serial++; __futex_wake(&pa->serial, INT32_MAX); } else { pa = __system_property_area__; if(pa->count == PA_COUNT_MAX) return -1; pi = pa_info_array + pa->count; pi->serial = (valuelen << 24); memcpy(pi->name, name, namelen + 1); memcpy(pi->value, value, valuelen + 1); pa->toc[pa->count] = (namelen << 24) | (((unsigned) pi) - ((unsigned) pa)); pa->count++; pa->serial++; __futex_wake(&pa->serial, INT32_MAX); } /* If name starts with "net." treat as a DNS property. */ if (strncmp("net.", name, strlen("net.")) == 0) { if (strcmp("net.change", name) == 0) { return 0; } /* * The ‘net.change‘ property is a special property used track when any * ‘net.*‘ property name is updated. It is _ONLY_ updated here. Its value * contains the last updated ‘net.*‘ property. */ property_set("net.change", name); } else if (persistent_properties_loaded && strncmp("persist.", name, strlen("persist.")) == 0) { /* * Don‘t write properties to disk until after we have read all default properties * to prevent them from being overwritten by default values. */ write_persistent_property(name, value); } else if (strcmp("selinux.reload_policy", name) == 0 && strcmp("1", value) == 0) { selinux_reload_policy(); } property_changed(name, value); return 0; }这个函数定义在文件system/core/init/property_service.c中。参数name表示要增加或者修改的属性的名称,参数value表示要增加或者修改的属性的新值。
函数property_set首先是调用函数__system_property_find检查名称为name的属性是否已经存在属性内存区域中:
1. 如果存在的话,那么就会得到一个类型为prop_info的结构体pi,表示接下来要做的是修改属性。这时候就会调用我们在上面分析的函数update_prop_info进指定的属性进行修改。
2. 如果不存在的话,那么指针pi的值就会等于NULL。表示接下来要做的增加属性。这时候只需要在属性内存区域的属性值列表pa_info_array的最后增加一项即可。
注意,无论是修改属性,还是增加属性,都需要将属性内存区域头部的serial值增加1,表示对属性内存区域进行了一次修改,并且调用函数__futex_wake通知正在等待属性内存区域修改完成的进程。
增加或者修改完成属性之后,还要进行以下的检查:
1. 如果属性的名称是以“net.”开头,但是又不等于“net.change”,那么就将名称为“net.change”的属性设置为1,表示网络属性发生了变化。
2. 如果属性的名称是以“persist.”开头,那么就表示该属性应该是持久化储存到文件中去,因此就会调用函数write_persistent_property执行这个操作,以便系统下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。
3. 如果属性的名称等于“selinux.reload_policy”,并且前面给它设置的值等于1,那么就表示要重新加载SEAndroid策略,这是通过调用函数selinux_reload_policy来实现的。SEAndroid安全策略的加载过程可以参考前面SEAndroid安全机制框架分析一文。
最后,函数property_set调用另外一个函数property_changed发送一个名称为name的属性发生了变化的通知。以便init进程可以执行在启动脚本init.rc中配置的操作。
至此,我们就分析完成SEAndroid安全机制对Android属性设置的保护支持了,并且顺便分析了Android属性的实现原理。属性是Android系统中特有的资源,通过它们除了可以获得系统的信息之外,还可以对系统的行为进行控制,因此,SEAndroid安全机制需要像文件一样对它们进行保护。在Android系统中,除了属性是特有的概念之外,还有Binder IPC也是特有的机制。在接下来的一篇文章中,我们就继续分析SEAndroid安全机制对Binder IPC的保护支持,敬请关注!更多信息可以关注老罗的新浪微博:http://weibo.com/shengyangluo。