PIE

Table of Contents

1. PIE

  • PIC 是针对 so 的位置无关代码, 主要考虑的是 so 如何能被加载到任意位置
  • PIE 是针对 exectuable 的位置无关代码, 主要考虑的是 ASLR, 即 exectuable 如何能被加载到随机的位置开始执行

实现上两者很相似, 主要是:

  1. 需要通过 GOT 来访问全局变量 (某此情况下也可以通过 PC-relative 方式)
  2. 需要通过 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)

Author: [email protected]
Date: 2017-05-02 Tue 00:00
Last updated: 2024-09-02 Mon 14:43

知识共享许可协议