【问题标题】:why non-pic code can't be totally ASLR using run-time fixups?为什么非 pic 代码不能使用运行时修复完全成为 ASLR?
【发布时间】:2021-01-22 20:12:34
【问题描述】:

我了解 PIC 代码使 ASLR 随机化更高效、更容易,因为代码可以放置在内存中的任何位置而无需更改代码。但是,如果我根据 Wikipedia relocation 理解正确,则动态链接器可以在运行时进行“修复”,因此尽管代码不是与位置无关的,但可以定位符号。但是根据我在这里看到的许多答案,非 pic 代码不能 ASLR 除了堆栈之外的部分(因此不能随机化程序入口点)。如果这是正确的,那么运行时修复的用途是什么?为什么我们不能在程序开始之前在运行时修复代码中的所有位置以使程序入口点随机化。

【问题讨论】:

    标签: linux x86-64 elf aslr position-independent-code


    【解决方案1】:

    TL:DR:并非所有绝对地址的使用都会在非 PIE 可执行文件(ELF 类型 EXEC,而不是 DYN)中具有重定位信息。因此内核的程序加载器无法找到它们全部应用修正。

    因此,无法为构建为非 PIE 的可执行文件追溯启用 ASLR。传统的可执行文件无法将自己标记为在每次使用绝对地址时都具有重定位元数据,并且添加这样的功能也没有任何意义,因为如果您想要文本 ASLR,您只需构建一个 PIE。

    因为保证 ELF 类型的 EXEC Linux 可执行文件被加载/映射到链接器在链接时选择的固定基地址,所以在可执行文件中为内部符号创建符号表条目会浪费空间。所以工具链没有这样做,也没有理由开始。这就是传统 ELF 可执行文件的设计方式;早在 90 年代中期,Linux 就从 a.out 切换到了 ELF,那时堆栈 ASLR 还没有出现,所以它并没有引起人们的注意。

    例如static char buf[100] 的绝对地址可能嵌入在使用它的机器代码中的某个位置(如果我们谈论的是 32 位代码,或者将地址放入寄存器的 64 位代码),但没有办法知道在哪里或多少次。

    此外,特别是对于 x86-64,非 PIE 可执行文件的默认代码模型保证静态地址(文本/数据/bss)都将位于虚拟地址空间的低 2GiB 中,因此 32 位绝对有符号或未签名的地址可以工作,rel32 位移可以从任何地方到达任何地方。这就是为什么非 PIE 编译器输出使用 mov $symbol, %edi(5 个字节)而不是 lea symbol(%rip), %rdi(7 个字节)将地址放入寄存器中的原因。 https://godbolt.org/z/89PeK1

    因此,即使您确实知道每个绝对地址在哪里,您也只能在低 2GiB 中对其进行 ASLR,从而限制您可以引入的熵位数。 (我认为 Windows 有这样的模式:LargeAddressAware = no。但 Linux 没有。32-bit absolute addresses no longer allowed in x86-64 Linux? 同样,PIE 是允许文本 ASLR 的更好方法,所以如果人们(发行版)想要它的好处,就应该为此编译.)

    与 Windows 不同,Linux 不会在可以通过从源代码重新编译二进制文件来更好、更高效地处理的事情上花费大量精力。

    话虽如此,即使在 PIC / PIE ELF 共享对象中,GNU/Linux 确实 支持 64 位 绝对地址的修复重定位。这就是为什么像 NASM mov rdi, BUFFER 这样的初学者代码甚至可以在共享库中工作的原因:使用 objdump -drwC -Mintelmov reg, imm64 指令中查看有关该符号使用的重定位信息。如果BUFFER 不是全局符号,则lea rdi, [rel BUFFER] 不需要任何重定位条目。 (等效于 C static。)


    您可能想知道为什么元数据是必不可少的:

    没有可靠的方法在文本/数据中搜索可能的绝对地址;可能会出现误报。例如/usr/bin/ld 可能包含 0x401000 作为 x86-64 可执行文件的默认起始地址。您不希望ld 的代码+数据的 ASLR 也更改其默认值。或者该整数值可能在许多程序中以多种方式出现,例如作为位图。当然,x86-64 机器码是可变长度的,所以在最一般的情况下,甚至没有可靠的方法来区分操作码和立即操作数。

    还有潜在的假阴性。 x86 程序不太可能在具有多条指令的寄存器中构造绝对地址,但这当然是可能的。但是在非 x86 代码中,这很常见。

    具有固定长度指令的 RISC 机器不能将 32 位地址放入 32 位指令中;就没有其他空间了。因此,要从静态存储中加载,必须将绝对地址拆分为多个指令,例如 MIPS lui $t0, %hi(0x612300) / lw $t1, %lo(0x612300)($t0) 从绝对地址 0x612300 处的静态变量加载。 (asm 源代码中通常会有一个符号名称,但它不会出现在最终链接的二进制文件中,除非它是 .globl,所以我使用数字作为提醒。)这样的指令不必进来对;在后面的指令中,相同的地址的高半部分可以被其他访问相同的数组或结构重用。

    【讨论】:

    • 我的印象是,在没有虚拟内存的 16 位系统上,人们认为重定位是一种必要的邪恶,所有程序共享相同的地址空间,并欣然放弃了对它的支持,迁移到 32-位系统,程序总是可以在一个恒定的虚拟地址上加载。正如你所说,没有预见到 ASLR 的用处。
    • @NateEldredge:是的,这是查看主要可执行文件的好方法。共享库仍然需要重定位或与位置无关的代码,因为只有主可执行文件才能保证在虚拟地址空间中占据自己的位置。 (IIRC,Linux a.out 库 did 需要手动重新定位,您可以通过提前将它们重新定位到不冲突的基地址来优化您的共享库或其他东西。或者也许我正在考虑 DLL?我在 ELF 替换 a.out 后不久就开始使用 Linux,所以我阅读了有关转换但从未体验过的信息。
    • @NateEldredge:还请注意,Linux ELF 共享对象确实支持运行时修复,通常用于将绝对地址作为数据的跳转表。但是,是的,由于缺少 EIP 相对寻址,32 位 PIC 的开销很大,因此对于共享库,修复的性能会显着提高。 但是 然后他们无法在进程之间共享文本,并且加载时间更长。 Unix 系统通常会启动许多短暂的进程。
    【解决方案2】:

    在了解 Linux 之前,让我们先了解一下 Windows:

    Windows 的.EXE 文件(程序)通常有一个所谓的“基本重定位表”,并且它们有一个“映像库”。

    “镜像库”是程序“想要的”起始地址;如果 Windows 将程序加载到该地址,则无需进行重定位。

    “基本重定位表”包含程序中表示地址的所有值的列表。如果程序加载到与“图像库”不同的地址,Windows 必须将差异添加到该表中列出的所有值中。

    如果.EXE 文件不包含“基本重定位表”(据我所知,某些 32 位 GCC 版本会生成此类文件),则无法将文件加载到另一个地址。

    这是因为如果变量someVariable位于地址12340000,下面的C代码语句会产生完全相同的机器码(二进制码),无法区分它们:

    long myVariable = 12340000;
    

    还有:

    int * myVariable = &someVariable;
    

    在第一种情况下,值 12340000 在任何情况下都不得更改;在第二种情况下,如果程序加载到另一个地址,地址(即12340000)必须更改为真实地址。

    如果缺少“基本重定位表”,则没有信息 12340000 是整数值(不得更改)还是地址(必须更改)。

    所以程序必须加载到某个固定地址。

    我不确定最新的 32 位 Linux 版本,但至少在较旧的 32 位 Linux 版本中,没有像“基本重定位表”这样的东西,并且程序不使用 PIC。这意味着必须将这些程序加载到它们“最喜欢的”地址。

    我不了解 64 位 Linux 程序,但如果一个程序的编译方式与(较旧的)32 位程序相同,它们也必须加载到某个地址,并且 ASLR 是不可能的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-19
      • 1970-01-01
      • 1970-01-01
      • 2022-09-29
      • 2021-05-05
      相关资源
      最近更新 更多