Execute-only 内存 (XOM)(适用于 AArch64 二进制文件)

默认情况下,AArch64 系统二进制文件的可执行代码段标记为 execute-only(不可读),以此作为针对即时代码重用攻击的强化缓解措施。混合了数据和代码的代码以及有意检查这些代码段(但未先将内存段重新映射为可读)的代码不再起作用。如果应用尝试读取内存中启用了 execute-only 内存 (XOM) 的系统库的代码段,但未先将该代码段标记为可读,则目标 SDK 为 10(API 级别 29 或更高级别)的应用会受到影响。

为了充分利用此缓解措施,需要硬件和内核支持。如果没有这种支持,缓解措施可能只能部分实施。Android 4.9 通用内核包含相应的补丁,可在 ARMv8.2 设备上为此提供全面支持。

实现

编译器生成的 AArch64 二进制文件假定代码和数据未混合。启用此功能不会对设备的性能产生负面影响。

对于必须对其可执行段执行有意内存内省的代码,建议在需要检查的代码段上调用 mprotect 以使其可读,然后在检查完成后移除可读性。
此实现会导致读取标记为 execute-only 的内存段时产生段错误 (SEGFAULT)。这可能是由错误、漏洞、与代码混合的数据(字面量池)或有意的内存内省造成的。

设备支持和影响

没有所需补丁的较早硬件或较早内核(低于 4.9)的设备可能无法完全支持此功能或从中受益。没有内核支持的设备可能无法强制执行用户对 execute-only 内存的访问,但显式检查页面是否可读的内核代码可能仍然会强制执行此属性,例如 process_vm_readv()

内核标志 CONFIG_ARM64_UAO 必须在内核中设置,以确保内核尊重标记为 execute-only 的用户空间页面。较早的 ARMv8 设备或禁用用户访问替代 (UAO) 的 ARMv8.2 设备可能无法完全从中受益,并且可能仍然能够使用系统调用读取 execute-only 页面。

重构现有代码

从 AArch32 移植的代码可能包含混合的数据和代码,从而导致出现问题。在许多情况下,解决这些问题就像将常量移到汇编文件中的 .data 段一样简单。

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

示例

Clang 编译器生成的二进制文件在数据与代码混合方面应没有任何问题。如果包含 GNU 编译器集合 (GCC) 生成的代码(来自静态库),请检查输出二进制文件,以确保常量未池化到代码段中。

如果需要在可执行代码段上进行代码内省,请先调用 mprotect 将代码标记为可读。然后在操作完成后,再次调用 mprotect 以将其标记为不可读。

启用 XOM

默认情况下,构建系统中为所有 64 位二进制文件启用 Execute-only。

停用 XOM

您可以在模块级别、整个子目录树级别或全局级别(针对整个构建)停用 Execute-only。

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

// Android.mk
LOCAL_XOM := false

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

如果在静态库中停用了 execute-only 内存,则构建系统会将此应用于该静态库的所有依赖模块。您可以使用 xom: true, 覆盖此设置。

要在特定子目录(例如 foo/bar/)中停用 execute-only 内存,请将值传递给 XOM_EXCLUDE_PATHS

make -j XOM_EXCLUDE_PATHS=foo/bar

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

您可以通过将 ENABLE_XOM=false 传递到 make 命令来全局停用 execute-only 二进制文件。

make -j ENABLE_XOM=false

验证

Execute-only 内存没有可用的 CTS 或验证测试。您可以使用 readelf 并检查段标志来手动验证二进制文件。