Jak zmieni kod, np. Wywołania funkcji?
PIE ma obsługiwać randomizację układu przestrzeni adresowej (ASLR) w plikach wykonywalnych.
Przed utworzeniem trybu PIE, pliku wykonywalnego programu nie można było umieścić pod losowym adresem w pamięci, tylko biblioteki dynamiczne z kodem niezależnym od pozycji (PIC) mogły zostać przeniesione do losowego przesunięcia. Działa bardzo podobnie do tego, co robi PIC dla bibliotek dynamicznych, różnica polega na tym, że nie jest tworzona tabela łączenia procedur (PLT), zamiast tego używana jest relokacja względem komputera.
Po włączeniu obsługi PIE w gcc / linkers, treść programu jest kompilowana i linkowana jako kod niezależny od pozycji. Dynamiczny linker wykonuje pełne przetwarzanie relokacji w module programu, podobnie jak biblioteki dynamiczne. Jakiekolwiek użycie danych globalnych jest konwertowane w celu uzyskania dostępu za pośrednictwem tabeli globalnych przesunięć (GOT) i dodawane są relokacje GOT.
PIE jest dobrze opisana w tej prezentacji OpenBSD PIE .
Zmiany funkcji są pokazane na tym slajdzie (PIE vs PIC).
x86 pic vs ciasto
Lokalne zmienne globalne i funkcje są zoptymalizowane w postaci kołowej
Zewnętrzne zmienne globalne i funkcje są takie same jak na rys
i na tym slajdzie (SROKA a linkowanie w starym stylu)
x86 pie vs no-flags (naprawione)
Lokalne zmienne i funkcje globalne są podobne do stałych
Zewnętrzne zmienne globalne i funkcje są takie same jak na rys
Należy pamiętać, że PIE może być niezgodne z -static
Minimalny działający przykład: GDB plik wykonywalny dwukrotnie
Dla tych, którzy chcą zobaczyć jakąś akcję, zobaczmy, jak ASLR działa na pliku wykonywalnym PIE i zmienia adresy między uruchomieniami:
main.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
Dla kogoś z
-no-pie
wszystkim jest nudno: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
Przed rozpoczęciem wykonywania
break main
ustawia punkt przerwania na0x401126
.Następnie podczas obu egzekucji
run
zatrzymuje się pod adresem0x401126
.Ten z
-pie
jednak jest znacznie bardziej interesujący: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
Przed przystąpieniem do wykonania, GDB zajmuje tylko „atrapę” adres, który jest obecny w pliku wykonywalnego:
0x1139
.Jednak po uruchomieniu GDB inteligentnie zauważa, że dynamiczny moduł ładujący umieścił program w innym miejscu, a pierwsza przerwa zatrzymała się na
0x5630df2d6139
.Następnie drugi przebieg również inteligentnie zauważył, że plik wykonywalny ponownie się poruszył i zakończył się przerwaniem
0x55763ab2e139
.echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
zapewnia, że ASLR jest włączony (ustawienie domyślne w Ubuntu 17.10): Jak mogę tymczasowo wyłączyć ASLR (randomizacja układu przestrzeni adresowej)? | Zapytaj Ubuntu .set disable-randomization off
jest potrzebne, w przeciwnym razie GDB, jak sugeruje nazwa, domyślnie wyłącza ASLR dla procesu, aby nadawać stałe adresy w przebiegach, aby poprawić wrażenia z debugowania: Różnica między adresami gdb a adresami „rzeczywistymi”? | Przepełnienie stosu .readelf
analizaPonadto możemy również zauważyć, że:
podaje rzeczywisty adres ładowania środowiska uruchomieniowego (pc wskazał następującą instrukcję 4 bajty dalej):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
podczas:
daje tylko przesunięcie:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
Wyłączając ASLR (za pomocą jednego
randomize_va_space
lubset disable-randomization off
drugiego), GDB zawsze podajemain
adres0x5555555547a9
:, więc wnioskujemy, że-pie
adres składa się z:0x555555554000 + random offset + symbol offset (79a)
TODO, gdzie jest na stałe zakodowany kod 0x555555554000 w jądrze Linux / programie ładującym glibc / gdziekolwiek? Jak jest określany adres sekcji tekstowej pliku wykonywalnego PIE w systemie Linux?
Minimalny przykład montażu
Kolejną fajną rzeczą, którą możemy zrobić, jest pobawienie się kodem asemblera, aby bardziej konkretnie zrozumieć, co oznacza PIE.
Możemy to zrobić za pomocą wolnostojącego zestawu Linux x86_64 hello world:
sieć elektryczna
.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 upstream
i łączy się i działa dobrze z:
Jeśli jednak spróbujemy połączyć go jako PIE z (
--no-dynamic-linker
jest to wymagane, jak wyjaśniono w: Jak utworzyć statycznie powiązany plik wykonywalny niezależny od pozycji w systemie Linux? ):wtedy połączenie zakończy się niepowodzeniem z:
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
Ponieważ linia:
mov $msg, %rsi /* buffer */
koduje na stałe adres komunikatu w
mov
operandzie i dlatego nie jest niezależny od pozycji.Jeśli zamiast tego napiszemy to w sposób niezależny od pozycji:
lea msg(%rip), %rsi
wtedy łącze PIE działa dobrze, a GDB pokazuje nam, że plik wykonywalny jest ładowany za każdym razem w innym miejscu w pamięci.
Różnica polega na tym, że
lea
zakodował adresmsg
względny w stosunku do bieżącego adresu komputera ze względu narip
składnię, zobacz także: Jak używać adresowania względnego RIP w 64-bitowym programie asemblerowym?Możemy to również ustalić, demontując obie wersje za pomocą:
które dają odpowiednio:
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
Widzimy więc wyraźnie, że
lea
ma już pełny poprawny adresmsg
zakodowany jako aktualny adres + 0x19.mov
Wersja jednak ustawił adres, na00 00 00 00
, co oznacza, że przeniesienie będą tam wykonywane: Czego łączniki zrobić? TajemniczyR_X86_64_32S
wld
komunikacie o błędzie jest rzeczywisty typ relokacji, który był wymagany i który nie może mieć miejsca w plikach wykonywalnych PIE.Kolejną zabawną rzeczą, którą możemy zrobić, jest umieszczenie
msg
w sekcji danych zamiast.text
:.data msg: .ascii "hello\n" len = . - msg
Teraz
.o
zbiera się, aby:e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
więc offset RIP wynosi teraz
0
i domyślamy się, że asembler zażądał relokacji. Potwierdzamy to:co daje:
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
tak oczywiste
R_X86_64_PC32
jest relokacja względna komputera, którald
może obsłużyć pliki wykonywalne PIE.Ten eksperyment nauczył nas, że sam linker sprawdza, czy program może być SROKĄ i oznacza go jako taki.
Następnie podczas kompilacji z GCC
-pie
mówi GCC, aby wygenerował niezależny montaż pozycji.Ale jeśli sami piszemy assembler, musimy ręcznie upewnić się, że osiągnęliśmy niezależność pozycji.
W ARMv8 aarch64 niezależną pozycję hello world można uzyskać za pomocą instrukcji ADR .
Jak ustalić, czy ELF jest niezależny od pozycji?
Oprócz uruchamiania go przez GDB, niektóre metody statyczne są wymienione na:
Testowane w Ubuntu 18.10.
źródło