Wie wird der Code geändert, z. B. Funktionsaufrufe?
Antworten:
PIE soll die Adressraum-Layout-Randomisierung (ASLR) in ausführbaren Dateien unterstützen.
Bevor der PIE-Modus erstellt wurde, konnte die ausführbare Datei des Programms nicht an einer zufälligen Adresse im Speicher abgelegt werden, sondern nur dynamische Bibliotheken mit positionsunabhängigem Code (PIC) konnten auf einen zufälligen Versatz verschoben werden. Es funktioniert sehr ähnlich wie PIC für dynamische Bibliotheken. Der Unterschied besteht darin, dass keine Prozedurverknüpfungstabelle (PLT) erstellt wird, sondern eine PC-relative Verlagerung verwendet wird.
Nach Aktivierung der PIE-Unterstützung in gcc / linkers wird der Programmkörper kompiliert und als positionsunabhängiger Code verknüpft. Ein dynamischer Linker führt die vollständige Verschiebungsverarbeitung auf dem Programmmodul aus, genau wie dynamische Bibliotheken. Jede Verwendung globaler Daten wird über die Global Offsets Table (GOT) in den Zugriff konvertiert, und GOT-Verschiebungen werden hinzugefügt.
PIE wird in dieser OpenBSD PIE-Präsentation gut beschrieben .
Änderungen an Funktionen werden auf dieser Folie angezeigt (PIE vs PIC).
x86 Bild gegen Kuchen
Lokale globale Variablen und Funktionen werden in pie optimiert
Externe globale Variablen und Funktionen sind identisch mit pic
und in dieser Folie (PIE vs Old-Style-Linking)
x86 pie vs no-flags (behoben)
Lokale globale Variablen und Funktionen ähneln festen
Externe globale Variablen und Funktionen sind identisch mit pic
Beachten Sie, dass PIE möglicherweise nicht mit kompatibel ist -static
Minimal ausführbares Beispiel: GDB die ausführbare Datei zweimal
Für diejenigen, die eine Aktion sehen möchten, sehen wir, wie ASLR an der ausführbaren PIE-Datei arbeitet und Adressen über Läufe hinweg ändert:
Haupt 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
Für den mit -no-pie
ist alles langweilig:
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
Setzt vor Beginn der Ausführung break main
einen Haltepunkt auf 0x401126
.
Stoppt dann während beider Ausführungen run
an der Adresse 0x401126
.
Der mit ist -pie
jedoch viel interessanter:
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
Vor dem Start der Ausführung nimmt GDB nur eine "Dummy" -Adresse an, die in der ausführbaren Datei vorhanden ist : 0x1139
.
Nach dem Start bemerkt GDB jedoch auf intelligente Weise, dass der dynamische Lader das Programm an einem anderen Ort platziert hat und die erste Pause bei angehalten hat 0x5630df2d6139
.
Dann bemerkte der zweite Lauf auch auf intelligente Weise, dass sich die ausführbare Datei erneut bewegte und bei brach 0x55763ab2e139
.
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
stellt sicher, dass ASLR aktiviert ist (Standardeinstellung in Ubuntu 17.10): Wie kann ich ASLR (Address Space Layout Randomization) vorübergehend deaktivieren? | Fragen Sie Ubuntu .
set disable-randomization off
wird benötigt, andernfalls deaktiviert GDB, wie der Name schon sagt, ASLR für den Prozess standardmäßig, um feste Adressen über Läufe hinweg anzugeben, um das Debugging-Erlebnis zu verbessern: Unterschied zwischen GDB-Adressen und "echten" Adressen? | Stapelüberlauf .
readelf
Analyse
Darüber hinaus können wir auch Folgendes beobachten:
readelf -s ./no-pie.out | grep main
gibt die tatsächliche Laufzeit-Ladeadresse an (PC zeigt 4 Byte später auf die folgende Anweisung):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
während:
readelf -s ./pie.out | grep main
gibt nur einen Versatz:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
Durch Deaktivieren der ASLR (mit entweder randomize_va_space
oder set disable-randomization off
) gibt GDB immer main
die Adresse: an 0x5555555547a9
. Daher schließen wir, dass die -pie
Adresse aus folgenden Elementen besteht:
0x555555554000 + random offset + symbol offset (79a)
TODO wo ist 0x555555554000 im Linux-Kernel / glibc loader / wo fest codiert? Wie wird die Adresse des Textabschnitts einer ausführbaren PIE-Datei unter Linux bestimmt?
Minimales Montagebeispiel
Eine andere coole Sache, die wir tun können, ist, mit einem Assembler-Code herumzuspielen, um konkreter zu verstehen, was PIE bedeutet.
Wir können das mit einer freistehenden Linux x86_64-Baugruppe tun. Hallo Welt:
Netz
.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
und es baut zusammen und läuft gut mit:
as -o main.o main.S
ld -o main.out main.o
./main.out
Wenn wir jedoch versuchen, es als PIE mit zu verknüpfen ( --no-dynamic-linker
ist erforderlich, wie unter: So erstellen Sie eine statisch verknüpfte positionsunabhängige ausführbare ELF unter Linux? ):
ld --no-dynamic-linker -pie -o main.out main.o
dann schlägt die Verknüpfung fehl mit:
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
Weil die Linie:
mov $msg, %rsi /* buffer */
codiert die Nachrichtenadresse im mov
Operanden fest und ist daher nicht positionsunabhängig.
Wenn wir es stattdessen positionsunabhängig schreiben:
lea msg(%rip), %rsi
dann funktioniert der PIE-Link einwandfrei und GDB zeigt uns, dass die ausführbare Datei jedes Mal an einer anderen Stelle im Speicher geladen wird.
Der Unterschied besteht darin, dass lea
die Adresse msg
relativ zur aktuellen PC-Adresse aufgrund der rip
Syntax codiert wurde. Siehe auch: Wie wird die relative RIP-Adressierung in einem 64-Bit-Assemblyprogramm verwendet?
Wir können das auch herausfinden, indem wir beide Versionen zerlegen mit:
objdump -S main.o
welche jeweils geben:
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
So sehen wir deutlich, dass lea
bereits die volle korrekte Adresse von hatmsg
als aktuelle Adresse + 0x19 codiert ist.
Die mov
Version hat jedoch die Adresse auf festgelegt 00 00 00 00
, was bedeutet, dass dort ein Umzug durchgeführt wird: Was machen Linker? Die Kryptik R_X86_64_32S
in der ld
Fehlermeldung ist die tatsächliche Art der Verlagerung, die erforderlich war und in ausführbaren PIE-Dateien nicht vorkommen kann.
Eine andere lustige Sache, die wir tun können, ist, das msg
in den Datenabschnitt zu setzen, anstatt .text
mit:
.data
msg:
.ascii "hello\n"
len = . - msg
Nun .o
versammelt sich das zu:
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
Der RIP-Offset ist jetzt 0
und wir vermuten, dass der Assembler einen Umzug angefordert hat. Wir bestätigen dies mit:
readelf -r main.o
was gibt:
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
so klar R_X86_64_PC32
ist ein PC relativer Umzug, dassld
die für ausführbare PIE-Dateien geeignet ist.
Dieses Experiment hat uns gelehrt, dass der Linker selbst prüft, ob das Programm PIE sein kann, und es als solches markiert.
Weist GCC dann beim Kompilieren mit GCC an, -pie
eine positionsunabhängige Baugruppe zu generieren.
Wenn wir jedoch selbst eine Versammlung schreiben, müssen wir manuell sicherstellen, dass wir die Positionsunabhängigkeit erreicht haben.
In ARMv8 aarch64 kann die positionsunabhängige Hallo-Welt mit dem ADR-Befehl erreicht werden .
Wie kann festgestellt werden, ob ein ELF positionsunabhängig ist?
Neben dem einfachen Ausführen über GDB werden einige statische Methoden erwähnt:
Getestet in Ubuntu 18.10.