SeAndroid 使用极速上手

7/5/2023

# 1. 基本概念

SEAndroid 是一种安全系统,相关的概念和术语对于初学者来说都相对晦涩难懂。我们可以简单地理解:

  • 在 Android 系统里面有很多资源(资源主要包括了根文件系统下的文件,属性系统中的属性,binder 服务,进程,用户等)
  • 为了方便描述,我们需要给这些资源取名字
  • 为方便管理,我们需要给资源进行分类管理
  • 另外,我们需要定义一些规则,来规范资源的使用

# 1.1 资源的名字——安全上下文(security context)

资源的名字在 SEAndroid 中称之为安全上下文(security context),我们看一个例子:

# 这里的意思是 /dev/myse_dev 的名字(security context)是 u:object_r:myse_testdev_t:s0
/dev/myse_dev u:object_r:myse_testdev_t:s0
1
2

u:object_r:myse_testdev_t:s0 就是一个安全上下文(security context) 由4部分组成:

  • u 是 selinux 的用户名(user),在 Android 中只定义了一个用户,固定为 u
  • object_r 是 selinux 中的角色(role),类似于 linux 中的用户组,一个用户可以拥有多个角色
  • myse_testdev_t 是资源的类型,对于文件来说叫 type,对于进程叫 domain,内容为用户自定义
  • S0 是 SELinux 为了满足军用和教育行业而设计的 Multi-Level Security(MLS)机制有关,在 Android 中固定为 s0

上面说到,资源的类型可以自定义,接下来我们看看资源类型具体如何定义:

# 定义一个类型 myse_testdev_t,后面的 dev_type 表示 myse_testdev_t 是一个设备类型
type myse_testdev_t, dev_type;
1
2

这里的 dev_type 可以理解为安全上下文类型的类型,通过 attribute 定义:

# 定义一个类型 dev_type,表示设备类型  来自 system/sepolicy/public/attributes
attribute dev_type
1
2

当定义好安全上下文后,我们需要将安全上下文(资源的名字)与具体的资源相关联:

# 这里的意思是 /dev/myse_dev 的名字(security context)是 u:object_r:myse_testdev_t:s0
/dev/myse_dev u:object_r:myse_testdev_t:s0
1
2

# 1.2 定义资源的使用规则

以上的准备工作完成后,我们就可以添加相应的规则来限制资源的访问:

# 示例来自于 system/sepolicy/private/adbd.te 
# 允许 adbd (安全上下文), 对安全上下文为 anr_data_file 的目录(dir)有读目录权限
allow adbd anr_data_file:dir r_dir_perms;
1
2
3

上面这条规则的意思是:允许 adbd (安全上下文), 对安全上下文为 anr_data_file 的目录(dir)有读目录的权限。其中 adbd 称之为主体,anr_data_file 称为客体,dir 表示客体的类型是目录,类型的定义位于:system/sepolicy/private/security_classes :

class security
class process
class system
class capability

# file-related classes
class filesystem
class file
class dir
class fd
class lnk_file
class chr_file
class blk_file
class sock_file
class fifo_file

# network-related classes
class socket
class tcp_socket
class udp_socket
class rawip_socket
class node
class netif
class netlink_socket
class packet_socket
class key_socket
class unix_stream_socket
class unix_dgram_socket

# 省略
#.......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

r_dir_perms 表示目录的读权限,定义在 system/sepolicy/public/global_macros

# ......
define(`x_file_perms', `{ getattr execute execute_no_trans map }')
define(`r_file_perms', `{ getattr open read ioctl lock map }')
define(`w_file_perms', `{ open append write lock map }')
define(`rx_file_perms', `{ r_file_perms x_file_perms }')
define(`ra_file_perms', `{ r_file_perms append }')
define(`rw_file_perms', `{ r_file_perms w_file_perms }')
define(`rwx_file_perms', `{ rw_file_perms x_file_perms }')
define(`create_file_perms', `{ create rename setattr unlink rw_file_perms }')
#......
1
2
3
4
5
6
7
8
9
10

# 1.3 类型转换(Domain/Type Transition)

系统在运行的过程中,资源的名字(安全上下文)是会变化的,一个常见的例子:init 进程的名字(安全上下文)为 u:r:init:s0,而 init fork 的子进程显然不会也不应该拥有和 init 进程一样的名字(安全上下文),否则这些子进程就有了和 init 一样的权限,这不是我们需要的结果。这样的问题称之为类型转换(Domain/Type Transition)

接着我们来看一个类型转换的例子(例子来自深入理解SELinux SEAndroid(第一部分) (opens new window)):

type_transition init_t apache_exec_t : process apache_t;
1

type_transition 用于定义类型转换,上述语句的意思是:init_t 在执行(fork 并 execv 可执行文件)类型为 apache_exec_t 的可执行文件时,对应的进程(process)的类型要切换到 apache_t

上面的切换并不完整,需要完成切换,还需要下面的规则配合:

# init_t 进程能够执行 type 为 apache_exec_t 的文件
allow init_t apache_exec_t : file execute;
# 允许 init_t 切换进入 apache_t
allow init_t apache_t : process transition;
# 切换入口(对应为entrypoint权限)为 apache_exec_t
allow apache_t apache_exec_t : file entrypoint;
1
2
3
4
5
6

每次都这样写稍显麻烦,SeAndroid 定义了一个宏 domain_auto_trans 来帮我们完成上述所有功能:

domain_auto_trans(init_t, apache_exec_t, apache_t)
1

针对文件,也需要做类型转换,比如:

file_type_auto_trans(appdomain, download_file, download_file)
1

上述规则的意思是,当资源名(安全上下文)为 appdomain 的进程在资源名(安全上下文)为 download_file 的文件夹中创建新的文件时,新文件的资源名(安全上下文)为 download_file

# 2. Hello SeAndroid 示例

SEAndroid 相关的内容繁多,要完全掌握其细节费神费力,一般通过示例来学习和积累。这里演示一个访问设备文件的 C 可执行程序。

device/Jelly/Rice14 目录下添加如下的文件和文件夹:

hello_seandroid
├── Android.bp
└── hello_seandroid.c

sepolicy
├── device.te
├── file_contexts
└── hello_se.te
1
2
3
4
5
6
7
8

hello_seandroid 目录下是一个读取文件的可执行程序:

hello_seandroid.c:

#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define LOG_TAG "helloseandroid"
#include <log/log.h>

int main(int argc, char *argv[])
{
	
	int fd  = -1;
	int ret = -1;
	char *content = "hello test for selinux";
	char *dev_name = "/dev/hello_seandroid_dev";
	fd = open(dev_name, O_RDWR);
	if(fd < 0)
	{
		ALOGE("open %s error: %s", dev_name, strerror(errno));
		return -1;
	}

	ret = write(fd, content, strlen(content));	
	if(ret < 0)
	{
		ALOGE("write testfile error: %s", strerror(errno));
		return -1;
	}else
	{
		ALOGD("write testfile ok: %d",  ret);
	}
	
	while(1);

	close(fd);

	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

这段程序的主要作用就是向 /dev/hello_seandroid_dev 文件写入一段字符串。

Android.bp:

cc_binary {                 
    name: "helloseandroid",         
    srcs: ["hello_seandroid.c"],    
    cflags: [
        "-Werror",
        "-Wno-unused-parameter"
    ],    
	//Android10 上貌似不支持配置 product 分区的 sepolicy(Android 11 及以后是支持的)
	//所以只能选择 vendor 分区了
    vendor: true,
    shared_libs: [
        "libcutils",
        "liblog"
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

hello_se.te:


# 进程对应的类型
type  hello_se_dt, domain;
# 可执行文件对应的类型
type  hello_se_dt_exec, exec_type, vendor_file_type, file_type;
#表示该程序如果从 init 进程启动 hello_se_dt_exec,其安全上下文的 domain 部分从 init 转化为 hello_se_dt
init_daemon_domain(hello_se_dt);
#从 shell 启动 type 为 hello_se_dt_exec 的可执行程序,其对应进程的 domain 为 hello_se_dt
domain_auto_trans(shell, hello_se_dt_exec, hello_se_dt);
1
2
3
4
5
6
7
8
9

device.te:

# 定义设备 /dev/hello_seandroid_dev 的类型
type hello_se_dev_t, dev_type;
1
2

file_contexts:


/vendor/bin/helloseandroid     u:object_r:hello_se_dt_exec:s0
/dev/hello_seandroid_dev        u:object_r:hello_se_dev_t:s0
1
2
3

需要注意的是file_contexts 的最后一行必须是空行,否则编译不过,这是初学者容易忽略的一点。

最后修改 device/Jelly/Rice14/Rice14.mk

BOARD_SEPOLICY_DIRS += \
    device/Jelly/Rice14/sepolicy
1
2

编译运行:

source build/envsetup.sh
# 注意这里的版本和前面不一样了,选择了 userdebug
lunch Rice14-userdebug
make -j16
1
2
3
4

准备工作:

#进入Android shell 环境
adb shell
 
# 创建待访问的设备文件
su #使用 root 
touch /dev/hello_seandroid_dev
ls -Z /dev/hello_seandroid_dev                     
u:object_r:device:s0 /dev/hello_seandroid_dev
# 加载 file_contexts
restorecon /dev/hello_seandroid_dev
# 查看文件的安全上下文
ls -Z /dev/hello_seandroid_dev           
u:object_r:hello_se_dev_t:s0 /dev/hello_seandroid_dev
#放宽权限
chmod 777 /dev/hello_seandroid_dev 
# 查看可执行文件的安全上下文
ls -Z /vendor/bin/helloseandroid
u:object_r:hello_se_dt_exec:s0 /vendor/bin/helloseandroid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们配置的 selinux 权限不完善,所以我们的程序是不能成功执行的,但是我们可以通过 setenforce 0 命令切换到 selinux permissive 模式,该模式下不会阻止进程的行为,只会打印权限缺失信息,这样我们就可以执行我们的程序,并得到缺失的权限信息,然后通过源码提供的 audit2allow 工具帮我们生成对于的 selinux 权限信息:

setenforce 0
exit #退出 root
# 执行程序
helloseandroid &
1
2
3
4

接着查看 log:

logcat | grep "helloseandroid" --line-buffered | grep "avc"               

04-08 15:11:05.290  6595  6595 I helloseandroid: type=1400 audit(0.0:36): avc: denied { use } for path="/dev/pts/0" dev="devpts" ino=3 scontext=u:r:hello_se_dt:s0 tcontext=u:r:adbd:s0 tclass=fd permissive=1
04-08 15:11:05.290  6595  6595 I helloseandroid: type=1400 audit(0.0:37): avc: denied { read write } for path="/dev/pts/0" dev="devpts" ino=3 scontext=u:r:hello_se_dt:s0 tcontext=u:object_r:devpts:s0 tclass=chr_file permissive=1
04-08 15:11:05.290  6595  6595 I helloseandroid: type=1400 audit(0.0:38): avc: denied { read write } for path="socket:[10425]" dev="sockfs" ino=10425 scontext=u:r:hello_se_dt:s0 tcontext=u:r:adbd:s0 tclass=unix_stream_socket permissive=1
04-08 15:11:05.290  6595  6595 I helloseandroid: type=1400 audit(0.0:39): avc: denied { use } for path="/vendor/bin/helloseandroid" dev="dm-1" ino=71 scontext=u:r:hello_se_dt:s0 tcontext=u:r:shell:s0 tclass=fd permissive=1
04-08 15:11:05.300  6595  6595 I helloseandroid: type=1400 audit(0.0:40): avc: denied { read write } for name="hello_seandroid_dev" dev="tmpfs" ino=23235 scontext=u:r:hello_se_dt:s0 tcontext=u:object_r:hello_se_dev_t:s0 tclass=file permissive=1
1
2
3
4
5
6
7

我们把权限相关的 log 复制下来,在源码目录下,保存到 avc_log.txt 文件中,并执行一下命令:

source build/envsetup.sh
audit2allow -i avc_log.txt

allow hello_se_dt adbd:unix_stream_socket { read write };
allow hello_se_dt devpts:chr_file { read write };
allow hello_se_dt hello_se_dev_t:file { read write };
allow hello_se_dt shell:fd use;
1
2
3
4
5
6
7

这里就会输出相应的权限规则,我们将其添加到源码中 hello_se.te 后面即可:

# 进程对应的类型
type  hello_se_dt, domain;
# 可执行文件对应的类型
type  hello_se_dt_exec, exec_type, vendor_file_type, file_type;
#表示该程序如果从 init 进程启动 hello_seandroid_dt_exec,其安全上下文的 domain 部分从 init 转化为 hello_seandroid_dt
init_daemon_domain(hello_se_dt);
#从 shell 启动 type 为 hello_seandroid_dt_exec 的可执行程序,其对应进程的 domain 为 hello_seandroid_dt
domain_auto_trans(shell, hello_se_dt_exec, hello_se_dt);

allow hello_se_dt adbd:unix_stream_socket { read write };
allow hello_se_dt devpts:chr_file { read write };
allow hello_se_dt hello_se_dev_t:file { read write };
allow hello_se_dt shell:fd use;
1
2
3
4
5
6
7
8
9
10
11
12
13

再次编译运行系统,即可正常使用 helloseandroid 程序

# 参考资料

# 关于

我叫阿豪,2015 年本科毕业于国防科学技术大学指挥信息系统专业,毕业后从事信息化装备的研发工作,主要研究方向是 Android Framework 与 Linux Kernel。

如果你对 Android Framework 感兴趣或者正在学习 Android Framework,可以关注我的微信公众号和抖音,我会持续分享我的学习经验,帮助正在学习的你少走一些弯路。学习过程中如果你有疑问或者你的经验想要分享给大家可以添加我的微信,我拉你进技术交流群。