探索安卓安全改进(二)
原文:
zh.annas-archive.org/md5/1E165BD192C4C9DE01CC57BFDF8623B4译者:飞龙
第八章:应用上下文到文件
在上一章中,我们升级了系统,收集了审计日志,并开始分析审计记录。我们发现文件系统上的一些对象未标记。在本章中,我们将:
-
了解文件系统和文件系统对象如何获取它们的标签
-
展示更改标签的技术
-
引入扩展属性进行标记
-
调查文件上下文和动态类型转换
标记文件系统
Linux 上的文件系统源自 mount,Android 上的ramdisk rootfs除外。Linux 上的文件系统差异极大。通常,为了支持 SELinux 的所有功能,你需要一个支持xattr和security命名空间的文件系统。我们在设置内核配置时遇到了这个要求。
文件系统对象在创建时,都带有初始上下文,就像所有其他内核对象一样。文件上的上下文简单地继承自它们的父级,因此如果父级未标记,则子级未标记,除非有类型转换规则。通常,如果上下文未标记,它推断数据是在启用 SELinux 支持之前的文件系统上创建的,或者当前加载的策略中不存在xattr中的类型标签。
初始标签或初始安全 ID(sid)在sepolicy文件initial_sid_contexts中。每个对象类都有其相关的初始sid。例如,让我们看一下以下代码片段:
...
sid fs u:object_r:labeledfs:s0
sid file u:object_r:unlabeled:s0
...
fs_use
文件系统可以通过多种方式进行标记。最佳的情况是文件系统支持xattrs。在这种情况下,策略中应该会出现fs_use_xattr声明。这些声明位于sepolicy目录中的fs_use文件中。fs_use_xattr的语法如下:
fs_use_xattr <fstype> <context>
要查看sepolicy中的fs_use,我们可以参考ext4文件系统的示例:
...
fs_use_xattr ext3 u:object_r:labeledfs:s0;
fs_use_xattr ext4 u:object_r:labeledfs:s0;
fs_use_xattr xfs u:object_r:labeledfs:s0;
...
这告诉 SELinux,当它遇到ext4 fs对象时;在扩展属性中查找标签或文件上下文。
fs_task_use
文件系统可以通过在创建对象时使用进程上下文来进行标记。这对于伪文件系统来说是有意义的,因为这些对象实际上是进程上下文,如pipefs和sockfs。这些伪文件系统管理管道和套接字系统调用,并不真正挂载到用户空间。它们存在于内核内部,供内核使用。然而,它们确实有对象,并且像任何其他对象一样,它们需要被标记。在这种情况下,fs_task_use策略声明是有意义的。这些内部文件系统只能被进程直接访问,并为这些进程提供服务。因此,用创建者进行标记是合理的。语法如下:
fs_task_use <fstype> <context>
sepolicy文件fs_use中的示例如下:
...
# Label inodes from task label.
fs_use_task pipefs u:object_r:pipefs:s0;
fs_use_task sockfs u:object_r:sockfs:s0;
...
fs_use_trans
你可能希望设置的下一个在实际上挂载的伪文件系统上设置标签的方法是使用fs_use_trans。这为伪文件系统设置一个文件系统范围的标签。这个的语法如下:
fs_use_trans <fstype> <context>
sepolicy文件中fs_use的示例如下:
...
fs_use_trans devpts u:object_r:devpts:s0;
fs_use_trans tmpfs u:object_r:tmpfs:s0;
...
genfscon
如果没有任何fs_use_*语句符合你的使用场景,比如vfat文件系统和procfs,那么你会使用genfscon语句。为genfscon指定的标签适用于所有该文件系统挂载的实例。例如,你可能希望对vfat文件系统使用genfscon。如果你有两个vfat挂载点,它们将针对每个挂载点使用相同的genfscon语句。然而,genfscon在处理procfs时行为不同,允许你为文件系统内的每个文件或目录设置标签。
genfscon的语法如下:
genfscon <fstype> <path> <context>
sepolicy genfs_contexts的示例如下:
...
# Label inodes with the fs label.
genfscon rootfs / u:object_r:rootfs:s0
# proc labeling can be further refined (longest matching prefix).
genfscon proc / u:object_r:proc:s0
genfscon proc /net/xt_qtaguid/ctrl u:object_r:qtaguid_proc:s0
...
请注意,rootfs的部分路径是/。它不是procfs,所以不支持对其标记的任何细粒度控制;因此,/是你唯一可以使用的。然而,你可以对procfs进行任意粒度的设置。
挂载选项
如果这些选项都不符合你的需求,另一个选项是可以通过mount命令行传递context选项。这设置一个文件系统范围的挂载上下文,如genfscon,但在需要分别设置标签的多个文件系统中很有用。例如,如果你挂载了两个vfat文件系统,你可能希望分开访问它们。使用genfscon语句,两个文件系统将使用由genfscon提供的相同标签。通过在挂载时指定标签,你可以让两个vfat文件系统使用不同的标签挂载。
以以下命令为例:
mount -ocontext=u:object_r:vfat1:s0 /dev/block1 /mnt/vfat1
mount -ocontext=u:object_r:vfat2:s0 /dev/block1 /mnt/vfat2
除了作为挂载选项的上下文之外,还有:fscontext和defcontext。这些选项与上下文是互斥的。fscontext选项设置用于某些操作(如挂载)的元文件系统类型,但不会改变每个文件的标签。defcontext设置未标记文件的默认上下文,覆盖initial_sid语句。最后,另一个选项rootcontext允许你设置文件系统中的根 inode 上下文,但仅适用于该对象。根据 mount 的手册页(man 8 mount),在无状态 Linux 中它被证明是有用的。
使用扩展属性进行标记
最后,最常用于标记的方法之一是使用扩展属性支持,也称为xattr或 EA 支持。即使有xattr支持,新对象也会继承其父目录的上下文;然而,这些标签具有基于每个文件系统对象或 inode 的细粒度。如果你记得,我们需要为 Android 上的文件系统启用或验证XATTR(CONFIG_EXT4_FS_XATTR)支持,并通过配置选项CONFIG_EXT4_FS_SECURITY配置 SELinux 使用它。
扩展属性是文件的键值元数据存储。SELinux 安全上下文使用security.selinux键,值是一个字符串,即安全上下文或标签。
file_contexts文件
在sepolicy目录中,你会找到file_contexts文件。这个文件用于设置支持每个文件安全标签的文件系统的属性。请注意,一些伪文件系统也支持这一点,如tmpfs、sysfs以及最近的rootfs。file_context文件具有基于正则表达式的语法,如下所示,其中regexp是路径的正则表达式:
regexp <type> ( <file label> | <<none>> )
如果为文件定义了多个正则表达式,将使用最后一个匹配项,因此顺序很重要。
下面的列表显示了每种文件系统对象的类型字段值,它们的含义以及系统调用接口:
-
--:这表示一个常规文件。 -
-d:这表示一个目录。 -
-b:这表示一个块文件。 -
-s:这表示一个套接字文件。 -
-c:这表示一个字符文件。 -
-l:这表示一个链接文件。 -
-p:这表示一个命名管道文件。
如你所见,类型本质上是ls -la命令输出的模式。如果没有指定,它将匹配所有内容。
下一个字段是文件标签或特殊标识符<<none>>。两者都可以提供上下文或标识符<<none>>。如果你指定了上下文,那么咨询file_contexts的 SELinux 工具将使用与指定上下文最后的匹配项。如果指定的上下文是<<none>>,这意味着没有分配上下文。所以,保留我们找到的那个。关键字<<none>>没有在 AOSP 参考的sepolicy中使用。
需要注意的是,前一段明确指出 SELinux 工具使用了file_contexts策略。内核并不知道这个文件的存在。SELinux 通过明确地从用户空间设置工具来给所有对象贴上标签,这些工具会在file_context中查找上下文,或者通过fs_use_*和genfs策略声明。换句话说,file_contexts没有内置于核心策略文件中,也没有被内核直接加载或使用。在构建时,file_contexts文件被构建在 ramdisk 的 rootfs 中,可以在/file_contexts找到。此外,在构建时,系统镜像被贴上标签,从而减轻设备本身的负担。
在 Android 中,init、ueventd和installd都已经修改为在创建对象时查找它们的上下文;这样它们可以正确地给它们贴上标签。因此,所有创建文件系统对象的 init 内置命令,如mkdir,都已被修改以使用存在的file_contexts文件,installd和ueventd也是如此。
让我们来看一些来自sepolicy目录中的file_context文件的部分内容:
...
/dev(/.*)? u:object_r:device:s0
/dev/accelerometer u:object_r:sensors_device:s0
/dev/alarm u:object_r:alarm_device:s0
...
在这里,我们为/dev中的文件设置上下文。请注意,条目是从最通用到更具体的dev文件的顺序。因此,未被更具体条目覆盖的任何文件最终将具有上下文u:object_r:device:s0,而匹配到更下面文件的将具有更具体的标签。例如,在/dev/accelerometer的加速度计将获得上下文u:object_r:sensors_device:s0。注意类型字段被省略了,这意味着它匹配所有文件系统对象,如目录(type -d)。
你可能想知道/dev目录本身是如何获得文件上下文的。查看一些代码片段,我们看到根目录/通过genfs_context文件中的声明genfscon rootfs / u:object_r:rootfs:s0被标记。本章前面提到,“新对象继承其父目录的上下文。”因此,我们可以推断出/dev的上下文是u:object_r:rootfs:s0,因为这是/的标签。我们可以通过向ls传递-Z标志来显示/dev的标签来测试这一点。在 UDOО串行连接上,执行以下命令:
130|root@udoo:/ # ls -laZ /
...
drwxr-xr-x root root u:object_r:device:s0 dev
...
看起来这个假设是错误的,但请注意,确实一切都有标签,如果没有明确指定,那么它将从父级继承。回顾sepolicy,我们可以看到dev文件系统最初设置了fs_use_trans devtmpfs u:object_r:device:s0;这样的策略声明。因此,当文件系统被挂载时,它是全局设置的。后来,当init或ueventd添加条目时,它们使用file_contexts条目将新创建的文件系统对象的上下文设置为file_contexts文件中指定的内容。在/dev的文件系统,它是一个devtmps伪文件系统,就是一个同时具有通过fs_use_trans声明设置的全局标签,同时也能通过file_contexts;支持细粒度标签的文件系统示例。在 Linux 上,文件系统的能力并不一致。
动态类型转换
由 SELinux 策略语句type_transition指示的动态类型转换是一种允许文件动态确定其类型的方法。因为这些是编译到策略中的,所以它们与file_contexts文件无关。这些策略语句允许策略作者基于文件创建的上下文动态地指示文件上下文。在你不控制源代码,或者不想以任何方式将 SELinux 耦合在一起的情况下,这些是非常有用的。例如,wpa请求者,这是一个为 Wi-Fi 支持运行的服务,在其数据目录中创建一个套接字文件。其数据目录被标记为类型wifi_data_file,如预期的那样,套接字最终也具有该标签。然而,此套接字由系统服务器共享。现在,我们可以允许系统服务器访问类型和对象类,但是hostapd和其他东西正在该目录中创建套接字和其他对象,因此这些对象也具有此类型。我们确实希望确保两个有问题的套接字,一个由hostapd使用,另一个由系统服务器使用,彼此保持独立。为此,我们需要能够以更细的粒度标记其中一个套接字,为此,我们可以修改代码或使用动态类型转换。与其修改代码,不如使用以下类型的转换:
type_transition wpa wifi_data_file:sock_file wpa_socket;
这是来自sepolicy文件wpa_supplicant.te中的实际语句。它表示,当类型为wpa的进程创建类型为wifi_data_file的文件且对象类为sock_file时,在创建时将其标记为wpa_socket。语句语法如下:
type_transition <creating type> <created type>:<class> <new type>;
从 SELinux 策略版本 25 开始,type_transition语句可以支持带名称的类型转换,其中第四个参数存在,且是文件的名称:
type_transition <creating type> <created type>:<class> <new type> <file name>;
我们将在sepolicy文件system_server.te中看到一个关于此文件名的示例使用:
type_transition system_server system_data_file:sock_file system_ndebug_socket "ndebugsocket";
请注意文件名或基名称,而不是路径,并且必须完全匹配。不支持正则表达式。有趣的是,动态转换不仅限于文件对象,还包括任何对象类事件进程。我们将在第九章《将服务添加到域》中看到如何使用动态进程转换。
示例和工具
理论知识我们已经有了,现在让我们看看系统中标记文件的工具和技术。首先,我们从挂载一个ramfs文件系统开始。由于/是只读的,我们将重新挂载它并为文件系统创建一个挂载点。通过 UDOOUDOO 串行控制台,执行以下命令:
root@udoo:/ # mount -oremount,rw /
root@udoo:/ # mkdir /ramdisk
root@udoo:/ # mount -t ramfs -o size=20m ramfs /ramdisk
现在,我们想要查看文件系统具有哪个标签:
# ls -laZ / | grep ramdisk
drwxr-xr-x root root u:object_r:unlabeled:s0 ramdisk
如你所记得,initial_sid_context文件为此文件系统设置了此初始sid:
sid file u:object_r:unlabeled:s0
如果我们想要在新标签中获取这个 ramdisk,我们需要在策略中创建类型,并设置一个新的genfscon语句来使用它。我们将在 sepolicy 文件file.te中声明新类型:
type ramdisk, file_type, fs_type;
类型策略语句的语法如下:
type <new type>, <attribute0,attribute1…attributeN>;
SELinux 中的属性是允许你定义常见组的语句。它们是通过attribute语句定义的。在 Android SELinux 策略中,我们已经定义了file_type和fs_type。我们将在这里使用它们,因为我们要创建的这种新类型具有file_type和fs_type属性。file_type属性与文件的类型相关联,而fs_type属性意味着此类型也与文件系统相关联。目前,属性并不是非常重要;所以不要在细节上纠结。
下一个要修改的是sepolicy文件,genfs_context,通过添加以下内容:
genfscon ramfs / u:object_r:ramdisk:s0
现在,我们将编译引导映像并将其闪存到设备上,或者更好的是,让我们使用如下所示的动态策略重新加载支持。
从 UDOOb 项目的根目录仅构建sepolicy项目:
$ mmm external/sepolicy/
通过adb推送新策略,如下所示:
$ adb push $OUT/root/sepolicy /data/security/current/sepolicy
544 KB/s (86409 bytes in 0.154s)
使用setprop命令触发重新加载:
$ adb shell setprop selinux.reload_policy 1
如果你连接了串行控制台,你应该会看到:
SELinux: Loaded policy from /data/security/current/sepolicy
如果你没有,只有adb,检查dmesg:
$ adb shell dmesg | grep "SELinux: Loaded"
<4>SELinux: Loaded policy from /sepolicy
<6>init: SELinux: Loaded property contexts from /property_contexts
<4>SELinux: Loaded policy from /data/security/current/sepolicy
成功加载应该使用我们在路径/data/security/current/sepolicy上的策略。让我们卸载 ramdisk 并重新挂载它,以查看其类型:
root@udoo:/ # umount /ramdisk
root@udoo:/ # mount -t ramfs -o size=20m ramfs /ramdisk
root@udoo:/ # ls -laZ / | grep ramdisk
drwxr-xr-x root root u:object_r:ramdisk:s0 ramdisk
我们能够修改策略并使用genfscon更改文件系统类型,现在为了显示继承,让我们继续在文件系统上使用touch创建一个文件:
root@udoo:/ # cd /ramdisk
root@udoo:/ramdisk # touch hello
root@udoo:/ramdisk # ls -Z
-rw------- root root u:object_r:ramdisk:s0 hello
正如我们所预期的,新文件被标记为 ramdisk 类型。现在,假设我们从 shell 执行 touch 操作,希望文件是另一种类型,比如ramdisk_newfile,我们该如何操作?我们可以通过修改 touch 本身来咨询file_contexts,或者我们可以定义一个动态类型转换;让我们尝试动态类型转换的方法。type_transition语句的第一个参数是创建类型;那么我们的 shell 是什么类型呢?你可以通过执行以下操作来获取:
root@udoo:/ramdisk # echo `cat /proc/self/attr/current`
u:r:init_shell:s0
更简单的方法是运行id -Z命令,该命令使用前述的proc文件。对于串行控制台,执行:
root@udoo:/ramdisk # id -Z
uid=0(root) gid=0(root) context=u:r:init_shell:s0
并为adb shell 运行相同的命令:
$ adb shell id -Z
uid=0(root) gid=0(root) context=u:r:shell:s0
注意我们在串行控制台 shell 和adb shell 之间的差异,在第九章将服务添加到域中,我们将修复这个问题。因此,我们现在编写的策略将解决这两种情况。
首先,打开sepolicy文件,init_shell.te,并在文件的末尾添加以下内容:
type_transition init_shell ramdisk:file ramdisk_newfile;
对sepolicy文件,shell.te执行以下操作:
type_transition shell ramdisk:file ramdisk_newfile;
现在,我们需要声明新类型;因此,打开sepolicy文件,file.te,并在末尾添加以下内容:
type ramdisk_newfile, file_type;
请注意,这里我们只使用了file_type属性。这是因为文件系统不应该有ramdisk_newfile类型,只有位于该文件系统内的文件才应该有。
现在,构建adb策略,将其推送到设备上,并触发重新加载。完成这些操作后,创建文件并检查结果:
$ adb shell 'touch /ramdisk/shell_newfile'
$ adb shell 'ls -laZ /ramdisk'
-rw-rw-rw- root root u:object_r:ramdisk:s0 shell_newfile
所以它没有起作用。让我们通过尝试一个ext4文件系统的例子来调查原因。我们将使用以下命令:
root@udoo:/ # cd /data/
root@udoo:/data # mkdir ramdisk
现在检查其上下文:
root@udoo:/data # ls -laZ | grep ramdisk
drwx------ root rootu:object_r:system_data_file:s0 ramdisk
标签是system_data_file。这并不有用,因为它不适用于我们的类型转换规则;为了修复这个问题,我们可以使用chcon命令显式地更改文件的上下文:
root@udoo:/data # chcon u:object_r:ramdisk:s0 ramdisk
root@udoo:/data # ls -laZ | grep ramdisk
drwx------ root root u:object_r:ramdisk:s0 ramdisk
现在,将上下文更改为与我们之前尝试的内存盘相匹配,让我们尝试在这个目录中创建一个文件:
root@udoo:/data/ramdisk # touch newfile
root@udoo:/data/ramdisk # ls -laZ
-rw------- root root u:object_r:ramdisk_newfile:s0 newfile
如你所见,类型转换已经发生。这是为了说明你在使用 SELinux 和 Android 时可能会遇到的问题。既然我们已经证明了我们的type_transition语句是有效的,那么失败只有两种可能:文件系统不支持它,或者我们在某个地方遗漏了“开启”它的内容。事实证明是后者;我们遗漏了fs_use_trans语句。那么打开sepolicy文件,fs_use并添加以下行:
fs_use_trans ramfs u:object_r:ramdisk:s0;
这个语句在这个文件系统上启用了 SELinux 动态转换。现在,重建sepolicy项目,使用adb push推送策略文件,并通过setprop启用动态重载:
$ mmm external/sepolicy
$ adb push $OUT/root/sepolicy /data/security/current/sepolicy546 KB/s (86748 bytes in 0.154s)
$ adb shell setprop selinux.reload_policy 1
root@udoo:/ # cd ramdisk
root@udoo:/ramdisk # touch foo
root@udoo:/ramdisk # ls -Z
-rw------- root root u:object_r:ramdisk_newfile:s0 foo
你看,对象具有由动态类型转换确定的正确值。我们遗漏了fs_use_trans,它启用了不支持xattrs的文件系统上的类型转换。
现在,假设我们想挂载另一个内存盘,会发生什么?由于它被genfscon语句标记,所有使用该类型挂载的文件系统都应该得到上下文u:object_r:ramdisk:s0。我们将在/ramdisk2挂载这个文件系统,并验证这种行为:
root@udoo:/ # mkdir ramdisk2
root@udoo:/ # mount -t ramfs -o size=20m ramfs /ramdisk2
同时,检查上下文:
root@udoo:/ # ls -laZ | grep ramdisk
drwxr-xr-x root root u:object_r:ramdisk:s0 ramdisk
drwxr-xr-x root root u:object_r:ramdisk:s0 ramdisk2
如果我们想要编写允许规则以分隔对这些文件系统的访问,我们需要将它们的目标文件放在不同的类型中。为此,我们可以使用上下文选项挂载新的内存盘。但首先,我们需要创建新的类型;让我们打开sepolicy文件,file.te并添加一个名为ramdisk2的新类型:
type ramdisk2, file_type, fs_type;
现在,使用命令mmm构建sepolicy,然后使用命令adb push推送策略,并通过setprop命令触发重载:
$ mmm external/sepolicy/
$ adb push out/target/product/udoo/root/sepolicy /data/security/current/sepolicy542 KB/s (86703 bytes in 0.155s)
$ adb shell setprop selinux.reload_policy 1
在这一点上,让我们卸载/ramdisk2并使用context=选项重新挂载它:
root@udoo:/ # umount /ramdisk2/
root@udoo:/ # mount -t ramfs -osize=20m,context=u:object_r:ramdisk2:s0 ramfs /ramdisk2
现在,验证上下文:
root@udoo:/ # ls -laZ | grep ramdisk
drwxr-xr-x root root u:object_r:ramdisk:s0 ramdisk
drwxr-xr-x root root u:object_r:ramdisk2:s0 ramdisk2
我们可以使用mount选项context=<context>覆盖genfscon上下文。实际上,如果我们查看dmesg,我们可以看到一些很好的信息。当我们没有使用上下文选项挂载ramfs时,我们得到了:
<7>SELinux: initialized (dev ramfs, type ramfs), uses genfs_contexts
当我们使用context=<context>选项挂载它时,我们得到了:
<7>SELinux: initialized (dev ramfs, type ramfs), uses mountpoint labeling
我们可以看到,当 SELinux 试图找出其标签来源时,它给出了一些有用的信息。
现在,让我们开始给支持xattr的文件系统,如ext4打标签。我们将从工具箱命令chcon开始。chcon命令允许你显式地设置文件系统对象的上下文,它不会咨询file_contexts。
让我们看看/system/bin目录,以及其中的前 10 个文件:
$ adb shell ls -laZ /system/bin | head -n10
-rwxr-xr-x root shell u:object_r:system_file:s0 InputDispatcher_test
-rwxr-xr-x root shell u:object_r:system_file:s0 InputReader_test
-rwxr-xr-x root shell u:object_r:system_file:s0 abcc
-rwxr-xr-x root shell u:object_r:system_file:s0 adb
-rwxr-xr-x root shell u:object_r:system_file:s0 am
-rwxr-xr-x root shell u:object_r:zygote_exec:s0 app_process
-rwxr-xr-x root shell u:object_r:system_file:s0 applypatch
-rwxr-xr-x root shell u:object_r:system_file:s0 applypatch_static
drwxr-xr-x root shell u:object_r:system_file:s0 asan
-rwxr-xr-x root shell u:object_r:system_file:s0 asanwrappe
我们可以看到,其中许多文件都有system_file标签,这是该文件系统的默认标签;让我们将am类型更改为am_exec。同样,我们需要通过向sepolicy文件file.te中添加以下内容来创建一种新类型:
type am_exec, file_type;
现在,重新构建策略文件,将其推送到 UDO,并触发重新加载。之后,让我们开始重新挂载系统,因为它是只读的:
root@udoo:/ # mount -orw,remount /system
现在执行chcon:
root@udoo:/ # chcon u:object_r:am_exec:s0 /system/bin/am
验证结果:
root@udoo:/ # la -laZ /system/bin/am
-rwxr-xr-x root shell u:object_r:am_exec:s0 am
此外,restorecon命令将使用file_contexts,并将该文件恢复为file_contexts文件中设置的内容,这应该是system_file:
root@udoo:/ # restorecon /system/bin/am
root@udoo:/ # la -laZ /system/bin/am
-rwxr-xr-x root shell u:object_r:system_file:s0 am
如你所见,restorecon能够咨询file_contexts并恢复该对象的指定上下文。
安卓系统的文件系统在构建时进行构造,因此,其所有文件对象在此过程中都被标记。我们还可以在构建时通过更改file_contexts来更改这一点。更改后,重新构建系统分区,并在重新刷新系统后,我们应该会看到具有am_exec类型的am文件。我们可以通过在system/bin部分的末尾添加这一行来修改sepolicy文件file_contexts进行测试:
/system/bin/am u:object_r:am_exec:s0
使用以下命令重新构建整个系统:
$ make -j8 2>&1 | tee logz
现在刷新并重启,然后让我们按照以下方式查看/system/bin/am的上下文:
root@udoo:/ # ls -laZ /system/bin/am
-rwxr-xr-x root shell u:object_r:am_exec:s0 am
这表明系统分区尊重构建时的文件上下文标记,以及我们如何控制这些标签。
修复/data
在审计日志中,我们还发现了一堆未标记的文件,例如下面的拒绝访问记录:
type=1400 msg=audit(86559.780:344): avc: denied { append } for pid=2668 comm="UsbDebuggingHan" name="adb_keys" dev=mmcblk0p4 ino=42 scontext=u:r:system_server:s0 tcontext=u:object_r:unlabeled:s0 tclass=file
我们可以看到设备是mmcblk0p4,挂载命令会告诉我们这个文件系统挂载到了哪里,其输出如下:
root@udoo:/ # mount | grep mmcblk0p4
/dev/block/mmcblk0p4 /data ext4 rw,seclabel,nosuid,nodev,noatime,nodiratime,errors=panic,user_x0
那么/data文件系统为什么有这么多未标记的文件呢?原因是 SELinux 应该从空设备开始启用,即从第一次启动时。Android 按需构建数据目录结构。因此,由于它是ext4,所有/data的标签都由file_contexts文件处理。同时,这些由创建/data文件和目录的系统处理。这些系统已经被修改为根据file_contexts规范对数据分区进行标记。因此,这里有两个选择:擦除/data并重启,或者执行restorecon -R /data。
第一个选项有点激烈,但如果你弹出 SD 卡并删除数据分区partition 4上的所有文件,Android 将重新构建,你就不会再看到任何未标记的问题。然而,对于升级时的已部署设备,这并不推荐;你将破坏所有用户的数据。
在部署场景中,第二个选项更受欢迎,但也有其局限性。特别是,执行restorecon -R /data将花费很长时间,并且必须在启动早期,在挂载之后立即进行。然而,目前这确实是唯一的选择。不过,谷歌在这一领域做了大量工作,并创建了一个系统,可以在策略更新时智能地重新标记/data。考虑到我们的使用情况,我们将选择第二个选项的一个变体,尤其是考虑到/data文件系统的稀疏性;我们实际上还没有安装或生成大量用户数据。基于这一点,执行:
root@udoo:/ # restorecon -R /data
root@udoo:/ # reboot
由于我们的系统处于宽容模式,且不在部署场景中,因此我们无需在启动早期执行restorecon。现在,让我们拉取audit.log文件,并将其与已拉的audit.log进行比较:
$ adb pull /data/misc/audit/audit.log audit_data_relabel.log
170 KB/s (14645 bytes in 0.084s)
让我们使用grep来计算每个文件中出现的次数:
$ grep -c unlabeled audit.log
185
$ grep -c unlabeled audit_data_relabel.log
0
太棒了,我们已经修复了/data上的所有未标记问题!
关于安全的补充说明
请注意,尽管我们运行了所有这些命令并更改了所有这些内容,但这并不是 SELinux 中的安全漏洞。更改类型标签、挂载文件系统以及将文件系统与类型关联,都需要允许规则。如果你查看审核日志,你会看到一系列的拒绝记录;以下是一个示例:
type=1400 msg=audit(90074.080:192): avc: denied { associate } for pid=3211 comm="touch" name="foo" scontext=u:object_r:ramdisk_newfile:s0 tcontext=u:object_r:ramdisk:s0 tclass=filesystem
type=1400 msg=audit(90069.120:187): avc: denied { mount } for pid=3205 comm="mount" name="/" dev=ramfs ino=1992 scontext=u:r:init_shell:s0 tcontext=u:object_r:ramdisk:s0 tclass=filesystem
如果我们处于强制模式,我们将无法执行这里展示的任何实验。
总结
在本章中,我们看到了如何通过重新标记文件将文件放入上下文中。我们使用了各种技术来完成这项任务,从工具箱命令如chcon和restorecon,到挂载选项和动态转换。有了这些工具,我们可以确保所有文件系统对象都被正确标记。这样,我们最终得到了正确的目标上下文,以便我们编写的策略能够有效。在下一章中,我们将关注进程,确保它们处于正确的域或上下文中。
第九章:向域添加服务
在上一章中,我们介绍了将文件对象放入正确域的过程。在大多数情况下,文件对象是目标。然而,在本章中,我们将:
-
强调标记进程——尤其是由 init 运行和管理的 Android 服务。
-
管理由 init 创建的辅助关联对象。
Init —— 守护进程之王
在 Linux 系统中,init 进程至关重要,Android 在这方面也不例外。然而,Android 有其自己的 init 实现。Init 是系统上的第一个进程,因此具有进程 ID(PID)为 1。所有其他进程都是直接从 init 进行fork()的结果,因此所有进程最终都会直接或间接地成为 init 的子进程。Init 负责清理和维护这些进程。例如,任何父进程死亡的孩子进程都会被内核重新设置为 init 的子进程。这样,当进程退出时,init 可以调用wait()(更多详情请查看man 2 wait)来清理进程。
注意
已经终止但尚未调用wait()的进程是僵尸进程。在调用此函数之前,内核必须保留进程数据结构。如果做不到这一点,将会无限期地消耗内存。
由于 init 是所有进程的根,它还提供了一种通过其自己的脚本语言声明和执行命令的机制。使用这种语言来控制 init 的文件称为 init 脚本,我们已经修改了一些。在源代码树中,我们使用了init.rc文件,您可以通过导航到device/fsl/imx6/etc/init.rc来找到它,但在设备上,它与 ramdisk 一起打包在/init.rc,并可供 init 使用,init 也包含在 ramdisk 中的/init。
要向 init 脚本添加服务,您可以修改init.rc文件并添加一个声明,如下所示:
service <name> <path> [ <argument>... ]
在这里,name是服务名称,path是可执行文件的路径,而argument是要传递给可执行文件在它的argv数组中的以空格分隔的参数字符串。
例如,以下是rild的 service 声明,即无线接口层守护进程(RILD):
Service ril-daemon /system/bin/rild
通常情况下,可以并且需要添加额外的服务选项。init 脚本的service语句支持丰富的选项集合。要查看完整列表,请参考位于system/core/init/readme.txt的信息文件。此外,我们在第三章中介绍了针对 Android 特定的 SE 更改,Android Is Weird。
继续剖析rild,我们看到在 UDO 的init.rc中的声明其余部分如下:
Service ril-daemon /system/bin/rild
class main
socket rild stream 660 root radio
socket rild-debug stream 660 radio system
socket rild-ppp stream 660 radio system
user root
group radio cache inet misc audio sdcard_rw log
这里需要注意的是,它会创建相当多的套接字。init.rc中的socket关键字由readme.txt文件描述:
注意
来自源代码树文件system/core/init/readme.txt:
socket <name> <type> <perm> [ <user> [ <group> [ <context> ] ] ]
创建一个名为 /dev/socket/<name> 的 Unix 域套接字,并将其 fd 传递给启动的进程。类型必须是 dgram、stream 或 seqpacket。user 和 group ID 默认为 0。套接字的 SELinux 安全上下文是 context。默认为服务安全上下文,由 seclabel 指定,或者基于服务可执行文件的 security context 计算。
让我们查看这个目录,看看我们发现了什么。
root@udoo:/dev/socket # ls -laZ | grep adb
srw-rw---- system system u:object_r:adbd_socket:s0 adbd
这引发了这样一个问题:“它是如何进入那个域的?”根据我们上一章的知识,我们知道 / dev 是一个 tmpfs,所以我们知道它不是通过 xattrs 进入这个域的。它必须是一个代码修改或类型转换。让我们检查是否是类型转换。如果是,我们预计会在扩展的 policy.conf 中看到一条声明。SELinux 策略基于 m4 宏语言。在构建期间,它被扩展到 policy.conf,然后编译。第十二章,掌握工具链,对此有更多细节。
我们可以通过使用 sesearch 来查找 adbd_socket 的类型转换来发现这一点:
$ sesearch -T -t adbd_socket $OUT/sepolicy
正如您从空输出中看到的,没有这样的行,所以这不是策略所做的事情,而是代码更改。
在 Linux 中,进程是通过 fork() 然后 exec() 创建的。因此,我们能够提供很好的关键字来搜索 init 守护进程。我们怀疑设置套接字的代码就在子进程中的 fork() 调用之后,在 exec() 调用之前:
$ grep -n fork system/core/init/init.c
235: pid = fork();
因此,我们要找的 fork 在 init.c 的第 235 行;让我们在文本编辑器中打开 init.c 并查看。我们将找到以下代码段进行审查:
...
NOTICE("starting '%s'\n", svc->name);
pid = fork();
if (pid == 0) {
struct socketinfo *si;
struct svcenvinfo *ei;
char tmp[32];
int fd, sz;
umask(077);
if (properties_inited()) {
get_property_workspace(&fd, &sz);
sprintf(tmp, "%d,%d", dup(fd), sz);
add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
}
for (ei = svc->envvars; ei; ei = ei->next)
add_environment(ei->name, ei->value);
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, si->socketcon ?: scon);
if (s >= 0) {
publish_socket(si->name, s);
}
...
根据 man 2 fork,子进程中的 fork() 返回代码是 0。子进程在此 if 语句内执行,父进程跳过它。函数 create _ socket() 也似乎很有趣。它似乎接受服务名称、套接字类型、权限标志、uid、gid 和 socketcon。什么是 socketcon?让我们检查是否可以追溯到它的设置位置。
如果我们查看 fork() 之前的内容,我们可以看到父进程根据两个因素获取其 scon:
...
if (svc->seclabel) {
scon = strdup(svc->seclabel);
if (!scon) {
ERROR("Out of memory while starting '%s'\n", svc->name);
return;
}
} else {
...
当 svc->seclabel 不为空时,通过 if 语句的第一个路径发生。这个 svc 结构用与服务相关的选项填充。从第三章,安卓很奇怪 中回想一下,seclabel 允许您显式设置服务的上下文,硬编码到 init.rc 中的值。else 子句要复杂和有趣得多。
在else子句中,我们通过调用getcon()获取当前进程的上下文。由于我们是在 init 中运行,这个函数应该返回u:r:init:s0并将其存储在mycon中。下一个函数getfilecon()传递了可执行文件的路径,并检查文件本身的上下文。第三个函数是这里的工作马:security_compute_create()。它接收mycon、fcon和target类别,并计算安全上下文scon。给定这些输入,它会尝试根据策略类型转换确定子进程的结果域。如果没有定义转换,scon将与mycon相同。
create_socket()函数内的条件表达式另外决定了传递的套接字上下文。变量si是一个结构体,其中包含了 init service部分中套接字语句的所有选项。如readme.txt文件所述,si->socketcon是套接字上下文参数。换句话说,套接字上下文可能来自以下三个地方(按优先级递减):
-
service声明中套接字选项的socketcon选项 -
service关键字上的seclabel选项 -
从源和目标上下文动态计算
套接字上下文被传递给create_socket()。现在,让我们看看create_socket()。这个函数在system/core/init/util.c:87定义。围绕socket()的代码片段似乎很有趣:
...
if (socketcon)
setsockcreatecon(socketcon);
fd = socket(PF_UNIX, type, 0);
if (fd < 0) {
ERROR("Failed to open socket '%s': %s\n", name, strerror(errno));
return -1;
}
if (socketcon)
setsockcreatecon(NULL);
...
setsockcreatecon()函数设置了进程的套接字创建上下文。这意味着通过socket()调用创建的套接字将具有通过setsockcreatecon()设置的上下文。创建后,进程通过使用setsockcreatecon(NULL)将其重置为原始上下文。
下一段有趣的代码是关于bind()的:
...
filecon = NULL;
if (sehandle) {
ret = selabel_lookup(sehandle, &filecon, addr.sun_path, S_IFSOCK);
if (ret == 0)
setfscreatecon(filecon);
}
ret = bind(fd, (struct sockaddr *) &addr, sizeof (addr));
if (ret) {
ERROR("Failed to bind socket '%s': %s\n", name, strerror(errno));
goto out_unlink;
}
setfscreatecon(NULL);
freecon(filecon);
...
在这里,我们设置了文件创建的上下文。这些功能与setsock_creation()类似,但适用于文件系统对象。然而,selabel_lookup()函数会在file_contexts中查找文件的上下文。你可能遗漏的部分是,对于基于路径的套接字,bind()的调用会在sockaddr_un struct指定的路径上创建一个文件。因此,套接字对象和文件系统节点条目是截然不同的,并且可以具有不同的上下文。通常,套接字属于进程的上下文,而文件系统节点被赋予其他上下文。
动态域转换
我们看到 init 计算了 init 套接字的上下文,但在为子进程设置域时从未遇到过。在本节中,我们将深入探讨两种实现方法:使用 init 脚本显式设置和 sepolicy 动态域转换。
设置子进程域的第一种方式是在 init 脚本服务声明中使用seclabel语句。在fork()之后的子进程执行中,我们发现了这个语句:
if (svc->seclabel) {
if (is_selinux_enabled() > 0 && setexeccon(svc->seclabel) < 0) {
ERROR("cannot setexeccon('%s'): %s\n", svc->seclabel, strerror(errno));
_exit(127);
}
}
为了澄清,svc变量是包含服务选项和参数的结构,所以svc->seclabel就是seclabel。如果它被设置了,它会调用setexeccon(),后者为进程通过exec()执行的任何东西设置执行上下文。再往下,我们看到exec()函数调用。exec()系统调用在成功时永远不会返回;它只在失败时返回。
为子进程设置域的另一种方式,这种方式更为推荐,就是使用 sepolicy。之所以推荐,是因为策略不依赖于其他任何东西。通过在 init 中硬编码上下文,你就在 init 脚本和 sepolicy 之间耦合了一个依赖关系。例如,如果 sepolicy 移除了在 init 脚本中硬编码的类型,init setcon将失败,但两个系统都能正确编译。如果你移除了一个类型转换的类型,并留下了转换语句,你可以在编译时捕获错误。由于我们查看了rild服务语句,让我们看看位于sepolicy中的rild.te策略文件。我们应该在这个文件中使用grep搜索type_transition关键字:
$ grep -c type_transition rild.te
0
没有找到type_transition的实例,但这个关键字必须存在,类似于文件。然而,它可能隐藏在一个未展开的宏中。SELinux 策略文件是用 m4 宏语言编写的,它们在编译之前会被展开。让我们查看rild.te文件,看看我们是否能找到一些宏。它们具有参数,看起来像函数。我们遇到的第一个宏是init_daemon_domain(rild)。现在,我们需要在sepolicy中找到这个宏的定义。m4 语言使用define关键字来声明宏,所以我们可以搜索这个:
$ grep -n init_daemon_domain * | grep define
te_macros:99:define(`init_daemon_domain', `
我们的宏在te_macros中声明,碰巧它包含了与类型强制执行(TE)相关的所有宏。让我们更详细地看看这个宏的作用。首先,它的定义是:
...
#####################################
# init_daemon_domain(domain)
# Set up a transition from init to the daemon domain
# upon executing its binary.
define(`init_daemon_domain', `
domain_auto_trans(init, $1_exec, $1)
tmpfs_domain($1)
')
...
上述代码中的注释行(以#开头的 m4 行),表明它设置了一个从 init 到守护进程域的转换。这似乎是我们想要的东西。然而,包含它们的语句都是宏,我们需要递归地展开它们。我们将从domain_auto_trans()开始:
...
#####################################
# domain_auto_trans(olddomain, type, newdomain)
# Automatically transition from olddomain to newdomain
# upon executing a file labeled with type.
#
define(`domain_auto_trans', `
# Allow the necessary permissions.
domain_trans($1,$2,$3)
# Make the transition occur by default.
type_transition $1 $2:process $3;
')
...
这里的注释表明我们正朝着正确的方向前进;然而,在搜索过程中,我们需要继续展开宏。根据注释,domain_trans()宏允许仅发生转换。请记住,在 SELinux 中几乎所有的操作都需要来自策略的明确许可才能进行,包括类型转换。宏中的最后一条语句是我们一直在寻找的:
type_transition $1 $2:process $3;
如果你展开这条语句,你会得到:
type_transition init rild_exec:process rild;
这条语句传达的意思是,如果你在一个类型为rild_exec的文件上执行exec()系统调用,并且执行域是 init,那么将子进程的域设置为rild。
通过 seclabel 显示上下文
设置上下文的另一种方法非常直接。就是在service声明中通过初始化脚本将它们硬编码。在service声明中,正如我们在第三章《安卓很奇怪》中所看到的,对 init 语言进行了修改。其中一个添加项是seclabel。这个选项只是让 init 明确地将服务的上下文更改为传递给seclabel的参数。以下是adbd的一个例子:
Service adbd /sbin/adbd
class core
socket adbd stream 660 system system
disabled
seclabel u:r:adbd:s0
那么为什么有些使用动态转换,而另一些使用seclabel呢?答案取决于你从哪里执行。像adbd这样的东西很早就从 ramdisk 中执行,因为 ramdisk 实际上不使用每个文件的标签,所以你不能正确设置转换——目标具有相同的上下文。
重新标记进程
既然我们现在拥有了动态进程转换功能,而且需要从初始化脚本中设置套接字上下文。让我们尝试重新标记那些处于不正确上下文中的服务。我们可以通过以下规则检查它们是否不正确:
-
除了 init,不应该有其他进程处于初始化上下文
-
没有长时间运行的进程应该处于
init_shell域 -
除了 zygote,不应该有其他进程处于 zygote 域
注意
一个更全面的测试套件是 AOSP 上的 CTS 的一部分。更多详细信息请参考 Android CTS 项目:(git clone)android.googlesource.com/platform/cts。注意./hostsidetests/security/src/android/cts/security/SELinuxHostTest.java和./tests/tests/security/src/android/security/cts/SELinux.*.java测试。
让我们运行一些基本的命令,并通过adb连接评估我们的 UDOO 的状态:
$ adb shell ps -Z | grep init
u:r:init:s0 root 1 0 /init
u:r:init:s0 root 2267 1 /sbin/watchdogd
u:r:init_shell:s0 root 2278 1 /system/bin/sh
$ adb shell ps -Z | grep zygote
u:r:zygote:s0 root 2285 1 zygote
我们有两个进程处于不正确的域中。第一个是watchdogd,第二个是sh进程。我们需要找到这些进程并将它们纠正。
我们将从神秘的sh程序开始。正如你在上一章中可以回忆起,我们的 UDOO 串行控制台进程具有init_shell的上下文,所以这是一个很好的嫌疑对象。让我们检查 PID 并找出。从 UDOO 串行控制台执行:
root@udoo:/ # echo $$
2278
我们可以将这个 PID 与adb shell ps输出的 PID 字段(PID 字段是第三个字段,索引为 2)进行比较,正如你所看到的,我们有一个匹配项。
接下来,我们需要找到这个服务的声明。我们知道它在init.rc中,因为它运行在init_shell中,根据 SELinux 策略,只能由 init 直接转换到这种类型的运行状态。另外,init 只通过服务声明开始处理事情,所以为了处于init_shell状态,你必须通过服务声明由 init 启动。
注意
使用sesearch查找编译后的 sepolicy 二进制文件上的此类信息:
$ sesearch -T -s init -t shell_exec -c process $OUT/root/sepolicy
如果我们在udoo/device/fsl/imx6/etc中的 UDOO 的init.rc文件中搜索/system/bin/sh这个有疑问的命令,可以使用grep来查找其内容。如果我们这样做,我们会发现:
$ grep -n "/system/bin/sh" init.rc
499:service console /system/bin/sh
702:service wifi_mac /system/bin/sh /system/etc/check_wifi_mac.sh
让我们看看499,因为我们对 Wi-Fi 没有涉及任何事情:
service console /system/bin/sh
class core
console
user root
group root
如果这就是问题服务,我们应该能够禁用它,并验证我们的串行连接不再工作:
$ adb shell setprop ctl.stop console
我的实时串行连接在以下位置断开:
root@udoo:/ # avc: denied { set } for property=ctl.console scontext=u:r:shell:s0 tcontext=u:e
现在我们已经验证了它是什么,我们可以重新启动它:
$ adb shell setprop ctl.start console
当系统恢复到工作状态后,我们现在需要解决修正此服务标签的最佳方法。我们有两个选项:
-
在
init.rc中使用明确的seclabel条目 -
使用类型转换
我们在这里将使用第一个选项。原因是 init 会不时执行 shell,我们不希望所有这些都在 console 进程域中。我们希望最小权限来隔离运行中的进程。通过使用明确的 seclabel,我们不会改变沿途中执行的其他 shell。
为此,我们需要修改init.rc中关于 console 的条目;添加:
service console /system/bin/sh
class core
console
user root
group root
seclabel u:r:shell:s0
此可执行文件适当的域是shell,因为它应该与adb shell具有相同的权限集。在您进行此更改后,重新编译引导映像,刷新,然后重新启动。我们可以看到它现在处于 shell 域中。要从 UDOO 串行连接中验证,执行以下操作:
root@udoo:/ # id -Z
uid=0(root) gid=0(root) context=u:r:shell:s0
或者,使用adb执行以下命令:
$ adb shell ps -Z | grep "system/bin/sh"
u:r:shell:s0 root 2279 1 /system/bin/sh
下一个我们需要处理的是watchdogd。watchdogd进程已经有了一个域,并且在watchdog.te中允许规则;所以我们只需要添加一个seclabel语句并将其放入适当的域中。修改init.rc:
# Set watchdog timer to 30 seconds and pet it every 10 seconds to get a 20 second margin
service watchdogd /sbin/watchdogd 10 20
class core
seclabel u:r:watchdogd:s0
要使用adb验证,执行以下命令:
$ adb shell ps -Z | grep watchdog
u:r:watchdogd:s0 root 2267 1 /sbin/watchdogd
在这一点上,我们已经对 UDOO 需要的实际策略进行了更正。然而,我们需要练习使用动态域转换。一个好的教学示例应该有一个在其自己域中的 shell 的子 shell。让我们从定义一个新域并设置转换开始。
我们将在sepolicy中创建一个名为subshell.te的新.te文件,并编辑其内容如下:
type subshell, domain, shelldomain, mlstrustedsubject;
# domain_auto_trans(olddomain, type, newdomain)
# Automatically transition from olddomain to newdomain
# upon executing a file labeled with type.
#
domain_auto_trans(shell, shell_exec, subshell)
现在,本书前面使用的mmm技巧可以用来仅编译策略。同时,使用adb push命令将新策略推送到/data/security/current/sepolicy,并执行setprop以重新加载策略,正如我们在第八章 将上下文应用于文件中所做的那样。
为了测试这一点,我们应该能够输入sh,并验证域转换。我们将从获取当前上下文开始:
root@udoo:/ # id -Z
uid=0(root) gid=0(root) context=u:r:shell:s0
然后通过执行以下命令来启动一个 shell:
root@udoo:/ # sh
root@udoo:/ # id -Z
uid=0(root) gid=0(root) context=u:r:subshell:s0
我们能够使用动态类型转换让一个新进程进入一个域。如果你将此与第八章中提出的给文件打标签相结合,你就有了一个强大的工具来控制进程权限。
对应用标签的限制
这些动态进程转换的一个基本限制是它们需要一个exec()系统调用来执行。只有这样,SELinux 才能计算出新域,并触发上下文切换。唯一的其他方法是通过修改代码,本质上当你指定seclabel()时,init 就是这样做的。init 代码为其进程设置了执行上下文,导致下一次exec进入指定的域。实际上,我们可以在init.c代码中看到这一点:
if (svc->seclabel) {
if (is_selinux_enabled() > 0 && setexeccon(svc->seclabel) < 0) {
ERROR("cannot setexeccon('%s'): %s\n", svc->seclabel, strerror(errno));
_exit(127);
}
}
在这里,子进程通过调用setexeccon()设置了其执行上下文,在exec()系统调用将控制权交给新的二进制映像之前。在安卓中,应用程序不是以这种方式生成的,并且在进程创建路径中不存在exec()系统调用;因此需要一个新的机制。
概述
在本章中,我们学习了如何通过类型转换以及通过seclabel语句来标记进程。我们还研究了 init 如何管理服务套接字,以及如何正确标记它们。然后,我们修正了串行控制台以及看门狗守护进程的进程上下文。
安卓中的应用程序在启动程序执行时,永远不会显式调用exec()。由于没有exec(),我们必须通过代码更改来标记应用程序。在下一章中,我们将介绍这是如何发生的,以及应用程序是如何被标记的。
第十章:将应用程序置于域中
在第三章,安卓古怪,我们介绍了 zygote,所有应用程序(在安卓中称为 APK)都源自 zygote,就像服务源自init进程一样。因此,它们需要被标记,正如我们在前一章所做的那样。回想一下,标记等同于将进程放置在相应标签的域中。应用程序也需要被标记。
注意
APK 是安卓上可安装应用程序包的文件扩展名和格式。它类似于桌面包格式,如 RPM(基于 Redhat)或 DEB(基于 Debian)。
在本章中,我们将学习:
-
正确标记应用程序的私有数据目录及其运行时上下文
-
进一步检查 zygote 及其安全方法
-
了解一个完成的
mac_permssions.xml文件是如何分配seinfo值的 -
创建一个新的自定义域
保护 zygote 的情况
安卓上具有提升权限和能力的应用程序是从 zygote 中产生的。一个例子就是系统服务器,这是一个由本地和非本地代码组成的大型进程,提供各种服务。系统服务器包含了活动管理器、包管理器、GPS 信息等。系统服务器也以高度敏感的system UID(1000)运行。此外,许多 OEM 将所谓的系统应用打包,这些是使用system UID 独立运行的应用程序。
zygote 还产生不需要提升权限的应用程序。所有第三方应用程序都属于这一类。第三方应用程序以自己的 UID 运行,与敏感的 UID(如system)分开。此外,应用程序会被放入各种 UID 中,如media、nfc等。OEM 倾向于定义额外的 UID。
需要注意的是,要进入像system这样的特殊 UID,你必须使用适当的密钥签名。安卓有四个主要密钥用于签名应用程序:media、platform、shared和testkey。它们位于build/target/product/security目录中,以及一个README文件。
根据README,密钥使用如下:
-
testkey:对于那些没有指定密钥的包的通用密钥。 -
platform:为核心平台部分包的测试密钥。 -
shared:用于在 home/contacts 进程中共享事物的测试密钥。 -
media:用于媒体/下载系统中部分的包的测试密钥。
为了为你的应用程序请求system UID,你必须使用platform密钥进行签名。在这些更加特权的环境中执行,需要拥有私钥。
如您所见,我们的应用程序在不同的权限级别和信任级别下执行。我们不能信任第三方应用程序,因为它们是由未知实体创建的,而我们可以信任使用我们的私钥签名的实体。然而,在 SELinux 之前,应用程序权限仍然受到与第一章中提到的Linux 访问控制相同的 DAC 权限限制。由于这些特性,zygote 成为了攻击的主要目标,同时也需要用 SELinux 来加固。
加固 zygote
既然我们已经确定了 zygote 的问题,下一步就是了解如何将应用程序放入适当的域中。我们需要 SELinux 策略或代码更改来将新进程放入一个域中。在第九章中,我们讨论了基于 init 服务的动态域转换,并在章节末尾提到了exec()系统调用在“应用程序标签限制”部分的重要性。这是动态域转换发生的触发器。如果路径中没有exec,我们将不得不依赖代码更改。但是,在这个安全模型中,我们还必须考虑签名密钥,而纯粹的 SELinux 策略语言无法表达进程签名的密钥。
我们不必探索整个 zygote,可以剖析以下引入应用程序标签到 Android 的补丁。此外,我们可以发现引入的设计如何满足尊重签名密钥、在 SELinux 和 zygote 的设计内工作的要求。
管理 zygote 套接字
在第三章中,我们了解到 zygote 通过监听套接字来等待请求启动新的应用程序。要检查的第一个补丁是android-review.googlesource.com/#/c/31066/。这个补丁修改了 Android 基础框架中的三个文件。第一个文件是Process.java中的startViaZygote()方法。这个方法是相对于构建字符串参数并将它们通过zygoteSendArgsAndGetResult()传递给 zygote 的其他方法的主要入口点。补丁引入了一个名为seinfo的新参数。稍后,我们将看到如何使用它。看起来这个补丁正在通过套接字传输这个新的seinfo参数。请注意,这段代码是在 zygote 进程外部调用的。
在这个补丁中要查看的下一个文件是ZygoteConnection.java。这段代码从上下文中执行。补丁首先在ZygoteConnection类中声明了一个字符串成员变量peerContext。在构造函数中,这个peerContext成员被设置为调用SELinux.getPeerContext(mSocket.getFileDescriptor())得到的值。
由于底层的LocalSocket mSocket是 Unix 域套接字,你可以获取连接客户端的凭据。在这种情况下,调用getPeerContext()获取客户端的安全上下文,或者更正式地说,是进程标签。初始化后,在方法runOnce()中进一步向下,我们看到它在调用applyUidSecurityPolicy和其他apply*SecurityPolicy例程时被使用。受保护的runOnce()方法被调用以从套接字读取一个启动命令和参数。最终,在apply*SecurityPolicy检查之后,它调用forkandSpecialize()。每个安全策略检查都已修改为在现有的 DAC 安全控制之上使用 SELinux。如果我们审查applyUidSecurityPolicy,我们会看到他们进行如下调用:
boolean allowed = SELinux.checkSELinuxAccess(peerSecurityContext, peerSecurityContext, "zygote", "specifyids");
这是一个用户空间利用强制访问控制的示例,这在对象管理器中是众所周知的。此外,在applyseInfoSecurityPolicy()方法中为神秘的seinfo字符串添加了一个安全检查。这里所有的 SELinux 安全检查都指定了目标类zygote。所以如果我们查看sepolicy access_vectors,我们会看到添加的类zygote。这是 Android 的一个自定义类,定义了所有在安全检查中检查的向量。
我们将从这个补丁中考虑的最后一个文件是ActivityManagerService.java。ActivityManager负责启动应用程序并管理它们的生命周期。它是Process.start API 的使用者,需要指定seinfo。这个补丁很简单,目前只是发送了null。稍后,我们将看到启用其使用的补丁。
下一个补丁,android-review.googlesource.com/#/c/31063/,在 Android Dalvik VM 的上下文中执行,并在 VM zygote 进程空间中编码。我们在ZygoteConnection中看到的forkAndSpecialize()最终进入了这个本地例程。它通过static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer)进入。这个例程负责创建成为应用程序的新进程。
它从 Java 开始,将清理代码移动到 C,并设置 C 风格字符串的niceName和seinfo值。最终,代码调用fork(),子进程开始执行操作,如执行setgid和setuid。uid和gid值通过Process.start方法指定给 zygote 连接。我们还看到一个对setSELinuxContext()的新调用。顺便说一下,这些事件的顺序在这里很重要。如果你太早设置新进程的 SELinux 上下文,那么进程在新上下文中需要额外的能力才能执行像setuid和setgid这样的操作。然而,这些权限最好留给zygote域,这样我们进入的应用程序域可以尽可能最小化。
接着,setSELinuxContext最终调用了selinux_android_setcontext()。注意,在这个提交之后,移除了HAVE_SELINUX条件编译宏,但在 4.3 版本发布之前。还要注意,selinux_android_setcontext()在libselinux中定义,所以我们的旅程将带我们到那里。在这里我们看到神秘的seinfo仍然在传递。
下一个要评估的补丁是android-review.googlesource.com/#/c/39601/。这个补丁实际上从 Java 层传递了一个更有意义的seinfo值。这个补丁没有设置为null,而是引入了从 XML 文件中解析的逻辑,并将其传递给Process.start方法。
这个补丁修改了两个主要组件:PackageManager和installd。PackageManager在system_server内部运行,执行应用程序安装。它维护系统中所有已安装包的状态。第二个组件,称为installd的服务,是一个非常特权级的 root 服务,在磁盘上创建所有应用程序的私有目录。这种方法不是给系统服务器,因此PackageManager提供创建这些目录的能力,只有installd拥有这些权限。即使系统服务器也无法读取您的私有数据目录中的数据,除非您将其设置为全局可读。
这个补丁比其他的要大,因此我们只检查与讨论直接相关的部分。我们将从查看PackageManagerService.java开始。这个类是 Android 的包管理器。在PackageManagerService()的构造函数中,我们看到了添加了mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();这一行。
根据命名,我们可以推测这个方法是在寻找某种策略配置文件,如果找到,返回 true,并设置mFoundPolicyFile成员变量。我们还看到一些对createDataDirs和mInstaller.*的调用。我们可以忽略这些,因为那些调用是发送给installd的。
下一个主要部分添加了以下内容:
if (mFoundPolicyFile) {
SELinuxMMAC.assignSeinfoValue(pkg);
}
重要的是要注意这段代码被添加到了scanPackageLI()方法中。每次需要扫描包以进行安装时,都会调用这个方法。因此,在高级别上,如果在服务启动期间找到某些策略文件,那么就会为包分配一个seinfo值。
下一个要查看的文件是ApplicationInfo.java,这是一个用于维护关于包的元信息的容器类。正如我们所见,seinfo值在这里指定以供存储。此外,还有一些通过 Android 特定的Parcel实现序列化和反序列化类的代码。
在这一点上,我们应该仔细查看SELinuxMMAC.java代码,以确认我们对正在发生的事情的理解。这个类开始时声明了两个策略文件的位置。
// Locations of potential install policy files.
private static final File[] INSTALL_POLICY_FILE = {
new File(Environment.getDataDirectory(), "system/mac_permissions.xml"),
new File(Environment.getRootDirectory(), "etc/security/mac_permissions.xml"),
null };
根据这个,策略文件可以存在于两个位置:/data/system/mac_permissions.xml和/system/etc/security/mac_permissions.xml。最终,我们看到PackageManagerService初始化时对类中定义的方法readInstallPolicy()的调用,最终简化为以下调用:
private static boolean readInstallPolicy(File[] policyFiles) {
FileReader policyFile = null;
int i = 0;
while (policyFile == null && policyFiles != null && policyFiles[i] != null) {
try {
policyFile = new FileReader(policyFiles[i]);
break;
} catch (FileNotFoundException e) {
Slog.d(TAG,"Couldn't find install policy " + policyFiles[i].getPath());
}
i++;
}
...
当policyFiles设置为INSTALL_POLICY_FILE时,这段代码使用数组在指定位置查找文件。它是基于优先级的,/data位置优先于/system。这个方法中的其余代码看起来像解析逻辑,并填充了在类声明中定义的两个哈希表:
// Signature seinfo values read from policy.
private static final HashMap<Signature, String> sSigSeinfo =
new HashMap<Signature, String>();
// Package name seinfo values read from policy.
private static final HashMap<String, String> sPackageSeinfo =
new HashMap<String, String>();
sSigSeinfo将Signatures(或签名密钥)映射到seinfo字符串。另一个映射sPackageSeinfo将包名映射到字符串。
在这一点上,我们可以从mac_permissions.xml文件中读取一些格式化的 XML,并从签名密钥到seinfo以及包名到seinfo创建内部映射。
PackageManagerService 类调用这个类的另一个方法来自于 void assignSeinfoValue(PackageParser.Package pkg)。
让我们调查一下这个方法能做什么。它首先检查应用程序是否为系统 UID 或系统安装的应用程序。换句话说,它检查应用程序是否为第三方应用程序:
if (((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) ||
((pkg.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) {
这段代码后来被谷歌删除,最初是合并的要求。然而,我们可以继续进行评估。代码遍历包中的所有签名,并与哈希表进行对比。如果它使用该映射中的某个内容签名,它就会使用关联的seinfo值。另一种情况是它通过包名匹配。在任一情况下,包的ApplictionInfo类的seinfo值都会更新以反映这一点,并供installd和 zygote 应用程序生成在其他地方使用:
// We just want one of the signatures to match.
for (Signature s : pkg.mSignatures) {
if (s == null)
continue;
if (sSigSeinfo.containsKey(s)) {
String seinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(s);
if (DEBUG_POLICY_INSTALL)
Slog.i(TAG, "package (" + pkg.packageName +
") labeled with seinfo=" + seinfo);
return;
}
}
// Check for seinfo labeled by package.
if (sPackageSeinfo.containsKey(pkg.packageName)) {
String seinfo = pkg.applicationInfo.seinfo = sPackageSeinfo.get(pkg.packageName);
if (DEBUG_POLICY_INSTALL)
Slog.i(TAG, "package (" + pkg.packageName +
") labeled with seinfo=" + seinfo);
return;
}
}
}
顺便一提,主线 AOSP(Android Open Source Project)中合并的内容与 NSA 在 Bitbucket 仓库中维护的内容略有不同。NSA 在这些策略文件中有额外的控制,可能导致应用程序安装被终止。可以说,谷歌和 NSA 在这个问题上“分道扬镳”。在 NSA 版本的SELinuxMMAC.java中,你可以指定匹配特定签名或包名的应用程序被允许拥有某些 Android 级别的权限集。例如,你可以阻止所有请求CAMERA权限的应用程序安装,或者阻止使用某些密钥签名的应用程序。这也突显了在大型代码库中找到补丁并快速了解项目如何发展的重要性,这往往可能显得有些困难。
在这个补丁中,我们需要考虑的最后一个文件是ActivityManagerService.java。这个补丁用app.info.seinfo替换了 null。经过所有这些工作和管道铺设,我们最终有了完全解析的神秘的seinfo值,与每个应用程序包关联,并传递给 zygote,在selinux_android_setcontext()中使用。
现在让我们回顾一下,我们希望在标记应用程序时实现的一些属性。其中之一是以某种方式将安全上下文与应用程序签名密钥耦合,这正是 seinfo 的主要好处。这是一个高度敏感且受信任的与签名密钥相关联的字符串值。字符串的实际内容是任意的,在 mac_permissions.xml 中指定,这是我们冒险旅程的下一站。
mac_permissions.xml 文件
mac_permissions.xml 文件的名字非常容易混淆。展开来看,名字是 MAC 权限。然而,其主要主流功能是将签名密钥映射到一个 seinfo 字符串。其次,它还可以用于配置非主流的安装时权限检查功能,称为安装时 MMAC。MMAC 控制是国家安全局(NSA)在中层实现强制访问控制工作的一部分。MMAC 代表“中间件强制访问控制”。谷歌没有合并任何 MMAC 功能。但是,由于我们使用了 NSA 的 Bitbucket 仓库,我们的代码库包含了这些功能。
mac_permissions.xml 是一个 XML 文件,应遵循以下规则,其中斜体部分仅在 NSA 分支上支持:
-
签名是一个十六进制编码的 X.509 证书,每个签名者标签都需要。
-
<signer signature="" >元素可能有多个子元素:-
allow-permission:它生成一组最大允许的权限集合(白名单)。 -
deny-permission:它生成一个要拒绝的权限黑名单。 -
allow-all:这是一个通配符标签,将允许所有请求的权限。 -
package:这是一个复杂的标签,定义了一个特定包名的签名保护的允许、拒绝和通配符子元素。
-
-
零个或多个全局
<package name="">标签是被允许的。这些标签允许在特定包名的外部设置策略,不受任何签名限制。 -
允许使用
<default>标签,其中可以包含未使用先前列出的证书签名的所有应用的安装策略,且没有每个包的全局策略。 -
任何级别的未知标签将被跳过。
-
零个或多个签名者标签是被允许的。
-
每个签名者标签允许零个或多个包标签。
-
<package name="">标签可能不包含另一个<package name="">标签。如果发现,则跳过。 -
当一个标签出现多个子元素时,以下逻辑用于最终确定执行类型:
-
如果至少找到一个 deny-permission 标签,则使用黑名单。
-
如果没有黑名单,则使用白名单,并且至少找到一个 allow-permission 标签。
-
如果没有黑名单和白名单,且至少存在一个 allow-all 标签,则使用通配符(接受所有权限)策略。
-
如果找到
<package name="">子元素,则根据之前的逻辑使用该子元素的策略,并覆盖任何签名全局策略类型。 -
为了使策略段落得到执行,至少需要满足前述情况之一。这意味着,不接受空签名人、默认或软件包标签。
-
-
每个
signer/default/package(全局或附加到签名人)标签允许包含一个<seinfo value=""/>标签。这个标签表示每个应用程序可以在设置 SELinux 安全上下文时使用的附加信息,在最终的处理过程中。 -
在大多数情况下,并不严格执行任何 XML 段落的规则。这主要适用于允许的重复标签。如果已经存在一个标签,则原始标签将被替换。
-
同时也没有检查权限名称的有效性。尽管预期是有效的安卓权限,但并未阻止未知权限。
-
以下是执行决策:
-
用于签署应用程序的所有签名都将根据签名人标签检查策略。然而,只有一个签名策略需要通过。
-
如果所有的签名策略都未通过,或者没有任何匹配项,那么将寻求全局软件包策略。如果找到,此策略将调解安装。
-
如果需要,最后将咨询默认标签。
-
本地软件包策略总是覆盖任何父策略。
-
如果没有任何情况适用,那么应用程序将被拒绝。
-
以下示例忽略了安装 MMAC 支持,并专注于seinfo映射的主要用途。以下是将所有使用平台密钥签名的项映射到seinfo值平台的段落映射示例:
<!-- Platform dev key in AOSP -->
<signer signature="@PLATFORM" >
<seinfo value="platform" />
</signer>
下面是一个将使用发布密钥签名的所有内容映射到发布域的示例,但浏览器除外。浏览器被分配了一个seinfo值为browser,如下所示:
<!-- release dev key in AOSP -->
<signer signature="@RELEASE" >
<seinfo value="release" />
<package name="com.android.browser" >
<seinfo value="browser" />
</package>
</signer>
...
任何具有未知密钥的内容,都会被映射到默认标签:
...
<!-- All other keys -->
<default>
<seinfo value="default" />
</default>
签名标签值得关注,@PLATFORM和@RELEASE是在构建期间使用的特殊处理字符串。另一个映射文件将这些映射到实际的关键值。处理过的文件被放置在设备上,所有密钥引用都被替换为十六进制编码的公钥,而不是这些占位符。它还删除了所有的空白和注释,以减少大小。让我们通过从设备中提取构建的文件并格式化它来查看。
$ adb pull /system/etc/security/mac_permissions.xml
$ xmllint --format mac_permissions.xml
现在,滚动到格式化输出的顶部,你应该看到以下内容:
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- AUTOGENERATED FILE DO NOT MODIFY -->
<policy>
<signer signature="308204ae30820396a003020102020900d2cba57296ebebe2300d06092a864886f70d0101050500308196310b300906035504061302555331133...
dec513c8443956b7b0182bcf1f1d">
<allow-all/>
<seinfo value="platform"/>
</signer>
请注意,signature=@PLATFORM现在是一个十六进制字符串。这个十六进制字符串是一个有效的 X509 证书。
keys.conf
实际上,从mac_permissions.xml中的signature=@PLATFORM到keys.conf的映射才是魔法所在。这个配置文件允许你将一个 pem 编码的 x509 映射到一个任意的字符串。约定是使用@开始,但这不是强制性的。该文件的格式基于 Python 配置解析器,并包含部分。部分名称是你在mac_permissions.xml文件中希望用密钥值替换的标签。平台示例是:
[@PLATFORM]
ALL : $DEFAULT_SYSTEM_DEV_CERTIFICATE/platform.x509.pem
在 Android 中,构建时你可以有三个级别的构建:engineering,userdebug 或 user。在 keys.conf 文件中,你可以将一个密钥与 ALL 区段属性关联以用于所有级别,或者你可以为每个级别分配不同的密钥。这对于使用非常特殊的发布密钥构建发布或用户版本很有帮助。我们在 @RELEASE 区段看到了一个这样的例子:
[@RELEASE]
ENG : $DEFAULT_SYSTEM_DEV_CERTIFICATE/testkey.x509.pem
USER : $DEFAULT_SYSTEM_DEV_CERTIFICATE/testkey.x509.pem
USERDEBUG : $DEFAULT_SYSTEM_DEV_CERTIFICATE/testkey.x509.pem
该文件还允许通过传统的 $ 特殊字符使用环境变量。pem 文件的默认位置是 build/target/product/security。然而,你绝不能将这些密钥用于用户发布版本。这些密钥是 AOSP 测试密钥,是公开的!这样做的话,任何人都可以使用系统密钥来签署他们的应用并获得系统权限。keys.conf 文件只在构建过程中使用,并且不在系统上。
seapp_contexts
到目前为止,我们已经了解了完成的 mac_permssions.xml 文件如何分配 seinfo 值。现在我们应该探讨标记实际上是如何配置并使用这个值的。应用程序的标记是在另一个配置文件 seapp_contexts 中管理的。与 mac_permissions.xml 一样,它被加载到设备上。然而,默认位置是 /seapp_contexts。seapp_contexts 的格式是每行遵循 key=value 对映射,以下规则:
-
输入选择器:
-
isSystemServer(布尔值) -
user(字符串) -
seinfo(字符串) -
name(字符串) -
sebool(字符串)
-
-
输入选择器规则:
-
isSystemServer=true只能使用一次。 -
未指定的
isSystemServer默认为 false。 -
未指定的字符串选择器将匹配任何值。
-
以
*结尾的用户字符串选择器将执行前缀匹配。 -
user=_app将匹配任何常规的应用 UID。 -
user=_isolated将匹配任何隔离服务 UID。 -
一个条目中所有指定的输入选择器必须匹配(逻辑与)。
-
匹配不区分大小写。
-
优先级规则如下:
-
isSystemServer=true优先于isSystemServer=false -
指定的
user=字符串优先于未指定的user=字符串。 -
修复了
user=字符串,使其优先于以*结尾的user=前缀。 -
较长的
user=前缀优先于较短的前缀。 -
指定的
seinfo=字符串优先于未指定的seinfo=字符串。 -
指定的
name=字符串优先于未指定的name=字符串。 -
指定的
sebool=字符串优先于未指定的sebool=字符串。
-
-
-
输出:
-
domain(字符串):它指定了应用程序的进程域。 -
type(字符串):它指定了应用程序私有数据目录的磁盘标签。 -
levelFrom(字符串;值为none,all,app或user):它给出了 MLS 指示符。 -
level(字符串):它显示硬编码的 MLS 值。
-
-
输出规则:
-
只有指定了
domain=的条目会被用于应用进程标记。 -
只有指定了
type=的条目才会用于应用目录标记。 -
levelFrom=user只支持_app或_isolatedUIDs。 -
levelFrom=app或levelFrom=all只支持_appUIDs。 -
level可用于为任何 UID 指定固定的级别。
-
在应用程序生成期间,selinux_android_setcontext() 和 selinux_android_setfilecon2() 函数会使用此文件来查找适当的应用程序域或文件系统上下文。这些函数的源代码可以在 external/libselinux/src/android.c 中找到,推荐阅读。例如,以下条目将所有具有 UID bluetooth 的应用程序放在 bluetooth 域中,数据目录标签为 bluetooth_data_file:
user=bluetooth domain=bluetooth type=bluetooth_data_file
此示例将所有第三方或“默认”应用程序放入 untrusted_app 的进程域和 app_data_file 的数据目录中。它还使用基于 MLS 的 levelFrom=app 类别以帮助提供额外的分离。
user=_app domain=untrusted_app type=app_data_file levelFrom=app
目前,此功能是实验性的,因为它破坏了一些已知的应用程序兼容性问题。在撰写本文时,这成为了谷歌和美国国家安全局工程师的热门关注点。由于它是实验性的,让我们验证其功能,然后禁用它。
我们还没有安装任何第三方应用程序,因此我们需要安装一个以便进行实验。FDroid 是一个寻找第三方应用程序的好地方,因此我们可以从那里下载并安装一些内容。我们可以使用位于 f-droid.org/repository/browse/?fdid=org.zeroxlab.zeroxbenchmark 的 0xbenchmark 应用程序,APK 下载地址为 f-droid.org/repo/org.zeroxlab.zeroxbenchmark_9.apk,如下所示:
$ wget https://f-droid.org/repo/org.zeroxlab.zeroxbenchmark_9.apk
$ adb install org.zeroxlab.zeroxbenchmark_9.apk
567 KB/s (1193455 bytes in 2.052s)
pkg: /data/local/tmp/org.zeroxlab.zeroxbenchmark_9.apk
Success
提示
检查 logcat 中的安装时 seinfo 值:
$ adb logcat | grep SELinux
I/SELinuxMMAC( 2557): package (org.zeroxlab.zeroxbenchmark) installed with seinfo=default
从 UDOO 中启动 0xbenchmark APK。我们应在 ps 中看到它正在运行,并带有其标签:
$ adb shell ps -Z | grep untrusted
u:r:untrusted_app:s0:c40,c256 u0_a40 17890 2285 org.zeroxlab.zeroxbenchmark
注意上下文字符串中的级别部分 s0:c40,c256。这些类别是在 seapp_contexts 中使用 level=app 设置创建的。
要禁用它,我们可以简单地从 seapp_contexts 中的条目中删除 level 的键值对,或者我们可以利用 sebool 条件赋值。让我们使用布尔值方法。修改 sepolicy seapp_contexts 文件,以便修改现有的 untrusted_app 条目,并添加一个新条目。将 user=_app domain=untrusted_app type=app_data_file 更改为 user=_app sebool=app_level domain=untrusted_app type=app_data_file levelFrom=app。
使用 mmm external/sepolicy 进行构建,如下所示:
Error:
out/host/linux-x86/bin/checkseapp -p out/target/product/udoo/obj/ETC/sepolicy_intermediates/sepolicy -o out/target/product/udoo/obj/ETC/seapp_contexts_intermediates/seapp_contexts out/target/product/udoo/obj/ETC/seapp_contexts_intermediates/seapp_contexts.tmp
Error: Could not find selinux boolean "app_level" on line: 42 in file: out/target/product/udoo/obj/ETC/seapp_contexts_intermediates/seapp_contexts
Error: Could not validate
好吧,在 seapp_contexts 的第 42 行有一个构建错误,抱怨找不到 selinux 布尔值。让我们尝试通过声明布尔值来纠正问题。在 app.te 中添加:bool app_level false;。现在将新构建的 seapp_contexts 和 sepolicy 文件推送到设备上,并触发动态重载:
$ adb push $OUT/root/sepolicy /data/security/current/
$ adb push $OUT/root/seapp_contexts /data/security/current/
$ adb shell setprop selinux.reload_policy 1
我们可以通过以下方式验证布尔值是否存在:
$ adb shell getsebool -a | grep app_level
app_level --> off
由于设计限制,我们需要卸载并重新安装应用程序:
$ adb uninstall org.zeroxlab.zeroxbenchmark
在启动进程后,重新安装并检查进程的上下文内容:
$ adb shell ps -Z | grep untrusted
u:r:untrusted_app:s0:c40,c256 u0_a40 17890 2285 org.zeroxlab.zeroxbenchmark
很好!它失败了。在经过一些调试后,我们发现问题的根源是 /data/security 路径不是全局可搜索的,导致 DAC 权限失败。
注意
我们通过在 android.c 中打印结果和错误代码找到这个,我们看到在检查 fp = fopen(seapp_contexts_file[i++], "r") 的结果时,selinux_android_seapp_context_reload() 中的 seapp_contexts_file[] 数组(按优先级排序的文件)上的 fopen,并使用 selinux_log() 将数据转储到 logcat。
$ adb shell ls -la /data | grep security
drwx------ system system 1970-01-04 00:22 security
请记住,set selinux 上下文发生在 UID 切换之后,因此我们需要使其对其他人可搜索。我们可以通过更改 device/fsl/imx6/etc/init.rc 中的 UDOO init.rc 脚本的权限来修复权限。具体来说,将行 mkdir /data/security 0700 system system 更改为 mkdir /data/security 0711 system system。构建并刷新 bootimage,然后再次尝试上下文测试。
$ adb uninstall org.zeroxlab.zeroxbenchmark
$ adb install ~/org.zeroxlab.zeroxbenchmark_9.apk
<launch apk>
$ adb shell ps -Z | grep org.zeroxlab.zeroxbenchmark
u:r:untrusted_app:s0 u0_a40 3324 2285 org.zeroxlab.zeroxbenchmark
迄今为止,我们已经演示了如何使用 seapp_contexts 上的 sebool 选项来禁用 MLS 类别。需要注意的是,在更改 APK 的类别或类型时,需要卸载并重新安装 APK,否则在大多数情况下,由于没有访问权限,该进程会与其数据目录脱离。
接下来,让我们拿这个 APK,卸载它,并通过更改其 seinfo 字符串为其分配一个唯一的域。通常,你使用这个特性将一组用共同密钥签名的应用程序放入自定义域以执行自定义操作。例如,如果你是 OEM,你可能需要允许未用 OEM 控制的密钥签名的第三方应用程序拥有自定义权限。首先卸载 APK:
$ adb uninstall org.zeroxlab.zeroxbenchmark
通过添加以下内容在 mac_permissions.xml 中创建一个新条目:
<signer signature="@BENCHMARK" >
<allow-all />
<seinfo value="benchmark" />
</signer>
现在,我们需要为 keys.conf 获取一个 pem 文件。因此,解压 APK 并提取公共证书:
$ mkdir tmp
$ cd tmp
$ unzip ~/org.zeroxlab.zeroxbenchmark_9.apk
$ cd META-INF/
$ $ openssl pkcs7 -inform DER -in *.RSA -out CERT.pem -outform PEM -print_certs
我们需要从生成的 CERT.pem 文件中删除任何多余的内容。如果你打开它,你应该会在顶部看到这些行:
subject=/C=UK/ST=ORG/L=ORG/O=fdroid.org/OU=FDroid/CN=FDroid
issuer=/C=UK/ST=ORG/L=ORG/O=fdroid.org/OU=FDroid/CN=FDroid
-----BEGIN CERTIFICATE-----
MIIDPDCCAiSgAwIBAgIEUVJuojANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJV
SzEMMAoGA1UECBMDT1JHMQwwCgYDVQQHEwNPUkcxEzARBgNVBAoTCmZkcm9pZC5v
...
它们需要被删除,因此只删除主题和发行者行。文件应以 BEGIN CERTIFICATE 开头,以 END CERTIFICATE 剪切线结尾。
让我们将这个移动到工作区中名为 certs 的新文件夹,并将证书移动到这个文件夹,并赋予其一个更好的名字:
$ mkdir UDOO_SOURCE_ROOT/certs
$ mv CERT.pem UDOO_SOURCE_ROOT/certs/benchmark.x509.pem
我们可以通过添加以下内容来设置 keys.conf:
[@BENCHMARK]
ALL : certs/benchmark.x509.pem
别忘了更新 seapp_contexts 以使用新的映射:
user=_app seinfo=benchmark domain=benchmark_app type=benchmark_app_data_file
现在声明要使用的新类型。域类型应在 sepolicy 中名为 benchmark_app.te 的文件中声明:
# Declare the new type
type benchmark_app, domain;
# This macro adds it to the untrusted app domain set and gives it some allow rules
# for basic functionality as well as object access to the type in argument 2.
untrustedapp_domain(benchmark_app, benchmark_app_data_file)
还在 file.te 中添加 benchmark_app_data_file:
type benchmark_app_data_file, file_type, data_file_type, app_public_data_type;
提示
你可能并不总是想要这些所有属性,尤其是如果你在做一些安全关键的事情。确保你查看每个属性和宏以及其用法。你不想因为过于宽松的域而打开一个未预期的大门。
重新构建策略,推送所需的部分,并触发重新加载。
$ mmm external/sepolicy/
$ adb push $OUT/system/etc/security/mac_permissions.xml /data/security/current/
$ adb push $OUT/root/sepolicy /data/security/current/
$ adb push $OUT/root/seapp_contexts /data/security/current/
$ adb shell setprop selinux.reload_policy 1
启动一个 shell 并使用 grep logcat 查看基准测试 APK 安装时的 seinfo 值。然后安装该 APK:
$ adb install ~/org.zeroxlab.zeroxbenchmark_9.apk
$ adb logcat | grep -i SELinux
在logcat输出中,你应该看到:
I/SELinuxMMAC( 2564): package (org.zeroxlab.zeroxbenchmark) installed with seinfo=default
它应该是seinfo=benchmark!可能发生了什么?
问题出在frameworks/base/services/java/com/android/server/pm/SELinuxMMAC.java中。它查看/data/security/mac_permissions.xml;所以我们可以直接推送mac_permissions.xml。这是动态策略重载中的另一个错误,与加载过程中历史更改有关。罪魁祸首在frameworks/base/services/java/com/android/server/pm/SELinuxMMAC.java文件中:
private static final File[] INSTALL_POLICY_FILE = {
new File(Environment.getDataDirectory(), "security/mac_permissions.xml"),
new File(Environment.getRootDirectory(), "etc/security/mac_permissions.xml"),
null};
为了解决这个问题,重新挂载system并将其推送到默认位置。
$ adb remount
$ adb push $OUT/system/etc/security/mac_permissions.xml /system/etc/security/
这不需要setprop selinux.reload_policy 1。卸载并重新安装基准测试 APK,并检查日志:
I/SELinuxMMAC( 2564): package (org.zeroxlab.zeroxbenchmark) installed with seinfo=default
好的,它仍然没有工作。当我们检查代码时,发现mac_permissions.xml文件在包管理器服务启动时被加载。没有重启的情况下,这个文件不会被重新加载,所以让我们卸载基准测试 APK,并重启 UDOО。启动后,启用adb,触发动态重载,安装 APK,并检查logcat。它应该包含:
I/SELinuxMMAC( 2559): package (org.zeroxlab.zeroxbenchmark) installed with seinfo=benchmark
现在让我们通过启动 APK,检查ps,并验证其应用程序私有目录来验证进程域:
<launch apk>
$ adb shell ps -Z | grep org.zeroxlab.zeroxbenchmark
u:r:benchmark_app:s0 u0_a45 3493 2285 org.zeroxlab.zeroxbenchmark
$ adb shell ls -Z /data/data | grep org.zeroxlab.zeroxbenchmark
drwxr-x--x u0_a45 u0_a45 u:object_r:benchmark_app_data_file:s0 org.zeroxlab.zeroxbenchmark
这一次,所有类型都检查通过了。我们成功创建了一个新的自定义域。
总结
在本章中,我们研究了如何通过配置文件和 SELinux 策略正确标记应用程序的私有数据目录及其运行时上下文。我们还探讨了使这一切正常工作的子系统及代码,以及在此过程中可能出错的一些基本问题。在下一章中,我们将通过查看 SE for Android 构建系统,详细介绍策略和配置文件是如何构建的。
第十一章:标签属性
在本章中,我们将介绍如何通过property_contexts文件标记属性。
属性是我们在第三章,Android Is Weird中学到的 Android 的独特特性。我们希望对这些属性进行标签化,以限制设置属性仅限于应设置它们的域,防止经典的 DAC 根攻击无意中更改其值。在本章中,我们将学习:
-
创建新属性
-
标签新属性和现有属性
-
解释和处理属性拒绝
-
列举特殊的 Android 属性及其行为
通过 property_contexts 标签化
所有属性都使用property_contexts文件进行标记,其语法类似于file_contexts。但是,它不是在文件路径上工作,而是在属性名称或属性键上工作(Android 中的属性是键值存储)。属性键本身通常用句点(.)分隔。这类似于file_contexts,只不过斜杠(/)变成了句点。一些示例属性及其在property_contexts中的条目可能如下所示:
ctl.ril-daemon u:object_r:ctl_rildaemon_prop:s0
ctl. u:object_r:ctl_default_prop:s0
注意到所有ctl.属性都被标记为ctl_default_prop类型,但ctl.ril-daemon具有不同的类型标签ctl_rildaemon_prop。这代表了你如何可以从通用开始,并根据需要移动到更具体的值/类型。
此外,任何未明确标记的属性默认通过property_contexts中的“匹配所有”表达式设置为default_prop:
# default property context
* u:object_r:default_prop:s0
属性上的权限
可以查看系统上的当前属性,并使用命令行工具getprop和setprop创建新属性,如下代码片段所示:
root@udoo:/ # getprop
...
[sys.usb.state]: [mtp,adb]
[wifi.interface]: [wlan0]
[wlan.driver.status]: [unloaded]
回顾第三章,Android Is Weird,我们知道属性被映射到每个人的地址空间,因此任何人都可以读取它们。然而,并不是每个人都可以设置(写入)它们。属性的 DAC 权限模型硬编码在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 },
...
{ "persist.service.bdroid.", AID_BLUETOOTH, 0 },
{ "selinux." , AID_SYSTEM, 0 },
{ "persist.audio.device", AID_SYSTEM, 0 },
{ NULL, 0, 0 }
如果要在property_perms数组中设置与任何属性前缀匹配的属性,你必须具有 UID 或 GID。例如,为了设置selinux.属性,你必须具有 UID AID_SYSTEM(uid 1000)或 root 权限。是的,root 总是可以设置属性,这是将 SELinux 应用于 Android 属性的关键优势。不幸的是,目前没有方法使用getprop -Z列出属性及其标签,就像使用ls -Z和文件一样。
重新标记现有属性
为了更熟悉标签属性,让我们重新标记wifi.interface属性。首先,让我们通过引发拒绝并查看拒绝日志来验证其上下文,如下代码所示:
root@udoo:/ # setprop wifi.interface wlan0
avc: denied { set } for property=wifi.interface scontext=u:r:shell:s0 tcontext=u:object_r:default_prop:s0 tclass=property_service
当我们通过 UDOOUART 控制台执行setprop命令时,发生了一件有趣的事情。打印出了 AVC 拒绝记录。这是因为串行控制台包括了使用printk()从内核打印的任何内容。这里发生的情况是,如第三章 安卓古怪 中详细控制的init进程,向内核日志写入一条消息。当我们执行setprop命令时,这条日志消息会显示出来。如果你通过adb shell运行,你会在串行控制台上看到这个消息,但在adb控制台上看不到。然而,要做到这一点,你必须重新启动你的系统,因为 SELinux 在宽容模式下只打印一次拒绝记录。
使用adb shell的命令如下:
$ adb shell setprop wifi.interface wlan0
使用串行控制台的命令如下:
root@udoo:/ # avc: denied {set} for property=wifi.interface scontext=u:r:shell:s0 tcontext=u:object_r:default_prop
usb 2-1.3: device descriptor read/64, error -110
从拒绝输出中,我们可以看到属性类型标签是default_prop。让我们将其更改为wifi_prop。
我们首先在sepolicy目录中编辑property.te文件,通过添加以下行来声明新类型,以便对这些属性进行标签化:
type wifi_prop, property_type;
类型声明后,下一步是通过对property_contexts进行修改来应用标签,添加以下内容:
# wifi properties
wifi. u:object_r:wifi_prop:s0
按如下方式构建策略:
$ mmm external/sepolicy
推送新的property_contexts文件:
$ adb push out/target/product/udoo/root/property_contexts /data/security/current
51 KB/s (2261 bytes in 0.042s)
触发动态重载:
$ adb shell setprop selinux.reload_policy 1
# setprop wifi.interface wlan0
avc: denied { set } for property=wifi.interface scontext=u:r:shell:s0 tcontext=u:object_r:default_prop:s0 tclass=property_service
好吧,那不起作用!property_contexts文件必须在/data/security中,而不是/data/security/current。
要发现这一点,请搜索libselinux/src/android.c文件。这个文件中没有提到property_contexts;因此,它必须在其他地方提到。这引导我们搜索system/core,其中包含属性服务对该文件的引用。在init.c中的匹配代码从优先位置加载文件。
$ grep -rn property_contexts *
init/init.c:745: { SELABEL_OPT_PATH, "/data/security/property_contexts" },
init/init.c:746: { SELABEL_OPT_PATH, "/property_contexts" },
init/init.c:760: ERROR("SELinux: Could not load property_contexts: %s\n",
让我们将property_contexts文件推送到正确的位置,并再次尝试:
$ adb push out/target/product/udoo/root/property_contexts /data/security
51 KB/s (2261 bytes in 0.042s)
$ adb shell setprop selinux.reload_policy 1
root@udoo:/ # setprop wifi.interface wlan0
avc: received policyload notice (seqno=3)
init: sys_prop: permission denied uid:0 name:wifi.interface
哇!又失败了。这个练习是为了指出如果你忘记做一些事情,这会有多么棘手。没有显示任何有用的拒绝信息,只有一个被拒绝的指示。这是因为包含wifi_prop类型声明的sepolicy文件从未被推送。这导致system/core/init/property_service.c中的check_mac_perms()在selinux_check_access()函数中失败,因为它找不到要计算访问检查的类型,尽管在property_contexts中的查找成功了。没有来自此的详细错误日志。
我们可以通过确保也推送sepolicy来更正这个问题:
$ adb push out/target/product/udoo/root/sepolicy /data/security/current/
550 KB/s (87385 bytes in 0.154s)
$ adb shell setprop selinux.reload_policy 1
root@udoo:/ # setprop wifi.interface wlan0
avc: received policyload notice (seqno=4)
avc: denied { set } for property=wifi.interface scontext=u:r:shell:s0 tcontext=u:object_r:wifi_prop:s0 tclass=property_service
现在我们看到了预期的拒绝消息,但目标的标签(或属性)是u:object_r:wifi_prop:s0。
现在目标属性已标记,你可以允许访问它。请注意,这是一个虚构的例子,在现实世界中,你可能不希望允许从 shell 访问大多数属性。策略应与你的安全目标和最小权限属性保持一致。
我们可以在shell.te中以下面的方式添加一个allow规则:
# wifi prop
allow shelldomain wifi_prop:property_service set;
编译策略,将其推送到手机上,并触发动态重新加载:
$ mmm external/sepolicy/
$ adb push out/target/product/udoo/root/sepolicy /data/security/current/
547 KB/s (87397 bytes in 0.155s)
$ adb shell setprop selinux.reload_policy 1
现在尝试设置wifi.interface属性,并注意没有拒绝。
root@udoo:/ # setprop wifi.interface wlan0
avc: received policyload notice (seqno=5)
创建和标记新属性
所有属性都是通过使用setprop调用或在 C (bionic/libc/include/sys/system_properties.h) 和 Java (android.os.SystemProperties)中执行等效功能的函数调用在系统中动态创建的。请注意,System.getProperty()和System.setProperty() Java 调用是针对应用程序私有属性存储的,并且没有与全局属性存储绑定。
对于 DAC 控制,您需要按照之前提到的修改property_perms[],以便非 root 用户可以创建或设置属性。请注意,除非受到 SELinux 策略的限制,否则 root 用户始终可以set和create。
假设我们想要创建udoo.name和udoo.owner属性;我们只希望 root 用户和 shell 域访问它们。我们可以这样创建它们:
root@udoo:/ # setprop udoo.name udoo
avc: denied { set } for property=udoo.name scontext=u:r:shell:s0 tcontext=u:object_r:default_prop:s0 tclass=property_service
root@udoo:/ # setprop udoo.owner William
注意否认显示这些为default_prop类型。要纠正这一点,我们会像前一部分重新标记现有属性中所做的那样重新标记这些属性。
特殊属性
在 Android 中,有些特殊属性具有不同的行为。我们在接下来的部分列举了属性名称及其含义。
控制属性
以ctl开头的属性被保留为控制属性,用于通过init控制服务:
-
start:启动服务(setprop ctl.start <服务名>) -
stop:停止服务(setprop ctl.stop <服务名>) -
restart:重启服务(setprop ctl.restart <服务名>)
持久属性
任何以persist为前缀的属性在重启后会保留并恢复。数据被保存到/data/property目录下,文件名与属性相同。
root@udoo:/ # ls /data/property/
persist.gps.oacmode
persist.service.bdroid.bdaddr
persist.sys.profiler_ms
persist.sys.usb.config
SELinux 属性
selinux.reload_policy属性是特殊的。正如我们所见,它的用途是触发动态重新加载事件。
总结
在本章中,我们探讨了如何创建和标记新属性和现有属性,以及在这样做时出现的一些异常情况。我们还检查了property_service.c中属性的硬编码 DAC 权限表,以及像ctl.系列这样的硬编码特殊属性。在下一章中,我们将了解工具链如何构建和创建我们一直在使用的所有策略文件。