Was ist die Option -fPIE für positionsunabhängige ausführbare Dateien in gcc und ld?

89

Wie wird der Code geändert, z. B. Funktionsaufrufe?

osgx
quelle

Antworten:

95

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

osgx
quelle
3
Auch in Wikipedia: en.wikipedia.org/wiki/…
osgx
5
Warum sind -pie und -static auf ARM kompatibel und auf x86 NICHT kompatibel? Meine SO-Frage: stackoverflow.com/questions/27082959/…
4ntoine
51

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-pieist 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 maineinen Haltepunkt auf 0x401126.

Stoppt dann während beider Ausführungen runan der Adresse 0x401126.

Der mit ist -piejedoch 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_spacestellt 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 offwird 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_spaceoder set disable-randomization off) gibt GDB immer maindie Adresse: an 0x5555555547a9. Daher schließen wir, dass die -pieAdresse 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

GitHub stromaufwärts

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-linkerist 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 movOperanden 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 leadie Adresse msgrelativ zur aktuellen PC-Adresse aufgrund der ripSyntax 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 leabereits die volle korrekte Adresse von hatmsg als aktuelle Adresse + 0x19 codiert ist.

Die movVersion 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_32Sin der ldFehlermeldung 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 msgin den Datenabschnitt zu setzen, anstatt .textmit:

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

Nun .oversammelt sich das zu:

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

Der RIP-Offset ist jetzt 0und 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_PC32ist 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, -pieeine 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.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle
1
Hallo Ciro! Können Sie eine separate Frage für die ASLR-off-Startadresse erstellen und hier verlinken?
Osgx
1
@osgx Fertig. Weißt du es schon oder wirst du es im Handumdrehen ausgraben? :-) Wenn Sie schon dabei sind, wäre es cool zu erklären, wie der Linux-Kernel / Dyn-Loader feststellt, ob etwas PIE hat oder nicht: unix.stackexchange.com/questions/89211/…
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功
Ich weiß es noch nicht, aber ich weiß, dass es aus rtld von glibc gegraben werden sollte - glibc / elf github.com/lattera/glibc/tree/master/elf (wenn der Dolmetscher noch ld-linux.so ist). Vor drei Jahren Basile war nicht sicher 0x55555555 auch stackoverflow.com/questions/29856044 , aber diese Frage war etwa Adresse des Beginnens ld.so selbst, so graben sich in Kernel fs / binfmt_elf.c oder readelf / objdump und Linker - Skripte.
Osgx