大多数 Linux 发行版现在对许多程序使用 ASLR,以随机化内存布局。
用于此的随机性多久更改一次?如果我多次重新运行同一个程序,它每次都会收到相同的布局,还是每次都会有所不同?每次运行程序时是否都会产生新的随机性?每次重启机器?随机值何时刷新/重置为新值?这是否取决于 Linux 发行版?它是否取决于我们正在谈论的内存区域(例如,可执行文件、动态库、堆栈、堆等),还是所有这些的答案都相同?
大多数 Linux 发行版现在对许多程序使用 ASLR,以随机化内存布局。
用于此的随机性多久更改一次?如果我多次重新运行同一个程序,它每次都会收到相同的布局,还是每次都会有所不同?每次运行程序时是否都会产生新的随机性?每次重启机器?随机值何时刷新/重置为新值?这是否取决于 Linux 发行版?它是否取决于我们正在谈论的内存区域(例如,可执行文件、动态库、堆栈、堆等),还是所有这些的答案都相同?
维基百科页面显示答案是“视情况而定”。周围有多种实现和补丁。
需要考虑的要点如下:
Linux 可执行文件包含一个主二进制文件和动态加载的DLL(共享对象)。在传统的 Linux 中,主二进制文件位于链接时选择的固定地址,而 DLL 是与位置无关的代码。
Linux 中的 DLL 可以廉价地“移动”。实际上,当 DLL 映射到地址空间时,必须调整对该 DLL 元素的所有引用(来自 DLL 本身,以及来自其他 DLL 和主二进制文件)以指向该 DLL。这意味着必须对主二进制文件和某些 DLL 中的某些字节进行一些修改,具体取决于为该特定可执行实例组织地址空间的方式。但是,问题是任何修改过的页面(4 或 8 kB,取决于架构)不再与其他实例共享(在物理 RAM 中)以用于同时执行的其他可执行文件。这往往会使 DLL 的 RAM 使用优势失效(从历史上看,这是 使用 DLL 的原因,但如今软件升级的便利性可以说是一个更强有力的原因)。
在 Linux(和其他操作系统,特别是所有使用ELF的操作系统)中,解决此问题的方法是将 DLL“特别”编译为Position-Independent Code:实际上,二进制代码因此安排对所有(或大多数)引用进行分组必须使用间接表(“GOT”,又名“全局偏移表”)将其调整到相同的页面中,该表通过其相对于调用代码的位置动态定位。这意味着大多数 DLL 页面在加载 DLL 时将保持不变,因此将在另一个可执行文件的地址空间中与同一 DLL 的任何其他实例共享。
这与在 Windows 中完成事情的方式形成对比,在 Windows 中,操作系统尝试在相同地址为所有使用它的进程加载相同的 DLL,以便可以“就地”修改引用并且仍然可以共享,因为所有进程将在同一地址看到相同的 DLL。(20 多年前,在“libc4”时代,DLL 在 Linux 中不是“可移动的”,并且对于所有进程总是在固定位置加载。)
主二进制文件应该链接到一个固定地址;然而,一个类似于 PIC DLL 的机制也被用于主要的二进制文件:它被称为PIE。我在那里详细解释了它。由于 PIE 可以破坏一些非 C 应用程序,使用它的 Linux 发行版倾向于将其保留给一些被认为“易受攻击”的应用程序(即具有某些网络活动的应用程序)。
Linux 还具有VDSO,可以将其视为内核提供的 DLL,而不是从文件映射。内核对 ASLR 的支持将在每个进程的基础上随机化 VDSO 地址。
任何随机化都需要空间,因此可能意味着解决空间碎片问题(大块的分配可能会失败,因为可用空间被分成几个较小的孔)。对于 32 位 Linux 变体尤其如此;在 64 位架构上,地址空间仍然足够大(与物理 RAM 大小相比)以避免出现问题。因此,对于某些应用程序(例如照片/视频编辑),可能必须禁用 ASLR。这可以使用setarch在每个进程的基础上完成。
从这些中,我们得到以下信息:
在 Linux 中应用 ASLR 时,它通常是在每个进程的基础上应用的。如果你启动同一个进程两次,那么 DLL 将被加载到不同的地址(如果它是为 PIE 编译的,那么主二进制文件也会加载)。通过在 中运行可执行文件很容易看到这一点gdb,事实上,当您跟踪错误时,您要做的第一件事就是禁用 ASLR。
如果应用了 Prelink,则可以防止 ASLR(至少对于 DLL 和二进制文件;堆和堆栈位置可能仍然是随机的)。预链接可以通过自己进行随机化来抵消这一点,但是,通过构造,这将为每个可执行文件固定:预链接的可执行文件将始终在相同的位置找到它的二进制文件和 DLL;但是,同一台机器上的其他预链接可执行文件将在不同地址看到相同的 DLL,这是特定于机器的,因此其他机器将获得不同布局的地址空间。还建议定期进行新的预链接(例如每周),这会使事情重新随机化(您也会在每次 DLL 升级时获得它)。
堆栈和堆也是随机的。初始堆栈位置以及堆的起点(用于brk()系统调用)由内核在进程启动时选择,并且每次执行都会将它们放在一个新位置(如果启用了 ASLR)。此外,内存分配器可能依赖于mmap()(它通常会为大块这样做),它是随机的,并且这扩展到动态分配的线程堆栈。默认情况下,glibc将对mmap()超过 128 kB 的任何块使用调用,新线程的堆栈默认为 1 MB。
当一个进程被分叉时,子进程必然获得与其父进程相同的布局。但是,在分叉之后加载的 DLL 可能最终位于不同的地址。
由于所有这些都取决于内核版本和补丁以及可配置选项,因此您的问题的答案必然取决于分布。例如,您可能会在那里看到 Ubuntu 中的情况(基本上:ASLR 无处不在,PIE 仅用于特定的包列表,没有预链接)。唯一的共同点是 Linux 上的 ASLR 从不依赖于启动:当没有对每个流程实例应用随机化时,这是由于预链接,它在重新启动后是永久的(但应该定期重新应用)。
用于对齐堆栈和mmap分配的 ASLR 随机性由内核的内部get_random_int函数为每个新进程生成。
get_random_int如果 CPU 支持,则使用 RDRAND 指令生成随机值。在其他 CPU 上,它使用一个在启动时从内核的非阻塞 ( /dev/urandom) 池中初始化一次的 PRNG。(这个 PRNG 被优化为快速,而不是加密安全。)
动态链接器/加载器(ld.so,glibc 的一部分)用于mmap加载可执行文件和任何共享库。
所以,底线:堆栈的位置和mmap分配(包括堆和任何可执行文件)是随机的,并且对于每个新进程都是不同的。