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 main
define um ponto de interrupção em 0x401126
.
Então, durante ambas as execuções, run
para no endereço 0x401126
.
Aquele com -pie
poré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_space
garante 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_space
ou set disable-randomization off
), o GDB sempre fornece main
o endereço 0x5555555547a9
:, então deduzimos que o -pie
endereç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 mov
operando 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 lea
codificou o endereço de msg
relativo ao endereço atual do PC devido à rip
sintaxe, 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 lea
já tem o endereço correto completo demsg
codificado como endereço atual + 0x19.
A mov
versã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_32S
na ld
mensagem 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 msg
na seção de dados em vez de .text
com:
.data
msg:
.ascii "hello\n"
len = . - msg
Agora, ele se .o
monta 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, -pie
diz 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.