gcc和ld中与位置无关的可执行文件的-fPIE选项是什么?


94

它将如何更改代码,例如函数调用?

Answers:


100

PIE将在可执行文件中支持地址空间布局随机化(ASLR)

在创建PIE模式之前,无法将程序的可执行文件放置在内存中的随机地址处,只能将位置无关代码(PIC)动态库重新放置到随机偏移量。它的工作方式与PIC对动态库的工作方式非常相似,不同之处在于未创建过程链接表(PLT),而是使用PC相对重定位。

在gcc / linkers中启用PIE支持后,程序主体将作为位置无关的代码进行编译和链接。与动态库一样,动态链接器在程序模块上执行完整的重定位处理。全局数据的任何用法都将转换为通过全局偏移表(GOT)进行访问,并添加GOT重定位。

此OpenBSD PIE演示文稿中对PIE进行了很好的描述。

幻灯片中显示对功能的更改(PIE与PIC)。

x86图片vs馅饼

在局部优化全局局部变量和函数

外部全局变量和函数与pic相同

此幻灯片中(PIE与旧式链接)

x86 pie vs无标志(已修复)

局部全局变量和函数类似于固定的

外部全局变量和函数与pic相同

请注意,PIE可能与 -static


3
也在维基百科中:en.wikipedia.org/wiki/…–
osgx

5
为什么-pie和-static在ARM上兼容,而在x86上不兼容?我的SO问题:stackoverflow.com/questions/27082959/...
4ntoine

56

最小的可运行示例:GDB两次执行

对于那些想要执行某些操作的用户,让我们看看ASLR在PIE可执行文件上工作并在运行之间更改地址:

main.c

#include <stdio.h>

int main(void) {
    puts("hello");
}

main.sh

#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
  exe="${pie}.out"
  gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
  gdb -batch -nh \
    -ex 'set disable-randomization off' \
    -ex 'break main' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    "./$exe" \
  ;
  echo
  echo
done

对于的来说-no-pie,一切都很无聊:

Breakpoint 1 at 0x401126: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

在开始执行之前,break main在处设置一个断点0x401126

然后,在两次执行期间,都run在address处停止0x401126

与一个-pie不过是更加有趣:

Breakpoint 1 at 0x1139: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x5630df2d6139

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x55763ab2e139

在开始执行之前,GDB仅使用可执行文件中存在的“虚拟”地址:0x1139

但是,启动后,GDB会智能地注意到动态加载程序将程序放置在其他位置,并且第一个中断在处停止0x5630df2d6139

然后,第二次运行还聪明地注意到可执行文件再次移动,并最终在处中断0x55763ab2e139

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space确保ASLR处于打开状态(Ubuntu 17.10中的默认设置):如何临时禁用ASLR(地址空间布局随机化)?| 询问Ubuntu

set disable-randomization off否则需要GDB,顾名思义,GDB默认情况下会关闭进程的ASLR,以在运行中提供固定地址,以改善调试体验:gdb地址与“真实”地址之间的区别?| 堆栈溢出

readelf 分析

此外,我们还可以观察到:

readelf -s ./no-pie.out | grep main

给出实际的运行时加载地址(pc指向以下指令后4个字节):

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

而:

readelf -s ./pie.out | grep main

给出一个偏移量:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

通过关闭ASLR(使用randomize_va_spaceset disable-randomization off),GDB始终提供main地址:0x5555555547a9,因此我们推断出该-pie地址由以下组成:

0x555555554000 + random offset + symbol offset (79a)

TODO在Linux内核/ glibc loader /哪里将0x555555554000硬编码在哪里?在Linux中如何确定PIE可执行文件的文本部分的地址?

最少的组装示例

我们可以做的另一件很酷的事情是使用一些汇编代码来更具体地了解PIE的含义。

我们可以使用Linux x86_64独立式程序集hello world来做到这一点:

电源

.text
.global _start
_start:
asm_main_after_prologue:
    /* write */
    mov $1, %rax   /* syscall number */
    mov $1, %rdi   /* stdout */
    mov $msg, %rsi  /* buffer */
    mov $len, %rdx /* len */
    syscall

    /* exit */
    mov $60, %rax   /* syscall number */
    mov $0, %rdi    /* exit status */
    syscall
msg:
    .ascii "hello\n"
len = . - msg

GitHub上游

它可以通过以下方式组装并正常运行:

as -o main.o main.S
ld -o main.out main.o
./main.out

但是,如果我们尝试将其作为PIE进行链接(--no-dynamic-linker如以下说明中所述,是必需的:如何在Linux中创建静态链接的位置无关的可执行ELF?):

ld --no-dynamic-linker -pie -o main.out main.o

然后链接将失败并显示:

ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output

因为这行:

mov $msg, %rsi  /* buffer */

将消息地址硬编码在mov操作数中,因此与位置无关。

如果我们改为以与位置无关的方式编写它:

lea msg(%rip), %rsi

然后PIE链接就可以正常工作,并且GDB向我们显示,可执行文件确实每次都加载到内存中的其他位置。

此处的区别在于,由于语法原因leamsg相对于当前PC地址的地址是经过编码的rip,另请参见:如何在64位汇编程序中使用RIP相对寻址?

我们还可以通过以下两种方式来分解这两个版本:

objdump -S main.o

分别给出:

e:   48 c7 c6 00 00 00 00    mov    $0x0,%rsi
e:   48 8d 35 19 00 00 00    lea    0x19(%rip),%rsi        # 2e <msg>

000000000000002e <msg>:
  2e:   68 65 6c 6c 6f          pushq  $0x6f6c6c65

因此,我们可以清楚地看到lea已经具有完整正确的地址,msg编码为当前地址+ 0x19。

mov但是,该版本将地址设置为00 00 00 00,这意味着将在此处执行重定位:链接器做什么?隐秘R_X86_64_32Sld错误消息是这是必需的,哪些不能在PIE可执行发生迁移的实际类型。

我们可以做的另一件有趣的事情是将放入msg数据部分,而不是.text使用:

.data
msg:
    .ascii "hello\n"
len = . - msg

现在.o汇编到:

e:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 15 <_start+0x15>

因此RIP偏移量为now 0,并且我们猜测汇编器已请求重定位。我们通过以下方式确认:

readelf -r main.o

这使:

Relocation section '.rela.text' at offset 0x160 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  000200000002 R_X86_64_PC32     0000000000000000 .data - 4

显然R_X86_64_PC32,PC相对重定位ld可以处理PIE可执行文件。

这个实验告诉我们,链接器本身会检查程序是否为PIE并将其标记为PIE。

然后在使用GCC进行编译时,-pie告诉GCC生成与位置无关的程序集。

但是,如果我们自己编写程序集,则必须手动确保实现位置独立性。

在ARMv8 aarch64中,可以使用ADR指令实现位置无关的hello世界。

如何确定ELF是否与位置无关?

除了仅通过GDB运行之外,还提到了一些静态方法:

在Ubuntu 18.10中测试。


1
嗨Ciro!您可以为ASLR-off粘贴式起始地址创建单独的问题,并在此处链接吗?
osgx

1
@osgx完成。您是否已经知道或打算即时进行挖掘?:-)当你在它,它会很酷介绍一下如何在Linux内核/ DYN装载机确定的东西有PIE与否:unix.stackexchange.com/questions/89211/...
西罗桑蒂利郝海东冠状病六四事件法轮功

我还不知道,但是我知道应该从glibc的rtld中提取它-glibc / elf github.com/lattera/glibc/tree/master/elf(如果解释器仍然是ld-linux.so)。三年前,Basile还不确定0x55555555还是stackoverflow.com/questions/29856044,但是这个问题是关于ld.so本身的起始地址,因此请深入研究内核的fs / binfmt_elf.c或readelf / objdump和链接程序脚本。
osgx
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.