Comment va-t-il changer le code, par exemple appels de fonction?
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
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
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.