Qual é a opção -fPIE para executáveis ​​independentes de posição no gcc e ld?

Respostas:

100

PIE é apoiar randomização do layout do espaço de endereço (ASLR) em arquivos executáveis.

Antes do modo PIE ser criado, o executável do programa não podia ser colocado em um endereço aleatório na memória, apenas as bibliotecas dinâmicas de código independente de posição (PIC) podiam ser realocadas para um deslocamento aleatório. Funciona de forma muito semelhante ao que o PIC faz para bibliotecas dinâmicas, a diferença é que uma Tabela de ligação de procedimentos (PLT) não é criada, em vez disso, é usada a relocação relativa ao PC.

Depois de habilitar o suporte PIE em gcc / linkers, o corpo do programa é compilado e vinculado como código independente de posição. Um vinculador dinâmico realiza o processamento de realocação completo no módulo do programa, assim como as bibliotecas dinâmicas. Qualquer uso de dados globais é convertido para acesso por meio da Global Offsets Table (GOT) e realocações GOT são adicionadas.

PIE é bem descrito em nesta apresentação do OpenBSD PIE .

As alterações nas funções são mostradas neste slide (PIE vs PIC).

x86 foto vs pie

Variáveis ​​globais locais e funções são otimizadas em pizza

Variáveis ​​globais externas e funções são as mesmas que pic

e neste slide (PIE vs links de estilo antigo)

torta x86 vs sem sinalizadores (corrigido)

Variáveis ​​globais locais e funções são semelhantes a fixas

Variáveis ​​globais externas e funções são as mesmas que pic

Observe que o PIE pode ser incompatível com -static

osgx
fonte
3
Também na wikipedia: en.wikipedia.org/wiki/…
osgx
5
Por que -pie e -static são compatíveis com ARM e NÃO são compatíveis com x86? Minha pergunta SO: stackoverflow.com/questions/27082959/…
4ntoine
56

Exemplo de execução mínima: GDB, o executável duas vezes

Para aqueles que desejam ver alguma ação, vamos ver o ASLR trabalhar no executável PIE e alterar os endereços nas execuções:

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

Para quem tem -no-pie, tudo é enfadonho:

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

Antes de iniciar a execução, break maindefine um ponto de interrupção em 0x401126.

Então, durante ambas as execuções, runpara no endereço 0x401126.

Aquele com -pieporém é muito mais interessante:

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

Antes de iniciar a execução, GDB só tem um endereço "fictício" que está presente no executável: 0x1139.

Depois de iniciar, no entanto, o GDB percebe de forma inteligente que o carregador dinâmico colocou o programa em um local diferente, e o primeiro intervalo parou em 0x5630df2d6139 .

Então, a segunda execução também percebeu de forma inteligente que o executável se moveu novamente e acabou quebrando em 0x55763ab2e139.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_spacegarante que o ASLR esteja ativado (o padrão no Ubuntu 17.10): Como posso desabilitar temporariamente o ASLR (randomização do layout do espaço de endereço)? | Pergunte ao Ubuntu .

set disable-randomization offé necessário caso contrário GDB, como o nome sugere, desativa ASLR para o processo por padrão para fornecer endereços fixos entre execuções para melhorar a experiência de depuração: Diferença entre endereços gdb e endereços "reais"? | Stack Overflow .

readelf análise

Além disso, também podemos observar que:

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

fornece o endereço de carregamento real do tempo de execução (pc apontado para a seguinte instrução 4 bytes depois):

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

enquanto:

readelf -s ./pie.out | grep main

dá apenas um deslocamento:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

Ao desligar o ASLR (com randomize_va_spaceou set disable-randomization off), o GDB sempre fornece maino endereço 0x5555555547a9:, então deduzimos que o -pieendereço é composto de:

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

TODO, onde 0x555555554000 está codificado no kernel Linux / glibc loader / qualquer lugar? Como o endereço da seção de texto de um executável PIE é determinado no Linux?

Exemplo de montagem mínima

Outra coisa legal que podemos fazer é brincar com algum código assembly para entender mais concretamente o que significa PIE.

Podemos fazer isso com um conjunto independente Linux x86_64 hello world:

main.S

.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 upstream

e ele monta e funciona bem com:

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

No entanto, se tentarmos vinculá-lo como PIE com ( --no-dynamic-linkeré necessário conforme explicado em: Como criar um ELF executável independente de posição vinculada estaticamente no Linux? ):

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

então o link falhará com:

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

Porque a linha:

mov $msg, %rsi  /* buffer */

codifica o endereço da mensagem no movoperando e, portanto, não é independente da posição.

Se, em vez disso, escrevermos de forma independente da posição:

lea msg(%rip), %rsi

então o link PIE funciona bem, e o GDB nos mostra que o executável é carregado em um local diferente na memória todas as vezes.

A diferença aqui é que leacodificou o endereço de msgrelativo ao endereço atual do PC devido à ripsintaxe, consulte também: Como usar o endereçamento relativo RIP em um programa assembly de 64 bits?

Também podemos descobrir isso desmontando as duas versões com:

objdump -S main.o

que dão respectivamente:

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

Então vemos claramente que leajá tem o endereço correto completo demsg codificado como endereço atual + 0x19.

A movversão, entretanto, definiu o endereço para 00 00 00 00, o que significa que uma realocação será realizada lá: O que os vinculadores fazem? O ponto crítico R_X86_64_32Sna ldmensagem de erro é o tipo real de realocação que foi necessária e que não pode acontecer em executáveis ​​PIE.

Outra coisa divertida que podemos fazer é colocar o msgna seção de dados em vez de .textcom:

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

Agora, ele se .omonta para:

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

então o deslocamento RIP é agora 0, e achamos que uma realocação foi solicitada pelo montador. Confirmamos isso com:

readelf -r main.o

que dá:

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

tão claramente R_X86_64_PC32é uma realocação relativa de PC queld pode lidar com executáveis ​​PIE.

Este experimento nos ensinou que o próprio vinculador verifica se o programa pode ser TORTA e o marca como tal.

Então, ao compilar com o GCC, -piediz ao GCC para gerar uma montagem independente de posição.

Mas se escrevermos montagem nós mesmos, devemos manualmente garantir que alcançamos independência de posição.

No ARMv8 aarch64, a posição hello world independente pode ser alcançada com a instrução ADR .

Como determinar se um ELF é independente da posição?

Além de apenas executá-lo por meio do GDB, alguns métodos estáticos são mencionados em:

Testado em Ubuntu 18.10.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
1
Oi Ciro! Você pode criar uma pergunta separada para o endereço inicial ASLR-off pie-on e vinculá-la aqui?
osgx
1
@osgx Concluído. Você já sabe ou vai desenterrá-lo na hora? :-) Já que você está nisso, seria legal explicar como o kernel / carregador dinâmico
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功
Não sei ainda, mas sei que deve ser extraído de rtld de glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (se o interpretador ainda for ld-linux.so). Três anos atrás, Basile não tinha certeza sobre 0x55555555 também stackoverflow.com/questions/29856044 , mas essa pergunta era sobre o endereço inicial do próprio ld.so, então vá até os scripts fs / binfmt_elf.c ou readelf / objdump e linker do kernel .
osgx