探索安卓安全改进-全-
探索安卓安全改进(全)
原文:
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 作为一个工具来测试系统,并对预期行为生成策略违规。
第八章:应用上下文到文件
在上一章中,我们升级了系统,收集了审计日志,并开始分析审计记录。我们发现文件系统上的一些对象未标记。在本章中,我们将:
-
了解文件系统和文件系统对象如何获取它们的标签
-
展示更改标签的技术
-
引入扩展属性进行标记
-
调查文件上下文和动态类型转换
标记文件系统
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
或_isolated
UIDs。 -
levelFrom=app
或levelFrom=all
只支持_app
UIDs。 -
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.
系列这样的硬编码特殊属性。在下一章中,我们将了解工具链如何构建和创建我们一直在使用的所有策略文件。
第十二章:掌握工具链
迄今为止,我们已经深入探讨了推动 SE for Android 技术的代码和政策,但构建系统和工具常常被忽视。掌握工具链将帮助你提高开发实践。在本章中,我们将了解 SE for Android 构建的所有组件及其工作原理。我们将涵盖以下主题:
-
构建特定目标
-
sepolicy
Android.mk
文件 -
自定义构建策略配置
-
构建工具:
-
check_seapp
-
insertkeys.py
-
checkpolicy
-
checkfc
-
sepolicy-check
-
sepolicy-analyze
-
构建子组件——目标和项目
迄今为止,我们已经运行了一些神奇的命令,如 mm
、mmm
和 make bootimage
,实际上构建了 SE for Android 代码的各个部分。谷歌在文档 source.android.com/source/building-running.html
中正式描述了其中一些工具,但大多数命令并未列出。尽管如此,elinux.org/Android_Build_System
有一个更全面的相关编写。
在谷歌的“构建和运行”文档中,他们将目标描述为设备,这最终是你启动的目标。在构建 Android 时,lunch
命令为稍后执行的 make
命令设置环境变量。它设置构建系统以输出目标设备的正确配置。本章将不会讨论这种目标概念。相反,当提到 target
时,它指的是一个特定的 make
目标。然而,在需要提及目标设备的情况下,将使用完整的短语“target device
”。虽然有些令人困惑,但这种术语是标准的,现场的工程师将会理解。
我们已经多次运行了 make
命令,可选地提供一个目标作为参数和选项,例如 -j16
选项。像 make
或 make -j16
这样的命令本质上构建了整个 Android。可选地,你可以指定一个或多个目标作为命令参数。例如,当构建 boot.img
时,可以通过指定 bootimage
目标来构建和重新构建 boot.img
文件。我们为此目的使用的命令是 make bootimage
。它通过仅重建系统中需要的部分来加快构建速度。但如果你只需要重新构建一个特定文件呢?或许,你只想重新构建 sepolicy
。你可以将其指定为构建目标,如 make sepolicy
。这引出了一个问题:“其他文件如 mac_permissions.xml
、seapp_contexts
等怎么办?”它们也可以以同样的方式构建。更有趣的问题是:“一个人如何知道目标名称是什么?它总是输出文件的名称吗?”
Android 的构建系统是建立在 GNU make
之上的(www.gnu.org/software/make/
)。Android 构建系统的核心 makefile 系统可以在build/core
中找到,而文档可以在 NDK 中找到(developer.android.com/tools/sdk/ndk/index.html
)。从阅读中可以得出的主要结论是,一个典型的Android.mk
文件定义了称为LOCAL_MODULE := mymodulename
的东西,以及构建名为mymodulename
的东西。目标名称由这些LOCAL_MODULE
语句定义。让我们查看外部 sepolicy 的Android.mk
,关注其中 sepolicy 部分,因为在该Makefile
中还定义了其他本地模块或目标。以下是从 Android 4.3 的一个示例:
include $(CLEAR_VARS)
LOCAL_MODULE := sepolicy
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)
...
只需要查找以LOCAL_MODULE
声明开始的行,并且是全词匹配,就可以在Android.mk
文件中找到所有模块:
$ grep -w '^LOCAL_MODULE' Android.mk
LOCAL_MODULE := sepolicy
LOCAL_MODULE := file_contexts
LOCAL_MODULE := seapp_contexts
LOCAL_MODULE := property_contexts
LOCAL_MODULE := selinux-network.sh
LOCAL_MODULE := mac_permissions.xml
LOCAL_MODULE := eops.xml
正则表达式规定,^
表示行的开始,而grep
的手册页指出-w
提供全词搜索。
前面的列表对于我们目前在 UDOO 上使用的 Android 版本来说是很全面的。但是,你应该在你的Makefile
的确切版本上运行命令,以了解可以构建哪些内容。
Android 还有一些额外的工具,这些工具与构建目标分开,并在你使用source build/envsetup.sh
时添加到你的环境中。这些工具是mm
和mmm
。它们执行相同的任务,即构建Android.mk
文件中指定的所有目标,但不同之处在于它们不构建任何依赖项。这两个命令的区别仅在于它们查找构建目标所在的Android.mk
的位置。mm
命令使用当前工作目录,而mmm
使用提供的路径。此外,这两个命令的一个很好的选项是-B
,它强制重新构建。工程师使用mm(m)
命令而不是make <target>
可以节省大量时间。完整的make
命令在确定依赖关系树上浪费了很多时间,所以如果在之前构建的源代码树(如果你知道你的所有更改都在一个项目中)上执行mmm path/to/project
可以节省几分钟。但是,由于它不构建依赖项,你需要确保它们已经构建并且没有依赖性更改。
探索 sepolicy 的 Android.mk
位于external/sepolicy
的项目与其他 Android 项目一样,使用Android.mk
文件来构建它们的输出。让我们剖析这个文件,看看它都做了些什么。
构建 sepolicy
我们将从中间开始,查看针对sepolicy
的目标。它以相当标准化的Android.mk
内容开始:
...
include $(CLEAR_VARS)
LOCAL_MODULE := sepolicy
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)
include $(BUILD_SYSTEM)/base_rules.mk
...
接下来的部分有点类似于标准的make
。它首先声明一个目标文件,该文件会被构建到intermediates
位置。intermediates
位置是由 Android 构建系统定义的。然后它将MLS_SENS
和MLS_CATS
的值赋给一些局部变量以供后续使用。最后一条语句是最有趣的。它使用了一个名为build_policy
的make
函数,并接受文件名作为参数:
...
sepolicy_policy.conf := $(intermediates)/policy.conf
$(sepolicy_policy.conf): PRIVATE_MLS_SENS := $(MLS_SENS)
$(sepolicy_policy.conf): PRIVATE_MLS_CATS := $(MLS_CATS)
$(sepolicy_policy.conf) : $(call build_policy, security_classes initial_sids access_vectors global_macros mls_macros mls policy_capabilities te_macros attributes bools *.te roles users initial_sid_contexts fs_use genfs_contexts port_contexts)
...
接下来,我们定义了构建此中间目标policy.conf
的 recipe。recipe 中有趣的部分是m4
命令和sed
命令。
注意事项
有关m4
的更多信息,请参见www.gnu.org/software/m4/manual/m4.html
,有关sed
的更多信息,请参考www.gnu.org/software/sed/manual/sed.html
。
SELinux 策略文件是通过m4
处理的。m4
是一种通常被用作编译器前端的宏处理器语言。m4
命令取一些值,如PRIVATE_MLS_SENS
和PRIVATE_MLS_CATS
,并将它们作为宏定义传递。这类似于gcc -D
选项。然后它通过make
扩展$^
获取目标的依赖项,并使用make
扩展$@
将它们输出到目标名称。它还取该输出并生成一个.dontaudit
版本。该版本使用sed
从策略文件中删除所有dontaudit
行。MLS 值告诉 SELinux 生成多少类别和敏感性。这些必须在加载到内核的策略块中静态定义,如下所示:
...
@mkdir -p $(dir $@)
$(hide) m4 -D mls_num_sens=$(PRIVATE_MLS_SENS) -D mls_num_cats=$(PRIVATE_MLS_CATS) -s $^ > $@
$(hide) sed '/dontaudit/d' $@ > $@.dontaudit
...
下一个部分定义了构建实际目标 recipe,该目标名为LOCAL_MODULE_POLICY
,即使这一点并不明显。LOCAL_BUILT_MODULE
扩展为要构建的中间文件,在这种情况下是sepolicy
。最后,它由 Android 构建系统在幕后作为LOCAL_INSTALLED_MODULE
复制。此目标依赖于中间policy.conf
文件和checkpolicy
。它使用checkpolicy
将m4
扩展的policy.conf
和policy.conf.dontaudit
转换为两个 sepolicy 文件,sepolicy
和sepolicy.dontaudit
。实际用于将 SELinux 语句编译成二进制形式以加载到内核的工具是checkpolicy
,如下所示:
...
$(LOCAL_BUILT_MODULE) : $(sepolicy_policy.conf) $(HOST_OUT_EXECUTABLES)/checkpolicy
@mkdir -p $(dir $@)
$(hide) $(HOST_OUT_EXECUTABLES)/checkpolicy -M -c $(POLICYVERS) -o $@ $<
$(hide) $(HOST_OUT_EXECUTABLES)/checkpolicy -M -c $(POLICYVERS) -o $(dir $<)/$(notdir $@).dontaudit $<.dontaudit
...
最后,它通过设置局部变量built_policy
供Android.mk
文件中的其他地方使用,并清除policy.conf
以避免污染make
的全局命名空间,如下所示:
...
built_sepolicy := $(LOCAL_BUILT_MODULE)
sepolicy_policy.conf :=
...
此外,构建sepolicy
还依赖于POLICYVERS
变量,如果未设置,则条件赋值为26
。这是checkpolicy
使用的策略版本号,正如我们在本书前面看到的,我们不得不为我们的 UDOO 覆盖这一点。
控制策略构建
我们看到sepolicy
语句调用了build_policy
函数。我们还在Android.mk
文件中看到它的用途,用于构建sepolicy
、file_contexts
、seapp_contexts
、property_contexts
和mac_permissions.xml
,因此可以推断这个函数相当重要。该函数输出用于策略文件的完全解析路径列表。该函数以变量参数列表的文件名为输入,并支持正则表达式(注意build_policy
中的*.te
用于目标 sepolicy)。在内部,该函数使用一些技巧,允许你覆盖或追加当前策略构建,而无需直接修改external/sepolicy
目录。这是为了使 OEM 和设备构建者能够增加策略,以覆盖其特定设备。
在构建策略时,你可以在设备的Makefile
中设置以下make
变量,以控制生成的构建结果。这些变量如下:
-
BOARD_SEPOLICY_DIRS
:这是潜在策略文件的搜索路径。 -
BOARD_SEPOLICY_UNION
:这是一个要附加到所有同名文件的策略文件。 -
BOARD_SEPOLICY_REPLACE
:这是一个用于覆盖基础external/sepolicy
策略文件的策略文件。 -
BOARD_SEPOLICY_IGNORE
:这是用于从构建中移除特定策略文件,给定仓库的相对路径。
以 UDOO 为例,编写策略的正确方式是永远不要修改external/sepolicy
,而是在device/fsl/udoo/sepolicy
中创建一个目录:
$ mkdir <PATH>
然后我们修改BoardConfig.mk
:
$ vim BoardConfig.mk
接下来,我们添加以下几行:
BOARD_SEPOLICY_DIRS += device/fsl/udoo/sepolicy
提示
在使用+=
与:=
时要非常小心。在大型项目树中,这些变量可能会在构建树的更高位置被常见的BoardConfigs
设置,而你可能会覆盖它们的设置。通常,最安全的选择是+=
。有关详细信息,请参阅 GNU make 手册中的变量赋值部分,在www.gnu.org/software/make/manual/make.html
。
这将告诉Android.mk
中的build_policy()
函数不仅搜索external/sepolicy
目录,还要搜索device/fsl/udoo/sepolicy
目录下的策略文件。
接下来,我们可以在该目录中创建一个file_contexts
文件,并通过在该目录中创建一个新的file_contexts
文件,将我们对标签的更改移动到device/fsl/udoo/sepolicy
目录。
之后,我们需要指导构建系统将我们的file_contexts
文件与external/sepolicy
中的文件进行合并或联合。我们通过在BoardConfig.mk
文件中添加以下声明来实现这一点:
BOARD_SEPOLICY_UNION += file_contexts
你可以对任何策略文件执行此操作,甚至是自定义文件。它仅通过文件名(不包括目录)进行匹配。例如,如果你有一个名为watchdog.te
的规则文件,你想将其添加到基础的watchdog.te
规则文件中,你可以像下面这样只添加watchdog.te
:
BOARD_SEPOLICY_UNION += file_contexts watchdog.te
这将在构建过程中生成一个新的watchdog.te
文件,将你的新规则与在external/sepolicy/watchdog.te
中找到的规则进行联合。
还请注意,你可以使用BOARD_SEPOLICY_UNION
将新文件添加到构建中,因此要为自定义域(如custom.te
)添加一个.te
文件,你可以:
BOARD_SEPOLICY_UNION += file_contexts watchdog.te custom.te
假设你想用你自己的文件覆盖external/sepolicy watchdog.te
。你可以将其添加到BOARD_SEPOLICY_REPLACE
中,如下所示:
BOARD_SEPOLICY_REPLACE := watchdog.te
请注意,你不能替换在基础策略中不存在的文件。同时,你不能让同一个文件在UNION
和REPLACE
中出现,因为这是模棱两可的。在同一个策略文件上不能有超过一个的BOARD_SEPOLICY_REPLACE
规范。
假设我们正在为两个虚构的设备(设备 X 和设备 Y)进行分层构建。这两个设备,设备 X 和设备 Y,都从设备 A 继承了BoardConfigCommon.mk
。设备 A 不是一个真实的设备,但由于 X 和 Y 有共同点,因此将共同的部分保存在设备 A 中。
假设设备 A 的BoardConfigCommon.mk
包含以下语句:
BOARD_SEPOLICY_DIRS += device/OEM/A
BOARD_SEPOLICY_UNION += file_contexts custom.te
假设设备 X 的BoardConfig.mk
包含以下内容:
BOARD_SEPOLICY_DIRS += device/OEM/X
BOARD_SEPOLICY_UNION += file_contexts custom.te
最后,假设设备 Y 的BoardConfig.mk
包含以下内容:
BOARD_SEPOLICY_DIRS += device/OEM/Y
BOARD_SEPOLICY_UNION += file_contexts custom.te
用于构建设备 X 和设备 Y 的结果策略集如下:
设备 X 策略集:
device/OEM/A/file_contexts
device/OEM/A/custom.te
device/OEM/X/file_contexts
device/OEM/X/custome.te
external/sepolicy/* (base policy files)
设备 Y 也包含以下内容:
device/OEM/A/file_contexts
device/OEM/A/custom.te
device/OEM/Y/file_contexts
device/OEM/Y/custom.te
external/sepolicy/* (base policy files)
在一个常见场景中,你可能不希望设备 Y 的结果策略集包含device/OEM/A/custom.te
。这是BOARD_SEPOLICY_IGNORE
的一个用例。你可以用它来过滤特定的策略文件。但是,你必须具体指明并使用仓库的相对路径。例如,在设备 Y 的BoardConfig.mk
中:
BOARD_SEPOLICY_IGNORE += device/OEM/A/custom.te
现在,当你为设备 Y 构建策略时,策略集将不会包括那个文件。BOARD_SEPOLICY_IGNORE
也可以与BOARD_SEPOLICY_REPLACE
一起使用,允许在设备层次结构中多次使用,但只有一个BOARD_SEPOLICY_REPLACE
语句生效。
深入挖掘 build_policy
现在我们已经了解了如何使用一些新的机制来控制策略构建,让我们实际剖析构建过程发生在哪里。如前所述,策略构建由Android.mk
文件控制。我们之前遇到了对build_policy()
函数的调用,这正是与我们所设置的所有的BOARD_SEPOLICY_*
变量相关的魔法发生的地方。检查build_policy
函数,我们看到它引用了sepolicy_replace_paths
变量,所以让我们从查看这个变量开始。
sepolicy_replace_paths
变量在Makefile
执行时进行评估,换句话说,它会无条件执行。代码首先遍历所有的BOARD_SEPOLICY_REPLACE
文件,检查是否存在于BOARD_SEPOLICY_UNION
中。如果发现一个,就会打印错误信息,构建失败,显示Ambiguous request for sepolicy $(pf). Appears in both BOARD_SEPOLICY_REPLACE and BOARD_SEPOLICY_UNION
,其中$(pf)
会被扩展为有问题的策略文件。之后,它用BOARD_SEPOLICY_DIRS
设置的搜索路径中找到的条目来扩展BOARD_SEPOLICY_REPLACE
,从而得到从 Android 树的根目录开始的完整相对路径。然后它将这些条目与BOARD_SEPOLICY_IGNORE
进行过滤,删除任何应该被忽略的内容。接着确保只找到一个替换的文件候选。否则,它会发出适当的错误信息。最后,它会确保文件存在于LOCAL_PATH
或基础策略中,如果两者都找不到,它会发出错误信息:
...
# Quick edge case error detection for BOARD_SEPOLICY_REPLACE.
# Builds the singular path for each replace file.
sepolicy_replace_paths :=
$(foreach pf, $(BOARD_SEPOLICY_REPLACE), \
$(if $(filter $(pf), $(BOARD_SEPOLICY_UNION)), \
$(error Ambiguous request for sepolicy $(pf). Appears in both \
BOARD_SEPOLICY_REPLACE and BOARD_SEPOLICY_UNION), \
) \
$(eval _paths := $(filter-out $(BOARD_SEPOLICY_IGNORE), \
$(wildcard $(addsuffix /$(pf), $(BOARD_SEPOLICY_DIRS))))) \
$(eval _occurrences := $(words $(_paths))) \
$(if $(filter 0,$(_occurrences)), \
$(error No sepolicy file found for $(pf) in $(BOARD_SEPOLICY_DIRS)), \
) \
$(if $(filter 1, $(_occurrences)), \
$(eval sepolicy_replace_paths += $(_paths)), \
$(error Multiple occurrences of replace file $(pf) in $(_paths)) \
) \
$(if $(filter 0, $(words $(wildcard $(addsuffix /$(pf), $(LOCAL_PATH))))), \
$(error Specified the sepolicy file $(pf) in BOARD_SEPOLICY_REPLACE, \
but none found in $(LOCAL_PATH)), \
) \
)
在此之后,构建策略的调用可以使用replace_paths
作为在构建期间将被替换的文件的扩展列表。
build_policy
函数的参数是您希望使用BOARD_SEPOLICY_*
系列变量的提供的功能扩展到它们的 Android 根相对路径名称的文件名。例如,在我们的设备 A、X 和 Y 的上下文中调用$(build_policy, file_contexts)
将导致如下结果:
device/OEM/A/file_contexts
device/OEM/Y/file_contexts
build_policy
函数的阅读有点棘手。许多嵌套的函数调用导致最深的缩进首先运行。然而,像所有代码一样,我们从上到下,从左到右阅读,因此解释将从这里开始。该函数首先遍历作为参数传递的所有文件。然后针对BOARD_SEPOLICY_DIRS
进行一次替换和一次联合的扩展。检查sepolicy_replace_paths
变量以确保文件没有同时出现在替换和联合的位置。对于替换路径的扩展,它会检查扩展后的路径是否在sepolicy_replace_dirs
中,如果是,则替换它。对于联合部分,它只是进行扩展。这些扩展的结果随后通过BOARD_SEPOLICY_IGNORE
的过滤器,从而删除任何明确忽略的路径:
# Builds paths for all requested policy files w.r.t
# both BOARD_SEPOLICY_REPLACE and BOARD_SEPOLICY_UNION
# product variables.
# $(1): the set of policy name paths to build
build_policy = $(foreach type, $(1), \
$(filter-out $(BOARD_SEPOLICY_IGNORE), \
$(foreach expanded_type, $(notdir $(wildcard $(addsuffix /$(type), $(LOCAL_PATH)))), \
$(if $(filter $(expanded_type), $(BOARD_SEPOLICY_REPLACE)), \
$(wildcard $(addsuffix $(expanded_type), $(sort $(dir $(sepolicy_replace_paths))))), \
$(LOCAL_PATH)/$(expanded_type) \
) \
) \
$(foreach union_policy, $(wildcard $(addsuffix /$(type), $(BOARD_SEPOLICY_DIRS))), \
$(if $(filter $(notdir $(union_policy)), $(BOARD_SEPOLICY_UNION)), \
$(union_policy), \
) \
) \
) \
)
...
构建 mac_permissions.xml
如我们在第十章《将应用置于域中》所见,mac_permissions.xml
的构建有点棘手。首先,mac_permissions.xml
可以与迄今为止引入的所有BOARD_SEPOLICY_*
变量一起使用。最终结果是生成一个符合这些变量规则的 XML 文件。此外,原始 XML 文件由位于sepolicy/tools
目录下的一个名为insertkeys.py
的工具处理。insertkeys.py
工具使用keys.conf
将 XML 文件签名区域的标签与包含证书的.pem
文件进行映射。keys.conf
文件同样适用于BOARD_SEPOLICY_*
变量。构建配方首先对keys.conf
调用build_policy
,并使用m4
连接结果。因此,keys.conf
中的m4
声明将被尊重。然而,这尚未被使用。最初的意图是使用m4 -s
同步行,以便在m4
处理连接keys.conf
文件时,您可以跟随包含链。另一方面,当连接多个文件时,m4
会提供同步行,并提供符合#line NUM "FILE"'
格式的注释行。这些很有用,因为m4
将多个输入文件合并成一个扩展的输出文件。将会有指示每个文件开头的同步行,它们可以帮助您追踪问题。回到mac_permissions.xml
的构建,经过m4
对keys.conf
的扩展后,该文件以及通过调用build_policy()
获取的所有mac_permissions.xml
文件最终被传递给insertkeys.py
。insertkeys.py
工具然后使用keys.conf
文件将所有匹配的signature=<TAG>
行替换为来自 PEM 文件的十六进制编码的实际 X509,即signature=308E3600
。此外,insertkeys.py
工具将 XML 文件合并为一个文件,并去除空格和注释以减少其在磁盘上的大小。这不会对其他主要文件如sepolicy
、seapp_contexts
、property_contexts
和mac_permissions.xml
产生构建依赖。
构建 seapp_contexts
seapp_contexts
文件也受所有BOARD_SEPOLICY_*
变量的影响。来自build_policy()
调用结果的所有seapp_contexts
文件也通过m4 -s
处理,以获得包含同步行的单个seapp_contexts
文件。同样,类似于mac_permissions.xml
文件的keys.conf
构建,除了用于 synclines 之外,没有使用m4
。这个结果,连接的seapp_contexts
文件随后被输入到check_seapp
中。这个工具是用 C 编程语言编写的,并在构建过程中编译成可执行文件。源代码可以在tools/check_seapp
中找到。此工具读取seapp_contexts
文件并检查其语法。它验证没有无效的键值对,levelFrom
是一个有效的标识符,并且对于给定的sepolicy
,类型和域字段是有效的。此构建依赖于sepolicy
,以对策略文件中的域和类型字段进行严格类型检查。
构建 file_contexts
file_contexts
文件也受所有BOARD_SEPOLICY_*
变量的影响。生成的集合通过m4 -s
处理,单一输出通过checkfc
工具运行。checkfc
工具检查文件的语法和句法,并验证在构建的sepolicy
中存在这些类型。因此,它依赖于sepolicy
构建。
构建 property_contexts
property_contexts
的行为与file_contexts
构建完全一样,只不过它检查一个property_contexts
文件。它也使用checkfc
。
当前国家安全局的研究文件
此外,国家安全局已经在企业运营(eops
)方面开始了工作。由于此功能尚未合并到主流 Android 中,并且可能会发生巨大变化,因此这里不会介绍。然而,对于追求前沿技术的最佳地点始终是源代码和国家安全局的 Bitbucket 仓库。selinux-network.sh
也属于这一类;它尚未被主流采用,并且可能会从 AOSP 中删除(android-review.googlesource.com/#/c/114380/
)。
独立工具
还有一些为 Android 策略评估构建的独立工具,你可能会觉得有用。我们将探讨其中一些及其用途。大多数在其他参考资料中找到的标准桌面工具仍然适用于 SE for Android SELinux 策略。请注意,如果你运行以下任何工具并遇到段错误,你可能需要应用来自thread at http://marc.info/?l=seandroid-list&m=141684060409894&w=2的补丁。
sepolicy-check
这个工具允许你查看策略文件中是否存在给定的允许规则。其命令的基本语法如下:
sepolicy-check -s <domain> -t <type> -c <class> -p <permission> -P <policy_file>
例如,如果你想查看system_app
是否可以写入system_data_file
的 class 文件,你可以执行:
$ sepolicy-check -s system_app -t system_data_file -c file -p write -P $OUT/root/sepolicy
sepolicy-analyze
这是一个检查 SELinux 开发中常见问题的好工具,它捕捉到了一些新的 SELinux 策略编写者容易犯的常见陷阱。它可以检查等价域、重复的允许规则。它还可以执行策略类型差异检查。
域等价性检查功能非常有帮助。它能显示你可能(从理论上讲)希望不同的域,尽管在实际实现中它们可能已经收敛。这些类型的域将是合并的理想候选者。然而,它也可能揭示了政策设计中应该修正的问题。换句话说,你原本不期望这些域是等价的。调用该命令如下所示:
$ sepolicy-analyze -e -P $OUT/root/sepolicy
重复的允许规则检查是否存在这样的规则:类型上存在允许规则,而这些规则也存在于该类型继承的属性上。由于在属性上已经有一个allow
规则,因此特定类型上的允许规则是删除的候选者。要执行此检查,请运行以下命令:
$sepolicy-analyze -D -P $OUT/root/sepolicy
在文件内查看域类型差异的功能也很有用。如果你想了解两个域之间的差异,可以使用这个功能。这对于识别可能的合并域很有帮助。要执行此检查,请执行以下命令:
$sepolicy-analyze -d -P $OUT/root/sepolicy
总结
在本章中,我们介绍了控制设备上策略的各种组件是如何实际构建和创建的,例如sepolicy
和mac_permissions.xml
。本章还介绍了用于跨设备和配置管理构建策略的BOARD_SEPOLICY_*
变量。然后我们回顾了Android.mk
组件,详细说明了构建和配置管理核心的工作原理。
第十三章:进入强制模式
作为一名工程师,你拿到一部 Android 设备,要求你应用 SE for Android 控制来增强设备的安全态势。到目前为止,我们已经看到了需要配置的所有部分以及它们如何工作来启用这样一个系统。在本章中,我们将运用所学的所有技能,让 UDO 进入强制模式。我们将:
-
运行、评估并响应来自 CTS 的审核日志
-
为 UDO 开发安全策略
-
切换到强制模式
更新到 SEPolicy master
自 4.3 版本以来,AOSP master
分支的 sepolicy
目录发生了许多变化。在撰写本文时,external/sepolicy
项目的 master
分支在 Git 提交 SHA b5ffb
上。作者建议尝试使用最新的提交。然而,为了说明目的,我们将展示如何选择性地检出提交 b5ffb
,以便你可以准确地跟随本章中的示例。
首先,你需要克隆 external/sepolicy
项目。在这些说明中,我们假设你的工作目录中包含 UDO 源码的 ./udoo
目录:
$ git clone https://android.googlesource.com/platform/external/sepolicy
$ cd sepolicy
如果你想要精确地跟随本章的示例,你需要使用以下命令检出提交 b5ffb
。如果你跳过这一步,你最终将使用 master
分支中的最新提交:
$ git checkout b5ffb
现在,我们将用从谷歌获取的 sepolicy 替换 UDO 4.3 的 sepolicy:
$ cd ..
$ rm -rf udoo/external/sepolicy
$ cp -r sepolicy udoo/external/sepolicy
可选地,你可以使用以下命令从新复制的 sepolicy 中删除 .git
文件夹,但这不是必须的:
$ rm –rf udoo/external/sepolicy/.git
同时,复制 audit.te
文件并恢复它。
此外,从 NSA Bitbucket seandroid
仓库恢复 auditd
提交。作为参考,它的提交 SHA 是 d270aa3
。
之后,从 udoo/build/core/Makefile
中删除所有对 setool
的引用。这个命令将帮助你找到它们:
$ grep -nw setool udoo/build/core/Makefile
清除设备
在这一点上,我们的 UDO 很混乱,所以让我们重新刷新它,包括数据目录,并重新开始。我们希望只有代码和 init
脚本更改,而没有额外的 sepolicy。然后我们可以适当地编写一个策略,并应用我们遇到的所有的技术和工具。我们将从重置到一个类似于完成第四章,UDO 上的安装的状态开始。然而,主要区别是我们需要构建一个 userdebug
版本而不是工程 (eng
) 版本用于 CTS。版本在设置脚本中选择,最终调用 lunch
。要构建此版本,请从 UDO 工作区执行以下命令:
$ . setup udoo-userdebug
$ make -j8 2>&1 | tee logz
使用以下命令刷新系统,引导到 SD 卡,并清除 userdata
,假设 SD 卡已插入主机且 userdata
未挂载:
$ mkdir ~/userdata
$ sudo mount /dev/sdd4 ~/userdata
$ cd ~/userdata/
$ sudo rm -rf *
$ cd ..
$ sudo umount ~/userdata
设置 CTS
如果你的组织寻求 Android 品牌认证,你必须通过 CTS。然而,即使你不是,运行这些测试也是一个好主意,以确保设备将符合应用程序的要求。根据你的安全目标和愿望,如果你不是在寻求 Android 品牌认证,你可能会在 CTS 的某些部分失败。对于我们的情况,我们将 CTS 视为一种锻炼系统并发现阻止 UDOO 正常工作的政策问题的方式。其源代码位于 cts/
目录中,但我们建议直接从 Google 下载二进制文件。你可以从 source.android.com/compatibility/cts-intro.html
和 source.android.com/compatibility/android-cts-manual.pdf
获取更多信息及 CTS 二进制文件本身。
从 下载 选项卡下载 CTS 4.3 二进制文件。然后选择 CTS 二进制文件。兼容性定义文档(CDD)也值得一读。它涵盖了 CTS 和兼容性要求的高级详细信息。
从 source.android.com/compatibility/downloads.html
下载 CTS 并解压。选择与你的 Android 版本匹配的 CTS 版本。如果你不知道你的设备正在运行哪个版本,你可以通过 UDOO 使用 getprop ro.build.version.release
命令检查 ro.build.version.release
属性:
$ mkdir ~/udoo-cts
$ cd ~/udoo-cts
$ wget https://dl.google.com/dl/android/cts/android-cts-4.3_r2-linux_x86-arm.zip
$ unzip android-cts-4.3_r2-linux_x86-arm.zip
运行 CTS
CTS 对设备上的许多组件进行锻炼,并帮助测试系统的各个部分。一个好的通用政策应允许 Android 正常工作并通过 CTS。
按照 Android CTS 用户手册中的说明设置你的设备(见 第 3.3 节,设置你的设备)。通常,如果你没有精确地遵循所有步骤,你可能会看到一些失败,因为你可能无法获取到所有需要的资源或权限。然而,CTS 仍然会执行一些代码路径。至少,我们建议复制媒体文件并激活 Wi-Fi。设置好设备后,确保 adb
处于活动状态并启动测试:
$ ./cts-tradefed
11-30 10:30:08 I/: Detected new device 0123456789ABCDEF
cts-tf > run cts --plan CTS
cts-tf >
time passes here
11-30 10:30:28 I/TestInvocation: Starting invocation for 'cts' on build '4.3_r2' on device 0123456789ABCDEF
11-30 10:30:28 I/0123456789ABCDEF: Created result dir 2014.11.30_10.30.28
11-30 10:31:44 I/0123456789ABCDEF: Collecting device info
11-30 10:31:45 I/0123456789ABCDEF: -----------------------------------------
11-30 10:31:45 I/0123456789ABCDEF: Test package android.aadb started
11-30 10:31:45 I/0123456789ABCDEF: -----------------------------------------
11-30 10:32:15 I/0123456789ABCDEF: com.android.cts.aadb.TestDeviceFuncTest#testBugreport PASS
...
测试需要执行很多小时,所以请耐心等待;但你可以检查测试的状态:
cts-tf > l i
Command Id Exec Time Device State
1 8m:22 0123456789ABCDEF running cts on build 4.3_r2
插上扬声器,享受来自媒体测试和铃声的声音!同时,CTS 会重启设备。如果你的 ADB 会话在重启后没有恢复,ADB 可能不会执行任何测试。在运行 cts-tf > run cts --plan CTS --disable-reboot
计划时,使用 --disable-reboot
选项。
收集结果
首先,我们将考虑 CTS 的结果。虽然我们预计会有一些失败,但我们也预计在进入强制模式时问题不会变得更糟。其次,我们将查看审计日志。让我们从设备中提取这两个文件。
CTS 测试结果
每次运行 CTS 时,它都会创建一个测试结果目录。CTS 指出了目录名称,但未指出位置:
11-30 10:30:28 I/0123456789ABCDEF: Created result dir 2014.11.30_10.30.28
CTS 手册中提到了这个位置,可以在提取的 CTS 目录下的repository/results
中找到,通常在android-cts/repository/results
。测试目录包含一个 XML 测试报告,testResult.xml
。这可以在大多数网络浏览器中打开,并且有一个很好的测试概览以及所有执行测试的详细信息。通过:失败
的比例是我们的基线。作者有 18,736 个通过,只有 53 个失败,考虑到其中一半是功能问题,比如没有蓝牙或对摄像头支持返回真,这个结果相当不错。
审计日志
我们将使用审计日志来解决我们策略中的不足。使用本书中一直使用的标准adb pull
命令从设备上提取这些日志。由于这是一个userdebug
版本,默认的adb
终端是 shell uid
(不是 root),因此以 root 身份启动adb
,使用adb root
命令。在userdebug
版本上也可以使用su
。
提示
你可能会遇到错误提示/data/misc/audit/audit.log
不存在。解决方案是使用adb root
命令以 root 身份运行adb
。此外,在运行此命令时,它可能会挂起。只需进入设置,禁用,然后在开发者选项下重新启用USB 调试。然后杀死adb-root
命令,并通过运行adb shell
验证你是否具有 root 权限。现在你应该再次成为 root 用户了。
设备策略编写
通过audit2allow
运行audit.log
和audit.old
,看看发生了什么。audit2allow
的输出按源域名分组。我们不会全部查看,而是从audit2allow
的解释结果开始,突出不寻常的情况。假设你目前在审计日志目录中,执行cat audit.* | audit2allow | less
。所有策略工作将在设备特定的 UDOOPolicy 目录中进行。
adbd
以下是通过audit2allow
过滤的我们的adbd
拒绝:
#============= adbd ==============
allow adbd ashmem_device:chr_file execute;
allow adbd dumpstate:unix_stream_socket connectto;
allow adbd dumpstate_socket:sock_file write;
allow adbd input_device:chr_file { write getattr open };
allow adbd log_device:chr_file { write read ioctl open };
allow adbd logcat_exec:file { read getattr open execute execute_no_trans };
allow adbd mediaserver:binder { transfer call };
allow adbd mediaserver:fd use;
allow adbd self:capability { net_raw dac_override };
allow adbd self:process execmem;
allow adbd shell_data_file:file { execute execute_no_trans };
allow adbd system_server:binder { transfer call };
allow adbd tmpfs:file execute;
allow adbd unlabeled:dir getattr;
adbd
域中的拒绝相当奇怪。首先引起我们注意的是对字符驱动器/dev/ashmem
的execute
操作。通常,这只有在 Dalvik JIT 时才需要。查看原始审计(cat audit.* | grep adbd | grep execute
),我们看到以下内容:
type=1400 msg=audit(1417416666.182:788): avc: denied { execute } for pid=3680 comm="Compiler" path=2F6465762F6173686D656D2F64616C76696B2D6A69742D636F64652D6361636865202864656C6574656429 dev=tmpfs ino=412027 scontext=u:r:adbd:s0 tcontext=u:object_r:tmpfs:s0 tclass=file
type=1400 msg=audit(1417416670.352:831): avc: denied { execute } for pid=3753 comm="Compiler" path="/dev/ashmem" dev=tmpfs ino=1127 scontext=u:r:adbd:s0 tcontext=u:object_r:ashmem_device:s0 tclass=chr_file
我们发现编译器中的comm
字段进程正在ashmem
上执行。我们猜测这与 Dalvik 有关,但为什么它会出现在adbd
域中?还有,为什么adbd
要写入输入设备?这些都是奇怪的行为。通常,当你看到这类事情时,是因为子进程没有进入正确的域。运行以下命令检查域以确认我们的怀疑:
$ adb shell ps -Z | grep adbd
u:r:adbd:s0 root 20046 1 /sbin/adbd
u:r:adbd:s0 root 20101 20046 ps
然后,我们运行adb shell ps -Z | grep adbd
来查看哪些进程在adb
域中运行,进一步证实了我们的怀疑:
u:r:adbd:s0 root 20046 1 /sbin/adbd
u:r:adbd:s0 root 20101 20046 ps
ps
命令不应该在adbd
上下文中运行;它应该在shell
中运行。这证实了shell
不在正确的域中:
$ adb shell
root@udoo:/ # id
uid=0(root) gid=0(root) context=u:r:adbd:s0
首先要检查的是文件上下文:
root@udoo:/ # ls -Z /system/bin/sh
lrwxr-xr-x root shell u:object_r:system_file:s0 sh -> mksh
root@udoo:/ # ls -Z /system/bin/mksh
-rwxr-xr-x root shell u:object_r:system_file:s0 mksh
基本策略定义了当adbd
使用exec
加载 shell 以进入 shell 域时的域转换。这在外部 sepolicy 的adbd.te
中定义为domain_auto_trans(adbd, shell_exec, shell)
。
显然,shell 被错误地标记了,因此我们来看看外部 sepolicy 中的file_contexts
以找出原因。
$ cat file_contexts | grep shell_exec
/system/bin/sh -- u:object_r:shell_exec:s0
两个破折号意味着只有常规文件将被标记,而符号链接将被跳过。我们可能不想标记符号链接,而是mksh
的目标。通过向设备 UDOO sepolicy 添加自定义file_contexts
条目,并将文件添加到BOARD_SEPOLICY_UNION
配置中来实现。在file_contexts
中添加/system/bin/mksh -- u:object_r:shell_exec:s0
,在sepolicy.mk
中添加BOARD_SEPOLICY_UNION += file_contexts
。
提示
在本章的剩余部分,每当你创建或修改策略文件(例如,上下文文件或*.te
文件)时,别忘了将它们添加到sepolicy.mk
中的BOARD_SEPOLICY_UNION
。
由于这是策略和adbd
的一个相当严重的问题,我们现在不担心拒绝,除了未标记的。每当遇到未标记的文件时,都应该处理。导致此问题的avc
拒绝如下:
type=1400 msg=audit(1417405835.872:435): avc: denied { getattr } for pid=4078 comm="ls" path="/device" dev=mmcblk0p7 ino=2 scontext=u:r:adbd:s0 tcontext=u:object_r:unlabeled:s0 tclass=dir
因为这被挂载在/device
,而 Android 的挂载通常在/
,我们应该查看挂载表:
root@udoo:/ # mount | grep device
/dev/block/mmcblk0p7 /device ext4 ro,seclabel,nosuid,nodev,relatime,user_xattr,barrier=1,data=ordered 0 0
通常,挂载命令位于mkdir
之后的 init 脚本中,或者在带有内置 init 的fstab
文件中,通过mount_all
。在init.rc
中快速搜索device
和mkdir
没有发现任何内容,但我们确实在fstab.freescale
中找到了它。设备是只读的,因此我们应该能够为其指定类型,用文件上下文进行标记,并将其目录类别应用到getattr
域。既然它是只读且为空的,没有人应该需要更多权限。查看make_sd.sh
脚本,我们注意到块设备分区 7 是vender
目录。这是 OEM 放置专有 blob 的常见 vendor 目录的拼写错误。我们在file.te
中放置文件类型,并在domain.te
中放置域允许规则。
在file.te
中,添加以下内容:
type udoo_device_file, file_type;
在domain.te
中,添加以下内容:
allow domain udoo_device_file:dir getattr;
在file_contexts
中,添加以下内容:
/device u:object_r:udoo_device_file:s0
如果这个目录不为空,你必须手动运行restorecon -R
来标记现有文件。
如果你多次从 UDOO 提取审计日志,你也可能看到拒绝显示你这样做,因为adbd
将无法访问它们。你可能会看到如下内容:
#============= adbd ==============
allow adbd audit_log:file { read getattr open };
这条规则来自于当你通过adb pull
提取审计日志的测试结束时。我们可以安全地dontaudit
这个,并添加一个neverallow
以确保它不会意外被允许。审计日志包含恶意软件作者可能用来导航策略的信息,这些信息应该受到保护。在设备的sepolicy
文件夹中,添加一个adbd.te
文件,并在sepolicy.mk
文件中进行合并:
在adbd.te
文件中,添加以下内容:
# dont audit adb pull and adb shell cat of audit logs
dontaudit adbd audit_log:file r_file_perms;
dontaudit shell audit_log:file r_file_perms;
在auditd.te
中,添加以下内容:
# Make sure no one adds an allow to the audit logs
# from anything but system server (read only) and
# auditd, rw access.
neverallow { domain -system_server -auditd -init -kernel } audit_log:file ~getattr;
neverallow system_server audit_log:file ~r_file_perms;
如果auditd.te
仍然在external/sepolicy
中,将其移到device/fsl/udoo/sepolicy
,并带上所有依赖的类型。
neverallow
条目展示了如何使用补集~
和集合差集-
操作符进行强断言或简洁表达。第一个neverallow
从域开始,所有进程类型(域)都是 domain 属性的成员。我们通过集合差集阻止访问,留下了永远不应该访问的集合。然后我们对访问向量集取补集,只允许在日志上执行getattr
或stat
。第二个neverallow
使用补集确保system_server
仅限于读取操作。
bootanim
bootanim
域被分配给启动动画服务,该服务在启动时显示启动画面,通常是运营商的品牌:
#============= bootanim ==============
allow bootanim init:unix_stream_socket connectto;
allow bootanim log_device:chr_file { write open };
allow bootanim property_socket:sock_file write;
任何接触init
域的都是一个红旗。这里,bootanim
连接到一个 init Unix 域套接字。这是属性系统的一部分,我们可以看到连接后,它会写入属性套接字。套接字对象和它的 URI 是分开的。在这种情况下,它是文件系统,但它可能是一个匿名套接字:
type=1400 msg=audit(1417405616.640:255): avc: denied { connectto } for pid=2534 comm="BootAnimation" path="/dev/socket/property_service" scontext=u:r:bootanim:s0 tcontext=u:r:init:s0 tclass=unix_stream_socket
在新版本的 Android 中,log_device
已被弃用,并用logd
替换。然而,我们将新的主 sepolicy 回移植到 4.3,因此我们必须支持这个。移除支持的补丁在android-review.googlesource.com/#/c/108147/
。
我们不需要对 external sepolicy 应用反向补丁,只需将规则添加到我们的设备策略中的domain.te
文件中。我们可以安全地使用设备 UDO sepolicy
文件夹中的正确宏和样式允许这些操作。在bootanim.te
中,添加unix_socket_connect(bootanim, property, init)
,在domain.te
中,添加以下内容:
allow domain udoo_device_file:dir getattr;
allow domain log_device:dir search;
allow domain log_device:chr_file rw_file_perms;
debuggerd
#============= debuggerd ==============
allow debuggerd log_device:chr_file { write read open };
allow debuggerd system_data_file:sock_file write;
通过为所有域添加允许规则来使用log_device
,在bootanim
下解决了日志设备的拒绝。system_data_file:sock_file write
很奇怪。在大多数情况下,你几乎永远不会希望允许跨域写入,但这是一种特殊情况。看看原始拒绝:
type=1400 msg=audit(1417415122.602:502): avc: denied { write } for pid=2284 comm="debuggerd" name="ndebugsocket" dev=mmcblk0p4 ino=129525 scontext=u:r:debuggerd:s0 tcontext=u:object_r:system_data_file:s0 tclass=sock_file
拒绝是关于ndebugsocket
的。通过搜索这个发现了一个命名类型转换,而策略版本 23 不支持:
system_server.te:297:type_transition system_server system_data_file:sock_file system_ndebug_socket "ndebugsocket";
我们必须更改代码以设置正确的上下文,或者直接允许它,我们将会这样做。我们不会授予额外的权限,因为它从未请求过 open,而且我们正在跨域操作。阻止跨域文件打开是理想的,因为获取这个文件描述符的唯一方式是通过 IPC 调用到拥有该域的进程中。在debuggerd.te
中,添加allow debuggerd system_data_file:sock_file write;
。
drmserver
#============= drmserver ==============
allow drmserver log_device:chr_file { write open };
这由domain.te
规则处理,所以我们这里不需要做任何事情。
dumpstate
#============= dumpstate ==============
allow dumpstate init:binder call;
allow dumpstate init:process signal;
allow dumpstate log_device:chr_file { write read open };
allow dumpstate node:rawip_socket node_bind;
allow dumpstate self:capability sys_resource;
allow dumpstate system_data_file:file { write rename create setattr };
在dumpstate
上对init:binder call
的拒绝很奇怪,因为init
并不使用 binder。某些进程必须停留在 init 域中。让我们检查一下我们的进程列表中关于 init 的部分:
$ adb shell ps -Z | grep init
u:r:init:s0 root 1 0 /init
u:r:init:s0 root 2286 1 zygote
u:r:init:s0 radio 2759 2286 com.android.phone
在这里,zygote
和 com.android.phone
不应该以 init
身份运行。这一定是 app_process
文件的标签错误,它是 zygote。执行 ls -laZ /system/bin/app_process
命令会显示 u:object_r:system_file:s0 app_process
,因此需要在 file_contexts
中添加一个条目来纠正此错误。我们可以在基础 sepolicy 中的 zygote.te
文件找到要使用的标签,定义为 zygote_exec
类型:
# zygote
type zygote, domain;
type zygote_exec, exec_type, file_type;
在 file_contexts
中,添加 /system/bin/app_process u:object_r:zygote_exec:s0
。
安装守护进程
添加的 domain.te
规则处理 installd
。
密钥存储
#============= keystore ==============
allow keystore app_data_file:file write;
allow keystore log_device:chr_file { write open };
日志设备由 domain.te
规则处理。让我们看看原始的 app_data_file
拒绝:
type=1400 msg=audit(1417417454.442:845): avc: denied { write } for pid=15339 comm="onCtsTestRunner" path="/data/data/com.android.cts.stub/cache/CTS_DUMP" dev=mmcblk0p4 ino=131242 scontext=u:r:keystore:s0 tcontext=u:object_r:app_data_file:s0:c512,c768 tclass=file
类别在上下文中定义。这意味着激活了 app domains
的 MLS 支持。在 seapp_contexts
基础 sepolicy 中,我们看到以下内容:
user=_app domain=untrusted_app type=app_data_file levelFrom=user
user=_app seinfo=platform domain=platform_app type=app_data_file levelFrom=user
MLS 应用数据分离仍在开发中,在 4.3 上不起作用,因此我们可以禁用这个功能。我们只需在特定于设备的 seapp_contexts
文件中声明它们。在 seapp_contexts
中,添加 user=_app domain=untrusted_app type=app_data_file
和 user=_app seinfo=platform domain=platform_app type=app_data_file
。在 4.3 版本中,对数据上下文的任何更改都需要进行工厂重置。4.4 版本增加了智能重新标签的功能。
媒体服务器
#============= mediaserver ==============
allow mediaserver adbd:binder { transfer call };
allow mediaserver init:binder { transfer call };
allow mediaserver log_device:chr_file { write open };
日志设备在 domain.te
规则中处理。我们将跳过 init
和 adbd
,因为它们的问题是由不正确的进程域引起的。不要盲目添加允许规则,因为现有域的大部分工作可以通过小的标签更改或少数规则处理。
网络守护进程
#============= netd ==============
allow netd kernel:system module_request;
allow netd log_device:chr_file { write open };
netd
对日志设备的拒绝在 domain.te
中处理。然而,我们应该仔细审查任何请求能力的请求。在授予能力时,策略作者需要非常小心。如果一个域被授予加载系统模块的能力,而该域或模块二进制本身受到威胁,它可能导致通过可加载模块将恶意软件注入内核。然而,netd
需要可加载内核模块支持以支持某些卡片。在设备 UDOO sepolicy 中的名为 netd.te
的文件中添加允许规则。在 netd.te
中,添加 allow netd self:capability sys_module;
。
无线设备守护进程
#============= rild ==============
allow rild log_device:chr_file { write open };
这由 domain.te
规则处理,所以我们在这里不需要做任何事情。
服务管理器
#============= servicemanager ==============
allow servicemanager init:binder transfer;
allow servicemanager log_device:chr_file { write open };
同样,日志设备在 domain.te
中处理。我们将跳过 init
,因为其问题是由不正确的进程域引起的。
表面抛掷器
#============= surfaceflinger ==============
allow surfaceflinger init:binder transfer;
allow surfaceflinger log_device:chr_file { write open };
同样,日志设备在 domain.te
中处理。我们将跳过 init
,因为其问题是由不正确的进程域引起的。
系统服务器
#============= system_server ==============
allow system_server adbd:binder { transfer call };
allow system_server dalvikcache_data_file:file { write setattr };
allow system_server init:binder { transfer call };
allow system_server init:file write;
allow system_server init:process { setsched sigkill getsched };
allow system_server init_tmpfs:file read;
allow system_server log_device:chr_file write;
由于 log_device
由 domain.te
处理,而 init
和 adbd
被污染,我们只处理 Dalvik 缓存拒绝:
type=1400 msg=audit(1417405611.550:159): avc: denied { write } for pid=2571 comm="er.ServerThread" name="system@app@SettingsProvider.apk@classes.dex" dev=mmcblk0p4 ino=129458 scontext=u:r:system_server:s0 tcontext=u:object_r:dalvikcache_data_file:s0 tclass=file
type=1400 msg=audit(1417405611.550:160): avc: denied { setattr } for pid=2571 comm="er.ServerThread" name="system@app@SettingsProvider.apk@classes.dex" dev=mmcblk0p4 ino=129458 scontext=u:r:system_server:s0 tcontext=u:object_r:dalvikcache_data_file:s0 tclass=file
外部 sepolicy seandroid-4.3 分支允许domain.te:allow domain dalvikcache_data_file:file r_file_perms;
。system_app
通过system_app.te:allow system_app dalvikcache_data_file:file { write setattr };
允许写入。我们应该能够授予这种写入权限,因为可能需要更新其 Dalvik 缓存文件。在domain.te
中,添加allow domain dalvikcache_data_file:file r_file_perms;
,在system_server.te
中,添加allow system_server dalvikcache_data_file:file { write setattr };
。
toolbox
#============= toolbox ==============
allow toolbox sysfs:file write;
通常,人们不应该写入sysfs
。现在看看对违规sysfs
文件的原始否认:
type=1400 msg=audit(1417405599.660:43): avc: denied { write } for pid=2309 comm="cat" path="/sys/module/usbtouchscreen/parameters/calibration" dev=sysfs ino=2318 scontext=u:r:toolbox:s0 tcontext=u:object_r:sysfs:s0 tclass=file
从这里,我们正确标记了/sys/module/usbtouchscreen/parameters/calibration
。在file_contexts
中添加一个条目来标记sysfs
,在file.te
中声明一个类型,并允许toolbox
访问它。在file.te
中,添加type sysfs_touchscreen_calibration, fs_type, sysfs_type, mlstrustedobject;
,在file_contexts
中,添加/sys/module/usbtouchscreen/parameters/calibration -- u:object_r:sysfs_touchscreen_calibration:s0
,在toolbox.te
中,添加allow toolbox sysfs_touchscreen_calibration:file w_file_perms;
。
非可信应用
#============= untrusted_app ==============
allow untrusted_app adb_device:chr_file getattr;
allow untrusted_app adbd:binder { transfer call };
allow untrusted_app adbd:dir { read getattr open search };
allow untrusted_app adbd:file { read getattr open };
allow untrusted_app adbd:lnk_file read;
...
untrusted_app
有许多否认。考虑到域标记问题,我们现在不会解决大多数问题。但是,你应该注意错误标记和未标记的目标文件。在使用audit2allow
解释的否认日志中搜索时,发现了以下内容:
allow untrusted_app device:chr_file { read getattr };
allow untrusted_app unlabeled:dir { read getattr open };
对于chr_file
设备,我们得到这个:
type=1400 msg=audit(1417416653.742:620): avc: denied { read } for pid=3696 comm="onCtsTestRunner" name="rfkill" dev=tmpfs ino=1126 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417416666.152:784): avc: denied { getattr } for pid=3696 comm="onCtsTestRunner" path="/dev/mxs_viim" dev=tmpfs ino=1131 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417416653.592:561): avc: denied { getattr } for pid=3696 comm="onCtsTestRunner" path="/dev/.coldboot_done" dev=tmpfs ino=578 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:device:s0 tclass=file
因此,我们需要正确标记/dev/.coldboot_done
,/dev/rfkill
和/dev/mxs_viim
。/dev/rfkill
应该按照 4.3 政策的做法进行标记:
file_contexts:/sys/class/rfkill/rfkill[0-9]*/state -- u:object_r:sysfs_bluetooth_writable:s0
file_contexts:/sys/class/rfkill/rfkill[0-9]*/type -- u:object_r:sysfs_bluetooth_writable:s0
/dev/mxs_viim
设备似乎是一个全局可访问的 GPU。我们建议彻底审查源代码,但现在,我们将它标记为gpu_device
。/dev/.coldboot_done
是在coldboot
进程完成后由ueventd
创建的。如果ueventd
重新启动,它会跳过冷启动。我们不需要标记这个。这个否认是由源域 MLS 对目标文件造成的,该文件不是源类别子集,并且没有mlstrustedsubject
属性;当我们从应用中删除 MLS 支持时,它应该消失。
在file_contexts
中:
# touch screen calibration
/sys/module/usbtouchscreen/parameters/calibration -- u:object_r:sysfs_touchscreen_calibration:s0
#BT RFKill node
/sys/class/rfkill/rfkill[0-9]*/state -- u:object_r:sysfs_bluetooth_writable:s0
/sys/class/rfkill/rfkill[0-9]*/type -- u:object_r:sysfs_bluetooth_writable:s0
vold
#============= vold ==============
allow vold log_device:chr_file { write open };
同样,日志设备在domain.te
中被处理。
watchdogd
#============= watchdogd ==============
allow watchdogd device:chr_file { read write create unlink open };
监管机构的原始否认描绘了一幅有趣的画像:
type=1400 msg=audit(1417405598.000:8): avc: denied { create } for pid=2267 comm="watchdogd" name="__null__" scontext=u:r:watchdogd:s0 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417405598.000:9): avc: denied { read write } for pid=2267 comm="watchdogd" name="__null__" dev=tmpfs ino=2580 scontext=u:r:watchdogd:s0 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417405598.000:10): avc: denied { open } for pid=2267 comm="watchdogd" name="__null__" dev=tmpfs ino=2580 scontext=u:r:watchdogd:s0 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417405598.000:11): avc: denied { unlink } for pid=2267 comm="watchdogd" name="__null__" dev=tmpfs ino=2580 scontext=u:r:watchdogd:s0 tcontext=u:object_r:device:s0 tclass=chr_file
type=1400 msg=audit(1417416653.602:575): avc: denied { getattr } for pid=3696 comm="onCtsTestRunner" path="/dev/watchdog" dev=tmpfs ino=1095 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:watchdog_device:s0 tclass=chr_file
watchdog
创建并解除了对一个匿名文件的引用。在 unlink 之后,不存在文件系统引用,但文件描述符有效,只有watchdog
可以使用它。在这种情况下,我们可以允许 watchdog 这个规则。在watchdogd.te
中,添加allow watchdogd device:chr_file create_file_perms;
。然而,这个规则在基本策略中引起了neverallow
的违反:
out/host/linux-x86/bin/checkpolicy: loading policy configuration from out/target/product/udoo/obj/ETC/sepolicy_intermediates/policy.conf
libsepol.check_assertion_helper: neverallow on line 5375 violated by allow watchdogd device:chr_file { read write open };
Error while expanding policy
neverallow
规则位于 domain.te
基本策略中,为 neverallow { domain -init -ueventd -recovery } device:chr_file { open read write };
。对于这种简单的更改,我们只需将基本 sepolicy 更改为 neverallow { domain -init -ueventd -recovery -watchdogd } device:chr_file { open read write };
。
wpa
#============= wpa ==============
allow wpa device:chr_file { read open };
allow wpa log_device:chr_file { write open };
allow wpa system_data_file:dir { write remove_name add_name setattr };
allow wpa system_data_file:sock_file { write create unlink setattr };
同样,日志设备在 domain.te
中处理。系统数据访问需要进一步调查,从原始拒绝开始:
type=1400 msg=audit(1417405614.060:193): avc: denied { setattr } for pid=2639 comm="wpa_supplicant" name="wpa_supplicant" dev=mmcblk0p4 ino=129295 scontext=u:r:wpa:s0 tcontext=u:object_r:system_data_file:s0 tclass=dir
type=1400 msg=audit(1417405614.060:194): avc: denied { write } for pid=2639 comm="wpa_supplicant" name="wlan0" dev=mmcblk0p4 ino=129318 scontext=u:r:wpa:s0 tcontext=u:object_r:system_data_file:s0 tclass=sock_file
type=1400 msg=audit(1417405614.060:195): avc: denied { write } for pid=2639 comm="wpa_supplicant" name="wpa_supplicant" dev=mmcblk0p4 ino=129295 scontext=u:r:wpa:s0 tcontext=u:object_r:system_data_file:s0 tclass=dir
type=1400 msg=audit(1417405614.060:196): avc: denied { remove_name } for pid=2639 co
使用 ls -laR
定位到问题文件:
/data/system/wpa_supplicant:
srwxrwx--- wifi wifi 2014-12-01 06:43 wlan0
这个套接字是由 wpa_supplicant
自身创建的。在没有类型转换的情况下重新标记它是不可能的,因此我们必须允许它。在 wpa.te
中添加 allow wpa system_data_file:dir rw_dir_perms;
和 allow wpa system_data_file:sock_file create_file_perms;
。未标记的设备已经处理过了;它在 rfkill
上:
type=1400 msg=audit(1417405613.640:175): avc: denied { read } for pid=2639 comm="wpa_supplicant" name="rfkill" dev=tmpfs ino=1126 scontext=u:r:wpa:s0 tcontext=u:object_r:device:s0 tclass=chr_file
第二次策略传递
在加载草拟的策略后,设备在启动时仍然有拒绝:
#============= init ==============
allow init rootfs:file { write create };
allow init system_file:file execute_no_trans;
#============= shell ==============
allow shell device:chr_file { read write getattr };
allow shell system_file:file entrypoint;
所有这些拒绝都应该被调查,因为它们针对的是敏感类型,特别是 tcontext
。
init
init
的原始拒绝如下:
<5>type=1400 audit(4.380:3): avc: denied { create } for pid=2268 comm="init" name="tasks" scontext=u:r:init:s0 tcontext=u:object_r:rootfs:s0 tclass=file
<5>type=1400 audit(4.380:4): avc: denied { write } for pid=2268 comm="init" name="tasks" dev=rootfs ino=3080 scontext=u:r:init:s0 tcontext=u:object_r:rootfs:s0 tclass=file
这些情况发生在 init
将 /
重新挂载为只读之前。我们可以安全地允许这些,由于 init
正在非限制状态下运行,我们只需将其添加到 init.te
中。我们可以将 allow
规则添加到非限制集合中,但由于它即将消失,让我们将权限最小化到仅 init
:
allow int rootfs:file create_file_perms;
注意
未限制的域并不是完全不受限制的。随着 AOSP 趋向于零未限制域,这个域的规则会被剥离。
然而,这样做会导致另一个 neverallow
失败。我们可以修改 external/sepolicy domain.te
来绕过这个问题。将 neverallow
从这个更改为:
# Nothing should be writing to files in the rootfs.
neverallow { domain -recovery} rootfs:file { create write setattr relabelto append unlink link rename };
更改为以下内容:
# Nothing should be writing to files in the rootfs.
neverallow { domain -recovery -init } rootfs:file { create write setattr relabelto append unlink link rename };
注意
如果你需要修改 neverallow
条目以构建,你将无法通过 CTS。正确的方法是从 init
中移除这种行为。
此外,我们需要查看哪些内容在没有域转换的情况下通过 exec
加载,导致 execute_no_trans
拒绝:
<5>type=1400 audit(4.460:6): avc: denied { execute_no_trans } for pid=2292 comm="init" path="/system/bin/magd" dev=mmcblk0p5 ino=146 scontext=u:r:init:s0 tcontext=u:object_r:system_file:s0 tclass=file
<5>type=1400 audit(4.460:6): avc: denied { execute_no_trans } for pid=2292 comm="init" path="/system/bin/rfkill" dev=mmcblk0p5 ino=148 scontext=u:r:init:s0 tcontext=u:object_r:system_file:s0 tclass=file
为了解决这个问题,我们可以用其自己的类型重新标记 magd
,并将其放在自己的非限制域中。基本策略中的 neverallow
强迫我们将每个可执行文件移动到其自己的域中。
创建一个名为 magd.te
的文件,将其添加到 BOARD_SEPOLICY_UNION
中,并向其中添加以下内容:
type magd, domain;
type magd_exec, exec_type, file_type;
permissive_or_unconfined(magd);
同时更新 file_contexts
以包含以下内容:
/system/bin/magd u:object_r:magd_exec:s0
对 rfkill
重复为 magd
所做的步骤。只需在前面示例中将 magd
替换为 rfkill
。后来的测试发现了一个入口点拒绝,其中源上下文是 init_shell
,目标是 rfkill_exec
。在添加 shell 规则后,发现 rfkill
是从 init_shell
域使用 exec
加载的,因此我们也应该在 rfkill.te
文件中添加 domain_auto_trans(init_shell, rfkill_exec, rfkill)
。此外,与这一发现相关的是 rfkill
尝试打开、读取和写入 /dev/rfkill
。因此,我们必须用 rfkill_device
标记 /dev/rfkill
,允许 rfkill
访问它,并在 rfkill.te
文件中追加 allow rfkill rfkill_device:chr_file rw_file_perms;
。创建一个名为 device.te
的新文件来声明此设备类型,并添加 type rfkill_device, dev_type;
。之后,通过在 file_contexts
中添加 /dev/rfkill u:object_r:rfkill_device:s0
来标记它。
shell
我们将评估的第一个 shell 拒绝是在 entrypoint
上:
<5>type=1400 audit(4.460:5): avc: denied { entrypoint } for pid=2279 comm="init" path="/system/bin/mksh" dev=mmcblk0p5 ino=154 scontext=u:r:shell:s0 tcontext=u:object_r:system_file:s0 tclass=file
由于我们没有给 mksh
打标签,现在我们需要给它打标签。我们可以创建一个不受限制的域,让 init
启动的 shell 最终进入 init_shell
域。控制台仍然通过显式 seclabel
进入 shell
域,而其他调用则作为 init_shell
。创建一个新文件 init_shell.te
,并将其添加到 BOARD_SEPOLICY_UNION
中。
init_shell.te
type init_shell, domain;
domain_auto_trans(init, shell_exec, init_shell);
permissive_or_unconfined(init_shell);
更新 file_contexts
以包含以下内容:
/system/bin/mksh u:object_r:shell_exec:s0;
现在,我们将处理对原始设备的 shell 访问:
<5>type=1400 audit(6.510:7): avc: denied { read write } for pid=2279 comm="sh" name="ttymxc1" dev=tmpfs ino=122 scontext=u:r:shell:s0 tcontext=u:object_r:device:s0 tclass=chr_file
<5>type=1400 audit(7.339:8): avc: denied { getattr } for pid=2279 comm="sh" path="/dev/ttymxc1" dev=tmpfs ino=122 scontext=u:r:shell:s0 tcontext=u:object_r:device:s0 tclass=chr_file
这只是一个标签错误的 tty
,我们可以将其标记为 tty_device
。在文件上下文中添加以下条目:
/dev/ttymxc[0-9]* u:object_r:tty_device:s0
现场试验
在此阶段,重新构建源代码树,清除数据文件系统,刷新,并重新运行 CTS。重复此操作直到所有拒绝都被处理。
当你完成 CTS 和内部 QA 测试后,我们建议在宽容模式下进行现场试验。在此期间,你应该收集日志并完善策略。如果域不稳定,你可以在策略文件中将它们声明为宽容模式,但仍然将设备设置为执行模式;执行部分域总比一个都不执行要好。
转为执行模式
你可以使用 bootloader
传递执行模式(这部分内容这里不涉及),或者在启动早期通过 init.rc
脚本进行设置。你可以在 setcon
之后立即进行如下操作:
setcon u:r:init:s0
setenforce 1
一旦这条语句被编译到 init.rc
脚本中,就只能通过后续构建和重新刷新 boot.img
来撤销。你可以通过运行 getenforce
命令来检查这一点。另外,作为一个有趣的测试,你可以尝试从根串行控制台运行 reboot
命令,并观察它失败:
root@udoo:/ # getenforce
Enforcing
root@udoo:/ # reboot
reboot: Operation not permitted
摘要
在本章中,您之前对系统的所有理解都被用来为全新设备开发实际的 Android 安全增强(SE)策略。现在,您已经掌握了如何编写 Android 的 SELinux 策略、系统的各个组件在哪里以及如何工作,以及如何将这些特性移植并启用在各种 Android 平台上。由于这是一个相当新的特性,它影响许多系统交互,因此将出现需要代码变更以及策略变更的问题。理解这两者是至关重要的。
作为策略作者和一般的安全人员,确保系统的安全责任落在了我们的肩上。在大多数组织中,您可能需要在缺乏信息的情况下工作。然而,如果您可以,尽可能多地工作并在邮件列表中提出问题,绝不要接受现状。Android 安全增强(SE)和 AOSP 项目欢迎所有人贡献,通过贡献,您将帮助改进项目并增强所有功能的特性集。
附录 A.开发环境
为了构建 UDOO 提供的 Android 4.3 源码,你需要一个带有 Oracle Java 6 的 Ubuntu Linux 系统。虽然可能可以使用这种设置的变体,但 Google 为 Android 4.3 的标准目标开发平台是 Ubuntu 12.04。因此,我们将使用此设置以确保在探索 Linux、SE Linux、Android、UDOO 和 SE for Android 时获得最高的成功率。
在本附录中,我们将执行以下操作:
-
使用虚拟机(VM)下载并安装 Ubuntu 12.04
-
通过安装 VirtualBox 扩展包和 VirtualBox 客户机增强功能来提高我们的 VM 性能
-
搭建适合构建 Linux 内核和 UDOO 源的开发环境
-
安装 Oracle Java 6
提示
如果你已经使用 Ubuntu Linux 12.04,可以跳转到构建环境部分。如果你打算直接安装 Ubuntu(不在虚拟机中),应该跳转到Ubuntu Linux 12.04部分,并按照那些说明操作,忽略 VirtualBox 的步骤。
VirtualBox
有许多虚拟化产品可用于运行客户操作系统,如 Ubuntu Linux,但在此设置中我们将使用VirtualBox。VirtualBox 是一个广泛使用的开源虚拟化系统,适用于 Mac、Linux、Solaris 和 Windows 主机(及其他)。它支持各种客户操作系统。VirtualBox 还允许使用许多现代/常见处理器家族的硬件虚拟化来提高性能,为每个虚拟机提供其自己的私有地址空间。
VirtualBox 的文档为各种平台提供了出色的安装说明,我们建议你为自己的主机平台参考这些说明。你可以在www.virtualbox.org/manual/ch02.html
找到有关为你的主机操作系统安装和运行 VirtualBox 的信息。
Ubuntu Linux 12.04(精确的穿山甲)
要安装 Ubuntu Linux 12.04,首先需要下载合适的发行版镜像。可以在releases.ubuntu.com/12.04/
找到这些镜像。那里有许多可接受的镜像,我们将安装 64 位桌面版的发行版——releases.ubuntu.com/12.04/ubuntu-12.04.5-desktop-amd64.iso
。本例中使用的主机是一台运行 OS X 10.9.2 的 64 位 Macbook Pro,因此我们也目标是 64 位的客户机。如果你使用的是 32 位机器,我们涵盖的基本操作将相同;只有一些细节会有所不同,这部分留给你自行探索和解决。
在你的主机上启动 VirtualBox,等待VM 管理器窗口出现,并执行以下步骤:
-
点击新建。
-
对于名称和操作系统设置,做出以下选择:
-
名称:SE for Android Book
-
类型:Linux
-
版本:Ubuntu (64 位)
-
-
将内存大小设置为至少
16
GB。低于这个值将导致构建失败。 -
要设置硬盘,选择立即创建虚拟硬盘。将此值设置为至少
80
GB。 -
选择硬盘文件类型,VDI (VirtualBox 磁盘映像)。
-
确保物理硬盘上的存储设置为动态分配。
-
当提示输入文件位置和大小时,将新的虚拟硬盘命名为SE for Android Book,并将其大小设置为 80 GB。
确保在左侧窗格中选择了SE for Android Book VM。点击绿色启动箭头以首次启动 VM。将出现一个对话框,要求你选择一个虚拟光驱文件。点击小文件夹图标,找到你之前下载的ubuntu-12.04.5-desktop-amd64.iso
CD 映像。然后点击启动。
当屏幕变黑并在 VM 窗口底部中央显示键盘图像时,按任意键开始 Ubuntu 安装。只要你这样做,语言选择屏幕就会出现。选择对你来说最合适的语言,但在这个例子中,我们将选择英语。然后选择安装 Ubuntu。
有时,你可能会在虚拟机窗口上看到一条看起来不寻常的错误信息——类似于SMBus 基址未初始化。显示此消息是因为 VirtualBox 不支持 Ubuntu 12.04 默认加载的特定内核模块。然而,这不会造成任何困难,仅是外观上的困扰。几分钟后,一个不错的图形界面安装屏幕将出现,等待你再次选择语言。我们将再次选择英语。
在接下来的准备安装 Ubuntu屏幕上,显示了三个清单项目。由于你的虚拟驱动器远大于 Ubuntu 的最小要求,你应该已经满足第一个项目。为了满足其他项目,确保你的主系统已连接电源并建立了网络连接。尽管这对于我们的目的来说完全是多余的,但在继续之前,我们几乎总是勾选安装时下载更新和安装此第三方软件的复选框。
在安装类型屏幕上,我们将选择简单的方式,并选择擦除磁盘并安装 Ubuntu。请记住,这只会擦除你虚拟机的虚拟硬盘上的磁盘,而主系统将保持完整。在擦除磁盘并安装 Ubuntu的屏幕上,你的虚拟硬盘应该已经被选中,所以你只需点击立即安装。
在此之后的 Ubuntu 安装过程中,将同时进行两项任务:在后台线程中,安装程序将准备虚拟驱动器以安装基本系统;其次,你将配置新系统的某些基本方面。但首先,你需要通过点击世界地图上的适当位置来标识你的时区,然后继续。接下来,确认你的键盘布局并继续。
设置你的第一个用户账户。在这种情况下,它将是我们在本书中进行工作的账户,因此我们将输入以下信息:
-
您的姓名:书籍用户
-
计算机名称:SE-for-Android
-
选择一个用户名:bookuser
-
密码字段:(按您的喜好填写)
我们还将选择自动登录。虽然出于安全考虑我们通常不会这样做,但为了方便,我们将在本地 VM 中这样做;但你可以按照你喜欢的任何方式保护此账户。
Ubuntu 安装完成后,会出现一个提示你重启计算机的对话框。点击立即重启按钮,几分钟后,终端提示会通知你移除所有安装介质并按Enter。要移除虚拟安装 CD,请使用 VirtualBox 菜单栏的设备 | CD/DVD 设备 | 从虚拟驱动器中移除磁盘。然后按Enter重启 VM,但通过关闭 VM 窗口中断引导过程。它会询问你是否想要关闭机器。只需点击确定。
VirtualBox 扩展包和客户端附加组件
为了使你的 Ubuntu 客户端虚拟机获得最佳性能并访问与 UDOO 一起工作所需的虚拟 USB 设备,你需要安装 VirtualBox 扩展包和客户端附加组件。
VirtualBox 扩展包
从 VirtualBox 网站下载扩展包,网址为 www.virtualbox.org/wiki/Downloads
。那里会有一个针对所有支持的平台的下载链接。下载此文件后,你需要安装它。每个宿主系统的安装过程都不同,但非常直接。对于 Linux 和 Mac OS X 宿主系统,只需双击下载的扩展包文件即可。对于 Windows 系统,你需要运行下载的安装程序。
VirtualBox 客户端附加组件
完成扩展包的安装后,通过在左侧面板中选择虚拟机并在工具栏上点击启动来从 VirtualBox 启动你的 Ubuntu Linux 12.04 虚拟机。当你的 Ubuntu 桌面激活后,你会注意到它并不适合你的虚拟机窗口。请调整虚拟机窗口大小以使其更大,但虚拟机屏幕大小将保持不变。安装 VirtualBox 客户端附加组件可以解决这些问题,包括其他性能问题。你可能会看到在虚拟桌面上弹出一个窗口,提示有新的 Ubuntu 版本可用。不要升级,只需关闭该窗口。
通过 VirtualBox 菜单栏,转到设备 | 插入客户添加光盘镜像…。不久之后,会出现一个对话框,询问你是否想要运行刚刚插入的新介质上的软件。点击运行按钮。然后你需要通过输入用户密码(在设置过程中输入的密码)来验证你的用户身份。用户验证通过后,将自动构建并更新几个内核模块。脚本完成后,通过点击屏幕右上角的齿轮,选择关机…,并在随后出现的对话框中点击重启来重启虚拟机。
当虚拟机重启后,你首先应该注意到的是虚拟机屏幕现在可以适应虚拟机窗口了。此外,如果你调整虚拟机窗口的大小,虚拟机屏幕也会随之调整。这是确定你已经成功安装 VirtualBox 客户添加功能的简单方法。
使用共享文件夹节省时间
在为 UDOOU 开发镜像时,你可以通过设置宿主系统和 Ubuntu Linux 客户系统之间的共享文件夹来提升整体性能。这样,一旦你为 UDOOU 构建了新的 SD 卡镜像,就可以通过共享文件夹直接让宿主系统访问该镜像。然后宿主系统可以通过长时间运行的命令来烧录 SD 卡,而不会因为虚拟化层降低对宿主读卡器的访问速度而增加时间消耗。在我们编写这本书所使用的系统中,每烧录一个镜像可以节省大约 10 分钟的时间。
要设置共享文件夹,你需要在 VirtualBox 管理器打开的情况下,并且 Ubuntu 虚拟机已关闭。点击工具栏上的设置图标。然后在打开的设置对话框中选择共享文件夹标签页。点击右侧的添加共享文件夹图标。输入你想要共享的宿主系统上的文件夹的文件夹路径。在我们的例子中,我们创建了一个名为vbox_share
的新文件夹与虚拟机客户共享。VirtualBox 将生成文件夹名称,但在点击确定之前,请确保选择了自动挂载。从现在开始启动 Ubuntu 虚拟机时,共享文件夹可以在你的客户虚拟机中以/media/sf_<folder_name>
访问。但是,如果你尝试从客户机列出该目录中的文件,你可能会被拒绝。为了使我们的bookuser
完全访问这个文件夹(即读写权限),我们需要将那个 UID 添加到vboxsf
组中:
$ sudo usermod -a -G vboxsf bookuser
退出并重新登录客户系统,或者重启客户虚拟机以完成该过程。
构建环境
为了准备构建 Linux 内核、Android 以及 Android 应用程序的系统,我们需要安装并设置一些关键软件。点击屏幕左侧启动栏顶部的 Ubuntu 仪表盘图标。在出现的搜索栏中,输入term
并按Enter。将打开一个终端窗口。然后执行以下命令:
$ sudo apt-get update
$ sudo apt-get install apt-file git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev libc6-dev lib32ncurses5-dev ia32-libs x11proto-core-dev libx11-dev ia32-libs dialog liblzo2-dev libxml2-utils minicom
当系统询问你是否希望继续时,输入y
并按下Enter键。
Oracle Java 6
从 Oracle Java 归档网站下载最新版的 Java 6 SE 开发工具包(版本 6u45),网址为www.oracle.com/technetwork/java/javase/archive-139210.html
。你需要jdk-6u45-linux-x64.bin
版本以满足谷歌的目标开发环境。下载完成后,执行以下命令来安装 Java 6 JDK:
$ chmod a+x jdk-6u45-linux-x64.bin
$ sudo mkdir -p /usr/lib/jvm
$ sudo mv jdk-6u45-linux-x64.bin /usr/lib/jvm/
$ cd /usr/lib/jvm/
$ sudo ./jdk-6u45-linux-x64.bin
$ sudo update-alternatives --install "/usr/bin/java" "java" "/usr/lib/jvm/jdk1.6.0_45/bin/java" 1
$ sudo update-alternatives --install "/usr/bin/jar" "jar" "/usr/lib/jvm/jdk1.6.0_45/bin/jar" 1
$ sudo update-alternatives --install "/usr/bin/javac" "javac" "/usr/lib/jvm/jdk1.6.0_45/bin/javac" 1
$ sudo update-alternatives --install "/usr/bin/javaws" "javaws" "/usr/lib/jvm/jdk1.6.0_45/bin/javaws" 1
$ sudo update-alternatives --install "/usr/bin/jar" "jar" "/usr/lib/jvm/jdk1.6.0_35/bin/jar" 1
$ sudo update-alternatives --install "/usr/bin/javadoc" "javadoc" "/usr/lib/jvm/jdk1.6.0_45/bin/javadoc" 1
$ sudo update-alternatives --install "/usr/bin/jarsigner" "jarsigner" "/usr/lib/jvm/jdk1.6.0_45/bin/jarsigner" 1
$ sudo update-alternatives --install "/usr/bin/javah" "javah" "/usr/lib/jvm/jdk1.6.0_45/bin/javah" 1
$ sudo rm jdk-6u45-linux-x64.bin
总结
在本附录中,我们讨论了谷歌为 Android 设定的目标开发环境,并展示了如何在一个虚拟机中创建一个兼容的环境。你可以自由修改系统的其他元素,但安装本附录所列元素将为你提供一个能够执行第四章《在 UDOO 上的安装》及以后所有步骤的最低限度的可行环境。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)