它将如何更改代码,例如函数调用?
Answers:
PIE将在可执行文件中支持地址空间布局随机化(ASLR)。
在创建PIE模式之前,无法将程序的可执行文件放置在内存中的随机地址处,只能将位置无关代码(PIC)动态库重新放置到随机偏移量。它的工作方式与PIC对动态库的工作方式非常相似,不同之处在于未创建过程链接表(PLT),而是使用PC相对重定位。
在gcc / linkers中启用PIE支持后,程序主体将作为位置无关的代码进行编译和链接。与动态库一样,动态链接器在程序模块上执行完整的重定位处理。全局数据的任何用法都将转换为通过全局偏移表(GOT)进行访问,并添加GOT重定位。
在此OpenBSD PIE演示文稿中对PIE进行了很好的描述。
x86图片vs馅饼
在局部优化全局局部变量和函数
外部全局变量和函数与pic相同
在此幻灯片中(PIE与旧式链接)
x86 pie vs无标志(已修复)
局部全局变量和函数类似于固定的
外部全局变量和函数与pic相同
请注意,PIE可能与 -static
最小的可运行示例: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_space
或set 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
它可以通过以下方式组装并正常运行:
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向我们显示,可执行文件确实每次都加载到内存中的其他位置。
此处的区别在于,由于语法原因lea
,msg
相对于当前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_32S
的ld
错误消息是这是必需的,哪些不能在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中测试。