原文:mprotect 早期设计文档 2003.11.04
作者:The PaX team
译者:wnereiz
1. 设计
MPROTECT 的目标是防止在任务的地址空间引入新的可执行代码。通过限制 mmap() 和 mprotect() 界面可以达到此目的。
这种限制可以防止
- 创建可执行的匿名映射
- 创建可执行/可写文件映射
- 使可执行/只读文件映射变得可写 —— 在有 ET_DYN 的 ELF 文件上进行再 定位(非 PIC 类共享库)的情况除外
- 使不可执行映射变得可执行
为了理解这种限制,我们将映射的可写性/可执行性看作是状态信息。此状 态信息保存在 vm_flags 区域的 vma 结构体中,此信息决定了给定区域 (即被其覆盖的每一页)在当前是否处于可写/可执行状态,且/或可以(通 过在此区域上使用 mprotect())变成可写/可执行的。描述各属性的标志分 别为:VM_WRITE, VM_EXEC, VM_MAYWRITE 和 VM_MAYEXEC。
这四个属性意味着任何映射 (vma) 可以具有16种不同的状态(为了便于讨论, 我们在这里忽略其他的属性),而我们可以通过限制 vms 在整个生命周期内 可处于或可更改为何种状态来达到我们的目的。
处于下列任何一种(“良好的”)状态下,则无法向一个映射引入新的可执行代码:
VM_WRITE
VM_MAYWRITE
VM_WRITE | VM_MAYWRITE
VM_EXEC
VM_MAYEXEC
VM_EXEC | VM_MAYEXEC
处于任何其他状态下,都有可能在映射中直接写入新的可执行代码,或者映射 可以被 mprotect() 更改为可写/可执行状态。
请注意,默认的内核行为已经可以阻止某些特定的状态了(当一个映射分别 不存在 VM_MAYWRITE 和 VM_MAYEXEC 时,是不允许分别具有 VM_WRITE 和 VM_EXEC 的),所以只留给了我们4种良好状态:
VM_MAYWRITE
VM_MAYEXEC
VM_WRITE | VM_MAYWRITE
VM_EXEC | VM_MAYEXEC
现在我们来看一下内核可以创建何种映射,而 MPROTECT 都更改了些什么:
- 匿名映射(栈、受 brk() 和 mmap() 控制的堆):这些会创建为 VM_WRITE | VM_EXEC | VM_MAYWRITE | VM_MAYEXEC 状态,这并不是良好的状态。因为 这些映射必须是可写的,所以我们只能更改可执行状态(而这在实际中仍然 会损坏应用程序,后面会提到针对这一点的解决方法),MPROTECT 会直接将 他们的状态更改为 VM_WRITE | VM_MAYWRITE,
-
共享的内存映射:这些映射会被创建为 VM_WRITE VM_MAYWRITE 的良好状态。 -
文件映射:类似于匿名映射,文件映射可以被创建成所有的“不良”状态(为了 简明起见,暂不列出),尤其是内核会将 VM_MAYWRITE | VM_MAYEXEC 赋给 任何映射,而不管其对权限是否有请求。为了尽可能减少应用程序的损坏,同 时还要达到我们的目的,我们决定在文件映射方面使用如下状态:
- VM_WRITE | VM_MAYWRITE 或 VM_MAYWRITE —— 如果 PROT_WRITE 在 mmap() 的时候被请求。
-
VM_EXEC VM_MAYEXEC —— 如果 PROT_WRITE 没有被请求。
有效的可执行映射被强制转换为不可写,而可写映射被强制转换为非可执行 (同时不允许在其生存周期内修改此状态)。对此有一个特例,在有一种情况 下这些修改前的状态是有实际需求的,那就是应该允许动态连接器在 non-PIC 类 ELF 文件的可执行段上进行再定位操作。
通过以上这些限制,向任务的地址空间引入可执行代码的方法只剩下一个,那 就是在请求 PROT_EXEC 的同时将文件映射到内存上。对于攻击者而言,这意味 着他在能够向攻击任务地址空间 mmap() 之前,不得不在目标系统上创建/写一个 文件。有很多方法用来防止/探测到这类攻击,但这已经超出了 PaX 项目的范畴。
就像我们之前提到的,MPROTECT 的限制会损坏已有依赖不良 vma 状态的程序。 大多数情况下指的是被用来满足更高层内存分配需求(像是 C 中的 malloc() 家族)以及被假定为可执行(java,gcc trampoline,等等)的非可执行匿名 映射。为了让这些程序在 MPROTECT 下工作,使用的方法是扩充 mmap() 界面 并且允许将 VM_MAY* 置于特定状态。下面的例子演示了程序是如何使用此方法 的:
- mmap(..., PROT_READ | PROT_WRITE | PROT_MAYREAD | PROT_MAYEXEC, ...) - 在以上区域生成代码 - mprotect(..., PROT_READ | PROT_EXEC)
注意,初始 mmap() 调用从来不会请求和允许 PROT_EXEC ,因此程序的设计者 必须显式调用 mprotect(),这样就可以避免意外地违反 MPROTECT 规则。
2. 实现
前两种限制方式在 mm/mmap.c 的 do_mmap_pgoff() 和 do_brk() 中实现,而 其他两种方式在 mm/mprotect.c 的 sys_mprotect() 中(non-PIC ELF 库由 pax_handle_maywrite() 处理)。
由于只有在强制使用非可执行页的时候 MPROCTECT 才有意义,所以针对给定 任务,只有在 PAGEEXEC 或 SEGMEXEC 其中之一启用的情况下,此限制才会被 启用。此外,对于强制使用非可执行页来说,某些限制是有意义或是必须的。 因此即便是没有启用 MPROTECT,这些限制也会被使用(但启用 MPROTECT 对于 实现此特性是必须的)。
允许 non-PIC ELF 再定位的这种特殊行为是由 mm/mprotect.c 中的 pax_handle_maywrite() 管理的。逻辑上很明确,首先我们要确认任务所请求 的 PROT_WRITE 映射中哪些是再定位的候选映射(必须是可执行文件映射,而且 还没有被改为可写),然后我们确保映射的原始文件是 ET_DYN ELF 文件,此 文件的动态表中有一个条目说明了文本再定位的需求。如果没有问题,我们就 直接更改映射中包含其余 do_mprotect() 逻辑的状态,使其允许此请求,而且 我们还要设置 VM_MAYNOTWRITE 标志,这样就可以禁止针对此映射产生更多的 PROT_WRITE 请求。