AArch64 二进制文件的只执行内存 (XOM)

默认情况下,AArch64 系统二进制文件的可执行代码部分会被标记为只执行(不可读取),作为应对即时代码重用攻击的安全强化缓解方法。将数据和代码混合在一起的代码以及有目的地检查这些部分的代码(无需首先将内存段重新映射为可读)将不再起作用。如果目标 SDK 为 10(API 级别 29 或更高)的应用尝试读取内存中已启用只执行内存 (XOM) 的系统库的代码部分,而未首先将该部分标记为可读,则此类应用将会受到影响。

要从这个缓解方法中充分受益,您需要硬件和内核支持。如果没有这种支持,缓解方法可能只能实施一部分。Android 4.9 通用内核包含相应的补丁程序,以便在 ARMv8.2 设备上对此提供全面支持。

实现

编译器生成的 AArch64 二进制文件假设代码和数据没有混合在一起。启用此功能不会降低设备的性能。

如果代码必须对其可执行段执行有意的内存自省,最好在需要检查的代码段上调用 mprotect,从而使其变为可读状态,然后在检查完成时取消其可读状态。
此实现会导致读入已标记为只执行的内存段,从而产生分段错误 (SEGFAULT)。这可能是由于错误、漏洞、混合了代码的数据(文字池化)或有意的内存自省造成的。

设备支持和影响

配置有较早硬件或较早内核(低于 4.9)且没有必需补丁程序的设备可能无法全面支持此功能或从中受益。缺少内核支持的设备不能强制用户访问只执行内存,但是,可明确检查页面是否可读的内核代码仍然可以强制执行此属性,例如 process_vm_readv()

必须在内核中设置内核标记 CONFIG_ARM64_UAO,以确保内核遵循标记为只执行的用户空间页面。早期的 ARMv8 设备或停用了用户访问替换 (UAO) 的 ARMv8.2 设备可能无法从中充分受益,并且仍然可以使用系统调用来读取只执行页面。

重构现有代码

从 AArch32 移植的代码可能包含混合的数据和代码,这会引发问题。在许多情况下,解决这些问题就像将常量移至汇编文件中的 .data 部分一样简单。

手写汇编代码可能需要重构以分离本地池化常量。

示例:

Clang 编译器生成的二进制文件在数据与代码混合时不会出现问题。如果其中包含 GNU Compiler Collection (GCC) 生成的代码(来自静态库),请检查输出二进制文件,以确保常量没有池化到代码部分。

如果有必要对可执行代码部分执行代码自省,首先需要调用 mprotect 以将代码标记为可读。然后,在操作完成后,再次调用 mprotect 以将其标记为不可读。

启用

默认情况下,对编译系统中的所有 64 位二进制文件启用“只执行”。

停用

您可以在模块级别停用“只执行”,既可按整个子目录树停用,也可对整个 build 全局停用。

通过将 LOCAL_XOMxom 变量设置为 false,可以为无法重构或者需要读取其可执行代码的各个模块停用 XOM。

// Android.mk
LOCAL_XOM := false

// Android.bp
cc_binary { // or other module types
   ...
   xom: false,
}

如果在静态库中停用了只执行内存,则构建系统会将此设置应用于该静态库的所有相关模块。您可以使用 xom: true, 替换此设置。

如需在特定子目录(例如,foo/bar/)中停用只执行内存,请将值传递给 XOM_EXCLUDE_PATHS

make -j XOM_EXCLUDE_PATHS=foo/bar

或者,您可以在产品配置中设置 PRODUCT_XOM_EXCLUDE_PATHS 变量。

您可以通过将 ENABLE_XOM=false 传递给 make 命令来全局停用只执行二进制文件。

make -j ENABLE_XOM=false

验证

对于只执行内存,不提供 CTS 或验证测试。您可以通过使用 readelf 和检查段标志来手动验证二进制文件。