全局变量的地址如何进行随机化,如果它们已经硬编码在ELF文件中?

5

我在一些地方读到过,ASLR应该在每次运行程序时随机加载.data部分,这意味着全局变量的地址应该不同。然而,如果我有以下代码:

int global_var = 42;

int main()
{
    global_var = 10;
    return 0;
}

我使用 gcc -fpie -o global global.c 编译它,objdump -d -M intel 显示如下:

  4004ed:   55                      push   rbp
  4004ee:   48 89 e5                mov    rbp,rsp
  4004f1:   c7 05 3d 0b 20 00 0a    mov    DWORD PTR [rip+0x200b3d],0xa        # 601038 <global_var>

看起来global_var总是会被放置在601038的地址上。事实上,如果我编译时带有调试符号,global_var的DIE中就会硬编码这个地址:

$ gcc -ggdb3 -fpie -o global global.c
$ objdump --dwarf=info global
...
<1><55>: Abbrev Number: 4 (DW_TAG_variable)
   <56>   DW_AT_name        : (indirect string, offset: 0x30c): global_var  
   <5a>   DW_AT_decl_file   : 1 
   <5b>   DW_AT_decl_line   : 1 
   <5c>   DW_AT_type        : <0x4e>    
   <60>   DW_AT_external    : 1 
   <60>   DW_AT_location    : 9 byte block: 3 38 10 60 0 0 0 0 0    (DW_OP_addr: 601038)

在这些情况下,ASLR是如何工作的?

1
我相信ASLR只对代码、栈和堆起作用,而不一定对数据起作用。可能是错误的,所以不将其作为答案。 - Ricky Mutschlechner
你尝试过使用gdb运行和调试吗?我不确定,但也许有一些重定位操作。 - Tomer
@RickyMutschlechner:代码(.text)和数据(.data)之间的距离是链接时常量;这就是RIP相对寻址如何工作的原因(或32位PIC添加偏移量到GOT)。在非PIE中,代码和BSS也不能被ASLRed,只有堆栈和mmap分配可以。在非PIE中,所有静态存储都在静态链接时由链接器选择的地址处加载。 - Peter Cordes
2个回答

7
从反汇编指令中得到的指令输出为您提供了相对于任意基地址(0x400000)的方便值601038,但请阅读实际指令;它正在写入DWORD PTR [rip + 0x200b3d]。rip是指令指针。代码和数据相对固定的偏移量;随机化基地址不会改变这一点。通过使用指令指针加载,它使用了一个已经包含了ASLR重定位的地址。
在描述中提供601038作为方便映射是因为散布在代码中的从rip开始的固定偏移量都取决于指令的位置,因此如果不为指令位置进行调整,它们就无法进行比较;虽然反汇编器知道指令偏移量,但是它可以为您减去该指令偏移量,以获得可在常见的0x400000基础上进行全局比较的地址。

是的,但即使没有 -fpie,这也会发生。我猜编译器只是为了安全起见而这样做? - Martin
@Martin:RIP相对寻址是在x86-64上访问静态数据的最有效方式,因此即使使用了-fno-pie,编译器也会使用它。[为什么x86-64中的全局变量相对于指令指针访问?](https://dev59.com/TFMI5IYBdhLWcg3waq3a)但这将使它们使用5字节的“mov esi,offset foo”将静态地址放入寄存器,例如作为puts的参数,而不是需要使用7字节的“lea rsi,[rip + foo]”来使其与-pie安全链接(这是一个单独于-fPIE的链接器选项)。 - Peter Cordes

5
当你编译一个PIE时,实际上该文件在技术上是一个共享对象(ET_DYN,你可以使用readelf -h filename来检查)。这种类型的ELF文件(包括PIEs和.so文件)被设计为可在任何基址(通常是页面大小的模数)上加载。
对于这些文件,虚拟地址(在节头表、程序头表、符号表、DWARF DIE等中给出)是相对于此基址的偏移量。
这在System V ABI中有解释。
程序头中的虚拟地址可能不代表程序内存映像的实际虚拟地址。可执行文件通常包含绝对代码。另一方面,共享对象段通常包含位置无关代码。这使得段的虚拟地址可以从一个进程更改为另一个,而不会使执行行为失效。尽管系统为各个进程选择虚拟地址,但它维护段的相对位置。因为位置无关代码在段之间使用相对寻址,所以内存中虚拟地址之间的差异必须与文件中虚拟地址之间的差异相匹配。因此,在给定进程中,任何一个可执行文件或共享对象中,任何段在内存中的虚拟地址与相应虚拟地址之间的差值是一个固定值。这个差异就是基地址。
在DWARF中,这在第7.3节中有说明。 (section 7.3 of DWARF 4):

可执行目标文件中的调试信息中的重定位地址是虚拟地址,共享对象中的调试信息中的重定位地址是相对于从该共享对象加载的最低内存区域的起始位置的偏移量。

由于这些文件可以映射到任何基址,因此这个基址可以被随机化。


但实际代码会发生什么呢? 在我举的例子中,我有mov DWORD PTR [rip+0x200b3d],0xa,这取决于 rip 的值。 这意味着“随机”的.data.text的基地址是相关的,对吧? 生成的代码是否可以仅通过地址(而不是寄存器 + 偏移量)来引用全局变量? 如果是这样,那么启用ASLR会导致错误吗? - Martin
是的,给定 ELF 文件中不同部分的相对位置是固定的。当您编译 PIE 时,编译器仅使用“相对寻址”而不是绝对地址。这就是 PIC/PIE(位置无关)的核心思想。 - ysdx
1
哦,我明白了。我以为每个部分都有独立的基地址。不过,生成的代码是否可以仅使用地址(而不是寄存器+偏移量)来引用全局变量?如果可以,那么在启用ASLR时会出现问题吗? - Martin
如果您没有使用-fpie编译,就会出现这种情况。在这种情况下,我想到的是,链接编辑器可能无法正确链接或生成文本重定位(即在加载文件时修补指令)。 - ysdx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接