AOSP-Android安全机制SEAndroid/SELinux

Android 平台利用基于用户的 Linux 保护机制识别和隔离应用资源,作为 Android 安全模型的一部分,Android 使用安全增强型 Linux (SELinux) 对所有进程强制执行强制访问控制 (MAC),甚至包括以 Root/超级用户权限运行的进程(Linux 功能)。

Android权限控制流程

Android中自主访问控制是通过Linux UID/GID实现,而强制访问控制则是使用的SEAndroid!
在Android中SEAndroid安全机制(MAC)与传统的Linux UID/GID安全机制(DAC)是并存关系的,也就是说,它们同时用来约束进程的权限。当一个进程访问一个文件的时候,首先要通过基于UID/GID的DAC安全检查,接着才有资格进入到基于SEAndroid的MAC安全检查。只要其中的一个检查不通过,那么进程访问文件的请求就会被拒绝。

image

DAC自主访问控制

自主访问控制,正式的英文名称为Discretionary Access Control,简称为DAC。
比如通过 ls -l /system ,可以查看到该目录下存在一个manifest.xml文件,其输出为:

1
-rwxr-x--- 1 root root 2544 2022-09-29 17:02 manifest.xml

表示 manifest.xml是root用户组的root用户拥有,对于root用户来说,是rwx(可读可写可执行);而对于root用户
组其他用户来说,是可读可执行;对于其他用户则没有任何权限;也就是750权限。

那我们的程序能够对该文件进行写操作呢?在设备中运行程序执行:

1
2
File file = new File("/system/manifest.xml");
Log.i("Lance", file.canRead()+" "+file.canWrite()+" "+file.canExecute());

会输出:false false false,因为当前程序UID不可能是root(可以通过 data/system/packages.list 文件查看)。
我们知道,Android是一个基于Linux内核的系统,但是它不像传统的Linux系统,需要用户登录之后才能使用。然
而,Android系统又像传统的Linux系统一样有用户的概念。只不过这些用户不需要登录,也可以使用Android系统。
这是因为Android系统将每一个安装在系统的APK都映射为一个不同的Linux用户。也就是说,每一个APK都有一个对应的UID和GID。这些UID和GID是在APK安装的时候由系统安装服务PMS分配的:

在系统源码/base./services/core/java/com/android/server/pm/PackageManagerService.java:中如下:

1
2
3
4
5
6
7
8
9
10
11
12
private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg,
final int policyFlags, final int
scanFlags, long currentTime, @Nullable UserHandle user)
throws PackageManagerException {
......
if (pkgSetting == null) {
.......................
mSettings.addUserToSettingLPw(pkgSetting);
}
......
return pkg;
}

在系统源码/base./services/core/java/com/android/server/pm/Settings.java:中如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void addUserToSettingLPw(PackageSetting p) throws PackageManagerException {
if (p.appId == 0) {
// 分配uid
p.appId = newUserIdLPw(p);
}
......
}
private int newUserIdLPw(Object obj) {
final int N = mUserIds.size();
//从0开始,找到第一个未使用的ID,此处对应之前有应用被移除的情况,复用之前的ID
for (int i = mFirstAvailableUid; i < N; i++) {
if (mUserIds.get(i) == null) {
mUserIds.set(i, obj);
return Process.FIRST_APPLICATION_UID + i;
}
}
//最多只能安装 9999 个应用
if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
return -1;
}
mUserIds.add(obj);
return Process.FIRST_APPLICATION_UID + N;
}

在完成安装并运行程序后,可以通过 ps -A | grep PACKAGENAME 查看程序uid:

1
2
angler:/system # ps -A | grep com.enjoy
u0_a72 7124 562 2402748 170132 SyS_epoll_wait 78511081d4 S xxxxx

得到当前程序进程ID为7124,然后通过 cat /proc/7124/status 查看:

1
2
3
4
5
6
7
8
9
10
11
12
cat /proc/7124/status
#输出
Name: xxxxx
State: S (sleeping)
Tgid: 7124
Pid: 7124
PPid: 562
TracerPid: 0
Uid: 10072 10072 10072 10072
Gid: 10072 10072 10072 10072
FDSize: 64
Groups: 9997 20072 50072

可以看到当前程序UID为10072,通过这种方式,就可以保证每一个APK进程都以不同的身份来运行,从而保证了相
互之间不会受到干扰。这就是所谓的沙箱了,这完全是建立在Linux的UID和GID基础上的。
root的UID/GID可以通过下面方式查看:

在系统源码/system/core/include/private/android_filesystem_config.h

1
2
3
4
5
6
7
8
......
#define AID_ROOT 0 /* traditional unix root user */
#define AID_SYSTEM 1000 /* system server */
......
#define AID_APP 10000 /* TODO: switch users over to AID_APP_START */
#define AID_APP_START 10000 /* first app user */
#define AID_APP_END 19999 /* last app user */
......

可以看到,普通APP的UID从10000开始分配,最大到19999,而root用户的id为0。很显然程序并不具备对
manifest.xml文件的读写权限。
这种基于Linux UID/GID的安全机制,我们称之为自主访问控制,正式的英文名称为Discretionary Access Control,简称为DAC。

应用权限与DAC的关系

如何才能让我们的进程能通过Linux UID/GID的拦截呢?如果是一个Android APP若让其具备网络权限,我们只需要在AndroidManifest.xml中配置:

1
<uses-permission android:name="android.permission.INTERNET"/>

APP的UID和GID是在安装时候就由PMS分配好了的。为什么这样一个配置就能够让程序通过Linux UID/GID的拦截?
这是因为PMS在安装APK时,从Manifest文件中把App信息和权限存到 /data/system/packages.xml和 /data/system/packages.list 文件中。以《百度作业帮》为例,打开packages.list会存在下面的记录:

1
com.baidu.homework 10051 0 /data/user/0/com.baidu.homework default:targetSdkVersion=26 3002,3003,3001

其中3002,3003,3001代表的就是用户组,通过 /system/core/include/private/android_filesystem_config.h 查看可知

1
2
3
#define AID_NET_BT_ADMIN 3001 /* bluetooth: create any socket */
#define AID_NET_BT 3002 /* bluetooth: create sco, rfcomm or l2cap sockets */
#define AID_INET 3003 /* can create AF_INET and AF_INET6 sockets */

3001与3002代表了具备蓝牙相关权限的用户组,而3003则表示具备网络权限的用户组。PMS会将APK加入到相应的某个Linux用户组去,这样APK才能够具备对应的权限。
运行作业帮后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#执行
ps -A | grep com.baidu.homework
#输出
u0_a51 2516 563 1759664 113932 SyS_epoll_wait f62ef264 S com.baidu.homework
#执行
cat /proc/2516/status
#输出
Name: .baidu.homework
State: S (sleeping)
Tgid: 2516
Pid: 2516
PPid: 563
TracerPid: 0
Uid: 10051 10051 10051 10051
Gid: 10051 10051 10051 10051
FDSize: 64
Groups: 3001 3002 3003 9997 20051 50051

可以看到在Groups中存在3003,因此作业帮才具备网络访问的权限!Android应用权限与Linux UID/GID权限就是因此而关联起来的。

DAC的问题

在理想情况下,DAC机制是没有问题的。然而,现实很骨感。比如我们将某个系统文件的权限改为777(可读可写可执行),那是不是意味着我们的程序就能随意修改系统的配置了呢?我们还是以/system/manifest.xml为例:
我们通过以下命令将/system挂载为可读可写,并修改manifest文件读写权限:

1
2
3
4
5
6
7
8
9
10
11
12
adb root
adb disable-verity
adb reboot
adb root
adb remount
adb shell
mount |grep system
#假设输出:
#/dev/block/dm-0 on /system type ext4 (ro,seclabel,relatime,inode_readahead_blks=8)
#挂载点 /system 设备为:/dev/block/dm-0
mount -o remount,rw /dev/block/dm-0 /system
chmod 777 manifest.xml

修改完成后,如果只有DAC机制那么任何用户都能具备对该文件的读写以及执行权限,造成严重的安全问题。然而Android中我们会发现哪怕修改了777权限,app仍然无法对该文件进行写操作,这是因为Android中还是使用了一种更为强有力的安全机制来保证系统的安全,这种机制就是MAC!

Android MAC强制访问控制

完成上文的对manifest文件权限的修改后,再执行我们的程序运行:

1
2
File file = new File("/system/manifest.xml");
Log.i("Lance", file.canRead()+" "+file.canWrite()+" "+file.canExecute());

此时输出:true false true。可以看到我们以及可以在程序中对该文件进行读取,然而还是无法写这个文件。
这就是MAC强制访问控制的作用,在MAC机制中,用户、进程或者文件的权限是由管理策略决定的,而不是由它们自主决定的。例如,我们可以设定这样的一个管理策略,不允许用户A将它创建的文件F授予用户B访问。这样无论用户A如何修改文件F的权限位,用户B都是无法访问文件F的。这种安全访问模型可以强有力地保护系统的安全。

SELinux/SEAndroid

Android中使用的MAC机制就是SEAndroid。SELinux(Security-Enhanced Linux) 是美国国家安全局(NSA)在Linux
社区的帮助下设计的一个Linux历史上最杰出的安全系统,是一种MAC机制(Mandatory Access Control,强制访问控制)。在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。
由于Android系统有着独特的用户空间运行时,因此SELinux不能完全适用于Android系统。为此,NSA针对Android系统,在SELinux基础上开发了SEAndroid。

可参考官方文档:

https://source.android.google.cn/docs/security/features/selinux?hl=zh_cn

SEAndroid权限配置实战

SEAndroid安全机制又称为是基于TE(Type Enforcement)策略的安全机制。在/system/sepolicy目录中,所有
以.te为后缀的文件均为策略配置文件。我们以fdbus的name-server配置为例进行示例操作。

安全上下文

SEAndroid安全机制中的安全策略是在安全上下文的基础上进行描述的,也就是说,它通过主体和客体的安全上下
文,定义主体是否有权限访问客体。主体通常就是进程,而客体就是指进程所要访问的资源,例如文件、系统属性
等。
由于我们的服务程序可执行文件为/system/bin/name-server,首先我们需要在/system/sepolicy/private/file_contexts中声明该执行文件的SEAndroid系统文件的安全上下文:

1
/system/bin/name-server u:object_r:name-server_exec:s0

安全上下文实际上就是一个附加在对象上的标签(Tag)。这个标签实际上就是一个字符串,它由四部分内容组成,分别是:

SELinux用户:SELinux角色:类型:安全级别

每一个部分都通过一个冒号来分隔,格式为”user:role:type:sensitivity”。
在安全上下文中,只有类型(Type)才是最重要的,SELinux用户、SELinux角色和安全级别都几乎可以忽略不计的。正因为如此,SEAndroid安全机制又称为是基于TE(Type Enforcement)策略的安全机制。

用户与角色

/system/sepolicy/private/users中声明了SELinux用户u,它可用的SELinux角色为r,它的默认安全级别为s0,可
用的安全级别范围为s0 - mls_systemhigh:

1
user u roles { r } level s0 range s0 - mls_systemhigh;

mls_systemhigh为系统定义的最高安全级别。

/system/sepolicy/public/roles中声明了SELinux角色r与类型domain关联:

1
role r types domain;

在SEAndroid中,只定义了唯一一个用户u,两个角色r(适用于主题,如进程)与object_r(适用于对象,如文件),
这意味着只有u、r/object_r和domain可以组合在一起形成一个合法的安全上下文,那么ps -Z查看进程应为
u:r:domain:s0 ,ls -Z查看文件则为: u:object_r:domain:s0,而其它形式的安全上下文定义均是非法的。
但是以init进程为例,执行 adb shell ps -Z | grep zygote (Windows为: adb shell ps -Z | findStr zygote ),可以看到输出为:

1
u:r:zygote:s0 root 646 1 1577904 69500 poll_schedule_timeout eb7e743c S zygote

安全上下文 u:r:zygote:s0 ,按照上面的分析,这不是应该是一个不合法的上下文吗?原因是在 /system/sepolicy/public/zygote.te 中通过type声明了类型zygote并且将domain设置为类型zygote的属
性:

1
2
3
type zygote, domain;
type zygote_tmpfs, file_type;
type zygote_exec, system_file_type, exec_type, file_type;

因此它就可以像domain一样,可以和SELinux用户u和SELinux角色组合在一起形成合法的安全上下文。

类型

在SEAndroid中,每一个用来描述文件安全上下文的类型都将file_type设置为其属性,每一个用于进程安全上下文的类型都将domain设置为其属性。

安全策略

前面提到,SEAndroid安全机制主要是使用对象安全上下文中的类型来定义安全策略,这种安全策略就称Type
Enforcement,简称TE,.te文件即为安全策略配置文件。

修改 system/sepolicy/prebuilts/api/31.0/privatesystem/sepolicy/private/file_contexts 目录下同
样的:file_contexts、netd.te与untrusted_app_all.te文件,并在两个目录下都创建name-server.te文件。

按照google官方的介绍:

  • system/sepolicy/public:公共策略配置,将此目录视为相应平台的已导出政策 API,包括供应商特定
    策略。

  • system/sepolicy/private:系统正常运行所必需(但供应商映像政策应该不知道)的策略。

对于权限的配置不建议直接修改以上目录,应该在 /device/manufacturer/device-name/sepolicy 目录进
行自己设备的专用策略配置。
如Pixel5手机搭载AAOS,则应该在: /device/google_car/redfin_car/sepolicy 目录下配置。同时修改或
添加政策文件和上下文的描述文件后,需要修改 /device/manufacturer/device-name/BoardConfig.mk 以
引用 sepolicy 子目录和每个新的政策文件。

1
2
3
4
5
6
BOARD_SEPOLICY_DIRS += \
<root>/device/manufacturer/device-name/sepolicy
BOARD_SEPOLICY_UNION += \
genfs_contexts \
file_contexts \
sepolicy.te

由于我们的进程名为name-server,因此在 /system/sepolicy/private 目录下创建一个name-server.te作为该进
程的策略文件。文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 声明name-server类型,并将domain属性关联到该类型 (进程)
type name-server, domain;
# 声明name-server_exec类型(可执行文件,类型要与file_contexts中的相同)
type name-server_exec, exec_type, file_type;
#allow语句表示允许的权限。
allow name-server self:tcp_socket { read write getattr getopt setopt shutdown create bind
connect name_connect };
allow name-server self:netlink_route_socket {create write read nlmsg_readpriv nlmsg_read};
allow name-server fwmarkd_socket:sock_file {write};
allow name-server port:tcp_socket {name_connect};
allow name-server netd:unix_stream_socket {connectto};
allow name-server self:capability {net_raw};
allow name-server node:tcp_socket {node_bind};
#init_daemon_domain:system/sepolicy/public/te_macros中定义的宏(函数) 进行默认的一些配置
#domain_auto_trans(init,name-server_exec,name-server)
# domain_trans(init,name-server_exec,name-server)
# allow init name-server_exec:file { getattr open read execute };
# allow init name-server:process transition;
# allow name-server name-server_exec:file { entrypoint open read execute getattr };
# type_transition init name-server_exec:process name-server;
# ......
#声明 name-server 是从 init 衍生而来的,并且可以与其通信
init_daemon_domain(name-server)

Allow规则

allow表示开放权限,当某个进程执行,如果该进程不具备对应的权限,则可以在logcat 或者在执行 adb root 后执
行 adb shell dmesg 查看,可以以执行 adb shell dmesg > xx.txt 将内核日志都输出到xx.txt中查看。

官方资料:https://source.android.google.cn/docs/security/selinux/concepts?hl=zh_cn

1
2
3
4
avc: denied { bind } for pid=417 comm="name-server" scontext=u:r:name-server:s0
tcontext=u:r:name-server:s0 tclass=tcp_socket permissive=0
avc: denied { connectto } for pid=417 comm="name-server" scontext=u:r:name-server:s0
tcontext=u:r:netd:s0 tclass=unix_stream_socket permissive=0
说明 案例
缺少什么权限 { bind }
谁缺少权限 scontext=u:r:name-server:s0
对谁缺少权限 tcontext=u:r:name-server:s0
什么类型的权限 tclass=tcp_socket

此时就需要在TE文件中声明:

1
2
3
4
#allow [谁缺少权限] [对谁缺少权限]:[什么类型的权限] [缺少什么权限]
#当sconext与tcontext都是自己时候,可以将 [对谁缺少权限]写为:self
allow name-server self:tcp_socket {bind}
allow name-server netd:unix_stream_socket {connectto};

所以通过日志,就能完成对SEAndroid权限的赋予!

若验证是否为SEAndroid权限导致的程序无法正常运行可以通过关闭临时SEAndroid验证:

1
2
3
4
5
6
7
adb root
#关闭seandroid
adb shell setenforce 0
#开启seandroid
adb shell setenforce 0
#查看seandroid
adb shell getenforce

audit2allow

https://source.android.google.cn/docs/security/selinux/validate?hl=zh_cn#using_audit2allow
audit2allow 工具可以获取 dmesg 拒绝事件并将其转换成相应的 SELinux 政策声明。因此,该工具有助于大幅加快 SELinux 开发速度。
如需使用该工具,请运行以下命令:(需要切换到Python2)

1
2
3
#保证当前终端先执行了source与lunch指令
adb pull /sys/fs/selinux/policy
adb logcat -b events -d | audit2allow -p policy

image

如上图,表示需要在name-server.te中增加:

1
allow name-server self:netlink_route_socket read;

宏函数

在system/sepolicy/public/te_macros中定义了很多宏函数,其中init_daemon_domain表示声明 name-server
是从 init 衍生而来的,并且可以与其通信。
另外还有其他宏如:net_domain,其定义为:

1
2
3
4
5
6
#####################################
# net_domain(domain)
# Allow a base set of permissions required for network access.
define(`net_domain', `
typeattribute $1 netdomain;
')

如果调用该宏:net_domain(name-server)表示将name-server赋予netdomain类型。而netdomain类型可以在
system/sepolicy/public/net.te 中看到,存在如下规则:

1
2
3
#......
allow netdomain self:tcp_socket create_stream_socket_perms;
#......

那么使用该宏则表示,将name-server赋予tcp_socket对应的create_stream_socket_perms权限。整体而言
net_domain函数表示允许 使用 net 域中的常用网络功能,如读取和写入 TCP 数据包、通过套接字进行通信,以及
执行 DNS 请求等。也就是说,可以通过对应的宏函数调用完成对某些类型权限的统一allow。