与其给你一个直接的答案,因为它主要是“它取决于”,我认为带你了解我最近发现的描述 Windows 上内存分配行为的发现可能会更有趣,这样你就已经在这一切背后都有一些背景。我对 Linux 内存管理模型了解不够,所以我不会评论这方面的事情,尽管有人告诉我它非常相似。
最近我在思考 Windows 内核提供的 W^X 内存保护功能,称为“动态代码策略”。这个想法是,一旦映射到应用程序的页面被标记为可写,它就永远不能被标记为可执行。这是一个强大的漏洞利用缓解措施,因为它可以防止几乎所有类型的涉及传递 shellcode 的漏洞利用。您不能只是进行缓冲区溢出,控制指令指针,ROP 到 VirtualProtect,将有效负载标记为 RWX 并执行它 - VirtualProtect 调用失败,因为内存不可写和可执行。
我对这里的通用绕过的想法是,您可以 VirtualAlloc 一些读+写内存,VirtualFree 它,然后 VirtualAlloc 再次作为读+执行,希望重新分配相同的页面。事实证明,这是行不通的。原因与您的问题有关。
在三种情况下,Windows 内存管理会将页面清理为零:
- 当页面提交给进程时
- 当页面被标记为
MEM_RESET
- 当页面已被释放并被标记为脏页面时
第一个阻碍了我的计划。进程可以在不提交它们的情况下分配虚拟地址空间,例如通过调用 VirtualAlloc MEM_RESERVE。保留的地址空间不直接映射到任何物理内存或交换空间,但会保留以供进程在需要时使用。为了实际使用内存,必须提交页面,例如通过调用 VirtualAlloc MEM_RESERVE | MEM_COMMIT。提交后,如果尚未通过其他方式提交页面,则该页面将被归零。因此,我分配可写数据,释放它,然后将其分配为可执行文件的技巧被破坏了,因为内存管理器将页面归零。
第二个也很有趣。如果您暂时使用完一块内存,但稍后会再次使用它,您可以使用MEM_RESET. 这向内存管理器表明它不应该从进程中取消提交页面,但它不应该费心将它们交换到磁盘,因为其中包含的数据不再感兴趣。当有空闲时间时,系统将在后台将这些页面归零。但是,如果系统尚未将页面归零,则应用程序可以使用该标志请求撤消重置MEM_RESET_UNDO标志。
第三种选择是取消提交并释放一个页面,它可以完全免费地被内存管理器与任何其他进程重用。如果页面包含数据,则将其标记为脏。系统要么在后台清理它,要么在再次提交时主动将其归零。
这些是系统级内存管理的情况。进程级管理有很大不同,例如使用 HeapAlloc、库分配器 (malloc) 或完全自定义的分配器。这些设计可以分配和提交内存页面,并在该提交的块之上执行它们自己的内存使用管理。因此,libc free() 调用实际上可能不会取消底层内存页面。malloc() 调用也完全有可能返回尚未重置的页面 - 这就是 calloc() 存在的原因。
问题的下一部分是浏览器是否对此有任何积极的防御措施。我不知道他们有没有。当然,他们的内存分配器可能会将所有新分配的页面显式归零(或将它们设置为某个神奇的值,例如 0xDEADBEEF),以限制未初始化内存读取的可能性并更好地识别这种情况。此外,例如,Microsoft Edge 的内存分配器专门设计用于提供更高的安全性以防止内存损坏,因此重置分配是其中的一部分。
总而言之,这取决于!