简介
了解SEAndroid之前需要了解一下什么是SELinux,其全称为Security Enhanced Linux,即安全增强Linux。它是一种强制访问控制(MAC)体系。用来限制进程和用户对系统资源的访问。相对于自主访问控制(Discretionary Access Control),简称DAC,这种较粗的访问控制,MAC可以更细粒度的控制访问权限。DAC主要根据文件所属的分组来限制对资源的访问,如果用户获取了root权限,在可以对系统做任何事。MAC可以让用户只访问那些有明确授权的资源。在系统中通常先DAC检查,然后再MAC检查
而SEAndroid是SELinux在Android系统中的实现,用来提高Android的整体安全性。Android并没有全部实现SELinux的功能,而是对其进行了定制,以适应android的设备。
SELinux简介
LSM
了解SELinux之前需要了解LSM和SELinux之间的关系。
LSM(Linux Security Modules)是Linux内核中的一个框架,用于实现安全模块,LSM 不是一个具体的安全策略,而是一个框架,为各种安全模块提供了一个标准的接口。而SELinux正是具体的安全模块实现,它使用LSM提供的接口来实现一种强化的安全策略。除了SELinux还有其他安全模块也使用了LSM框架。LSM使不同的安全模块能够与核心内核交互,它提供了一组钩子(hooks),允许安全模块在特定的系统调用和内核操作点上执行自定义的安全策略。不同的安全模块可以根据需要拦截和审查不同的系统调用,从而提供额外的安全性层。
LSM架构图:
LSM hook: hook函数的实现在LSM安全模块中。LSM提供了一套hook函数,完成DAC检查后,调用这些hook函数可以完成更严格的安全检查。hook函数分散在内核的各个地方,每个hook函数可以认为是一种或者多种对客体资源的访问限制。
SELinux
SELinux是内置于Linux内核中的Linux安全模块。SELinux由可加载的策略规则驱动。当发生与安全相关的访问时,例如当进程尝试打开文件时,SELinux会在内核中拦截该操作。如果SELinux策略规则允许该操作,则该操作将继续,否则,该操作将被阻止并且进程会收到错误。
SELinux决策(例如允许或禁止访问)会被缓存。该缓存称为访问向量缓存 (AVC)。使用这些缓存的决策时,需要检查的SELinux策略规则较少,从而提高了性能。如果DAC规则首先拒绝访问,SELinux策略规则将不起作用。
SELinux架构图:
组成说明:
1.SELinux虚拟文件系统:SELinux内核和用户进程之间进行数据交换的接口。
2.安全服务模块:它的作用是根据安全策略来生成访问规则,生成的规则保存在访问向量缓存中,以提高速度。
3.访问向量缓存(Access Vector Cache): 为了提高系统性能,缓存了访问规则。如果缓存不能解决客体服务器的请求,则会把请求提交到安全模块中,由安全模块来响应请求,然后更新缓存。
5.主体:通常指用户或者用户运行的进程。
4.客体管理器,代表某个内核子系统,如文件系统,内存管理系统,进程管理系统等。它们是各种客体资源的管理者。客体管理器接收到来自用户进程(主体)的访问请求后,通过LSM钩子调用SELinux安全模块来做出允许访问或者拒绝访问的决定。
安全上下文(SELinux Context)
安全上下文,用于标识和控制Linux系统中的资源(如进程、文件、套接字)以及这些资源之间的访问权限。每个资源都被赋予一个SELinux上下文,这个上下文由多个部分组成,它们描述了资源的类型和安全策略。资源的安全上下文决定了它们的安全策略和可访问性。
1.查看安全上下文的命令:
查看文件的安全上下文命令:ls -Z (例:u:object_r:cgroup:s0)
查看进程的安全上下文命令:ps -Z (例:init进程安全上下文:u:r:init:s0)
2.安全上下文格式:
user:role:type:level
user: 指定用户,SELinux中通常有3种类型:user_u:代表普通用户,权限受限制。system_u:代表系统级别的进程。root: 代表root用户。注意,这里的user定义和Linux的用户有一定的相关性,但不是一个概念。
role:指定角色,不同的角色有不同的权限。文件,目录,socket等客体的角色通常是object_r, 主体进程的角色通常是r。一个用户可以有多个角色,但是同一时间,只能使用一个。
type: 定义主体和客体所属的类型。对进程而言,它的类型也称为domain。type是安全上下文中最重要的部分。SELinux通过policy文件定义类型,如:"cgroup"的定义:type cgroup, fs_type, mlstrustedobject。"init" 定义:type init, domain。客体和主体的类型定义都需要通过type语句完成。
type语句的语法如下:
type 类型名称 [,属性];
其中属性值都是预定义好的,有特别的含义。如:domain代表域,通常主体的type具体domain属性,因此,我们也把主体的type称为domain。fs_type表示文件,通常用于客体的类型(type)中。SELinux中大概定义了几十种属性。
level: 定义安全等级。只用于mls策略中。可能的值s0 ~ s15
访问规则
除了主体和客体的安全上下文外,SELinux系统还需要定义主体对客体的访问规则。它也是所有规则定义中规模最大的一部分。如:“init” 访问规则的定义:
allow init unlabeled:filesystem mount;
允许"init"域加载没有定义安全上下文的文件系统。
allow init {fs_type dev_type file_type}:dir_file_class_set relabelto;
允许"init"域重新设置文件,设备和文件系统的安全上下文。
allow init kernel:security load_policy;
允许"init"域为kernel装载policy
注意:我们只要把另一个进程(非init进程)的安全上下文的type字段指定为“init”,它就拥有了这里描述的能力。
allow语句,表示允许主体对客体执行进行的操作
allow语句定义:
allow source_type target_type : class Permission
source_type: 通常是某种属性为domain的类型(type),代表主体。
target_type: 允许访问的客体的类型(type)。target_type可以同时指定多个,如前面的第2个例子。
class: 客体类别。允许访问的客体的类型。客体的目标类型(target_type)可能会涵盖较广的范围。客体类别(class)可以对客体目标类型(target_type)进行限制。如前面第1个例子,目标类型是unlabeled,它可以代表文件,目录以及文件系统,通过filesystem对它进行了限制,因此这个规则只能代表文件系统。
Permission: 指定主体可以对客体进行操作的种类。SELinux中定义了大量的许可(Permission)。
allow语句,是SELinux中访问控制规则之一,目前SELinux策略语言支持4类访问控制规则。
其他访问规则:
dontaudit: 表示不记录某条违反规则的决策信息。
auditallow: 记录某项决策信息。通常SELinux只记录失败的决策信息,应用这条规则后,将会记录成功的决策信息。
neverallow: 表示不允许主体对客体的执行指定的操作。
SELinux的安全策略的实现是通过比较安全上下文中定义的类型(type)来完成的。这种方法称为类型强制(Type Enforcement, 简称TE)。
RBAC(一种基于角色的访问控制):
在类型强制的基础上,SELinux也提供了一种基于角色的访问控制。这是一种简化的管理的安全模式。系统中先创建出不同的角色,这些角色拥有不同的授权,然后通过给用户指定不同的角色来给用户授权。对于一个系统而言,角色的数量和权限相对比较固定,而用户却很容易发生变化,对于系统管理人员,他可以预先定义好角色并分配权限,当有新用户加入系统时,只需要把用户加入到一种或者几种角色中就可以了,大大简化了管理的负担。
类型强制(TE)模式,提供了非常细粒度的权限管理方式,但是如果每个用户都直接通过TE策略来定义权限,将会让系统维护的工作量变的非常巨大。角色相当于对系统一部分权限的抽象。这样即能提供细粒度的访问控制,又没有增加管理难度。
下面是以上提到的各种语法关系图,帮助理解:
域转移(Domain Transitions)
域转移是指把进程从一个域切换到另外一个域。
Linux中当以普通用户的身份登录系统时,要执行的一些超级用户才能完成的工作,必须使用su命令。SELinux如何让普通用户执行特殊权限操作?通过域转移来实现。
以su为例说明域转移:
su可执行文件的安全上下文:
-rwsr-sr-x root root u:object_r:su_exec:s0 su
其中su的安全上下文的type为su_exec,系统中关于su_exec的规则定义:
宏:domain_auto_trans(shell, su_exec, su)
最终展开宏:
allow shell su_exec:file {getattr open read execute}; //容许shell域对类型为su_exec的文件执行getattr open read execute 4种操作。也就是允许shell进程打开su文件并执行
allow shell su:process transition; //容许shell域对类型为su的进程执行transition操作。也就是允许su进程进行域切换操作。
allow su su_exec:file {entrypoint read execute}; //容许su域对类型为su_exec的文件执行entrypoint read execute 3种操作。
allow su shell:process sigchld; //容许su域对类型为shell的进程执行sigchld操作
dontaudit shell su:process noatsecure; // 如果域shell对类型为su的进程执行noatsecure操作失败了无需记录
allow shell su:process { siginh rlimitinh }; //容许shell域对类型为su的进程执行siginh rlimitinh 操作
type_transition shell su_exec:process su; //当shell域的进程启动一个类型su_exec的可执行文件时,把新的进程的域切换到su域。
从fork到exec中间需要多次权限检查,所以需要多个规则定义。这些定义大部分是为启动一个进程准备的,只有最后一条type_transition才是进行域转移的定义。
SEAndroid简介
SEAndroid(Security Enhancements for Android)安全增强型Android,即是把SELinux移植到android中,它是SELinux在android中的实现,现在已成为Android的核心部分。
下图为SEAndroid的基本框架图
策略文件
external/selinux/libselinux/目录下放了libselinux库的源码,这个库文件提供了一些函数,用来帮助用户进程使用SELinux的功能。
在system/sepolicy/private目录下定义了很多策略文件和一些安全上下文
1.角色定义文件roles_decl:
role r
定义SELinux系统的角色。
SEAndroid只定义了一种角色r。
2.用户定义文件users:
user u roles { r } level s0 range s0 - mls_systemhigh;
用来定义用户。SEAndroid中只定义了一种u。
3.Class定义文件security_classes
class security
class process
class system
class capability
#file-related classes
class filesystem
class file
class anon_inode
class dir
class fd
class lnk_file
class chr_file
class blk_file
class sock_file
class fifo_file
...
security_classes定义了所有系统中用到的class.
4.操作定义文件access_vectors
...
common file
{
ioctl
read
write
create
getattr
setattr
lock
relabelfrom
...
class filesystem
{
mount
remount
unmount
getattr
relabelfrom
relabelto
associate
quotamod
quotaget
watch
}
...
allow 语句的最后一项为允许的操作,所有操作文件都在文件access_vectors中定义。
access_vectors文件通过2种方式定义操作:
通过common语句,这种方式定义的操作是一种公共的操作,没有限定哪种类别的客体可以使用,而且可以被继承。
通过class语句,class语句后面的名称必须是客体限制类别。这意味着通过class语句定义的操作只能使用在相应的客体限制类别中。class语句可以继承common语句中定义的操作。
5.file_contexts文件
file_contexts文件保存的是系统中所有文件的安全上下文
# Root
/ u:object_r:rootfs:s0
# Data files
/adb_keys u:object_r:adb_keys_file:s0
/build.prop u:object_r:rootfs:s0
/default.prop u:object_r:rootfs:s0
/fstab..* u:object_r:rootfs:s0
/init..* u:object_r:rootfs:s0
/res(/.*)? u:object_r:rootfs:s0
/selinux_version u:object_r:rootfs:s0
/ueventd..* u:object_r:rootfs:s0
/verity_key u:object_r:rootfs:s0
# Executables
/init u:object_r:init_exec:s0
/sbin(/.*)? u:object_r:rootfs:s0
...
6.property_contexts文件
保存的系统中所有Android属性的安全上下文
# property service keys
#
#
net.rmnet u:object_r:net_radio_prop:s0
net.gprs u:object_r:net_radio_prop:s0
net.ppp u:object_r:net_radio_prop:s0
net.qmi u:object_r:net_radio_prop:s0
net.lte u:object_r:net_radio_prop:s0
net.cdma u:object_r:net_radio_prop:s0
net.dns u:object_r:net_dns_prop:s0
ril. u:object_r:radio_prop:s0
ro.ril. u:object_r:radio_prop:s0
gsm. u:object_r:radio_prop:s0
persist.radio u:object_r:radio_prop:s0
...
以上介绍了一些常用的策略文件和安全上下文。在system/sepolicy/private目录下还有大量的文件,有兴趣的可以自己查看。
下面再看看android中如何使用SELinux
Init进程设置SELinux的Policy
这个步骤很简单,事实上就是加载策略文件,然后将文件写入到load文件中。SELinux虚拟目录/sys/fs/selinux下的“load”文件,是内核和进程通信的接口,向它写入数据能传递到内核中。
具体代码如下:
system/core/init/main.cpp 文件是 Android 系统中 init 进程的主要入口点。
int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
__hwasan_set_error_report_callback(AsanReportCallback);
#endif
// Boost prio which will be restored later
setpriority(PRIO_PROCESS, 0, -20);
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv); // 设置selinux
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
return FirstStageMain(argc, argv);
}
在main方法中调用SetupSelinux() 方法。
int SetupSelinux(char** argv) {
....
//设置 selinux_callback cb
SelinuxSetupKernelLogging();
// TODO(b/287206497): refactor into different headers to only include what we need.
if (IsMicrodroid()) { // 是否是Microdroid系统。是则调用LoadSelinuxPolicyMicrodroid
LoadSelinuxPolicyMicrodroid();
} else { //非Microdroid系统
LoadSelinuxPolicyAndroid();
}
SelinuxSetEnforcement();
....
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast(args));
// execv() only returns if an error happened, in which case we
// panic and never return from this function.
PLOG(FATAL) FinishTransition();
snapuserd_helper = nullptr;
}
}
该方法主要是读取指定目录中的策略文件,保存在policy变量中。然后调用LoadSelinuxPolicy()方法加载策略。LoadSelinuxPolicy方法中执行具体加载过程
static void LoadSelinuxPolicy(std::string& policy) {
LOG(INFO) backend = backend;
rec->validating = selabel_is_validate_set(opts, nopts);
rec->digest = selabel_is_digest_set(opts, nopts, rec->digest);
//backend为0,所以调用initfuncs数组的第一个函数:selabel_file_init
if ((*initfuncs[backend])(rec, opts, nopts)) {
selabel_close(rec);
rec = NULL;
}
out:
return rec;
}
//initfuncs数组保存的是函数指针
static selabel_initfunc initfuncs[] = {
CONFIG_FILE_BACKEND(selabel_file_init),
CONFIG_MEDIA_BACKEND(selabel_media_init),
CONFIG_X_BACKEND(selabel_x_init),
CONFIG_DB_BACKEND(selabel_db_init),
CONFIG_ANDROID_BACKEND(selabel_property_init),
CONFIG_ANDROID_BACKEND(selabel_exact_match_init),//service init
CONFIG_ANDROID_BACKEND(selabel_exact_match_init),//keyStore key init
};
//selabel_file_init函数源码
int selabel_file_init(struct selabel_handle *rec,
const struct selinux_opt *opts,
unsigned nopts)
{
struct saved_data *data;
data = (struct saved_data *)malloc(sizeof(*data));
if (!data)
return -1;
memset(data, 0, sizeof(*data));
rec->data = data;
rec->func_close = &closef;
rec->func_stats = &stats;
rec->func_lookup = &lookup;
rec->func_partial_match = &partial_match;
rec->func_get_digests_all_partial_matches =
&get_digests_all_partial_matches;
rec->func_hash_all_partial_matches = &hash_all_partial_matches;
rec->func_lookup_best_match = &lookup_best_match;
rec->func_cmp = &cmp;
//解析传入的文件
return init(rec, opts, nopts);
}
文件的解析过程在init函数中。
selinux_android_file_context_handle函数最后返回selabel_handle指针。接着调用selinux_android_set_sehandle(sehandle)函数。将selabel_handle指针传入其中。selinux_android_set_sehandle函数比较简单,即将selabel_handle指针赋给静态指针变量fc_sehandle。解析出文件之后调用了SelinuxRestoreContext()函数,代码如下:
void SelinuxRestoreContext() {
LOG(INFO)