原文:
zh.annas-archive.org/md5/1E165BD192C4C9DE01CC57BFDF8623B4
译者:飞龙
第十二章:掌握工具链
迄今为止,我们已经深入探讨了推动 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 上的安装》及以后所有步骤的最低限度的可行环境。