Como isso mudará o código, por exemplo, chamadas de função?
Respostas:
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
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
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.