#include <stdio.h>
char *assembly(void);
int main(void)
{
printf("Resultado: %s\n", assembly());
return 0;
}
Explicando PIE e ASLR
Como vimos no tópico Endereçamento o processador calcula o endereço dos operandos na memória onde o resultado do cálculo será o endereço absoluto onde o operando está.
O problema disso é que o código que escrevemos precisa sempre ser carregado no mesmo endereço senão os endereços nas instruções estarão errados. Esse problema foi abordado no tópico sobre MS-DOS, onde a diretiva org 0x100
precisa ser usada para que o NASM calcule o offset correto dos símbolos senão os endereços estarão errados e o programa não funcionará corretamente.
Sistemas operacionais modernos têm um recurso de segurança chamado ASLR que dificulta a exploração de falhas de segurança no binário. Resumidamente ele carrega os endereços dos segmentos do executável em endereços aleatórios ao invés de sempre no mesmo endereço. Com o ASLR desligado os segmentos sempre são mapeados nos mesmos endereços.
Porém um código que acessa endereços absolutos jamais funcionaria apropriadamente com o ASLR ligado. É aí que entra o conceito de Position-independent executable (PIE) que nada mais é que um executável com código que somente acessa endereços relativos, ou seja, não importa em qual endereço (posição) você carregue o código do executável ele irá funcionar corretamente.
Na nossa PoC eu instruí para compilar o programa usando a flag -no-pie
no GCC para garantir que o linker não iria produzir um executável PIE já que ainda não havíamos aprendido sobre o assunto. Mas depois de aprender a escrever código com endereçamento relativo em Assembly fique à vontade para remover essa flag e começar a escrever programas independentes de posição.
Já vimos no tópico Endereçamento que em x86-64 se tem um novo endereçamento relativo à RIP. É muito mais simples escrever código independente de posição no modo de 64-bit devido a isso.
Podemos usar a palavra-chave rel
no endereçamento para dizer para o NASM que queremos que ele acesse um endereço relativo à RIP. Conforme exemplo:
Também podemos usar a diretiva default rel
para que o NASM compile todos os endereçamentos como relativos por padrão. Caso você defina o padrão como endereço relativo a palavra-chave abs
pode ser usada da mesma maneira que a palavra-chave rel
porém para definir o endereçamento como absoluto.
Um exemplo de PIE em modo de 64-bit:
Experimente compilar sem a flag -no-pie
para o GCC na hora de linkar:
Deveria funcionar normalmente. Mas experimente comentar a diretiva default rel
na linha 2 e compilar novamente, você vai obter um erro parecido com esse:
Repare que o erro foi emitido pelo linker (ld
) e não pelo compilador em si. Acontece que como usamos um endereço absoluto o NASM colocou o endereço do símbolo msg
na relocation table para ser resolvido pelo linker, onde o linker é quem definiria o endereço absoluto do mesmo.
Só que como removemos o -no-pie
o linker tentou produzir um PIE e por isso emitiu um erro avisando que aquela referência para um endereço absoluto não pode ser usada.
Como o endereço relativo ao Instruction Pointer só existe em modo de 64-bit, nos outros modos de processamento não é nativamente possível obter um endereçamento relativo. O compilador GCC resolve esse problema criando um pequeno procedimento cujo o único intuito é obter o valor no topo da pilha e armazenar em um registrador. Conforme ilustração abaixo:
Ao chamar o procedimento __x86.get_pc_thunk.bx
o endereço da instrução seguinte na memória é empilhado pela instrução CALL, portanto mov ebx, [esp]
salva o endereço que EIP terá quando o procedimento retornar em EBX.
Quando a instrução add ebx, 12345
é executada o valor de EBX
coincide com o endereço da própria instrução ADD.