默认情况下,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_XOM
和 xom
变量设置为 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
并检查段标志来手动验证二进制文件。