探索安卓安全改进(一)
原文:
zh.annas-archive.org/md5/1E165BD192C4C9DE01CC57BFDF8623B4译者:飞龙
前言
本书介绍了针对 Android 开源项目的安全增强(SE),并引导您通过使用 SE for Android 保护新嵌入式系统的过程。据我们所知,本书是第一个完整记录这一过程的来源,以便学生、DIY 爱好者以及工程师可以创建由 SE for Android 保护的定制系统。通常,只有原始设备制造商(OEM)会这样做,而且通常目标设备是手机或平板电脑。我们真心希望我们的书能改变这一点,让更广泛的受众参与开发,使他们能够使用和理解这些现代安全工具。
我们非常努力地确保这本书不仅仅是一本按部就班的技术书籍。特别是,我们选择了一个模式,指导您通过失败走向成功。您首先会对如何获得和执行安全性有适当的理论了解。然后,我们将介绍一个从未以这种方式保护过的系统(甚至在我们编写这本书之前,我们也没有这样做过)。接下来,我们会引导您完成我们所有的智能猜测工作,接受因新发现的特性而导致的意外失败,并最终执行我们的自定义安全策略。这需要您学会解决诸如 SELinux、SE for Android 和 Google Android 等主要开源项目之间的差异,这些项目各有独立的目标和部署计划。这为您保护其他设备做好准备,这个过程总是不同的,但希望现在能更容易实现。
本书涵盖的内容
第一章,Linux 访问控制,讨论了自主访问控制(DAC)的基础知识,一些 Android 漏洞如何利用 DAC 问题,并展示了需要更强大解决方案的需求。
第二章,强制访问控制和 SELinux,检查强制访问控制(MAC)及其在 SELinux 中的体现。这一章还探讨了具体的策略来控制 SELinux 对象交互。
第三章,Android 的奇妙之处,介绍了 Android 安全模型,并调查了 binder、zygote 和属性服务。
第四章,在 UDOO 上的安装,逐步讲解从源代码构建和部署 Android 到 UDOO 嵌入式主板,并开启 SELinux 支持。
第五章,启动系统,从策略加载的角度跟随启动过程,并在 UDOO 上纠正问题,使 SELinux 达到可用状态。
第六章,探索 SELinuxFS,检查 SELinuxFS 文件系统以及它是如何为高级别习惯用语提供内核到用户空间的接口。
第七章,利用审计日志,研究了审计子系统,揭示了如何解释 SELinux 审计日志以利于策略编写。
第八章,将上下文应用于文件,教你如何给文件系统及其对象分配标签和上下文,并展示更改它们的技术,包括动态类型转换。
第九章,向域添加服务,强调进程标签,尤其是由 init 运行和管理的 Android 服务。
第十章,将应用程序放入域中,教你如何正确地给应用程序的私有数据目录打标签,以及通过配置文件和 SELinux 策略设置应用程序运行时上下文。
第十一章,标签属性,演示如何创建并给新属性和现有属性打标签,以及在这样做时可能遇到的异常情况。
第十二章,掌握工具链,讲述了控制设备上策略的各种组件是如何实际构建和创建的。这一章回顾了 Android.mk 组件,详细介绍了构建和配置管理核心的工作原理。
第十三章,进入强制模式,利用你在前面章节学到的所有技能,来响应来自 CTS 的审计日志,并将 UDO0 置于强制模式。
附录,开发环境,引导你完成设置适合你跟随本书所有活动的 Linux 环境的必要步骤。
你需要为这本书准备的东西
硬件要求包括:
-
一块 UDO0 嵌入式开发板
-
一张 8GB 的 Mini SD 卡(虽然你可以使用容量更大的卡,但我们不推荐这样做)
-
至少 16GB 的 RAM
-
至少 80GB 的硬盘空间
软件要求包括:
-
一套 Ubuntu 12.04 LTS 桌面系统
-
Oracle JDK 6.0 版本 6u45
-
本书中需要一些额外的 Linux 软件,但这些都已在书中描述,并且可以免费获取。
这本书的目标读者
这本书面向那些对 Linux 实现的操作系统概念有一定了解的开发者和工程师。他们可能是希望保护自己 Android 设备创造的爱好者,制造手机的 OEM 工程师,或者是那些 Android 正在增长的领域的工程师。具备 C 语言编程的基本背景将有所帮助。
约定
在这本书中,你会发现多种文本样式,这些样式用于区分不同类型的信息。以下是一些样式示例及其含义的解释。
文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式将如下所示:"现在让我们尝试执行 hello.txt 文件,看看会发生什么。"
代码块设置如下:
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
命令行输入或输出将如下所示:
$ su testuser
Password:
testuser@ubuntu:/home/bookuser$
新术语 和 重要词汇 会以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的,会在文本中以这种方式出现:"通过选择 退出 来退出配置菜单,直到系统提示您保存新的配置。"
注意
警告或重要提示会以如下框中的形式出现。
小贴士
技巧和诀窍会以这种方式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
如果您想要发送一般性反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。
如果您有专业知识的话题,并且有兴趣撰写或为书籍做出贡献,请查看我们的作者指南 www.packtpub.com/authors。
客户支持
既然您已经拥有了 Packt 的一本书,我们有许多方法可以帮助您充分利用您的购买。
下载示例代码
您可以从您的账户 www.packtpub.com 下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,可以访问 www.packtpub.com/support 注册,我们会直接将文件通过电子邮件发送给您。
错误更正
尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做,您可以避免其他读者的困扰,并帮助我们改进本书后续版本。如果您发现任何错误更正,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击 错误更正提交表单 链接,并输入您的错误更正详情。一旦您的错误更正得到验证,您的提交将被接受,并将错误更正上传到我们的网站或添加到该标题错误更正部分下的现有错误更正列表中。
要查看之前提交的错误更正,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在 错误更正 部分下显示。
侵权行为
互联网上对版权材料进行盗版是一个所有媒体都面临的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如果您有疑似盗版材料的链接,请通过<copyright@packtpub.com>联系我们。
我们感谢您帮助保护我们的作者以及我们向您提供有价值内容的能力。
问题
如果您对这本书的任何方面有问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章:Linux 访问控制
Android 是一个由两个不同组件组成的操作系统。第一个组件是分叉的 Linux 主线内核,几乎与 Linux 共享所有内容。第二个组件,将在后面讨论,是用户空间部分,这部分非常定制且特定于 Android。由于 Linux 内核支撑这个系统并且负责大多数访问控制决策,所以从逻辑上讲,这是深入研究 Android 的一个很好的起点。
在本章中我们将:
-
检查自主访问控制的基础
-
介绍 Linux 权限标志和能力
-
在验证访问策略时跟踪系统调用
-
论证更强大的访问控制技术的必要性
-
讨论利用自主访问控制问题的 Android 漏洞
Linux 的默认且熟悉的访问控制机制称为自主访问控制(DAC)。这只是一个术语,意味着关于访问对象的权限由其创建者/所有者自行决定。
在 Linux 中,当一个进程调用大多数系统调用时,会执行一个权限检查。例如,希望打开一个文件的进程会调用open()系统调用。当调用这个系统调用时,会执行上下文切换,操作系统代码开始执行。操作系统有能力决定是否应该向请求的进程返回文件描述符。在做出这个决定的过程中,操作系统会检查请求进程以及它希望获得文件描述符的目标文件的访问权限。根据权限检查通过或失败,返回的将是文件描述符或 EPERM。
Linux 在内核中维护数据结构以管理这些权限字段,这些字段可以从用户空间访问,并且对于 Linux 和*NIX 用户来说应该是熟悉的。第一组访问控制元数据属于进程,构成了其凭据集的一部分。常见的凭据是用户和组。通常,我们使用组这个术语来指代主要组以及可能的次要组。你可以通过运行ps命令来查看这些权限:
$ ps -eo pid,comm,user,group,supgrp
PID COMMAND USER GROUP SUPGRP
1 init root root -
...
2993 system-service- root root root
3276 chromium-browse bookuser sudo fuse bookuser
...
如你所见,我们有以root和bookuser用户身份运行的进程。你还可以看到,他们的主要组只是等式的一部分。进程还有一组辅助组,称为补充组。这个集合可能是空的,由SUPGRP字段中的破折号表示。
我们希望打开的文件,被称为目标对象、目标或对象,同时也维护一组权限。该对象维护USER和GROUP,以及一组权限位。在目标对象的上下文中,USER可以被称为所有者或创建者。
$ ls -la
total 296
drwxr-xr-x 38 bookuser bookuser 4096 Aug 23 11:08 .
drwxr-xr-x 3 root root 4096 Jun 8 18:50 ..
-rw-rw-r-- 1 bookuser bookuser 116 Jul 22 13:13 a.c
drwxrwxr-x 4 bookuser bookuser 4096 Aug 4 16:20 .android
-rw-rw-r-- 1 bookuser bookuser 130 Jun 19 17:51 .apport-ignore.xml
-rw-rw-r-- 1 bookuser bookuser 365 Jun 23 19:44 hello.txt
-rw------- 1 bookuser bookuser 19276 Aug 4 16:36 .bash_history
...
如果我们查看前面命令的输出,我们可以看到hello.txt的USER是bookuser,GROUP是bookuser。我们还可以看到输出左侧的权限位或标志。还有七个字段需要考虑。每个空字段都用破折号表示。当使用ls打印时,第一个字段可能会因语义而变得混乱。因此,让我们使用stat来调查文件权限:
$ stat hello.txt
File: `hello.txt'
Size: 365 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 1587858 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/bookuser) Gid: ( 1000/bookuser)
Access: 2014-08-04 15:53:01.951024557 -0700
Modify: 2014-06-23 19:44:14.308741592 -0700
Change: 2014-06-23 19:44:14.308741592 -0700
Birth: -
第一行访问信息是最有力的。它包含了所有访问控制的重要信息。第二行只是一个时间戳,告诉我们文件最后被访问的时间。正如我们所见,对象的USER或UID是bookuser,GROUP也是bookuser。权限标志(0664/-rw-rw-r--)标识了两种表示权限标志的方式。第一种是八进制形式0664,将每个三标志字段压缩为一个三基数(八进制)数字。第二种是友好形式,-rw-rw-r--,等同于八进制形式,但视觉上更容易解读。在任何情况下,我们可以看到最左边的字段是 0,我们的其余讨论将忽略它。该字段用于setuid和setgid功能,这对于本讨论不重要。如果我们把剩下的八进制数字 664 转换为二进制,我们得到 110 110 100。这个二进制表示直接关联到友好形式。每个三重映射到读、写和执行权限。通常你会看到这个权限三重表示为RWX。第一个三重是给USER的权限,第二个是给GROUP的权限,第三个是给OTHERS的权限。翻译成常规英语就是,“用户bookuser有权从hello.txt中读取和写入。组bookuser有权从hello.txt中读取和写入,而其他人只有权从hello.txt中读取。”让我们通过一些现实世界的例子来测试这一点。
更改权限位
让我们以bookuser用户的身份测试示例运行过程中的访问控制。大多数进程在调用它们的用户的上下文中运行(不包括setuid和getuid程序),所以任何我们调用的命令都应该继承我们用户的权限。我们可以通过发出以下命令来查看:
$ groups bookuser
bookuser : bookuser sudo fuse
我的用户,bookuser,是USER bookuser,GROUP bookuser以及SUPGRP sudo和fuse。
要测试读取权限,我们可以使用cat命令,它打开文件并将其内容打印到stdout:
$ cat hello.txt
Hello, "Exploring SE for Android"
Here is a simple text file for
your enjoyment.
...
我们可以通过运行strace命令并查看输出来自省执行的系统调用:
$ strace cat hello.txt
...
open("hello.txt", O_RDONLY) = 3
...
read(3, "Hello, \"Exploring SE for Android\"\n"..., 32768) = 365
...
输出可能会相当冗长,因此我只展示了相关部分。我们可以看到cat调用了open系统调用并获得了文件描述符3。我们可以使用该描述符通过其他系统调用查找其他访问。稍后我们会看到在文件描述符3上发生了一个读取操作,它返回了365,即读取的字节数。如果我们没有从hello.txt读取的权限,打开操作将会失败,我们也永远不会得到该文件的有效的文件描述符。我们还会在strace输出中看到失败的信息。
既然已经验证了读取权限,让我们尝试写入。一个简单的方法是编写一个简单的程序,将内容写入现有文件。在本例中,我们将写入my new text\n(参考write.c文件)。
使用以下命令编译程序:
$ gcc -o mywrite write.c
现在使用新编译的程序运行:
$ strace ./mywrite hello.txt
在验证时,你会看到:
...
open("hello.txt", O_WRONLY) = 3
write(3, "my new text\n", 12) = 12
...
如你所见,写入操作成功,并返回了12,即写入到hello.txt的字节数。没有报告错误,所以权限似乎到目前为止是检查无误的。
现在尝试执行hello.txt,看看会发生什么。我们预期会看到错误。像执行普通命令那样执行它:
$ ./hello.txt
bash: ./hello.txt: Permission denied
这正是我们所预期的,但让我们用strace来更深入地了解究竟哪里出了问题:
$ strace ./hello.txt
...
execve("./hello.txt", ["./hello.txt"], [/* 39 vars */]) = -1 EACCES (Permission denied)
...
execve系统调用,它用于启动进程,由于EACCESS错误而失败。这正是当没有执行权限时所希望看到的情况。Linux 的访问控制按预期工作!
现在我们将在另一个用户的上下文中测试访问控制。首先,我们将使用adduser命令创建一个名为testuser的新用户:
$ sudo adduser testuser
[sudo] password for bookuser:
Adding user `testuser' ...
Adding new group `testuser' (1001) ...
Adding new user `testuser' (1001) with group `testuser' ...
Creating home directory `/home/testuser' ...
...
验证testuser的USER、GROUP和SUPGRP:
$ groups testuser
testuser : testuser
由于USER和GROUP与a.S上的任何权限都不匹配,所有的访问都将受到OTHERS权限检查,正如你所记得的,这是只读的(0664)。
首先临时作为testuser工作:
$ su testuser
Password:
testuser@ubuntu:/home/bookuser$
如你所见,我们仍然在 bookuser 的主目录中,但当前用户已经变更为testuser。
我们将先用cat命令测试read:
$ strace cat hello.txt
...
open("hello.txt", O_RDONLY) = 3
...
read(3, "my new text\n", 32768) = 12
...
与前面的示例类似,正如预期的那样,testuser可以顺利地读取数据。
现在让我们进行写入测试。预期没有适当的权限这将失败:
$ strace ./mywrite hello.txt
...
open("hello.txt", O_WRONLY) = -1 EACCES (Permission denied)
...
如预期的那样,系统调用操作失败了。当我们尝试以testuser的身份执行hello.txt时,也应该失败:
$ strace ./hello.txt
...
execve("./hello.txt", ["./hello.txt"], [/* 40 vars */]) = -1 EACCES (Permission denied)
...
现在我们需要测试组访问权限。我们可以通过向testuser添加一个补充组来实现这一点。为此,我们需要退出到有权限执行sudo命令的bookuser:
$ exit
exit
$ sudo usermod -G bookuser testuser
现在让我们检查testuser的组:
$ groups testuser
testuser : testuser bookuser
由于之前的usermod命令,testuser现在属于两个组:testuser和bookuser。这意味着当testuser访问具有bookuser组的文件或其他对象(如套接字)时,将应用GROUP权限,而不是OTHERS。在hello.txt的背景下,testuser现在可以读取和写入文件,但不能执行它。
通过执行以下命令切换到testuser:
$ su testuser
通过执行以下命令测试read:
$ strace cat ./hello.txt
...
open("./hello.txt", O_RDONLY) = 3
...
read(3, "my new text\n", 32768) = 12
...
与之前一样,testuser能够读取文件。唯一的区别是现在它可以通过OTHERS和GROUP的访问权限来read文件。
通过执行以下命令测试write:
$ strace ./mywrite hello.txt
...
open("hello.txt", O_WRONLY) = 3
write(3, "my new text\n", 12) = 12
...
这一次,testuser不仅能够写入文件,而不是像之前那样遇到EACCESS权限错误。
尝试执行文件应该仍然会失败:
$ strace ./hello.txt
execve("./hello.txt", ["./hello.txt"], [/* 40 vars */]) = -1 EACCES (Permission denied)
...
这些概念是 Linux 访问控制权限位、用户和组的基础。
更改所有者和组
在前面的章节中,我们使用hello.txt进行探索性工作,展示了对象的所有者如何通过管理对象的权限位来允许各种形式的访问。更改权限是通过使用chmod系统调用完成的。更改用户和/或组是通过chown系统调用来完成的。在本节中,我们将研究这些操作的具体细节。
让我们从仅向hello.txt文件的所有者bookuser授予读和写权限开始。
$ chmod 0600 hello.txt
$ stat hello.txt
File: `hello.txt'
Size: 12 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 1587858 Links: 1
Access: (0600/-rw-------) Uid: ( 1000/bookuser) Gid: ( 1000/bookuser)
Access: 2014-08-23 12:34:30.147146826 -0700
Modify: 2014-08-23 12:47:19.123113845 -0700
Change: 2014-08-23 12:59:04.275083602 -0700
Birth: -
如我们所见,现在文件权限设置为只允许bookuser读取和写入。一个细致的读者可以执行本章前面部分提到的命令来验证权限是否按预期工作。
更改组也可以通过chown以类似的方式进行。让我们将组更改为testuser:
$ chown bookuser:testuser hello.txt
chown: changing ownership of `hello.txt': Operation not permitted
这并没有按照我们的预期工作,但问题出在哪里呢?在 Linux 中,只有特权进程可以更改对象的USER和GROUP字段。在对象创建时,初始的USER和GROUP字段是从有效的USER和GROUP中设置的,在尝试执行该进程时会进行检查。只有进程可以创建对象。特权进程有两种形式:作为全能的root运行和设置了其功能的进程。我们稍后会详细介绍功能。现在,让我们关注root。
让我们切换到root用户,以确保执行chown命令可以更改该对象的组:
$ sudo su
# chown bookuser:testuser hello.txt
Now, we can verify the change occurred successfully:
# stat hello.txt
File: `hello.txt'
Size: 12 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 1587858 Links: 1
Access: (0600/-rw-------) Uid: ( 1000/bookuser) Gid: ( 1001/testuser)
Access: 2014-08-23 12:34:30.147146826 -0700
Modify: 2014-08-23 12:47:19.123113845 -0700
Change: 2014-08-23 13:08:46.059058649 -0700
Birth: -
更多情况的考虑
你可以看到GROUP(GID)现在是testuser,事情看起来相当安全,因为要更改对象的用户和组,你需要具备特权。只有在你拥有对象时,你才能更改对象的权限位,root用户除外。这意味着如果你以root身份运行,即使没有权限,你也可以对系统进行任何操作。这种绝对权威就是为什么以 root 运行的进程遭到成功攻击或错误时可能对系统造成严重损害的原因。此外,非 root 进程遭到成功攻击也可能通过无意中更改权限位造成损害。例如,假设你的 SSH 私钥上有一个非预期的chmod 0666命令。这将使你的密钥对所有系统用户暴露,这几乎肯定是你绝对不希望发生的事情。能力模型部分解决了 root 的限制。
能力模型
在 Linux 上执行许多操作时,对象权限模型并不完全适用。例如,更改UID和GID需要一种被称为root的神奇USER。假设你有一个需要利用这些功能的长运行服务。也许这个服务监听内核事件并为你创建设备节点?这样的服务确实存在,它被称为ueventd或用户事件守护进程。这个守护进程传统上以root身份运行,这意味着如果它被攻破,它可能会从你的主目录读取你的私钥并将其发送给攻击者。这可能是一个极端的例子,但它旨在展示以root身份运行进程可能很危险。假设你可以以root用户身份启动一个服务,并让进程将其UID和GID更改为不具备特权的用户,但保留一些较小的特权能力以完成其工作?这正是 Linux 中的能力模型所做的。
Linux 中的能力模型试图将root所具有的权限集分解为更小的子集。这样,进程可以被限制在执行预期功能所需的最小权限集中。这就是所谓的最小权限,这是在保护系统时减少成功攻击可能造成的损害的关键理念。在某些情况下,它甚至可以通过阻止其他开放的攻击向量来防止成功攻击的发生。
能力有很多种。能力的手册页是事实上的文档。让我们看看CAP_SYS_BOOT能力:
$ man capabilities
...
CAP_SYS_BOOT
Use reboot(2) and kexec_load(2).
这意味着具有此能力的进程可以重启系统。但是,该进程不能像以root身份运行或具有CAP_DAC_READ_SEARCH时那样任意更改USERS和GROUP。这限制了攻击者可以执行的操作:
<FROM MAN PAGE>
CAP_DAC_READ_SEARCH
Bypass file read permission checks and directory read and execute permission checks.
现在假设我们的重启进程运行时带有CAP_CHOWN权限。假设它使用这个功能确保在接收到重启请求时,在重启之前备份每个用户的家目录下的一个文件到服务器。假设这个文件是~/backup,权限是 0600,USER和GROUP分别是该家目录的相应用户和组。在这种情况下,我们已经尽可能最小化了权限,但进程仍然可以访问用户的 SSH 密钥,并且可能由于错误或攻击而上传这些密钥。另一种方法是设置组为backup,并以GROUP backup运行进程。然而,这也有局限性。假设你想与另一个用户共享这个文件。该用户需要backup的辅助组,但现在用户可以读取所有备份文件,而不仅仅是预期的那些。一个敏锐的读者可能会考虑到bind挂载,然而执行bind挂载和文件权限的进程也带有某些权限,因此也受到这个粒度问题的影响。
主要问题,以及另一个访问控制系统的案例可以用一个词来概括,那就是粒度。DAC 模型没有足够的粒度来安全处理复杂的访问控制模型,或者最小化一个进程可能造成的损害。这在 Android 上尤为重要,因为整个隔离系统都依赖于这种控制,一个恶意 root 进程可能会破坏整个系统。
Android 对 DAC 的使用
在 Android 沙盒模型中,每个应用程序都以其自己的UID运行。这意味着每个应用都可以将其存储的数据与其他应用隔离开来。用户和组设置为该应用程序的UID和GID,因此没有应用可以在应用程序显式对其对象执行chmod的情况下访问另一个应用程序的私有文件。此外,Android 中的应用程序不能拥有权限,因此我们不必担心如CAP_SYS_PTRACE这样的权限,即调试另一个应用程序的能力。在 Android 中,在一个完美的世界里,只有系统组件会运行带权限,应用程序不会意外地对所有用户执行chmod操作以读取私有文件。由于应用程序兼容性,当前 AOSP SELinux 策略没有纠正这个问题,但可以通过 SELinux 来解决。在 Android 上,应用程序之间共享数据的正确方式是通过 binder 和共享文件描述符。对于较小的数据量,提供者模型就足够了。
浏览 Android 漏洞
利用我们对 DAC 权限模型及其一些局限性的新理解,让我们来看一些针对它的 Android 攻击方式。我们将仅涵盖一些攻击方式,以了解 DAC 模型是如何失败的。
Skype 漏洞
CVE-2011-1717 在 2011 年发布。在这个漏洞中,Skype 应用程序留下了一个 SQLite3 数据库,该数据库可以被全世界读取(类似于 0666 权限)。这个数据库包含了用户名和聊天日志,以及如姓名和电子邮件等个人数据。一个名为 Skypwned 的应用程序能够演示这一功能。这是一个改变对象权限可能导致严重后果的例子,特别是在将权限从READ开放给OTHERS的情况下。
GingerBreak
CVE-2011-1823 展示了对 Android 系统的 root 攻击。Android 上的卷管理守护进程(vold)负责外部 SD 卡挂载和卸载。该守护进程通过 NETLINK 套接字监听消息。守护进程从未检查过消息的来源,任何应用都可以打开并创建 NETLINK 套接字向 vold 发送消息。攻击者一旦打开 NETLINK 套接字,就会发送一个精心构造的消息来绕过健全性检查。该检查测试了一个有符号整数的最小界限,但从未检查过它是否为负数。然后它被用来索引一个数组。这种负数访问将导致内存破坏,如果消息恰当,可能导致执行任意代码。GingerBreak 的实现使得任意用户获得了 root 权限,这是一个典型的权限执行攻击。设备一旦被 root,沙盒就不再有效。
Rage against the cage
CVE-2010-EASY 是通过 fork 炸弹攻击实现的setuid耗尽。它成功攻击了 Android 上的adb守护进程,该进程最初以 root 权限启动,如果不需要 root 权限则降级权限。这种攻击使adb保持为root,并向用户返回一个 root shell。在 Linux 内核 2.6 中,当运行进程数达到RLIMIT_NPROC时,setuid系统调用返回错误。adb守护进程代码没有检查setuid的返回值,这为攻击者留下了一个小的竞争窗口。攻击者需要分叉足够多的进程以达到RLIMIT_NPROC,然后杀死守护进程。adb守护进程降级到 shell UID,攻击者以 shell USER身份运行程序,因此 kill 命令将成功执行。此时,adb服务会被重新启动,如果RLIMIT_NPROC已达到最大值,setuid将失败,adb将保持以 root 权限运行。然后,从主机运行adb shell会向用户返回一个很好的 root shell。
MotoChopper
CVE-2013-2596 是高通视频驱动程序mmap功能中的一个漏洞。应用程序通过mmap访问 GPU 以进行高级图形渲染,例如 OpenGL 调用。mmap中的漏洞允许攻击者mmap内核地址空间,此时攻击者能够直接改变他们的内核凭据结构。这个漏洞是一个例子,其中 DAC 模型并没有出错。实际上,除了修补代码或移除直接图形访问权限之外,只有对mmap边界的编程检查才能防止这种攻击。
总结
DAC 模型非常强大,但其缺乏细粒度控制以及使用异常强大的root用户,仍有所不足。随着移动设备使用的敏感性增加,提高系统安全性的需求是有根据的。幸运的是,Android 建立在 Linux 之上,因此受益于一个由众多工程师和研究人员构成的庞大生态系统。自从 Linux 内核 2.6 起,一种名为**强制访问控制(MAC)**的新访问控制模型被加入。这是一个框架,通过它可以将模块加载到内核中,以提供一种新的访问控制模型。第一个模块被称为 SELinux。它被 Red Hat 等公司用于保护敏感的政府系统。因此,找到了一个解决方案,以实现对 Android 的这种访问控制。
第二章.强制访问控制和 SELinux
在第一章中,我们介绍了Linux 访问控制的一些不足之处。在这些系统中,对象的拥有者对其权限标志拥有完全控制权,在以root身份或具有某些能力执行时,可以表现出更大的能力(例如,能够执行chown)。在本章中,我们将:
-
检查 MAC 的基础知识
-
介绍一些 SELinux 的行业驱动因素
-
讨论标签、用户、角色和类型
-
探索实现具体策略以允许和限制对象交互的实现方式
理想的 MAC 系统保持提供对内核资源(如文件)的明确访问控制属性,无论对象的拥有者是谁。例如,在 MAC 系统中,对象的拥有者可能无法完全控制其权限。在 Linux 中,MAC 框架与当前的 DAC 控制正交工作。这意味着 MAC 控制不会干扰 DAC 控制。换句话说,为了避免 MAC 和 DAC 系统之间的潜在冲突,内核在检查 MAC 权限之前,会先使用 DAC 权限验证访问。如果 DAC 权限导致权限冲突,那么将不会检查 MAC 权限。只有当 DAC 权限通过时,内核才会针对 MAC 权限提供者验证访问。在任何一级失败都将导致返回EACCESS。如果 DAC 和 MAC 权限都通过,那么内核资源(例如,一个文件描述符)将被发送回用户空间。
在 Linux 中,在 2.6.x 系列的内核中合并了一个名为Linux 安全模块(LSM)的框架。此框架允许你通过将 LSM 钩子绑定到安全提供程序,以在构建时选择启用强制访问控制系统。安全增强型 Linux(SELinux)是内核内首个使用此 MAC 安全框架的消费者,它是一个强制访问控制系统的实现。SELinux 被广泛包含在各种 Linux 系统中,例如红帽企业级 Linux(RHEL)以及其衍生出的 Fedora。最近,它也开始随 Android 系统一起发布。想要查看 SELinux 的源代码可以在 Linux 源代码树的kernel/security/selinux目录下找到。
回到基础
SELinux 是由美国政府与犹他大学共同设计的一种名为 FLUX Advanced Security Kernel (FLASK) 的重新实现。SELinux 和 FLASK 架构提供了一个中央策略文件,在确定访问控制决策结果时使用。这个中央策略以白名单形式存在。这意味着所有访问控制规则必须由策略文件明确定义。这个策略文件被抽象化,并由一个名为安全服务器的软件组件提供服务。当 Linux 内核需要做出访问控制决策并且启用了 SELinux 时,内核通过 LSM 钩子与安全服务器进行交互。
在运行中的系统中,进程是获得 CPU 时间来执行任务的活动实体。用户只是调用这些进程来代表他们执行工作。这是一个重要的概念。在我们编写这本书时,我们相信运行在我们机器上的具有我们凭据的字处理器没有打开我们的 SSH 密钥并将它们嵌入到文档元数据中。现在,是进程控制着计算资源,而不是用户。进程是运行实体,是进程向内核请求资源的系统调用,而不是物理人类。考虑到这一点,SELinux 系统中的第一个参与者通常是进程,通常被称为 主体。是主体访问文件。是安全服务器用来做出访问决策的主体。
因此,主体使用内核资源。这种内核资源是 目标 的一个例子。主体在目标上执行操作。自然地,人们应该问:“主体执行哪些操作?”这些被称为访问向量,通常与执行的 syscall 名称相关联。例如,主体可以在目标上执行 open。需要注意的是,目标也可以是进程。例如,如果系统调用是 ptrace,主体可能是类似于调试器的东西,而目标则是你希望调试的进程。主体通常是进程,但目标可能是进程、套接字、文件或其他东西。
标签
SELinux 使用标签来描述与目标和主体相关的策略语义。标签是与对象关联的元数据,维护主体和目标的访问信息。与该对象关联的数据是一个字符串。回到调试器示例,gdb 进程可能有一个主体标签字符串为 debugger,而目标可能有一个标签为 debugee。然后在安全策略中,可以使用一些语义来表达具有主体标签 debugger 的进程被允许调试具有目标标签 debugee 的应用程序。
幸运的是,或许也是不幸的是,SELinux 并没有使用如此简单的标签。实际上,标签由四个冒号分隔的字段组成:用户、角色、类型和级别。这种额外的复杂性为非常灵活的控制选项提供了可能。
用户
标签中的第一个字段用于标识用户。用户字段作为基于用户的访问控制(UBAC)设计的一部分。然而,这通常并不与人机用户相关联,而是与 DAC 中的用户概念相关。SELinux 用户通常会定义一组传统用户。一个常见的例子是将所有正常用户标识为 SELinux 用户,如user_u。也许还会为系统进程设置一个单独的用户,比如system_u。在桌面 SELinux 社区的传统中,用户部分的字符串通常会以_u结尾。
角色
标签中的第二个字段是角色。角色作为基于角色的访问控制(RBAC)设计的一部分。角色用于向用户提供更细致的权限。例如,假设我们保留了用户字段sysadm_u给管理员。管理员可能会执行不同的任务,根据任务的不同,sysadm_u中的角色(以及相应的权限)可能会改变。例如,当管理员需要挂载和卸载文件系统时,角色字段可能会变为mount_admin_r。当管理员设置iptables规则时,角色可能会变为net_admin_r。角色允许在执行任务的范围内隔离权限。
类型
类型是冒号分隔标签的第三个字段。类型字段在 SELinux 的类型强制(TE)部分进行评估。TE 是推动 SELinux 安全能力的主要组成部分,正是在这一环节政策开始生效。
SELinux 基于一个白名单系统,默认情况下拒绝一切,并需要从策略中获得明确的允许,以便进行交互。这种允许最初是通过引用主体和目标类型的允许规则从策略中确定的。SELinux 类型还可以分配属性。属性可以帮助您为多种类型提供一组通用规则。属性可以像继承模型那样使用。
访问向量
数据是通过系统调用和可能的用户定义访问方法由进程访问的。用户定义的访问方法通常由用户空间对象管理器控制。这些访问路径,也称为向量,构成了一组可以应用于对象的行为。例如,如果一个进程打开一个文件,写入一些数据然后再次读取,那么执行的访问向量将是open、read和write。如果一个进程调试另一个进程,那么访问向量将是ptrace。
多级安全
SELinux 还支持一个多级安全(MLS)模型,该模型向Bell-LaPadula(BLP)模型致敬,但也可以使用其他模型。BLP 模型是为了正式化国防部的安全政策而创建的。例如,一个有秘密许可的人不应该能够阅读绝密材料。但是,假设这个人有一个绝妙的想法,最终需要以绝密级别保护;那么这些数据可以被"升级"为绝密。这被称为"不向上读或向下写"。
SELinux 对此字段的实现包含子字段。第一个字段是敏感性,将始终存在。在之前例子的背景下,相关的敏感性包括秘密和绝密。第二个子字段是类别,可能不存在。这些字段在政府分类的背景下也是有意义的。数据本身可能是分隔的,所以尽管敏感性相同,比如都是绝密,但数据只应该分发给同一隔间或类别内的人。敏感性通过优势关键词以层次化的方式定义。在典型的策略中,s0是最低敏感性,而n > 0的sN是最高敏感性。因此,s1的敏感性高于s0。类别是集合。与级别相关的控制,包括敏感性和可能的类别,遵循集合论概念,如优势和相等。在 MLS 安全中,所有交互默认都是允许的,与类型强制不同。敏感性和类别都可以是范围的,类别可以列举。因此,一个标签可能有一些数量的敏感性和不同数量的类别。
将其组合起来
SELinux 标签非常灵活,有时也相当复杂。通常,从关注类型强制的一个人为例子开始是有益的。随后,我们可以根据需要更细粒度的需求,添加其他字段。方便的是,你可以将这个模型投射到日常生活中的场景,为材料提供一定的实质性感觉。著名的 SELinux 人物 Dan Walsh 发表了一篇博客,使用宠物作为类比。让我们以此为基础,但在进行中我们会做一些修改,并定义自己的例子。最好从简单的类型强制开始,因为它最容易理解。
注意
你可以阅读 Dan Walsh 的原始博客文章,了解宠物类比,文章地址是opensource.com/business/13/11/selinux-policy-guide。
假设我们有一只猫和一只狗。我们不希望猫吃狗粮。我们不希望狗吃猫粮。在这一点上,我们已经识别出两个主体,一只猫和一只狗,以及两个目标,猫粮和狗粮。我们还识别出一个访问向量,即吃。我们可以使用允许规则来实现我们的策略。可能的规则可能如下所示:
allow cat cat_chow:food eat;
allow dog dog_chow:food eat;
让我们用这个例子来开始定义我们希望实施的表达访问控制的基本语法。第一个标记是 allow,表明我们希望允许主体和目标之间的交互。狗被分配类型 dog,猫为 cat。猫粮被分配类型 cat_chow,狗粮为 dog_chow。在这种情况下,访问向量是 eat。使用这种基本语法(也是有效的 SELinux 语法),我们限制动物只能吃它们应该吃的食物。注意类型后的 :food 注解。这是目标对象的类字段。例如,还可能有 dog_chow treat 和 cat_chow 类,这可能表明我们希望以可能与允许访问非零食食物不同的方式允许访问零食。
假设我们又得到两只狗,我们的场景有三只狗。这些狗的大小不同:小的、中等的和大的。我们希望确保这些新狗不要吃其他狗的食物。我们可以为每只狗创建一个新类型,并阻止狗吃其他狗的食物。它可能看起来像这样:
allow cat cat_chow:food eat;
allow dog_small dog_small_chow:food eat;
allow dog_medium dog_medium_chow:food eat;
allow dog_large dog_large chow:food eat;
这将起作用;然而,类型的总数将难以管理,如果我们允许大狗吃小品种的食物,那么类型将继续增长。我们可以做的是使用 MLS 支持,为每个目标或狗食碗分配一个敏感度。假设以下情况:
-
猫的食物碗具有敏感度,
tiny -
小狗的食物碗具有敏感度,
small -
中型狗的食物碗具有敏感度,
medium -
大狗的食物碗具有敏感度,
large
我们还需要确保对这些主题进行适当的敏感度标注:
-
猫应有敏感度,
tiny -
小狗应有敏感度,
small -
中型狗应有敏感度,
medium -
大狗应有敏感度,
large
在这一点上,我们需要引入额外的语法以允许交互,因为默认情况下,MLS 允许一切而 TE 拒绝一切。我们将使用 mlsconstrain 来限制系统内的交互。规则可能如下所示:
mlsconstrain food eat (l1 eq l2);
这个约束只允许主体吃具有相同敏感度级别的食物。SELinux 定义了关键字 l1 和 l2。l1 关键字是目标的级别,l2 是源的级别。因为规则是白名单的一部分,这也防止主体吃不具有等效敏感度级别的食物。
现在,假设我们又有了一条大型犬。现在我们有了两条大型品种的狗。然而,它们有不同的饮食,需要接触不同的食物。我们可以添加一个新的类型或修改现有的类型,但这将具有导致我们使用敏感性防止访问的相同限制。我们可以添加另一个敏感性,但可能会有点混淆,因为有large1和large2敏感性。在这一点上,类别将允许我们在控制上更加细化。假设我们添加了一个表示品种的类别。我们标签的 MLS 部分将看起来像这样:
large:golden_retriever
large:black_lab
这些规则可以用来防止黑拉布拉多犬吃金毛犬的食物。现在假设你又惊喜地得到了另一条狗,一条圣伯纳犬。假设这条新的伯纳犬可以吃任何大型犬的食物,但其他大型犬不能吃它的食物。我们可以给食物碗和狗贴上标签。
| 狗品种 | 主体标签 | 目标标签 |
|---|---|---|
| 金毛犬 | Dog:large:golden_retriver | dog_chow:large:golden_retriver |
| 黑拉布拉多犬 | Dog:large:black_lab | dog_chow:large:black_lab |
| 圣伯纳犬 | Dog:large:saint_bernard, black_lab, golden_retriever | dog_chow:large:saint_bernard |
| 猫 | Cat:tiny | cat_chow:tiny |
现有的mlsconstraint需要修改。如果圣伯纳犬的食物吃完了,去吃黑拉布拉多犬的食物,由于等级不同(Dog:large:saint_bernard, black_lab, golden_retriever与dog_chow:large:black_lab不同),圣伯纳犬将无法吃它。记住,这些等级是集合,因此我们需要引入某种概念,如果主体集合支配目标集合,那么应该允许这种交互。
这可以通过dom关键词实现:
mlsconstrain food eat (l1 dom l2);
主导关键词dom与等于不同,表示l1是l2的超集。换句话说,与目标l2相关的级别包含在与主体l1相关联的潜在更大级别集合中。在这一点上,我们能够保持所有食物的分离,按照我们的意愿使用。
在得到所有这些狗之后,你意识到是时候喂它们了,所以你拿了一袋狗粮,在每个碗中放一些。但是,在我们能在碗中添加狗粮之前,我们需要一些允许规则和标签。记住,SELinux 是一个基于白名单的系统,所有内容必须明确允许。
我们将人类标记为human标签,并定义一些规则。哦,对了...别忘了喂猫:
allow human dog_chow:food put;
allow human cat_chow:food put;
我们还需要给human标记上所有的敏感性和类别,但当我们需要在系统中添加额外的狗、品种和品种大小时,这将变得繁琐。如果类型是human,我们可以绕过这个约束。采用这种方法,我们总是相信human会将正确的食物放入适当的碗中:
mlsconstrain food eat (l1 dom l2);
mlsconstrain food put (t1 == human);
注意在 MLS 约束的访问向量中加入了put。瞧!现在人类可以喂养他日益增长的动物群体了。
所以你的生日到了,你收到了一个自动喂狗器作为礼物。你给食物分配器打上标签dispenser,并修改 MLS 约束:
mlsconstrain food eat (l1 dom l2);
mlsconstrain food put (t1 == human or t1 == dispenser);
再次,我们发现需要减少类型数量并组织起来,以防止不得不重复行。这时,属性就显得非常方便。我们可以首先定义一个属性,并将其分配给我们的human和dispenser类型。
attribute feeder;
然后我们可以将其添加到类型中:
typeattribute human, feeder;
typeattribute dispenser, feeder;
这也可以在类型声明时完成:
type human, feeder;
type dispenser, feeder;
在这一点上,我们可以修改 MLS 声明,使其看起来像这样:
mlsconstrain food eat (l1 dom l2);
mlsconstrain food put (t1 == feeder);
现在假设你雇佣了一个家政服务。你希望确保任何由家政服务派遣的人都能够喂养你的宠物。就此而言,让我们也让你的家庭成员喂养它们。这将是一个使用用户能力的良好案例。我们将定义以下用户:adults_u、kids_u和maid_u。然后我们需要添加一个约束声明,以允许这些用户的互动:
mlsconstrain food put (u1 == adults_u or u1 == maid_u);
这将防止儿童喂养狗,但允许家政和成人喂养。现在假设你雇佣了一个园丁。你可以创建另一个用户gardener_u,或者你可以将用户合并为几个类别并使用角色。假设我们将gardener_u和maid_u合并为staff_u。没有理由让园丁喂养狗,因此我们可以使用基于角色的转换来让员工在职责之间移动。例如,假设员工可以执行多项服务,即同一个人可能既园艺又打扫。在这种情况下,他们可能会承担gardener_r或maid_r的角色。我们可以使用 SELinux 的角色功能来满足这一需求:
mlsconstrain food put (u1 == adults_u or (u1 == staff_u and r1 == animal_care_r);
员工只有在animal_care_r角色中才能喂养狗。如何进入和退出该角色是唯一缺少的组件。你需要有一个明确的系统,规定员工如何进入动物护理角色并转换回来。在 SELinux 中,这些转换要么通过动态角色转换自动发生,要么通过源代码修改。我们将假设任何人类实体(园丁、成人、儿童)都从human_r角色开始。
动态角色转换遵循一个两部分的规则,第一部分允许通过一个允许规则发生转换:
allow human_r animal_care_r;
角色转换声明如下:
role_transition human_r dog_chow animal_care_r;
role_transition human_r cat_chow animal_care_r;
这将是一个将dog_chow和cat_chow类型归为一个新属性animal_chow的好案例,并重写前面的角色转换为:
typeattribute dog_chow, animal_chow;
typeattribute cat_chow, animal_chow;
role_transition human_r animal_chow animal_care_r;
使用这些角色转换,你只能从 human_r 角色转换到 animal_care_r。你还需要定义转换以返回。同样重要的是要注意,你可能会定义其他角色。假设你定义了 gardener_r 角色,并且当某人处于该角色时,他们不能转换到 animal_care_r。假设你制定这项政策的理由是园丁可能会使用对宠物不安全的化学物质,因此他们在喂宠物之前需要洗手。在这种情况下,他们应该只能从 hand_wash_r 角色转换到 animal_care_r。
复杂性与最佳实践
正如你现在所理解的,SELinux 是复杂的,可以被认为是一种通用的“元编程策略语言”。你实际上是在编程哪些交互被允许在一个非常复杂的操作系统中发生,比如 Linux,交互本身通常是复杂的。就像编程语言一样,你可以用不同的风格和方法做事情,这将产生不同的结果。也许在那个程序中使用 switch() 会使其更清晰易懂,而不是 else-if 块,尽管从功能上讲,你最终会得到相同的结果。SELinux 也是如此;你通常可以使用执行机制的一部分来完成更适合使用另一种机制来完成的事情。在后面的章节中,我们将介绍对目标和主体进行标记的过程,这是系统中较为困难的部分之一。
当某人编写一个程序时,他们通常会有一个要求软件应执行的一系列要求。这些是软件的要求。在 SELinux 中,你也应该这样做。你应该收集安全要求并了解你希望保护自己免受的威胁模型。一个设计良好的 SELinux 策略将满足这些目标。一个伟大的设计将以易于扩展的方式进行。这就是谨慎和明智地使用 UBAC、RBAC、TE 和 MLS 组合最终将帮助你实现要求和设计目标的地方。
总结
在本章中,我们介绍了 SELinux 的主要工作部分,包括类型强制执行、多级别和多类别安全以及用户和角色。此外,我们还了解了如何将这些技术应用于实现越来越复杂的访问策略到一个具体的示例。在下一章中,我们将走出内核,探索 Android 在其非常独特的用户空间中是如何工作的。
第三章:安卓的奇妙之处
确实如此。尽管它是建立在熟悉的 Linux 内核之上,但 Android 有一个完全定制的用户空间,而且其中许多功能都是对其 GNU 表亲的重写,有些是全新的,或者与其桌面版本的功能有显著不同。由于这些差异,这些系统不得不被修改以支持 SELinux。在本章中,我们将:
-
介绍安卓的安全模型
-
调查 binder、zygote 和属性服务
-
探讨为了补充这些系统而添加的 SELinux 元素及其原因
这些系统的覆盖范围将是适度的,但稍后在我们对 Android 的 SE 探索性调查中适当的时候,我们将详细介绍每个系统的更复杂细节。
安卓的安全模型
安卓的核心安全模型基于 Linux 的 DAC,包括能力。然而,Android 以一种非常非传统的方式使用 Linux 的 UID/GID 概念。系统上的每个进程都有自己的 UID,而不是启动它的用户的 UID。这些 UID(通常是唯一的)提供了沙箱和进程隔离。不过,在某些情况下,进程可以共享 UID 和 GID。通常,当一个进程与另一个进程共享 UID 时,是因为它们都需要系统上的同一组权限并共享数据。GID 也是如此。然而,在 Android 中,有些 GID 实际上用于获取访问底层系统(如 SD 卡文件系统)的权限。简而言之,UID 用于隔离进程,而不是系统的人类用户。实际上,直到安卓 Jelly Bean 4.3 版本,Android 才支持多个人类用户。它始终是为单个人类用户操作的设备而设计的……至少在运行时是这样。
在这个安全模型中,有两个进程类别。第一个被称为系统组件服务。这些是在系统初始化脚本中声明的服务。它们往往是高度特权的,因此几乎从不与其他进程共享 UID。一个示例系统组件服务是无线接口层守护进程(RILD)。RILD 负责处理 Android 用户空间与设备上的调制解调器之间的消息。由于它所做的事情的性质,它通常以 root UID 运行。没有要求进程必须是纯本地代码。系统服务器具有非本地组件,以系统 UID 运行,并且是高度特权的。几乎所有这些系统都有一个共同点;它们有一个 UID,要么是 root,要么被设置为许多敏感内核对象(如套接字、管道和文件)的所有者。
第二类是应用程序。这些应用程序通常是用 Java 编写的,尽管这不是必须的;这与系统组件服务通常用本地代码编写但不作为要求类似。这些应用程序在安装时会自动分配 UID,系统为这一目的保留这些 UID。包管理器负责向应用程序发放 UID。这些 UID 与系统上的任何敏感或危险的东西无关,应用程序不带任何功能运行。为了访问系统资源,应用程序必须将其附加组添加到其中,或者必须由单独的进程进行仲裁。
使用附加组的简单示例可以在应用程序需要使用 SD 卡时看到。为了访问 SD 卡,应用程序必须在它们的附加 GIDs 中拥有SDCARD_RW权限。这些权限通过内核使用标准的 Linux DAC 权限执行。附加组在应用程序安装期间由包管理器分配,基于声明的权限。在 Android 中,应用程序必须在应用程序的清单中声明一个名为uses-permission的东西。这个权限以字符串形式出现,并映射到一个附加 GID。这种映射在系统中的一个文件中维护,具体为/system/etc/permissions/platform.xml。你将在后面的章节中看到这些权限字符串的应用。
应用程序获取系统资源的第二种方式是通过另一个进程。希望使用系统资源的应用程序必须让另一个进程代表它执行此操作。大多数请求都由一个名为系统服务器的进程处理。系统服务器会检查发起仲裁请求的应用程序是否在其清单文件中声明了匹配的权限字符串。如果已声明,则允许其继续操作;否则,将抛出安全异常。实际上,即使是 Android 中的仲裁访问也本质上使用的是 DAC 模型。尽管对象所有者通过权限字符串控制对象上的访问规则,但受保护对象的任何使用者只需请求权限字符串即可获得访问权限。本质上,任何人都可以编写一个请求任何所需权限字符串的应用程序。在安装应用程序时,用户会看到应用程序请求的权限列表,并可以选择批量接受或拒绝。如果用户意图安装应用程序,则必须授予所有请求的权限。如果用户不够谨慎,可能会无意中允许应用程序以可能威胁设备、应用程序或用户数据安全的方式访问受保护的对象。设备所有者应始终确保他们对应用程序使用声明的权限感到满意。
注意
如果需要示例或进一步讨论,请参考developer.android.com/guide/topics/security/permissions.html。
Binder
之前讨论的仲裁访问方法需要某种形式的进程间通信(IPC),虽然 Android 确实使用了 Unix 域套接字,但它还引入了自己更广泛使用的 IPC 机制。这种 IPC 机制称为 binder,是 Android 操作系统中的核心 IPC 机制。它从 BeOS 和 Palm OS 的 OpenBinder 实现中具有历史相关性,由于最初的 Android 开发团队由许多 OpenBinder 工程师组成,因此 binder 也随之进入了 Android。然而,Android 对 binder 代码库进行了彻底的重新编写,专门针对 Linux。
注意
目前,binder 还没有完全融入 Linux 内核,Android 的许多内核更改仍然处于阶段性状态。
关于 binder 及其主要采用的实现有一些争议。一些人反对它在驱动程序中与竞争对手的实现(如dbus)相比所做的繁重工作。然而,在这场辩论得到解决之前,可能还需要很长时间。无论 binder 是否继续作为 Android 特定的技术,或在 Linux 内核中得到普及,或者最终在 Android 中被其他技术取代,binder 在可预见的未来都将存在。
Binder 的架构
Binder IPC 遵循客户端/服务器架构。服务发布一个接口,客户端从该接口消费。客户端可以通过两种方法之一绑定到服务:已知地址或服务名称。
系统中的每个 binder 接口被称为 binder 节点。每个 binder 节点都有一个地址。当客户端想要使用一个接口时,必须通过这个地址绑定到一个 binder 节点上。这类似于通过 IP 地址浏览网页。然而,与通常长时间固定不变的 IP 地址不同,binder 地址可能会因为发布服务的重启或设备启动时服务的启动顺序而改变。进程的顺序并不能完全保证,因此发布进程服务可能会导致分配不同的 binder 令牌(一个在进程间共享的简单 binder 对象)。此外,这种间接方式允许运行时仅通过已发布的服务名称重新定位服务实现,无需使用令牌。
这种重定向的方式类似于 DNS 为网络设备访问提供从名称到 IP 地址解析的方式。Binder 有一个称为上下文管理器(也称为服务管理器)的东西。上下文管理器位于固定的节点地址0。发布服务将名称和 Binder 令牌发送到上下文管理器,然后,当客户端需要通过名称查找服务时,他们会检查 Binder 节点 0 并将名称解析为 Binder 令牌。Binder 令牌是这个地址(或 ID)的正确名称,它唯一地标识了一个 Binder 接口。客户端绑定到实现 Binder 接口的进程后,这些进程就会使用已建立的 Binder 协议执行 Binder 事务。此协议允许类似于方法调用的同步事务。
由于 Binder 是一个内核驱动,因此它具有一些确定跨接口可以执行操作的良好特性。首先,它允许传输文件描述符。它还管理一个线程池,用于分派服务方法。此外,它采用了一种称为零拷贝的方法,即 Binder 在进程间不复制任何事务数据...而是共享它们。Binder 还支持对象的引用计数,并允许服务查询客户端应用程序的 Linux 凭证,如 UID、GID 和进程 ID(PID)。Binder 还允许服务和客户端通过其链接到死亡功能知道对方何时终止。
在 Android 系统中,通常情况下,我们不会直接与 Binder 交互。相反,我们通过服务和它的Android 接口描述语言(AIDL)接口来与 Service 交互。最后一章将提供关于 AIDL 在实际中应用的详细示例,以用于我们的自定义 Android 系统的 SE,但在此期间,以下是一个简单的 AIDL 接口示例,它提供了远程进程执行getAccountName()和putAccountName()函数的方法:
package com.example.sample;
interface IRemoteInterface {
String getAccountName();
boolean putAccountName(in String name);
}
使用 AIDL 接口的优美之处在于,它用于生成大量代码来管理数据和进程,而这些工作否则需要手动完成。例如,以下是从前面 AIDL 示例生成的代码的一小部分:
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getAccountName:
{
data.enforceInterface(DESCRIPTOR);
java.lang.String _result = this.getAccountName();
reply.writeNoException();
reply.writeString(_result);
return true;
}
case TRANSACTION_putAccountName:
{
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
...
Binder 与安全
Binder 的安全含义非常重大。你应该能够控制谁成为上下文管理器,因为恶意上下文管理器可能会通过将客户端发送到恶意服务而非适当服务来破坏整个系统。除此之外,你可能还想要控制哪些客户端可以绑定到哪些 Binder 对象。最后,你可能还希望控制是否可以通过 Binder 发送文件描述符。Binder 还允许某人通过接口伪造凭证,这种设计是为了好的用途。例如,一些特权系统进程,如活动管理服务(AMS),代表其他进程执行操作。在这种伪装中暴露的凭证是你正在为其工作的进程的凭证,而不是特权实体的凭证。这类似于授权委托,当有人代表你行事时使用。
安卓的 Binder IPC 机制传统上由 DAC 权限控制。然而,正如我们在第一章,Linux 访问控制中所看到的,这些权限有一些缺陷。因此,需要修改 Binder 以支持 SELinux,因为否则 Binder 驱动程序不会实现任何附加安全模块的钩子。为此,斯蒂芬·斯马利向谷歌发送了一个补丁,实现了这些功能。该补丁为被称为Linux 安全模块(LSM)框架的消费者实现了新的钩子。这个框架允许 LSMs 如 SELinux 被调用,然后做出访问决策。这个补丁的细节超出了本书的范围。重要的是 Binder 被打了补丁,现在 SELinux 可以使用 MAC 控制其功能。
注意
斯蒂芬·斯马利是美国国家安全局(NSA)的可信系统研究组织的一名计算机安全研究员,并领导着 SE Android 项目。他发送给谷歌以修改 Binder 以支持 SELinux 钩子的补丁可以在这里查看。
由于 SELinux 和 Binder 的集成,SE for Android 有一个带有访问向量(一种花哨的说法,即“它能执行的操作”)的附加类别。在之前的例子中,来自第二章,强制访问控制和 SELinux,目标类别是food。类似地,Binder 的 SELinux 类别是binder。它定义了下面列举的访问向量。如果你还记得,第二章中food的访问向量是eat。以下是为 Binder 可用的访问向量:
-
impersonate:这会在 Binder 接口上创建伪造凭证 -
call:这会将客户端绑定到一个 Binder 接口,并使用它 -
set_context_mgr:这会设置上下文管理器 -
transfer:这用于传输一个文件描述符
Zygote – 应用程序孵化
在 Android 中,非原生应用程序传统上使用 Dalvik 虚拟机(VM)并运行称为 DEX 的专有字节码。应用程序还通过称为分叉和专化的机制,从一个名为 zygote 的公共进程孵化而来。Zygote 本身是一个进程,其中加载了 Dalvik VM 和一些公共类,如java.util.*。从 zygote 到执行某些应用程序代码的 zygote 子进程的机制称为分叉和专化。
注意
自从 Android 4.4 版本以来,Android 正在用Android 运行时(ART)替换这个。据推测,Android L 将完全不用 Dalvik VM。
这个过程的第一部分涉及一个套接字连接。Zygote 通过这个套接字监听应用程序的孵化请求。一些参数包括应该加载的应用程序的包名,以及一个表示应用程序是否为系统服务器的标志。一旦接收到孵化命令,就可以进行分叉。
注意
跟踪这个初始套接字连接的一个很好的方式是使用app_process工具。这个命令以 Dalvik 启动一个进程。更多信息,请导航到frameworks/base/cmds/app_process/app_main.cpp。
分叉之后,现在的父 zygote 将返回监听套接字以接收更多请求。子进程正在执行,需要发生几件事情。首先需要发生的是 UID 和 GID 的切换。Zygote 以 root 的 UID 运行,为了符合 Android 的安全模型,它必须将子进程的 UIDs 和 GIDs 设置为非 root 的其他值。子进程将根据包管理器和补充 GIDs 定义设置 UID 和 GID。它还设置了进程的资源限制和调度策略。然后它将应用程序的权限集清零(无权限)。在系统服务器的情况下,权限集不是被清除,而是设置为通过套接字发送的参数之一。在此之后,子进程开始运行。Zygote 中更靠后的代码加载类,以及系统交互的其他部分,如意图传递,用于启动一个活动。这些部分超出了本书的范围。
属性服务
安卓系统中的属性服务提供了一个在所有进程之间共享的键值对映射。所有进程共享一部分专门用于此系统的内存页面。然而,所有进程中的映射都是只读的,除了 init 进程,它有读写映射。属性服务系统驻留在 init 中,这个系统的工作就是更新或添加键值映射中的值。若要更改一个值,必须通过属性服务,但任何人都可以读取一个值。务必注意,如果你使用属性服务,不要存储敏感信息。它主要旨在用于小数据值,而不是通用的大值存储。以下是属性服务的一个非常基础的介绍。稍后将会进行更彻底的调查。
要设置属性,必须通过 Unix 域套接字向属性服务发送请求。属性服务将解析请求,并在权限允许的情况下设置值。属性具有以句点分隔的段,如包名,在构建时静态分配权限。权限和属性服务代码可以在system/core/property_service.c一起找到。这个接口预期的参数包括一个命令、属性名称和属性值。对于那些好奇的人,这些都在prop_msg结构中定义,该结构在bionic/libc/include/sys/_system_properties.h中定义。收到消息后,属性服务会检查对等套接字的凭据与静态权限映射是否一致。如果 UID 是 root,它可以写入任何内容,否则它必须是 UID 或 GID 的匹配项。在非常新的安卓版本中,或者应用了来自android-review.googlesource.com/#/c/98428/补丁的版本,权限检查和硬编码的 DAC 已经被 SELinux 控制所取代。
由于设置值的权限是由用户空间使用 DAC 控制的,因此属性设置机制共享了固有的 rooting 漏洞缺陷。考虑到这一点,在 SELinux 中增强了属性服务代码。由于这是一个用户空间进程,它通过内核使用 SELinux API 来编程一个称为用户空间对象管理器的东西。这意味着用户空间应用程序会检查内核中的 SELinux,以确保它可以执行某项活动……在这种情况下,就是设置属性。
总结
安卓拥有一些非常独特的特性。从使用通用的 UID 和 GID 模型来提升其安全目标,到其自定义的 binder IPC 机制,这些系统对设备的安全性和功能性都有影响。在下一章,当我们让 UDOOUDOO 运行并启用其上的 Android SE 时,这些系统将再次发挥作用。
第四章:在 UDOO 上的安装
为了继续我们的探索,我们需要建立一个实际的系统来操作。在本章中,我们将:
-
从源代码为 UDOO 构建 Android 4.3
-
使用我们的启动镜像刷写 SD 卡
-
在捕获日志的同时让 UDOO 运行
-
建立与 UDOO 的
adb连接 -
重新构建带有 SELinux 支持的内核
-
验证我们的 SELinux UDOO 镜像是否按预期工作
我们将从公开可用的 UDOO Android 4.3 Jelly Bean 源代码开始,可以从www.udoo.org/downloads/下载。假设你已经有一个 UDOO 并确认它是可用的。建议你按照 UDOO 网站上的说明,使用 Android 4.3 预构建的镜像作为初步测试(更多信息,请参考www.udoo.org/getting-started/)。
你还需要一个适合使用 Android 和 UDOO 的开发系统,但这个细节超出了本章的范围。附录中提供了一个标准 Ubuntu Linux 12.04 系统的设置详情,以确保你有最大的可能性成功复制本书中的工作。
获取源代码
让我们从下载前文给出的链接中的 Android 4.3 Jellybean 源代码开始这项练习,并使用以下命令将下载的文件解压到工作空间中:
$ mkdir ~/udoo && cd ~/udoo
$ tar -xavf ~/Downloads/UDOO_Android_4.3_Source_v2.0.tar.gz
完成这些后,你应该查看以下 URL 上的 UDOO 文档和 Android 源代码构建说明:
前一个 URL 提供的说明讨论了如何使用 Open JDK 7 构建 Android。然而,这些说明适用于当前发布的 Android(L 预览版)并不完全相关。对于 Android 4.3,你必须使用 Oracle Java 6 进行构建,Oracle 已经将 Java 6 归档,可以在www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-javase6-419409.html找到。
假设你已经有了附录中详细描述的系统的副本,开发环境。该附录,除了其他事项,还指导你设置 Oracle Java 6 作为你唯一的 Java 实例。然而,对于那些希望从现有系统中工作的人,特别是那些拥有多个 Java SDK 的人,请记住,在阅读本书的其余部分时,你需要确保你的系统在使用 Oracle Java 6 工具。
通过切换到你的 UDOO 源代码树的根目录并执行以下命令来完成环境设置:
$ . setup udoo-eng
配置完环境后,我们需要构建bootloader:
$ cd bootable/bootloader/uboot-imx
$ ./compile.sh -c
将会出现一个图形菜单。确保设置如下:
-
DDR 大小:选择 1 吉字节,总线大小 64,激活 CS \ 1(256Mx4)
-
主板类型:选择 UDOO
-
CPU 类型:根据你拥有的系统选择四核或双核选项。我们碰巧使用的是四核系统。
-
操作系统类型:选择Android
-
环境设备:必须选择SD/MMC
-
额外选项:应选择清理(CLEAN)
-
编译器选项:在这里可以选择工具链的路径;只需采用默认设置
下面的截图展示了前一个命令显示的图形菜单:
退出时,请确保保存。然后开始编译:
$ ./compile.sh
Board type selected: UDOO
CPU Type: QUAD/DUAL
OS type: Android
...
/home/bookuser/udoo/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/bin/arm-eabi-objcopy -O srec u-boot u-boot.srec
/home/bookuser/udoo/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/bin/arm-eabi-objcopy --gap-fill=0xff -O binary u-boot u-boot.bin
为了保险起见,使用ls u-boot.bin验证你的构建是否成功,以确保现在存在bootloader镜像。现在,使用以下命令构建 Android:
$ croot
$ make –j4 2>&1 | tee logz
第一个命令是 Android 设置脚本中引入的内容,可以让我们返回到项目树的根目录。第二个命令make构建系统。大多数情况下,你应该将j的选项设置为 CPU/核心数的两倍。由于你们许多人可能使用的是双核机器,我们将使用–j4。例如,这本书的其中一位作者使用 8 个 CPU 核心,并使用-j16标志。文件重定向和tee命令将构建输出捕获到一个文件中。这对于帮助调试构建问题非常重要。这个构建过程,根据你的系统,可能需要很长时间。在之前提到的 8 核系统(16GB 内存)上,这需要超过 35 分钟。在其他系统上,我们经历过超过 3 小时的构建时间。
在这种情况下,捕获日志证明非常有用。构建以错误结束,通过搜索日志中的error,我们找到了以下内容:
$ grep error logz
...
external/mtd-utils/mkfs.ubifs/mkfs.ubifs.h:48:23: fatal error: uuid/uuid.h: No such file or directory
external/mtd-utils/mkfs.ubifs/mkfs.ubifs.h:48:23: fatal error: uuid/uuid.h: No such file or directory
external/mtd-utils/mkfs.ubifs/mkfs.ubifs.h:48:23: fatal error: uuid/uuid.h: No such file or directory
...
通过评估这些错误,我们发现缺少了uuid和lzo1x的头文件。我们还可以打开 Android 的 makefile,external/mtd-utils/mkfs.ubifs/Android.mk,从行LOCAL_LDLIBS:= -lz -llzo2 -lm -luuid -m64确定可能涉及的库。搜索揭示了我们缺少的特定 Ubuntu 包;我们将安装它们并重新构建。搜索字符串末尾的$字符确保我们只得到以uuid/uuid.h结尾的结果。没有它,我们可能会匹配以.html或.hpp结尾的文件:
"
uuid-dev: /usr/include/uuid/uuid.h
$ sudo apt-get install uuid-dev
$ make –j4 2>&1 | tee logz
成功的构建应该产生一些类似以下的最终输出:
...
Running: mkuserimg.sh out/target/product/udoo/system out/target/product/udoo/obj/PACKAGING/systemimage_intermediates/system.img ext4 system 293601280 out/target/product/udoo/root/file_contexts
Install system fs image: out/target/product/udoo/system.img
out/target/product/udoo/system.img+out/target/product/udoo/obj/PACKAGING/recovery_patch_intermediates/recovery_from_boot.p maxsize=299747712 blocksize=4224 total=294120167 reserve=3028608
在 SD 卡上刷新镜像
当bootloader、Android 用户空间和 Linux 内核构建完成后,是时候插入 SD 卡并刷入镜像了。将 SD 卡插入你的主机电脑,并确保它未被挂载。在 Ubuntu 中,可移动媒体会被自动挂载,因此你需要找到你的 U 盘的/dev/sd*设备,并执行umount命令。在本文剩余部分,我们将使用/dev/sdd作为 U 盘,但重要的是要使用适合你系统的正确设备。如果你之前使用这张 SD 卡安装过 UDOO,这张卡将包含多个分区,所以你可能会看到多次挂载/dev/sdd<num>:
$ mount | grep sdd
/dev/sdd7 on /media/vender type ext4 (rw,nosuid,nodev,uhelper=udisks)
/dev/sdd4 on /media/data type ext4 (rw,nosuid,nodev,uhelper=udisks)
/dev/sdd5 on /media/57f8f4bc-abf4-655f-bf67-946fc0f9f25b type ext4 (rw,nosuid,nodev,uhelper=udisks)
/dev/sdd6 on /media/cache type ext4 (rw,nosuid,nodev,uhelper=udisks)
$ sudo bash -c "umount /dev/sdd4 && umount /dev/sdd5 && umount /dev/sdd6 && umount /dev/sdd7"
一旦 SD 卡被正确卸载,我们可以刷入我们的镜像:
$ sudo -E ./make_sd.sh /dev/sdd
提示
你必须在sudo中使用-E参数以保留 Android 构建中导出的所有变量。你必须处于构建 Android 的同一个终端会话中。否则你会看到错误No OUT export variable found! Setup not called in advance…。
完成此操作后(这将需要一段时间),重要的是使用命令sudo sync将块设备缓存刷新回磁盘。然后,你可以取出 SD 卡,将其插入 UDOO 并启动!
UDOO 串行和 Android 调试桥
既然 UDOO 正在启动到 Android,我们希望确保我们也能通过串行端口以及Android 调试桥(adb)访问它。你需要适合你系统的 UDOO 串行驱动程序。有关 Mac、Linux 和 Windows 的详细信息可以在
www.udoo.org/ProjectsAndTutorials/connecting-via-serial-cable/。
串行端口是系统将使用的第一种通信方式,它由bootloader初始化。它是调试你稍后可能遇到的任何内核或系统问题的关键链接。它还用于配置 USB 端口,以便通过 CN3(UDOO 上的 USB OTG 端口)进行adb连接。为了配置端口,我们需要配置并使用 minicom 将 shell 连接到设备。首先,将一根 micro USB 线从 CN6(靠近电源按钮的 micro USB 端口)连接到主机。接下来,让我们通过查看dmesg中的 TTY 通过 USB 的连接信息来查找串行连接。
$ sudo dmesg | tail -n 5
[ 9019.090058] usb 4-1: Manufacturer: Silicon Labs
[ 9019.090061] usb 4-1: SerialNumber: 0078AEDB
[ 9019.096089] cp210x 4-1:1.0: cp210x converter detected
[ 9019.208023] usb 4-1: reset full-speed USB device number 4 using uhci_hcd
[ 9019.359172] usb 4-1: cp210x converter now attached to ttyUSB0
我们的 TTY 终端在最后一行。让我们通过它使用minicom进行连接:
$ sudo minicom -sw
选择串行端口设置,输入a,将串行设备更改为/dev/ttyUSB0,并输入f以关闭硬件流控制:
要退出,请按回车键,选择保存设置和 DFL,然后选择从 Minicom 退出,并按回车键。现在运行minicom以连接到你的 UDOO,并观察它启动:
$ sudo minicom -w
如果设备启动并运行,你将得到一个友好的 root shell:
如果它正在启动,你会看到日志。只需等待 root shell 提示:
现在我们需要翻转一些 GPIO 引脚,将 CN3 micro USB 设置为调试模式:
root@udoo:/ # echo 0 > /sys/class/gpio/gpio203/value
root@udoo:/ # echo 0 > /sys/class/gpio/gpio128/value
然后,通过移除并重新插入 J16 跳线,重置使用该总线的 SAM3X8E 处理器。现在从宿主到 CN3 连接一根 micro USB 线缆。你现在应该能看到一个 USB 设备以及adb:
$ lsusb
Bus 001 Device 009: ID 18d1:4e42 Google Inc.
$ adb devices
List of devices attached
0123456789ABCDEF offline
当 UDOO Android 端出现提示时,你需要选择允许 USB 调试。当你这样做时,设备应该从离线状态变为在线状态;这样你就可以使用adb。
现在测试连接并通过adb获取截图:
$ adb shell
root@udoo:/ #
$ adb shell screencap -p | perl -pe 's/\x0D\x0A/\x0A/g' > screen.png
这是一张截图:
在此阶段,我们拥有了一个可用的开发系统。我们通过串行控制台拥有了早期的启动日志和救援 shell。我们还拥有一个adb桥接,通过它我们可以使用标准的 Android 调试工具!现在要做的就是用 SELinux 来增强这个系统的安全性!
翻转开关
既然我们现在要在 UDOO 上启用 SELinux,我们需要确认它没有被开启。做到这一点的方法是检查/proc文件系统中的已知filesystem类型。SELinux 有自己的伪文件系统,所以如果它被启用了,我们应该能在列表中看到它:
$ adb shell cat /proc/filesystems
nodev sysfs
nodev rootfs
nodev bdev
nodev proc
nodev cgroup
nodev cpuset
nodev tmpfs
nodev debugfs
nodev sockfs
nodev pipefs
nodev anon_inodefs
nodev rpc_pipefs
nodev devpts
ext3
ext2
ext4
cramfs
nodev ramfs
vfat
msdos
nodev nfs
nodev jffs2
nodev fuse
fuseblk
nodev fusectl
nodev mtd_inodefs
nodev ubifs
这里没有发现 SELinux 的踪迹,因此让我们找到内核配置并将其开启。从~/udoo/kernel_imx目录执行这个命令,最终你会看到一个图形化编辑界面:
$ make menuconfig
首先,你需要启用审计支持,因为这是 SELinux 的依赖项。在通用设置 | 审计支持下,启用审计支持和启用系统调用审计。使用上下箭头键来高亮一个条目,并按空格键启用它。当一个项目被启用时,你会在它旁边看到一个星号(*****):
通过选择退出回到主菜单...这并不是很直观。进入文件系统菜单,对于三个文件系统中的每一个——Ext2、Ext3和Ext4,确保启用了扩展属性和安全标签。然后,通过选择退出回到主菜单:
从那个屏幕退出回到主菜单,然后转到安全选项。一旦进入安全选项子菜单,启用启用不同的安全模型和套接字和网络安全性钩子选项:
启用这些之后,会出现更多选项。启用NSA SELinux 支持并确保从以下截图中复制其他的选择和值:
最后,将默认安全模块设置为 SELinux:
一旦你选择默认安全模块,一个新的窗口将出现,从中你可以选择SELinux。通过选择退出退出配置菜单,直到你被要求保存新的配置:
保存新的配置并将这些更改写入原始内核配置文件。否则,在后续构建时它将被覆盖。为此,我们需要找出在默认构建中使用了哪个配置文件,这是我们之前在使用make menuconfig制作我们自己的配置之前构建的:
$ grep defconfig logz make -C kernel_imx imx6_udoo_android_defconfig ARCH=arm CROSS_COMPILE=`pwd`/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/bin/arm-eabi-
你可以看到imx6_udoo_android_defconfig被用作默认配置。复制你的自定义配置并再次构建:
$ cp .config arch/arm/configs/imx6_udoo_android_defconfig
$ croot
$ make –j4 bootimage 2>&1 | tee logz
快速检查日志文件以验证 SELinux 实际上是否已构建到内核中,这总是一个好主意:
$ grep -i selinux logz
HOSTCC scripts/selinux/mdp/mdp
HOSTCC scripts/selinux/genheaders/genheaders
GEN security/selinux/flask.h security/selinux/av_permissions.h
CC security/selinux/avc.o
...
现在,使用支持 SELinux 的构建内核,将 SD 卡插入主机并运行以下命令:
$ sudo -E ./make_sd.sh /dev/sdd
$ sudo sync
提示
不要忘记像之前一样从 SD 卡卸载任何自动挂载的分区。
将 SD 卡插入 UDOO 并启动它。你应该会像之前一样在串行控制台看到日志:
最终,串行连接应该能让我们进入根 shell。
它是活的
我们如何知道我们已经成功在内核中启用了 SELinux?在本章早些时候,你运行了命令adb shell cat /proc/filesystems。我们将做同样的事情并寻找一个名为selinuxfs的新文件系统。如果它存在,那就表明我们已经成功启用了 SELinux。在串行终端运行以下命令:
# cat /proc/filesystems | grep selinux
nodev selinuxfs
我们可以看到selinuxfs是存在的!另一种常见的做法是检查dmesg中是否有任何 SELinux 的输出。为此,通过串行终端执行以下命令:
# dmesg | grep -i selinux
<6>SELinux: Initializing.
<7>SELinux: Starting in permissive mode
<7>SELinux: Registering netfilter hooks
<3>SELinux: policydb version 26 does not match my version range 15-23
<4>SELinux: Could not load policy: Invalid argument
概述
这是一个非常令人兴奋的章节。你学会了如何在内核配置中启用 SELinux,启动“安全”系统,以及如何验证其存在。我们还了解了如何为 UDOO 刷写和构建通用镜像以及如何通过串行和adb连接到它。在接下来的章节中,我们将重点介绍如何使用 SE for Android 功能使 UDOO 可用。
第五章:系统启动
既然我们已经有了适用于 Android 系统的安全增强(SE),我们需要了解如何使用它,并将其置于可用状态。在本章中,我们将:
-
修改日志级别以在调试时获取更多详细信息
-
跟踪与策略加载相关的启动过程
-
调查 SELinux API 和 SELinuxFS
-
更正最大策略版本号的问题
-
应用补丁以加载和验证 NSA 策略
你可能在 第四章,UDOO 上的安装 中注意到一些令人不安的错误信息 dmesg。为了刷新你的记忆,以下是一些错误信息:
# dmesg | grep –i selinux
<6>SELinux: Initializing.
<7>SELinux: Starting in permissive mode
<7>SELinux: Registering netfilter hooks
<3>SELinux: policydb version 26 does not match my version range 15-23
...
即使启用了 SELinux,看起来我们的系统仍然不是没有错误的。在这一点上,我们需要了解是什么原因导致了这个错误,以及我们可以做些什么来纠正它。在本章结束时,我们应该能够识别 SE for Android 设备在策略加载方面的启动过程,以及如何将策略加载到内核中。然后,我们将解决策略版本错误。
策略加载
Android 设备遵循类似于 *NIX 启动序列的启动顺序。引导加载程序启动内核,内核最终执行 init 进程。init 进程负责通过 init 脚本和守护程序中的一些硬编码逻辑来管理设备的启动过程。与所有进程一样,init 在 main 函数有一个入口点。这是第一个用户空间进程开始的地方。通过导航到 system/core/init/init.c 可以找到代码。
当 init 进程进入 main(参考以下代码摘录)时,它会处理 cmdline,挂载一些 tmpfs 文件系统,如 /dev,以及一些伪文件系统,如 procfs。对于 Android 设备的 SE,init 被修改为尽可能在启动过程的早期加载策略到内核中。在 SELinux 系统中,策略不是构建到内核中的;它位于一个单独的文件中。在 Android 中,早期启动时挂载的唯一文件系统是根文件系统,它是构建到 boot.img 中的 ramdisk。策略可以在 UDOO 或目标设备上的根文件系统中找到,位于 /sepolicy。此时,init 进程调用一个函数从磁盘加载策略并将其发送到内核,如下所示:
int main(int argc, char *argv[]) {
...
process_kernel_cmdline();
unionselinux_callback cb;
cb.func_log = klog_write;
selinux_set_callback(SELINUX_CB_LOG, cb);
cb.func_audit = audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);
INFO("loading selinux policy\n");
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");
}
…
在前面的代码中,你会注意到一个非常友好的日志信息,SELinux: Disabled due to failed policy load,并想知道为什么我们在之前运行 dmesg 时没有看到这个信息。这段代码在 init.rc 中的 setlevel 执行之前执行。
默认的 init 日志级别由 system/core/include/cutils/klog.h 中 KLOG_DEFAULT_LEVEL 的定义设置。如果我们真的想改变它,我们可以修改它,重新构建,并实际看到那条信息。
既然我们已经确定了策略加载的初始路径,那么让我们跟随它通过系统的过程。selinux_android_load_policy()函数可以在 Android 版本的libselinux中找到,位于 UDOObian 源树中。该库可以在external/libselinux中找到,所有 Android 的修改都可以在src/android.c中找到。
函数首先挂载一个名为SELinuxFS的伪文件系统。如果您回想一下,这是我们在第四章,在 UDOObian 上的安装中看到的/proc/filesystems中提到的新文件系统之一。在没有挂载sysfs的系统上,挂载点是/selinux;在挂载了sysfs的系统上,挂载点是/sys/fs/selinux。
您可以使用以下命令在运行中的系统上检查mountpoints:
# mount | grep selinuxfs
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
SELinuxFS 是一个重要的文件系统,因为它提供了内核与用户空间之间控制和管理 SELinux 的接口。因此,为了使策略加载工作,必须挂载它。策略加载使用文件系统将策略文件字节发送到内核。这发生在selinux_android_load_policy()函数中:
int selinux_android_load_policy(void)
{
char *mnt = SELINUXMNT;
int rc;
rc = mount(SELINUXFS, mnt, SELINUXFS, 0, NULL);
if (rc < 0) {
if (errno == ENODEV) {
/* SELinux not enabled in kernel */
return -1;
}
if (errno == ENOENT) {
/* Fall back to legacy mountpoint. */
mnt = OLDSELINUXMNT;
rc = mkdir(mnt, 0755);
if (rc == -1 && errno != EEXIST) {
selinux_log(SELINUX_ERROR,"SELinux: Could not mkdir: %s\n",
strerror(errno));
return -1;
}
rc = mount(SELINUXFS, mnt, SELINUXFS, 0, NULL);
}
}
if (rc < 0) {
selinux_log(SELINUX_ERROR,"SELinux: Could not mount selinuxfs: %s\n",
strerror(errno));
return -1;
}
set_selinuxmnt(mnt);
return selinux_android_reload_policy();
}
set_selinuxmnt(car *mnt)函数改变了libselinux中的一个全局变量,以便其他例程可以找到这个重要接口的位置。从那里它调用了另一个辅助函数selinux_android_reload_policy(),该函数位于相同的libselinux android.c文件中。它按优先顺序遍历一个可能的策略位置数组。这个数组定义如下:
Static const char *const sepolicy_file[] = {
"/data/security/current/sepolicy",
"/sepolicy",
0 };
由于此时只挂载了根文件系统,因此它选择了/sepolicy。其他路径用于策略的动态运行时重新加载。在获取到策略文件的有效的文件描述符后,系统将其内存映射到它的地址空间,并调用security_load_policy(map, size)将其加载到内核中。这个函数定义在load_policy.c中。这里,map 参数是指向策略文件开头的指针,size 参数是文件的大小(以字节为单位):
int selinux_android_reload_policy(void)
{
int fd = -1, rc;
struct stat sb;
void *map = NULL;
int i = 0;
while (fd < 0 && sepolicy_file[i]) {
fd = open(sepolicy_file[i], O_RDONLY | O_NOFOLLOW);
i++;
}
if (fd < 0) {
selinux_log(SELINUX_ERROR, "SELinux: Could not open sepolicy: %s\n",
strerror(errno));
return -1;
}
if (fstat(fd, &sb) < 0) {
selinux_log(SELINUX_ERROR, "SELinux: Could not stat %s: %s\n",
sepolicy_file[i], strerror(errno));
close(fd);
return -1;
}
map = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) {
selinux_log(SELINUX_ERROR, "SELinux: Could not map %s: %s\n",
sepolicy_file[i], strerror(errno));
close(fd);
return -1;
}
rc = security_load_policy(map, sb.st_size);
if (rc < 0) {
selinux_log(SELINUX_ERROR, "SELinux: Could not load policy: %s\n",
strerror(errno));
munmap(map, sb.st_size);
close(fd);
return -1;
}
munmap(map, sb.st_size);
close(fd);
selinux_log(SELINUX_INFO, "SELinux: Loaded policy from %s\n", sepolicy_file[i]);
return 0;
}
安全加载策略会打开<selinuxmnt>/load文件,在我们的案例中是/sys/fs/selinux/load。在这个阶段,策略通过这个伪文件写入到内核中:
int security_load_policy(void *data, size_t len)
{
char path[PATH_MAX];
int fd, ret;
if (!selinux_mnt) {
errno = ENOENT;
return -1;
}
snprintf(path, sizeof path, "%s/load", selinux_mnt);
fd = open(path, O_RDWR);
if (fd < 0)
return -1;
ret = write(fd, data, len);
close(fd);
if (ret < 0)
return -1;
return 0;
}
修复策略版本
在这一点上,我们对如何将策略加载到内核中有了清晰的认识。这非常重要。SELinux 与 Android 的集成始于 Android 4.0,因此在移植到各种分支和片段时,这会中断,并且经常缺少代码。然而,理解系统的所有部分,无论多么粗略,都将帮助我们在野外遇到问题时进行纠正和发展。这些信息也有助于理解整个系统,因此当需要修改时,您将知道在哪里查找以及事物是如何工作的。在这一点上,我们准备纠正策略版本。
日志和内核配置都很清楚;只支持到 23 的策略版本,我们试图加载 26 的策略版本。这可能是 Android 的一个常见问题,因为内核往往过时。
Google 提供的 4.3 版本 sepolicy 也存在问题。Google 的一些更改使得配置设备变得更加困难,因为他们调整了策略以满足发布目标。实际上,该策略几乎允许所有操作,因此生成的拒绝日志非常少。策略中的一些域通过每个域的宽容声明完全宽容,这些域也有规则允许所有操作,因此不会生成拒绝日志。为了纠正这个问题,我们可以使用来自 NSA 的更完整的策略。将external/sepolicy替换为从bitbucket.org/seandroid/external-sepolicy/get/seandroid-4.3.tar.bz2下载的内容。
提取 NSA 的策略后,我们需要更正策略版本。策略位于external/sepolicy中,并使用一个名为check_policy的工具进行编译。sepolicy 的Android.mk文件将不得不将此版本号传递给编译器,因此我们可以在这里进行调整。在文件顶部,我们找到了罪魁祸首:
...
# Must be <= /selinux/policyvers reported by the Android kernel.
# Must be within the compatibility range reported by checkpolicy -V.
POLICYVERS ?= 26
...
由于该变量可以通过?=赋值被覆盖,我们可以在BoardConfig.mk中重写这个设置。编辑device/fsl/imx6/BoardConfigCommon.mk,在文件底部添加以下POLICYVERS行:
...
BOARD_FLASH_BLOCK_SIZE := 4096
TARGET_RECOVERY_UI_LIB := librecovery_ui_imx
# SELinux Settings
POLICYVERS := 23
-include device/google/gapps/gapps_config.mk
由于策略在boot.img镜像中,因此需要构建策略和bootimage:
$ mmm -B external/sepolicy/
$ make –j4 bootimage 2>&1 | tee logz
!!!!!!!!! WARNING !!!!!!!!! VERIFY BLOCK DEVICE !!!!!!!!!
$ sudo chmod 666 /dev/sdd1
$ dd if=$OUT/boot.img of=/dev/sdd1 bs=8192 conv=fsync
弹出 SD 卡,将其插入 UDOО,并启动。
提示
前述命令中的第一条应该产生如下日志输出:
out/host/linux-x86/bin/checkpolicy: writing binary representation (version 23) to out/target/product/udoo/obj/ETC/sepolicy_intermediates/sepolicy
在这一点上,通过使用dmesg检查 SELinux 日志,我们可以看到以下内容:
# dmesg | grep –i selinux
<6>init: loading selinux policy
<7>SELinux: 128 avtab hash slots, 490 rules.
<7>SELinux: 128 avtab hash slots, 490 rules.
<7>SELinux: 1 users, 2 roles, 274 types, 0 bools, 1 sens, 1024 cats
<7>SELinux: 84 classes, 490 rules
<7>SELinux: Completing initialization.
我们还需要运行的另一个命令是getenforce。getenforce命令获取 SELinux 的强制状态。它可能处于三种状态之一:
-
禁用: 没有加载策略或没有内核支持
-
宽容: 加载了策略,设备记录拒绝操作(但不在强制模式)
-
强制: 这个状态与宽容状态类似,不同之处在于策略违规会导致 EACCESS 返回给用户空间
在启动 SELinux 系统时,其中一个目标就是达到强制(enforcing)状态。调试时使用宽容(permissive)模式,如下所示:
# getenforce
Permissive
概述
在本章中,我们介绍了通过 init 进程加载重要策略的工作流程。我们还更改了策略版本以适应我们的开发努力和内核版本。从那里,我们能够加载 NSA 策略并验证系统已加载它。本章还展示了一些 SELinux API 及其与 SELinuxFS 的交互。在下一章中,我们将检查文件系统,然后继续努力将系统设置为强制模式。
第六章:探索 SELinuxFS
在前面的几章中,我们看到 SELinuxFS 在许多场合出现。从它在 /proc/filesystems 中的条目到 init 守护进程中的策略加载,在启用了 SELinux 的系统中经常使用。SELinuxFS 是内核到用户空间的接口,也是构建更高用户空间习惯用法和 libselinux 的基础。在本章中,我们将探索这个文件系统的功能,以更深入地了解系统的工作原理。具体来说,我们将:
-
确定如何找到 SELinux 文件系统的挂载点
-
提取有关我们当前 SELinux 系统状态的信息
-
在 shell 中即时修改我们的 SELinux 系统状态,并通过代码进行修改
-
调查 ProcFS 接口
定位文件系统
我们需要做的第一件事是定位文件系统的挂载点。libselinux 在两个地方之一挂载文件系统:默认为 /selinux 或 /sys/fs/selinux。然而,这不是一个严格的要求,可以通过调用 void set_selinuxmnt(char *mnt) 来更改,它设置 SELinux 挂载点的位置。然而,在大多数情况下,这应该发生,不需要任何调整。
在系统中找到挂载点的最佳方式是运行 mount 命令并找到文件系统的位置。在串行控制台,发出以下命令:
root@udoo:/ # mount | grep selinux
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
如你所见,挂载点是 /sys/fs/selinux。让我们通过在串行终端提示符下发出以下命令,前往那个目录:
root@udoo:/ # cd /sys/fs/selinux
root@udoo:/sys/fs/selinux #
你现在处于 SELinux 文件系统的根目录。
询问文件系统
你可以询问 SELinuxFS 以找出内核支持的最高策略版本是什么。当你开始使用不是从源代码构建的系统时,这很有用。当你没有直接访问 KConfig 文件时也很有用。需要注意的是,DAC 和 MAC 权限都适用于此文件系统。关于 MAC 和 SELinux,这方面的访问向量在策略文件 external/sepolicy/access_vectors 中的 security 类别中枚举:
root@udoo:/sys/fs/selinux # echo 'cat policyvers'
23
提示
在前面的命令中,以及接下来几个命令中,我们不仅仅使用 cat 命令打印文件。这是因为这些文件在文件末尾没有换行符。没有换行符,命令执行后的命令提示符将位于输出最后一行的末尾。将 cat 命令用 echo 包裹可以保证有换行符。获取同样效果的另一种方法是使用 cat policyvers ; echo。
正如我们所预期的,支持的版本是 23。正如你所记得的,我们在 第四章 在 UDOO 上安装 中配置内核以使用 make menuconfig 启用 SELinux 时设置了此值,从 kernel_imx 目录中。这也可以通过 libselinux API 访问:
int security_policyvers(void);
它不应需要任何提升的权限,系统上的任何人都可以读取。
强制节点
在前面的章节中,我们讨论了 SELinux 在两种模式下运行,强制和宽容。这两种模式都会记录策略违规,但是强制模式会导致内核拒绝访问资源,并向调用用户空间进程返回错误(例如,EACCESS)。SELinuxFS 有一个接口来查询此状态——文件节点enforce。从该文件读取会根据我们是运行在宽容模式还是强制模式,返回状态0或1:
root@udoo:/sys/fs/selinux # echo 'cat enforce'
0
如你所见,我们的系统处于宽容模式。Android 有一个 toolbox 命令用于打印此状态。这个命令会根据我们是运行在宽容模式还是强制模式,返回Permissive或Enforcing状态:
root@udoo:/sys/fs/selinux # getenforce
Permissive
你也可以写入到enforce文件。此文件系统的 DAC 权限为:
Owner: root read, write
Group: root read
Others: read
任何人都可以获取强制状态,但要设置它,你必须要是 root 用户。进行此操作所需的 MAC 权限为:
class: security
vector: setenforce
一个名为setenforce的命令可以更改此状态:
root@udoo:/sys/fs/selinux # setenforce 0
要查看命令的作用,可以在strace中运行它:
root@udoo:/sys/fs/selinux # strace setenforce 0
...
open("/proc/self/task/3275/attr/current", O_RDONLY) = 4
brk(0x41d80000) = 0x41d80000
read(4, "u:r:init_shell:s0\0", 4095) = 18
close(4) = 0
open("/sys/fs/selinux/enforce", O_RDWR) = 4
write(4, "0", 1)
...
如我们所见,写入enforce的接口非常简单,只需写入0或1。在libselinux中执行此操作的功能是int security_setenforce(int value)。上述命令的另一个有趣之处是我们可以看到访问了procfs。SELinux 在procfs中也有一些额外的条目。这些将在本章中进一步介绍。
禁用文件接口
在运行时,也可以使用disable文件接口禁用 SELinux。但是,内核必须使用CONFIG_SECURITY_SELINUX_DISABLE=y进行构建。我们的内核没有使用此选项构建。此文件只能由所有者写入,并且没有与之关联的特定 MAC 权限。我们建议保持此选项禁用。此外,可以在加载策略之前禁用 SELinux。即使启用了该选项,一旦加载了策略,它也会被禁用。
策略文件
policy文件允许你读取当前加载到内核中的 SELinux 策略文件。这可以读取并保存到磁盘:
root@udoo:/sys/fs/selinux # cat policy > /sdcard/policy
通过启用adb接口,你现在可以从设备中提取它,并在宿主上使用标准的 SELinux 工具进行分析。此文件的 DAC 权限为所有者:root,read。对此文件没有特定的 SELinux 权限。
policy文件的对应文件是load文件。我们已经看到当通过libselinux API 加载策略文件时,会出现这个文件:
int security_load_policy(void *data, size_t len);
null文件
当域转换发生时,SELinux 使用null文件来重定向未授权的文件访问。记住,域转换是指从一种上下文转换到另一种上下文。在大多数情况下,这发生在程序执行 fork 和 exec 函数时,但也可能是程序化发生的。在任一情况下,进程都有无法再访问的文件引用,为了帮助防止进程崩溃,它们只需从 SELinux 空设备写入/读取。
mls文件
我们系统的一个功能是当前策略正在使用多级安全(MLS)支持。这是基于加载的策略文件是否使用它,要么是0要么是1。由于我们已经启用它,我们预计会从这个文件看到1:
root@udoo:/sys/fs/selinux # echo 'cat mls'
1
mls文件对所有用户可读,并有一个相应的 SELinux API:
int is_selinux_mls_enabled(void)
状态文件
version文件允许你了解 SELinux 内部发生的更新。例如,当策略重新加载时。一个用户空间对象管理器可以缓存决策结果,并使用reload事件作为触发器来刷新其缓存。status文件是只由所有人读取的,没有特定的 MAC 权限。libselinux API 接口是:
int selinux_status_open(int fallback);
void selinux_status_close();
int selinux_status_updated(void);
int selinux_status_getenforce(void);
int selinux_status_policyload(void);
int selinux_status_deny_unknown(void);
通过检查状态结构,你可以检测变化并刷新缓存。然而,目前你的libselinux中缺少这个 API,但我们在第七章,利用审计日志中会纠正这个问题。
文件树中有许多 SELinuxFS 文件;我们这里只介绍了几个文件,因为它们的重要性或与我们已做工作及未来方向的相关性。我们没有涵盖:
-
access -
checkreqprot -
commit_pending_bools -
context -
create -
deny_unknown -
member -
reject_unknown -
relabel
使用这些文件并不简单,通常是由使用libselinux API 的用户空间对象管理器来完成,以抽象化复杂性。
访问向量缓存
SELinuxFS 还有一些你可以探索的目录。第一个是avc。它代表“访问向量缓存”,可以用来获取内核中安全服务器统计信息:
root@udoo:/sys/fs/selinux # cd avc/
root@udoo:/sys/fs/selinux/avc # ls
cache_stats
cache_threshold
hash_stats
所有这些文件都可以使用cat命令读取:
root@udoo:/sys/fs/selinux/avc # cat cache_stats
lookups hits misses allocations reclaims frees
285710 285438 272 272 128 128
245827 245409 418 418 288 288
267511 267227 284 284 192 193
214328 213883 445 445 288 298
cache_stats文件对所有用户可读,不需要特殊的 MAC 权限。
下一个要查看的文件是hash_stats:
root@udoo:/sys/fs/selinux/avc # cat hash_stats
entries: 512
buckets used: 284/512
longest chain: 7
访问向量缓存的基础数据结构是一个哈希表;hash_stats列出了当前的属性。从前一条命令的输出中可以看出,表中我们有 512 个槽位,其中 284 个正在使用中。在冲突处理中,最长的链有 7 个条目。这个文件是全局可读的,不需要特殊的 MAC 权限。你可以通过cache_threshold文件修改此表中的条目数。
cache_threshold文件用于调整avc哈希表中的条目数。它是全局可读的,所有者可写的。它需要 SELinux 权限setsecparam,并且可以使用以下简单的命令分别进行写入和读取:
root@udoo:/sys/fs/selinux/avc # echo "1024" > cache_threshold
root@udoo:/sys/fs/selinux/avc # echo 'cat cache_threshold'
1024
你可以通过写入0来禁用缓存。然而,在基准测试之外,这不鼓励这样做。
布尔值目录
第二个要查看的目录是booleans。SELinux 的boolean允许策略声明通过boolean条件动态更改。通过改变boolean的状态,您可以影响已加载策略的行为。当前策略没有定义任何布尔值;因此这个目录是空的。在定义了布尔值的策略中,该目录将填充以每个布尔值命名的文件。然后,您可以读取和写入这些文件来改变boolean的状态。Android 工具箱已经进行了修改,包含了getsebool和setsebool命令。libselinux API 也公开了这些功能:
int security_get_boolean_names(char ***names, int *len);
int security_get_boolean_pending(const char *name);
int security_get_boolean_active(const char *name);
int security_set_boolean(const char *name, int value);
int security_commit_booleans(void);
int security_set_boolean_list(size_t boolcnt, SELboolean * boollist, int permanent);
布尔值是事务性的。这意味着它是一组“全有或全无”的设置。当您使用security_set_boolean*时,必须调用security_commit_booleans()使其生效。与 Linux 桌面系统不同,永久布尔值是不支持的。更改运行时值不会在重启后保留。另外,在 Android 上,如果您尝试达到 Android 兼容性测试套件 (CTS) 的合规性,布尔值将导致测试失败。布尔值可以根据目标具有不同的 DAC 权限,但它们总是需要 SELinux 权限,即setbool。
提示
您必须通过 Android 兼容性测试套件才能使用 Android 品牌。关于 CTS 的更多信息可以在source.android.com/compatibility/cts-intro.html找到。
类目录
下一个要查看的目录是class。class目录包含了在access_vectors SELinux 策略文件中定义的所有类,或者通过 SELinux 策略语言中的class关键字定义的类。对于策略中定义的每个类,都存在一个同名的目录。例如,在串行终端运行以下命令:
root@udoo:/sys/fs/selinux/class # ls -la
...
dr-xr-xr-x root root 1970-01-02 01:58 peer
dr-xr-xr-x root root 1970-01-02 01:58 process
dr-xr-xr-x root root 1970-01-02 01:58 property_service
dr-xr-xr-x root root 1970-01-02 01:58 rawip_socket
dr-xr-xr-x root root 1970-01-02 01:58 security
...
如您从前面的命令中看到的,有不少目录。让我们检查一下property_service目录。选择这个目录是因为它在 Android 上只有一个定义。然而,每个目录中存在的文件是相同的,包括index和perms:
root@udoo:/sys/fs/selinux/class/property_service # ls
index
perms
字符串和 SELinux 内核模块中定义的某些任意整数之间的映射是index。包含该类的所有可能权限的目录是perms:
root@udoo:/sys/fs/selinux/class/property_service # cd perms/
root@udoo:/sys/fs/selinux/class/property_service/perms # ls
set
如您所见,property_service类中可使用set访问向量。class目录可以非常有助于观察系统中已加载的策略文件。
initial_contexts目录
下一个要查看的目录条目是initial_contexts。这是初始安全上下文的静态映射,更广为人知的是安全标识符(sid)。这个映射告诉 SELinux 系统应该使用哪个上下文来启动每个内核对象:
root@udoo:/sys/fs/selinux/initial_contexts # ls
any_socket
devnull
file
...
我们可以通过执行以下操作来查看file的初始 sid:
root@udoo:/sys/fs/selinux/initial_contexts # echo 'cat file'
u:object_r:unlabeled:s0
这对应于external/sepolicy/initial_sid_contexts中的条目:
...
sid file u:object_r:unlabeled:s0
...
policy_capabilities目录
最后需要查看的目录是policy_capabilities。这个目录定义了策略可能具有的任何附加功能。对于我们当前的设置,我们应该有:
root@udoo:/sys/fs/selinux/policy_capabilities # ls
network_peer_controls
open_perms
每个文件条目都包含一个布尔值,指示功能是否启用:
root@udoo:/sys/fs/selinux/policy_capabilities # echo 'cat open_perms'
1
这些条目对所有人可读,对任何人不可写。
ProcFS
我们之前提到了一些正在导出的 procfs 接口。讨论的大部分内容是安全上下文,这意味着 shell 应该与某些安全上下文相关联...但我们应该如何实现这一点?由于这是所有 LSMs 使用的通用机制,因此安全上下文通过 procfs 进行读取和写入:
root@udoo:/sys/fs/selinux/policy_capabilities # echo 'cat /proc/self/attr/current'
u:r:init_shell:s0
你也可以获取每个线程的上下文:
root@udoo:/sys/fs/selinux/policy_capabilities # echo '/proc/self/task/2278/attr/current'
u:r:init_shell:s0
只需将2278替换为你想要的线程 ID。
当前文件上的 DAC 权限对所有人都是读写权限,但这些文件通常受到 MAC 权限的严格限制。通常,只有拥有 procfs 条目的进程可以读取这些文件,并且你必须拥有标准的写权限以及setcurrent的组合权限。注意,使用dyntransition必须允许“从”和“到”域。要读取,你必须拥有getattr。所有这些权限都来自process安全类。libselinux API 函数getcon和setcon允许你操作current。
prev文件可以用来查找你之前切换的上下文。这个文件是不可写的:
root@udoo:/proc/self/attr # echo 'cat prev'
u:r:init:s0
我们串行终端的前一个域或安全上下文是u:r:init:s0。
exec文件用于为子进程设置标签。这在进行 exec 之前设置。所有这些文件的权限与实际设置它们时使用的 MAC 权限相同。尝试设置此项的调用者还必须持有来自process类的setexec。可以使用 libselinux API int setexeccon(security_context_t context)和int getexeccon(security_context_t *context)来设置和检索标签。
fscreate、keycreate和sockcreate文件执行类似操作。当进程创建任何对应的对象时,如fs对象(文件、命名管道或其他对象)、密钥或套接字,这里设置的值将被使用。调用者还必须持有来自process类的setfscreate、setsockcreate和setkeycreate。以下 SELinux API 用于更改这些:
int set*createcon(security_context_t context);
int get*createcon(security_context_t *con);
其中*可以是fs、key或socket。
需要注意的是,这些特殊的process类权限可以让你更改proc/attr文件。你仍然需要通过 DAC 权限以及文件对象上设置的任何 SELinux 权限。只有在完成这些之后,你才需要额外的权限,如setfscreate。
Java SELinux API
对于之前讨论的 C API,Java 也有类似的 API。在这种情况下,假设你将使用平台来构建代码,因为这些 API 并未随 Android SDK 一起公开提供。该 API 位于frameworks/base/core/java/android/os/SELinux.java。然而,这只是 API 的一个非常有限的部分。
总结
在本章中,我们探讨了内核与用户空间之间关于 SELinux 的接口,并强化了访问向量类和安全上下文的概念。在下一章中,我们将对我们的系统进行一些升级,并查看审计日志,使我们离最终目标更近一步——在 SELinux 强制模式下可操作的设备。我们之所以说它是可操作的,是因为我们现在可以将其设置为强制模式。然而,如果你现在通过在 UDOO 上执行setenforce 1这样做,你的设备可能会变得不稳定。例如,在我们的系统上,如果我们这样做,浏览器将无法启动。
第七章:利用审计日志
到目前为止,我们已经看到 AVC 记录或 SELinux 拒绝消息在dmesg中出现,但dmesg是一个循环内存缓冲区,它可能会因为你的内核有多啰嗦而频繁翻滚。通过使用审计内核子系统,我们可以将这些消息路由到用户空间并将它们记录到磁盘上。在桌面上,执行这项工作的守护进程被称为auditd。auditd的最小端口在 NSA 分支中维护,但它尚未正式合并到 AOSP 中。由于我们正在 Android 4.3 上工作,我们将使用来自 NSA 分支的auditd版本。截至 2014 年 4 月 7 日的正式合并版本可以在android-review.googlesource.com/#/c/89645/找到。它是在logd中实现的,并在android-review.googlesource.com/#/c/83526/合并。
在本章中,我们将:
-
使用快速发展的 SE 更新我们的系统,为Android 开源社区(AOSP)
-
调查审计子系统的工作原理
-
学习阅读 SELinux 审计日志并开始编写策略
-
观察与日志相关的上下文
所有的 LSM 都应该将它们的日志信息记录到审计子系统中。审计子系统可以将这些信息通过printk路由到内核循环缓冲区,或者如果有的话,路由到用户空间的审计守护进程。内核和用户空间日志守护进程通过AUDIT_NETLINK套接字进行通信。我们将在本章中进一步剖析这个接口。
最后,审计子系统在发生策略违规时具有打印全面记录的能力。虽然你不需要这个功能来启用和操作 SELinux,但它可以让你更轻松。要启用这个系统,你必须使用auditd,因为logd目前不支持这个功能。你需要使用CONFIG_AUDITSYSCALL=y构建你的内核,并在/data/misc/audit/中放置一个audit.rules文件。在你按照以下说明补丁你的树之后,阅读system/core/auditd/README。
不幸的是,UDOO 内核版本 3.0.35 不支持CONFIG_AUDITSYSCALL。位于git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/commit/?id=29ef73b7a823b77a7cd0bdd7d7cded3fb6c2587b的补丁应该能够启用支持。然而,在 UDOO 上,它导致了一个我们无法追踪的死锁。
升级——大量的补丁
虽然谷歌发布的 Android 4.3 支持 SE for Android,但在审计方面仍然有限制。将此功能带入更易用状态的最简单方法之一是从国家安全局(NSA)的 SE for Android 4.3 分支获取一些项目的补丁。在这里,社区已经开发和部署了许多在 4.3 时间框架内未合并的更高级功能。
NSA 在bitbucket.org/seandroid/维护着仓库。项目众多,因此确定使用哪个项目以及哪个分支可能会让人望而却步。找到它们的方法是逐个查看每个项目,并找到具有SEAndroid-4.3分支的项目。由于我们不构建 AOSP 设备,因此无需深入设备树。这样的项目列表如下:
我们还可以安全地跳过sepolicy,因为我们已经将其更新到最前沿,但内核有点棘手。我们需要来自 kernel-common(bitbucket.org/seandroid/kernel-common)的更改和 binder 补丁(android-review.googlesource.com/#/c/45984/),可以按如下方式获取:
$ mkdir ~/sepatches
$ cd ~/sepatches
$ git clone https://bitbucket.org/seandroid/system-core.git
$ git clone https://bitbucket.org/seandroid/frameworks-base.git
$ git clone https://bitbucket.org/seandroid/external-libselinux.git
$ git clone https://bitbucket.org/seandroid/build.git
$ git clone https://bitbucket.org/seandroid/frameworks-native.git
我们可以先通过查看build/core/build_id.mk文件,以及使用网页source.android.com/source/build-numbers.html进行查询,来确定我们需要修补的确切版本。
文件显示BUILD_ID是JSS15J,查询结果显示我们正在使用android-4.3_r2.1版本为 UDOO 工作。
对于到目前为止下载的每个项目,通过运行命令git checkout origin/seandroid-4.3_r2生成补丁。最后,执行git format-patch origin/jb-mr2.0-release。由于没有4.3._r2.1分支,我们使用r2。
对于这些补丁中的每一个,你需要从对应的udoo/<project>文件夹中在树形结构中应用它们。重要的是,需要按照数字顺序为每个项目应用补丁,从0001*补丁开始,然后是0002*,依此类推。以下是为system-core项目应用第一个补丁的示例。请注意,这些 Git 仓库使用连字符代替源树中的斜杠;因此frameworks-base对应于frameworks/base。
首先,生成补丁:
$ cd sepatches/system-core
$ git checkout origin/seandroid-4.3_r2
$ git format-patch origin/jb-mr2.0-release
按如下方式应用第一个补丁:
$ cd <udoo_root>/system/core
$ patch -p1 < ~/sepatches/system-core/0001-Add-writable-data-space-for-radio.patch
patching file rootdir/init.rc
Reversed (or previously applied) patch detected! Assume -R? [n]
注意
注意对于 UDOO 来说,在frameworks/base中不要应用高于0005编号的补丁。对于其他项目,你应该应用所有补丁。
注意错误。一旦你看到这个,就按Ctrl + C退出补丁过程。Git 树并不完美,因此一些补丁已经存在于 UDOO 源码中。补丁命令会通知我们,并且当有警告时,我们可以通过取消它们(用Ctrl + C)来跳过这些补丁。继续检查补丁,取消已经应用的,并修复任何失败的部分。在补丁用户空间后,强烈建议你构建一次以确保没有任何东西被破坏。
一旦用户空间完全打上补丁,我们需要对内核进行补丁处理。首先使用git clone https://bitbucket.org/seandroid/kernel-common.git命令从 Bitbucket 克隆 kernel-common 项目。我们将使用与其他项目相同的方法对内核进行补丁处理,除了 binder 补丁。通过查看提到的 binder 补丁链接android-review.googlesource.com/#/c/45984/,我们发现 Git SHA 哈希是a3c9991b560cf0a8dec1622fcc0edca5d0ced936,如下截图中的补丁集 4参考字段所示:
然后,我们可以为这个 SHA 哈希生成补丁:
$ git format-patch -1 a3c9991b560cf0a8dec1622fcc0edca5d0ced936
0001-Add-security-hooks-to-binder-and-implement-the-hooks.patch
然后,像之前一样使用补丁命令应用该补丁。补丁有一个头文件包含的失败块;只需像其他一样使用拒绝文件修复它。当你构建时,你会在内核中得到这个错误。
security/selinux/hooks.c:1846:9: error: variable 'sad' has initializer but incomplete type
security/selinux/hooks.c:1846:28: error: storage size of 'sad' isn't known
去掉这一行以及所有相关引用。这是在 3.0 内核中做出的一个更改:
struct selinux_audit_data sad = {0,};
ad.selinux_audit_data = &sad;
注意
我们通过查看原始 3.0 补丁找出了这个问题,这些补丁可以在以下链接找到:
如你所知,UDOO 使用自定义的init.rc。我们需要将任何对init.rc的更改添加到 UDOO 实际使用的那个文件中。所有可以修改init.rc的补丁都会在 system-core 项目中,特别是这些:
-
0003-Auditd-initial-commit.patch -
0007-Handle-policy-reloads-within-ueventd-rather-than-res.patch -
0009-Allow-system-UID-to-set-enforcing-and-booleans.patch
去找这些补丁中对init.rc的更改,并使用同样的补丁技术将它们应用到device/fsl/imx6/etc/init.rc中。
审计系统
在上一节中,我们做了很多补丁工作;其目的是为了启用在 Android 及其依赖项上完成的审核集成工作。这些补丁还修复了一些代码中的错误,并且非常重要地启用了 SELinux/LSM binder 挂钩和政策控制。
Linux 中的审计系统被 LSMs 用来打印拒绝记录,以及收集非常详尽和完整的事件记录。无论何时,当 LSM 打印消息时,它都会传播到审计子系统并打印出来。然而,如果启用了审计子系统,那么你将获得与拒绝相关的更多上下文信息。审计子系统甚至支持加载规则来观察这些情况。例如,你可以观察所有对/system的写入操作,这些操作并非由系统 UID 执行。
auditd守护进程
auditd守护进程或服务在用户空间运行,并通过 NETLINK 套接字监听审计子系统。守护进程注册自己以接收内核消息,并且可以通过此套接字加载审计规则。一旦注册,auditd守护进程就会接收到所有审计事件。auditd守护进程被最小化移植,并且曾经尝试将其主线化到 Android 中,但后来被拒绝。然而,auditd已被多个 OEM(如三星)以及 NSA 的 4.3 分支使用。后来将记录放入 logcat 的替代方法被合并到 Android 中(更多信息,请参考android-review.googlesource.com/89645)。
之前,我们在dmesg中看到了来自 SELinux 的 AVC 拒绝消息。这个问题在于,当有大量拒绝或内核通信频繁时,循环内存日志容易发生翻转。使用auditd,所有消息都会发送到守护进程,并写入/data/misc/audit/audit.log文件。这个日志文件,即本文中的audit.log,可能在设备启动时存在,并轮换到/data/misc/audit/audit.old文件,即audit.old。守护进程将恢复到新的audit.log文件中记录。当超过大小阈值AUDITD_MAX_LOG_FILE_SIZEKB(在编译时在system/core/auditd/Android.mk文件中设置)时,会发生轮换事件。这个阈值通常是 1000 KB,但可以在设备的makefile中更改。此外,使用kill发送SIGHUP也会导致轮换,如下例所示。
验证守护进程正在运行并获取其 PID:
root@udoo:/ # ps -Z | grep audit
u:r:auditd:s0 audit 2281 1 /system/bin/auditd
u:r:kernel:s0 root 2293 2 kauditd
验证只存在一个日志文件:
root@udoo:/ # ls -la /data/misc/audit/
-rw-r----- audit system 79173 1970-01-02 00:19 audit.log
轮换日志:
root@udoo:/ # kill -SIGHUP 2281
验证audit.old:
root@udoo:/ # ls -la /data/misc/audit/
-rw-r----- audit system 319 1970-01-02 00:20 audit.log
-rw-r----- audit system 79173 1970-01-02 00:19 audit.old
auditd内部机制
由于 Linux 桌面版的auditd和libaudit代码采用 GPL 许可证,因此针对 Android 进行了重写,并在 Apache 许可证下发布。重写工作是最小化的,因此你只会找到为实现守护进程所必需的函数。不过,功能和头文件接口应该保持一致。
auditd守护进程的生命周期始于system/core/auditd.c中的main()函数。它迅速将权限从 root UID 更改为特殊的auditd UID。这样做时,它保留了CAPSYS_AUDIT,这是使用AUDIT NETLINK 套接字所需的 DAC 能力检查。它通过调用drop_privileges_or_die()来实现这一点。从那里,它使用getopt()进行一些选项解析,最终我们到达了审计特定的调用,第一个调用是使用audit_open()打开 NETLINK 套接字。这个函数简单地调用socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT),它打开到 NETLINK 套接字的文件描述符。打开套接字后,守护进程通过调用audit_log_open(const char *logfile, const char *rotatefile, size_t threshold)来打开对audit.log的句柄。这个函数检查audit.log文件是否存在,如果存在,将其重命名为audit.old。然后创建一个新的空日志文件来记录数据。
下一步是将守护进程注册到审计子系统,这样它就知道要向谁发送消息。通过设置守护进程的 PID,你可以确保只有这个守护进程会收到消息。由于 NETLINK 可以支持许多读取者,你不会希望一个"流氓auditd"读取这些消息。说到这一点,守护进程调用audit_set_pid(audit_fd, getpid(), WAIT_YES),其中audit_fd来自audit_open()的 NETLINK 套接字,getpid()返回守护进程的 PID,WAIT_YES使守护进程阻塞直到操作完成。接下来,守护进程通过调用audit_set_enabled(audit_fd, 1)启用审计子系统的先进功能,并通过audit_rules_read_and_add(audit_fd, AUDITD_RULES_FILE)向审计子系统添加规则。这个函数从该文件读取规则,格式化一些结构,并将这些结构发送到内核。
audit_set_enabled()和audit_rules_read_and_add()只有在内核构建时带有CONFIG_AUDITSYSCALL时才有效。在此之后,守护进程检查是否指定了-k选项。-k选项告诉auditd在dmesg中查找任何错过的审计记录。它这样做是因为在捕获审计记录之前,环形缓冲区溢出与用户空间启动许多服务、生成审计事件和政策违规之间存在竞争。本质上,这有助于将早期启动的审计事件合并到相同的日志文件中。
在此之后,守护进程进入一个循环,从 NETLINK 套接字读取,格式化消息,并将其写入日志文件。它通过使用poll()等待 NETLINK 套接字上的 IO 来开始这个循环。如果poll()以错误退出,循环继续检查quit变量。如果引发EINTR,则在信号处理程序中将循环保护变量quit设置为true,守护进程退出。如果poll()在 NETLINK 上有数据,守护进程调用audit_get_reply(audit_fd, &rep, GET_REPLY_BLOCKING, 0),通过rep参数获取一个audit_reply结构体。然后它将audit_reply结构体(带有格式化)写入audit.log文件,使用audit_log_write(alog, "type=%d msg=%.*s\n", rep.type, rep.len, rep.msg.data)。它这样做直到引发EINTR,此时守护进程退出。
当守护进程退出时,它会清除已注册到内核的 PID(audit_set_pid(audit_fd, 0)),通过audit_close()关闭审计套接字(实际上只是系统调用,close(audit_fd)),并使用audit_log_close()关闭audit.log。audit_log_*函数家族不是审计 GPLed 接口的一部分,是一种自定义写入方式。
当谷歌将auditd移植到 Android 的logd基础架构时,它使用了守护进程main()使用的相同函数和库代码,并将其包装到logd中。然而,谷歌并没有采用audit_set_enabled()和audit_rules_read_and_add()函数。
解释 SELinux 拒绝日志
SELinux 拒绝信息会被路由到内核审计子系统,到auditd,最终到达audit.log和audit.old。由于日志位于audit.log中,让我们通过adb拉取这个文件,并仔细查看它。
在主机上运行以下命令,确保已启用adb:
$ adb pull /data/misc/audit/audit.log
现在,让我们跟踪那个文件,查找这些行:
$ tail audit.log
...
type=1400 msg=audit(88526.980:312): avc: denied { getattr } for pid=3083 comm="adbd" path="/data/misc/audit/audit.log" dev=mmcblk0p4 ino=42 scontext=u:r:adbd:s0 tcontext=u:object_r:audit_log:s0 tclass=file
type=1400 msg=audit(88527.030:313): avc: denied { read } for pid=3083 comm="adbd" name="audit.log" dev=mmcblk0p4 ino=42 scontext=u:r:adbd:s0 tcontext=u:object_r:audit_log:s0 tclass=file
type=1400 msg=audit(88527.030:314): avc: denied { open } for pid=3083 comm="adbd" name="audit.log" dev=mmcblk0p4 ino=42 scontext=u:r:adbd:s0 tcontext=u:object_r:audit_log:s0 tclass=file
这里的记录由两个主要部分组成:type和msg。type字段指示了消息的类型。类型为 1400 的消息是 AVC 消息,即 SELinux 拒绝消息(还有其他类型)。前述策略的msg(消息的简称)部分包含我们需要分析的部分。
我们最后执行的命令是 adb pull /data/misc/audit/aduit.log,正如你所见,在audit.log文件的末尾我们有几处adb策略违规。让我们先从这个事件开始查看:
type=1400 msg=audit(88526.980:312): avc: denied { getattr } for pid=3083 comm="adbd" path="/data/misc/audit/audit.log" dev=mmcblk0p4 ino=42 scontext=u:r:adbd:s0 tcontext=u:object_r:audit_log:s0 tclass=file
我们可以看到comm字段是adbd。然而,相信这个值并不明智,因为它可以使用prctl()接口从用户空间进行控制。它只能被视为一个提示。最好的验证方法是使用ps -Z检查 PID:
# ps -Z | grep adbd
u:r:adbd:s0 root 3083 1 /sbin/adbd
在验证守护进程后,我们现在可以更详细地检查这个消息。消息由以下字段组成(可选字段由*标识):
-
avc: denied:这部分将始终发生,表示它是一个拒绝记录。 -
{ permission }:这是被拒绝的权限,在本例中是getattr。 -
for:这将始终被打印出来,使输出可读。 -
Path*:这是可选字段,包含有关对象路径的信息。它只对文件系统访问拒绝有意义。 -
dev*:这是可选字段,用于标识挂载文件系统的块设备。它只对文件系统访问拒绝有意义。 -
ino*:这是文件的可选 inode。Linux 中只有匿名文件会打印 inode。它只对文件系统访问拒绝有意义。 -
tclass:这是对象的目标类,在我们的案例中是file。
在这一点上,我们需要从非常精炼的层面理解拒绝记录中的msg部分在告诉我们什么。它说的是 Android 调试桥接守护进程想要在我们的策略文件上调用getattr。在下面几个事件中,我们还会看到它还想要read和open。这是运行adb pull的副作用。getattr权限拒绝来自stat()系统调用,而read/open则来自read()和open()系统调用。如果你想要在策略中允许这一点,这将是基于你的威胁模型的安全决策,你应该添加:
allow adbd audit_log:file { getattr read open };
或者,使用global_macros中定义的宏集合:
allow adbd audit_log:file r_file_perms;
大多数时候,你应该使用global_macros中定义的宏进行文件权限访问。通常,逐个添加它们非常耗时且繁琐。宏将权限分组在一个与读、写、执行 DAC 权限类似的环境中。例如,如果你给它open和read,那么源域在某个时刻可能需要 stat 文件。所以,r_file_perms宏已经包含了这些权限。
你应该将此规则添加到external/sepolicy/adbd.te中。.te文件(也称为type enforcement文件)是按源上下文组织的,因此请确保将其添加到正确的文件中。我们不推荐添加此允许规则——没有合法的理由让adbd需要访问审计日志——我们可以通过dontaudit规则安全地忽略这些:
dontaudit adbd audit_log:file r_file_perms;
dontaudit规则是一个策略声明,表示不要审计(打印)符合此规则的拒绝操作。
如果你不确定该怎么办,最好的建议是利用 SE for Android、SELinux 和 audit 的邮件列表。只需确保信息与特定邮件列表主题相关即可。
存在一个名为audit2allow的工具,可以帮助你编写策略允许规则。然而,它只是一个工具,可能会被误用。它将策略文件转换为策略的允许规则:
$ cat audit.log | audit2allow
#============= adbd ==============
allow adbd audit_log:file { read getattr open };
audit2allow工具不知道宏,也不清楚你是否真的想要将此允许规则添加到策略文件中。只有策略作者才能做出这个决定。
还有一个名为fixup.py的工具,用于启用r_file_*宏映射。你可以在bitbucket.org/billcroberts/fixup/overview获取此工具。下载后,使其可执行,并将其放在你的可执行路径中的某个位置:
$ chmod a+x fixup.py
$ cat audit.log | audit2allow | fixup.py
#============= adbd ==============
allow adbd audit_log:file r_file_perms;
上下文
从最简单的意义上说,编写策略只是识别策略违规并添加适当的允许规则到策略文件的活动。然而,为了使 SELinux 有效,源和目标上下文必须正确。如果它们不正确,允许规则就没有意义。
你可能首先遇到的是目标类型未标记的拒绝问题。在这种情况下,需要设置适当的目标标签(参考第十一章,标签属性)。此外,进程标签可能也会出错。多个进程可能属于一个域,除非通过策略明确操作,否则子进程会继承父进程的域。然而,在 Android 中,具有多个进程的域是非常有限的。你永远不会在init、system_server、adbd、auditd、debuggerd、dhcp、servicemanager、vold、netd、surfaceflinger、drmserver、mediaserver、installd、keystore、sdcardd、wpa和zygote域中看到多个进程。
在以下域中看到多个进程是可以的:
-
system_app -
untrusted_app -
platform_app -
shared_app -
media_app -
`
-
isolated_app -
shell
在已发布的设备上,不应该在su、recovery和init_shell域中运行任何东西。下表提供了域到预期可执行文件和量度的完整映射:
| 域 | 可执行文件 | 量度 (N) |
|---|---|---|
u:r:init:s0 | /init | N == 1 |
u:r:ueventd:s0 | /sbin/ueventd | N == 1 |
u:r:healthd:s0 | /sbin/healthd | N == 1 |
u:r:servicemanager:s0 | /system/bin/servicemanager | N == 1 |
u:r:vold:s0 | /system/bin/vold | N == 1 |
u:r:netd:s0 | /system/bin/netd | N == 1 |
u:r:debuggerd:s0 | /system/bin/debuggerd, /system/bin/debuggerd64 | N == 1 |
u:r:surfaceflinger:s0 | /system/bin/surfaceflinger | N == 1 |
u:r:zygote:s0 | zygote, zygote64 | N == 1 |
u:r:drmserver:s0 | /system/bin/drmserver | N == 1 |
u:r:mediaserver:s0 | /system/bin/mediaserver | N >= 1 |
u:r:installd:s0 | /system/bin/installd | N == 1 |
u:r:keystore:s0 | /system/bin/keystore | N == 1 |
u:r:system_server:s0 | system_server | N ==1 |
u:r:sdcardd:s0 | /system/bin/sdcard | N >=1 |
u:r:watchdogd:s0 | /sbin/watchdogd | N >=0 && N < 2 |
u:r:wpa:s0 | /system/bin/wpa_supplicant | N >=0 && N < 2 |
u:r:init_shell:s0 | null | N == 0 |
u:r:recovery:s0 | null | N == 0 |
u:r:su:s0 | null | N == 0 |
已经编写了几个围绕此问题的兼容性测试套件(CTS)测试,并提交到了 AOSP,地址为android-review.googlesource.com/#/c/82861/。
根据一个好的策略应该具备的这些通用断言,我们来评估我们的策略。
首先,我们将检查未标记的对象。从主机上,使用adb pull获取的audit.log文件:
$ cat audit.log | grep unlabeled
...
type=1400 msg=audit(86527.670:341): avc: denied { rename } for pid=3206 comm="pool-1-thread-1" name="com.android.settings_preferences.xml" dev=mmcblk0p4 ino=129664 scontext=u:r:system_app:s0 tcontext=u:object_r:unlabeled:s0 tclass=file
...
看起来我们有一些文件和其他东西没有正确标记;我们将在第十一章 标签属性 中解决这些问题。现在,让我们检查那些不应该有多个进程的域,并在这些域中找到不适当的二进制文件(参考之前的表格以获取完整的映射。)
Init:
$ adb shell ps -Z | grep u:r:init:s0
u:r:init:s0 root 1 0 /init
u:r:init:s0 root 2267 1 /sbin/watchdogd
Zygote:
$ adb shell ps -Z | grep u:r:zygote:s0
u:r:zygote:s0 root 2285 1 zygote
$ adb shell ps -Z | grep u:r:init_shell
u:r:init_shell:s0 root 2278 1 /system/bin/sh
… through all domains
在进行这项工作后,我们发现了一些问题,因为某些进程正在init_shell域中运行,而watchdogd在init域中。这些必须得到纠正。
概述
编写sepolicy相对简单,编写好的策略则是一门艺术。它要求策略作者理解系统和allow规则的含义。策略本身是一种元编程语言,这种语言控制用户空间和内核如何协同工作,与任何程序类似,策略可以针对特定的用途进行架构设计。策略可能过于宽松(基本上无用),或者非常严格,在不破坏已正常工作部分的情况下难以更改。
一个好的策略需要保持系统预期功能的正常运行,因此对 Android 内的所有系统进行彻底测试是至关重要的。CTS 在锻炼 Android 方面非常有帮助,但它通常并不能覆盖所有情况;建议进行用户测试。在下一章中,我们将介绍文件系统和文件系统对象如何获得其安全标签,以及我们如何更改它们。稍后,我们将介绍如何使用 CTS 作为一个工具来测试系统,并对预期行为生成策略违规。