PIE
Table of Contents
1. PIE
- PIC 是针对 so 的位置无关代码, 主要考虑的是 so 如何能被加载到任意位置
- PIE 是针对 exectuable 的位置无关代码, 主要考虑的是 ASLR, 即 exectuable 如何能被加载到随机的位置开始执行
实现上两者很相似, 主要是:
- 需要通过 GOT 来访问全局变量 (某此情况下也可以通过 PC-relative 方式)
- 需要通过 GOT/PLT 来访问其它函数
1.1. PIC
1.1.1. sample code
int x = 10; void foo() { x = 10; }
1.1.2. pic
$> arm-linux-androideabi-gcc -fpic -shared test.c -o libtest.so -O0 -g3 $> arm-linux-androideabi-objdump -D libtest.so 0000030c <foo>: 30c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 310: e28db000 add fp, sp, #0 314: e59f201c ldr r2, [pc, #28] ; 338 <foo+0x2c> <--- r2 是 literal pool 中记录的 pc 与 GOT 表的偏移量 I 318: e08f2002 add r2, pc, r2 <--- r2 指向 GOT 表 II 31c: e59f3018 ldr r3, [pc, #24] ; 33c <foo+0x30> <--- r3 是 GOT[x] 相对 GOT 表的偏移量 (0xfffffffc, 即 -4) III 320: e7923003 ldr r3, [r2, r3] <--- r3 是 GOT[x] 中保存的 x 变量的地址 IV 324: e3a0200a mov r2, #10 328: e5832000 str r2, [r3] 32c: e24bd000 sub sp, fp, #0 330: e49db004 pop {fp} ; (ldr fp, [sp], #4) 334: e12fff1e bx lr 338: 00001ccc andeq r1, r0, ip, asr #25 33c: fffffffc ; <UNDEFINED> instruction: 0xfffffffc I. PC = 0x31c, 所以 r2 = [0x31c+28] = [0x338] = 0x1ccc II. PC = 0x320, 所以 r2 = 0x320 + 0x1ccc = 0x1fec IV. r3 = [0x1fec + 0xfffffffc] = 0x1fe8
可见, 0x1fe8 应该是 x 变量的地址, 查看 GOT 的部分:
00001fe8 <_GLOBAL_OFFSET_TABLE_-0x4>: 1fe8: 00002004 andeq r2, r0, r4 <--- 1fe8 中保存的地址是 0x2004 00001fec <_GLOBAL_OFFSET_TABLE_>: ... 1ff8: 000002a4 andeq r0, r0, r4, lsr #5 1ffc: 000002a4 andeq r0, r0, r4, lsr #5 Disassembly of section .data: 00002000 <__dso_handle>: 2000: 00000000 andeq r0, r0, r0 00002004 <x>: 2004: 0000000a andeq r0, r0, sl <--- 0x2004 位于 .data, 其值为 0xa (10)
由于代码访问 literal pool 或 GOT 都是使用 PC-relative 方式, 所以 so 被加载到任意位置都不影响它对 GOT 的访问, 但有一个问题:
GOT 表中的保存的地址 0x2004 是写死在 GOT 表中, 如果 so 被加载到 0xabcd, 显然 GOT 表中的这个数据应该变成 0xabcd + 0x2004, 这个问题留待后面解决.
1.1.3. no-pic
$> arm-linux-androideabi-gcc -fno-pic -shared test.c -o libtest.so -O0 -g3 $> arm-linux-androideabi-objdump -D libtest.so 0000030c <foo>: 30c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 310: e28db000 add fp, sp, #0 314: e59f3010 ldr r3, [pc, #16] ; 32c <foo+0x20> <--- r3 直接通过 literal pool 获得 0x2004 这个地址 318: e3a0200a mov r2, #10 31c: e5832000 str r2, [r3] 320: e24bd000 sub sp, fp, #0 324: e49db004 pop {fp} ; (ldr fp, [sp], #4) 328: e12fff1e bx lr 32c: 00002004 andeq r2, r0, r4 Disassembly of section .data: 00002000 <__dso_handle>: 2000: 00000000 andeq r0, r0, r0 00002004 <x>: 2004: 0000000a andeq r0, r0, sl <--- 0x2004 就是 .data 中 x 变量的地址
可见 no-pic 的方式简化很多, 但它与前面 pic 的方式有同样的问题: 0x2004 这个值是直接被写到literal pool (即 .text) 中的, 如果处理 so 被加载到任意位置的问题?
1.1.4. pic/no-pic 的区别
pic/no-pic 都需要处理 0x2004 需要被重定位的问题, 处理的方法其实很直接: linker 直接把这个 0x2004 修改成 0xabcd + 0x2004 即可.
这时会显示出 pic/no-pic 的本质区别: pic 需要修改的是 GOT 表, 这个 GOT表位于 .data section, 而 no-pic 需要修改的是 literal pool, 这个与代码是在一起的, 位于 .text section.
由于 COW 的存在, no-pic 会导致 .text 中所有使用到 x 的部分的 page 都无法被多进程共享, 这是 no-pic 的主要问题.
linker 分别对 no-pic/pic 进行重定位:
no-pic: 0000030c <foo>: 30c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 310: e28db000 add fp, sp, #0 314: e59f3010 ldr r3, [pc, #16] ; 32c <foo+0x20> 318: e3a0200a mov r2, #10 31c: e5832000 str r2, [r3] 320: e24bd000 sub sp, fp, #0 324: e49db004 pop {fp} ; (ldr fp, [sp], #4) 328: e12fff1e bx lr 32c: 00002004 andeq r2, r0, r4 ---------+ | Relocation section '.rel.dyn' at offset 0x284 contains 2 entries: | Offset Info Type Sym.Value Sym. Name | 0000032c 00000017 R_ARM_RELATIVE <---------+ 00001ed0 00000017 R_ARM_RELATIVE pic: Disassembly of section .got: 00001fe8 <_GLOBAL_OFFSET_TABLE_-0x4>: 1fe8: 00002004 andeq r2, r0, r4 ----------+ | Relocation section '.rel.dyn' at offset 0x284 contains 2 entries: | Offset Info Type Sym.Value Sym. Name | 00001ed4 00000017 R_ARM_RELATIVE | 00001fe8 00000017 R_ARM_RELATIVE <---------+
1.2. PIE
PIE 主要是针对 exectuable 的, 它可以把 exectuable 加载到任意地址, 而不是以 0 地址为基准加载. PIC 主要是针对多个 so 加载时地址空间怎么分配的问题, 而 PIE 主要是针对 ASLR 的考虑, 毕竟只有一个 exectuable 需要加载, 而不像 so 那样有多个需要加载.
1.2.1. sample code
int x = 10; int main(int argc, char *argv[]) { x = 11; return 0; }
1.2.2. pie
$> arm-linux-androideabi-gcc -fPIE -pie test.c -O0 -g3 $> arm-linux-androideabi-objdump -D ./a.out 000003ec <main>: 3ec: e52db004 push {fp} ; (str fp, [sp, #-4]!) 3f0: e28db000 add fp, sp, #0 3f4: e24dd00c sub sp, sp, #12 3f8: e50b0008 str r0, [fp, #-8] 3fc: e50b100c str r1, [fp, #-12] 400: e59f301c ldr r3, [pc, #28] ; 424 <main+0x38> 404: e08f3003 add r3, pc, r3 <--- pc + r3 = 40c + 1bf4 = 0x2000, 与 pic 不同的是, 这里并没有通过 GOT 来查找, 而是通过 PC-relative 直接找到 .data 中 x 的地址 408: e3a0200b mov r2, #11 40c: e5832000 str r2, [r3] 410: e3a03000 mov r3, #0 414: e1a00003 mov r0, r3 418: e24bd000 sub sp, fp, #0 41c: e49db004 pop {fp} ; (ldr fp, [sp], #4) 420: e12fff1e bx lr 424: 00001bf4 strdeq r1, [r0], -r4 Disassembly of section .data: 00002000 <x>: 2000: 0000000a andeq r0, r0, sl
由于 x 是 exectuable 内部定义的全局符号, 这里可以仅仅通过 PC-relative 而不通过 GOT 做到 PIE
1.2.3. no-pie
$> arm-linux-androideabi-gcc -fno-pie test.c -O0 -g3 $> arm-linux-androideabi-objdump -D ./a.out 000083c4 <main>: 83c4: e52db004 push {fp} ; (str fp, [sp, #-4]!) 83c8: e28db000 add fp, sp, #0 83cc: e24dd00c sub sp, sp, #12 83d0: e50b0008 str r0, [fp, #-8] 83d4: e50b100c str r1, [fp, #-12] 83d8: e59f3018 ldr r3, [pc, #24] ; 83f8 <main+0x34> 83dc: e3a0200b mov r2, #11 83e0: e5832000 str r2, [r3] 83e4: e3a03000 mov r3, #0 83e8: e1a00003 mov r0, r3 83ec: e24bd000 sub sp, fp, #0 83f0: e49db004 pop {fp} ; (ldr fp, [sp], #4) 83f4: e12fff1e bx lr 83f8: 0000a000 andeq sl, r0, r0 <--- no-pie 下直接访问 .data 中 x 的地址 Disassembly of section .data: 0000a000 <x>: a000: 0000000a andeq r0, r0, sl
1.2.4. PIE 的例子
$> cat test.c int x = 10; int main(int argc, char *argv[]) { printf("%p\n", main); } $> gcc test.c -fno-pie ~@tj02433pcu> ./a.out 0x4004d7 <--- 这里 main 的地址是 0x400xxx, 是因为编译时 linker 默认把 .text 加载到 0x400000 位置 (参考 linker_script) ~@tj02433pcu> ./a.out 0x4004d7 ~@tj02433pcu> ./a.out 0x4004d7 $> gcc test.c -fpie ~@tj02433pcu> gcc test.c -fPIE -pie ~@tj02433pcu> ./a.out 0x55b97e93764a ~@tj02433pcu> ./a.out 0x5615d0ad064a ~@tj02433pcu> ./a.out 0x5576ed0c064a
1.2.5. kernel 对 PIE 的支持
加载 so 是 linker 的工作, 但加载 exectuable 是 kernel 的工作, 所以 kernel 需要支持 PIE
linux 2.6.12 以后支持 ASLR, 从而支持 PIE
load_elf_binary: if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space): current->flags |= PF_RANDOMIZE; // ... if (loc->elf_ex.e_type == ET_DYN): load_bias = ELF_ET_DYN_BASE - vaddr; if (current->flags & PF_RANDOMIZE) // 给 load_bias 加上一个随机偏移量 load_bias += arch_mmap_rnd(); load_bias = ELF_PAGESTART(load_bias); elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
通过 personality syscall 可以控制是否打开 PIE:
$> cat test.c #include <string.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #include <sys/personality.h> int x = 10; int main(int argc, char *argv[]) { personality(ADDR_NO_RANDOMIZE); printf("%p\n", main); } $> gcc test.c -fPIE -pie $> ./a.out 0x55f2abed368a $> ./a.out 0x5579b06f968a $> ./a.out 0x557a8bdc268a
Backlinks
ELF (ELF > ELF Section > dynamic linker 相关 > plt > 总结): 每个 so 均有其 PLT 表, 当使用 PIE 时所有非 static 的函数均通过 PLT 表引用.
Linker Relocation (Linker Relocation): pc-relative 地址, PCREL 对 PIE 至关重要, 通过 PCREL 才能以`位置无关`的形式找到 GOT, 进而支持 PIE
Linker Relocation (Linker Relocation): 和 PIE 有关
Linker Relocation (Linker Relocation > riscv relocation > R_RISCV_GOT_HI20): GOT_HI20 与 PCREL_HI20 类似, 不同的是 GOT_HI20 并不是使用的 hello_msg 的地址, 而 是 hello_msg 所在的 GOT 表项的地址, 因为 GOT 主要是为了解决动态链接时 static linker 无法确定符号地址的问题 (需要注意两点: 1. 静态链接也可以用 GOT, 但没有必要; 2. 动态链接也可以不用 GOT 而是让 rtld 直接 patch 代码本身, 参考 PIE)