120

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

4

2 回答 2

131

PIE 是为了支持可执行文件中的地址空间布局随机化 (ASLR)

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

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

PIE 在这个 OpenBSD PIE 演示文稿中有很好的描述。

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

x86 图片与馅饼

局部全局变量和函数在饼图中进行了优化

外部全局变量和函数同pic

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

x86 pie vs no-flags(固定)

局部全局变量和函数类似于fixed

外部全局变量和函数同pic

请注意,PIE 可能与-static

于 2011-02-17T14:53:58.253 回答
88

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

对于那些想要看到一些操作的人,让我们看看 ASLR 在 PIE 可执行文件上的工作并在运行中更改地址:

主程序

#include <stdio.h>

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

主文件

#!/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 main0x401126.

然后,在两次执行期间,都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,顾名思义,默认情况下会关闭进程的 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 0x555555554000 硬编码在 Linux 内核/glibc 加载器/哪里?在 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 向我们显示可执行文件每次都加载到内存中的不同位置。

这里的区别在于,由于语法,相对于当前 PC 地址lea的地址被编码,另请参见:如何在 64 位汇编程序中使用 RIP 相对寻址?msgrip

我们也可以通过反汇编这两个版本来解决这个问题:

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 的偏移量是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

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

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

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

但是如果我们自己写汇编,我们必须手动确保我们已经实现了位置独立。

在 ARMv8 aarch64 中,位置无关的 hello world 可以通过ADR 指令来实现。

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

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

在 Ubuntu 18.10 中测试。

于 2018-07-12T14:19:00.400 回答