默认情况下,AArch64 系统二进制文件的可执行代码部分会被标记为只执行(不可读取),作为应对即时代码重用攻击的安全强化缓解方法。将数据和代码混合在一起的代码以及有目的地检查这些部分的代码(无需首先将内存段重新映射为可读)将不再起作用。如果目标 SDK 为 10(API 级别 29 或更高)的应用尝试读取内存中已启用只执行内存 (XOM) 的系统库的代码部分,而未首先将该部分标记为可读,则此类应用将会受到影响。
为了从这个缓解方法中充分受益,您需要硬件和内核支持。如果没有这种支持,缓解方法可能只能实施一部分。Android 4.9 通用内核包含相应的补丁程序,以便在 ARMv8.2 设备上对此提供全面支持。
实现
编译器生成的 AArch64 二进制文件假设代码和数据没有混合在一起。启用此功能不会降低设备的性能。
如果代码必须对其可执行段执行有意的内存自省,最好在需要检查的代码段上调用 mprotect
,从而使其变为可读状态,然后在检查完成时取消其可读状态。
此实现会导致读入已标记为只执行的内存段,从而产生分段错误 (SEGFAULT
)。这可能是由于 bug、漏洞、混合了代码的数据(文字池化)或有意的内存自省造成的。
设备支持和影响
配置有较早硬件或较早内核(低于 4.9)且没有必需补丁的设备可能无法全面支持此功能或从中受益。缺少内核支持的设备不能强制用户访问只执行内存,但是,可明确检查页面是否可读的内核代码仍然可以强制执行此属性,例如 process_vm_readv()
。
必须在内核中设置内核标记 CONFIG_ARM64_UAO
,以确保内核遵循标记为只执行的用户空间页面。早期的 ARMv8 设备或停用了用户访问替换 (UAO) 的 ARMv8.2 设备可能无法从中充分受益,并且仍然可以使用系统调用来读取只执行页面。
重构现有代码
从 AArch32 移植的代码可能包含混合的数据和代码,这会引发问题。在许多情况下,解决这些问题就像将常量移至汇编文件中的 .data
部分一样简单。
手写汇编代码可能需要重构以分离本地池化常量。
示例:
Clang 编译器生成的二进制文件在数据与代码混合时不会出现问题。如果其中包含 GNU Compiler Collection (GCC) 生成的代码(来自静态库),请检查输出二进制文件,以确保常量没有池化到代码部分。
如果有必要对可执行代码部分执行代码自省,首先需要调用 mprotect
以将代码标记为可读。然后,在操作完成后,再次调用 mprotect
以将其标记为不可读。
启用 XOM
默认情况下,对编译系统中的所有 64 位二进制文件启用“只执行”。
停用 XOM
您可以在模块级别停用“只执行”,既可按整个子目录树停用,也可对整个 build 全局停用。
通过将 LOCAL_XOM
和 xom
变量设置为 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
和检查段标志来手动验证二进制文件。