为什么非图片代码不能完全使用 运行-time fixups 的 ASLR?
why non-pic code can't be totally ASLR using run-time fixups?
我知道 PIC 代码使 ASLR 随机化更高效、更容易,因为代码可以放在内存中的任何位置而无需更改代码。但是如果我根据维基百科理解正确的话 relocation 动态链接器可以在运行时进行“修复”,因此可以定位符号,尽管代码不是位置独立的。但是根据我在这里看到的许多答案,非图片代码不能 ASLR
除堆栈之外的部分(因此无法随机化程序入口点)。如果那是正确的,那么运行时修复的用途是什么?为什么我们不能在程序开始之前在运行时修复代码中的所有位置以使程序入口点随机化。
在看Linux之前先看一下Windows:
Windows' .EXE
文件(程序)通常有一个 so-called “base relocation table”并且它们有一个“image base”。
“image base”是程序的“所需”起始地址;如果 Windows 将程序加载到该地址,则不需要进行重定位。
“base relocation table”包含程序中代表地址的所有值的列表。如果程序加载到与“图像基址”不同的地址,Windows 必须将差异添加到 table.
中列出的所有值
如果 .EXE
文件不包含“base relocation table”(据我所知一些 32 位 GCC 版本会生成此类文件),则无法加载文件到另一个地址。
这是因为如果变量someVariable
位于地址12340000,下面的C代码语句将产生完全相同的机器代码(二进制代码),并且无法区分它们:
long myVariable = 12340000;
并且:
int * myVariable = &someVariable;
第一种情况,12340000这个值在任何情况下都不能改变;在第二种情况下,如果程序加载到另一个地址,则必须将地址(即 12340000)更改为真实地址。
如果缺少“base relocation table”,则如果值 12340000 是整数值(不得更改)或地址(必须更改),则没有任何信息。
所以程序必须加载到某个固定地址。
我不确定最新的 32 位 Linux 版本,但至少在旧的 32 位 Linux 版本中没有什么比“基本重定位 table " 并且程序没有使用 PIC。这意味着这些程序必须加载到它们的“收藏夹”地址。
我不知道 64 位 Linux 程序,但如果程序的编译方式与(较旧的)32 位程序相同,则它们也必须加载到特定地址并且无法使用 ASLR。
TL:DR:并非所有使用绝对地址的人都会在 non-PIE 可执行文件(ELF 类型 EXEC,而非 DYN)中拥有重定位信息。 因此内核的 program-loader 无法全部找到它们来应用修正。
因此无法为构建为 non-PIE 的可执行文件追溯启用 ASLR。传统的可执行文件没有办法将自己标记为每次使用绝对地址都具有重定位元数据,而且添加这样的功能也没有意义,因为如果你想要文本 ASLR,你只需构建一个 PIE。
因为 ELF-type EXEC Linux 可执行文件保证在 link 时加载/映射到 linker 选择的固定基地址,这将是在可执行文件中浪费 space 来为内部符号创建 symbol-table 条目。所以工具链没有这样做,也没有理由开始。这就是传统 ELF 可执行文件的设计方式; Linux 在 90 年代中期从 a.out 切换到 ELF,当时堆栈 ASLR 还没有出现,所以它不在人们的关注范围内。
例如static char buf[100]
的绝对地址可能嵌入在使用它的机器代码中的某处(如果我们谈论的是 32 位代码,或将地址放入寄存器的 64 位代码),但没有办法知道在哪里或多少次。
此外,对于 x86-64,non-PIE 可执行文件的默认代码模型保证静态地址(文本/数据/bss)都在虚拟地址的低 2GiB space ,因此 32 位绝对有符号或无符号地址都可以工作,并且 rel32
位移可以从任何地方到达任何地方。这就是为什么 non-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 -Mintel
查看有关在 mov reg, imm64
指令中使用该符号的重定位信息。如果 BUFFER
不是全局符号,则 lea rdi, [rel BUFFER]
不需要任何重定位条目。 (相当于 C static
。)
您可能想知道为什么元数据必不可少:
没有可靠的方法来搜索 text/data 可能的绝对地址;误报是可能的。例如/usr/bin/ld
可能包含 0x401000
作为 x86-64 可执行文件的默认起始地址。您不希望 ld
的代码+数据的 ASLR 也更改其默认值。或者该整数值可能在许多程序中以多种方式出现,例如作为位图。当然,x86-64 机器代码是可变长度的,因此在大多数情况下,甚至没有可靠的方法来区分操作码和直接操作数。
还有潜在的假阴性。 x86 程序不太可能在具有多条指令的寄存器中构造绝对地址,但这当然是可能的。但是在非 x86 代码中,这很常见。
具有fixed-length指令的RISC机器不能将32位地址放入32位指令;没有其他空间了。因此,要从静态存储加载,绝对地址必须拆分为多个指令,例如 MIPS lui $t0, %hi(0x612300)
/ lw $t1, %lo(0x612300)($t0)
从绝对地址 0x612300 处的静态变量加载。 (在 asm 源代码中通常会有一个符号名称,但它不会出现在最终的 linked 二进制文件中,除非它是 .globl
,所以我使用数字作为提醒。)这样的说明不必成对出现;该地址的相同 high-half 可以在以后的指令中被其他对同一数组或结构的访问重用。
我知道 PIC 代码使 ASLR 随机化更高效、更容易,因为代码可以放在内存中的任何位置而无需更改代码。但是如果我根据维基百科理解正确的话 relocation 动态链接器可以在运行时进行“修复”,因此可以定位符号,尽管代码不是位置独立的。但是根据我在这里看到的许多答案,非图片代码不能 ASLR
除堆栈之外的部分(因此无法随机化程序入口点)。如果那是正确的,那么运行时修复的用途是什么?为什么我们不能在程序开始之前在运行时修复代码中的所有位置以使程序入口点随机化。
在看Linux之前先看一下Windows:
Windows' .EXE
文件(程序)通常有一个 so-called “base relocation table”并且它们有一个“image base”。
“image base”是程序的“所需”起始地址;如果 Windows 将程序加载到该地址,则不需要进行重定位。
“base relocation table”包含程序中代表地址的所有值的列表。如果程序加载到与“图像基址”不同的地址,Windows 必须将差异添加到 table.
中列出的所有值如果 .EXE
文件不包含“base relocation table”(据我所知一些 32 位 GCC 版本会生成此类文件),则无法加载文件到另一个地址。
这是因为如果变量someVariable
位于地址12340000,下面的C代码语句将产生完全相同的机器代码(二进制代码),并且无法区分它们:
long myVariable = 12340000;
并且:
int * myVariable = &someVariable;
第一种情况,12340000这个值在任何情况下都不能改变;在第二种情况下,如果程序加载到另一个地址,则必须将地址(即 12340000)更改为真实地址。
如果缺少“base relocation table”,则如果值 12340000 是整数值(不得更改)或地址(必须更改),则没有任何信息。
所以程序必须加载到某个固定地址。
我不确定最新的 32 位 Linux 版本,但至少在旧的 32 位 Linux 版本中没有什么比“基本重定位 table " 并且程序没有使用 PIC。这意味着这些程序必须加载到它们的“收藏夹”地址。
我不知道 64 位 Linux 程序,但如果程序的编译方式与(较旧的)32 位程序相同,则它们也必须加载到特定地址并且无法使用 ASLR。
TL:DR:并非所有使用绝对地址的人都会在 non-PIE 可执行文件(ELF 类型 EXEC,而非 DYN)中拥有重定位信息。 因此内核的 program-loader 无法全部找到它们来应用修正。
因此无法为构建为 non-PIE 的可执行文件追溯启用 ASLR。传统的可执行文件没有办法将自己标记为每次使用绝对地址都具有重定位元数据,而且添加这样的功能也没有意义,因为如果你想要文本 ASLR,你只需构建一个 PIE。
因为 ELF-type EXEC Linux 可执行文件保证在 link 时加载/映射到 linker 选择的固定基地址,这将是在可执行文件中浪费 space 来为内部符号创建 symbol-table 条目。所以工具链没有这样做,也没有理由开始。这就是传统 ELF 可执行文件的设计方式; Linux 在 90 年代中期从 a.out 切换到 ELF,当时堆栈 ASLR 还没有出现,所以它不在人们的关注范围内。
例如static char buf[100]
的绝对地址可能嵌入在使用它的机器代码中的某处(如果我们谈论的是 32 位代码,或将地址放入寄存器的 64 位代码),但没有办法知道在哪里或多少次。
此外,对于 x86-64,non-PIE 可执行文件的默认代码模型保证静态地址(文本/数据/bss)都在虚拟地址的低 2GiB space ,因此 32 位绝对有符号或无符号地址都可以工作,并且 rel32
位移可以从任何地方到达任何地方。这就是为什么 non-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 -Mintel
查看有关在 mov reg, imm64
指令中使用该符号的重定位信息。如果 BUFFER
不是全局符号,则 lea rdi, [rel BUFFER]
不需要任何重定位条目。 (相当于 C static
。)
您可能想知道为什么元数据必不可少:
没有可靠的方法来搜索 text/data 可能的绝对地址;误报是可能的。例如/usr/bin/ld
可能包含 0x401000
作为 x86-64 可执行文件的默认起始地址。您不希望 ld
的代码+数据的 ASLR 也更改其默认值。或者该整数值可能在许多程序中以多种方式出现,例如作为位图。当然,x86-64 机器代码是可变长度的,因此在大多数情况下,甚至没有可靠的方法来区分操作码和直接操作数。
还有潜在的假阴性。 x86 程序不太可能在具有多条指令的寄存器中构造绝对地址,但这当然是可能的。但是在非 x86 代码中,这很常见。
具有fixed-length指令的RISC机器不能将32位地址放入32位指令;没有其他空间了。因此,要从静态存储加载,绝对地址必须拆分为多个指令,例如 MIPS lui $t0, %hi(0x612300)
/ lw $t1, %lo(0x612300)($t0)
从绝对地址 0x612300 处的静态变量加载。 (在 asm 源代码中通常会有一个符号名称,但它不会出现在最终的 linked 二进制文件中,除非它是 .globl
,所以我使用数字作为提醒。)这样的说明不必成对出现;该地址的相同 high-half 可以在以后的指令中被其他对同一数组或结构的访问重用。