web-dev-qa-db-fra.com

Qu'est-ce que l'option -fPIE pour les exécutables indépendants de la position dans gcc et ld?

Comment va-t-il changer le code, par exemple appels de fonction?

61
osgx

PIE doit prendre en charge randomisation de la disposition de l'espace d'adressage (ASLR) dans les fichiers exécutables.

Avant la création du mode PIE, l'exécutable du programme ne pouvait pas être placé à une adresse aléatoire en mémoire, seules les bibliothèques dynamiques à code indépendant de position (PIC) pouvaient être déplacées vers un décalage aléatoire. Cela fonctionne beaucoup comme ce que fait PIC pour les bibliothèques dynamiques, la différence est qu'une table de liaison de procédures (PLT) n'est pas créée, à la place une relocalisation relative au PC est utilisée.

Après avoir activé le support PIE dans gcc/linkers, le corps du programme est compilé et lié en tant que code indépendant de la position. Un éditeur de liens dynamique effectue un traitement de relocalisation complet sur le module de programme, tout comme les bibliothèques dynamiques. Toute utilisation des données globales est convertie en accès via le Global Offsets Table (GOT) et les délocalisations GOT sont ajoutées.

PIE est bien décrit dans cette présentation d'OpenBSD PIE .

Les modifications des fonctions sont affichées dans cette diapositive (PIE vs PIC).

x86 pic vs tarte

Les variables et fonctions globales locales sont optimisées dans le secteur

Les variables et fonctions globales externes sont les mêmes que pic

et dans cette diapositive (PIE vs liaison à l'ancienne)

tarte x86 vs sans drapeau (fixe)

Les variables et fonctions globales locales sont similaires aux variables fixes

Les variables et fonctions globales externes sont les mêmes que pic

Notez que PIE peut être incompatible avec -static

71
osgx

Exemple exécutable minimal: GDB l'exécutable deux fois

Pour ceux qui veulent voir une action, voyons le travail de l'ASLR sur l'exécutable PIE et changeons d'adresse à travers les exécutions:

principal 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

Pour celui avec -no-pie, tout est ennuyeux:

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

Avant de commencer l'exécution, break main définit un point d'arrêt à 0x401126.

Puis, lors des deux exécutions, run s'arrête à l'adresse 0x401126.

Celui avec -pie est cependant beaucoup plus intéressant:

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

Avant de démarrer l'exécution, GDB prend simplement une adresse "factice" qui est présente dans l'exécutable: 0x1139.

Cependant, après son démarrage, GDB remarque intelligemment que le chargeur dynamique a placé le programme dans un emplacement différent et que la première interruption s'est arrêtée à 0x5630df2d6139.

Ensuite, la deuxième exécution a également remarqué intelligemment que l'exécutable s'est déplacé à nouveau et a fini par casser à 0x55763ab2e139.

echo 2 | Sudo tee /proc/sys/kernel/randomize_va_space garantit que l'ASLR est activé (par défaut dans Ubuntu 17.10): Comment puis-je désactiver temporairement l'ASLR (randomisation de la disposition de l'espace d'adressage)? | Ask Ubunt .

set disable-randomization off est nécessaire sinon GDB, comme son nom l'indique, désactive ASLR pour le processus par défaut pour donner des adresses fixes à travers les exécutions pour améliorer l'expérience de débogage: Différence entre les adresses gdb et les adresses "réelles"? | Débordement de pile .

readelf analyse

De plus, nous pouvons également observer que:

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

donne l'adresse réelle de chargement du runtime (pc a indiqué l'instruction suivante 4 octets après):

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

tandis que:

readelf -s ./pie.out | grep main

donne juste un décalage:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

En désactivant ASLR (avec randomize_va_space ou set disable-randomization off), GDB donne toujours main l'adresse: 0x5555555547a9, nous déduisons donc que le -pie l'adresse est composée de:

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

TODO où est codé en dur 0x555555554000 dans le noyau Linux/chargeur glibc/où? Comment l'adresse de la section de texte d'un exécutable PIE est-elle déterminée sous Linux?

Exemple d'assemblage minimal

Une autre chose intéressante que nous pouvons faire est de jouer avec du code d'assemblage pour comprendre plus concrètement ce que PIE signifie.

Nous pouvons le faire avec un assemblage autonome 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 en amont

et il s'assemble et fonctionne bien avec:

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

Cependant, si nous essayons de le lier en tant que TARTE avec:

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

alors le lien échouera avec:

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

Parce que la ligne:

mov $msg, %rsi  /* buffer */

code en dur l'adresse du message dans l'opérande mov et n'est donc pas indépendant de la position.

--no-dynamic-linker est requis comme expliqué dans: Comment créer un ELF exécutable indépendant de position lié statiquement sous Linux?

Si nous l'écrivons de manière indépendante de la position:

lea msg(%rip), %rsi

alors le lien PIE fonctionne bien, et GDB nous montre que l'exécutable est chargé à un emplacement différent en mémoire à chaque fois.

La différence ici est que lea a codé l'adresse de msg par rapport à l'adresse PC actuelle en raison de la syntaxe rip, voir aussi: Comment utiliser RIP Relative Adressage dans un programme d'assemblage 64 bits?

Nous pouvons également comprendre cela en démontant les deux versions avec:

objdump -S main.o

qui donnent respectivement:

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

Nous voyons donc clairement que lea a déjà l'adresse correcte complète de msg codée comme adresse actuelle + 0x19.

La version mov a cependant défini l'adresse sur 00 00 00 00, ce qui signifie qu'une relocalisation y sera effectuée: Que font les éditeurs de liens? Le cryptique R_X86_64_32S dans le message d'erreur ld est le type réel de relocalisation qui était requis et qui ne peut pas se produire dans les exécutables PIE.

Une autre chose amusante que nous pouvons faire est de mettre le msg dans la section des données au lieu de .text avec:

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

Maintenant le .o s'assemble pour:

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

donc le décalage RIP est maintenant 0, et nous supposons qu'une relocalisation a été demandée par l'assembleur. Nous confirmons cela avec:

readelf -r main.o

qui donne:

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

si clairement R_X86_64_PC32 est une relocalisation relative du PC que ld peut gérer pour les exécutables PIE.

Cette expérience nous a appris que l'éditeur de liens vérifie lui-même que le programme peut être PIE et le marque comme tel.

Puis lors de la compilation avec GCC, -pie indique à GCC de générer un assemblage indépendant de la position.

Mais si nous écrivons l'Assemblée nous-mêmes, nous devons nous assurer manuellement que nous avons atteint l'indépendance de position.

Dans ARMv8 aarch64, la position hello world indépendante de la position peut être obtenue avec instruction ADR .

Comment déterminer si un ELF est indépendant de la position?

En plus de simplement l'exécuter via GDB, certaines méthodes statiques sont mentionnées à:

Testé dans Ubuntu 18.10.