Jak zmieni kod, np. Wywołania funkcji?
Odpowiedzi:
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 na 0x401126
.
Następnie podczas obu egzekucji run
zatrzymuje się pod adresem 0x401126
.
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
analiza
Ponadto możemy również zauważyć, że:
readelf -s ./no-pie.out | grep main
podaje rzeczywisty adres ładowania środowiska uruchomieniowego (pc wskazał następującą instrukcję 4 bajty dalej):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
podczas:
readelf -s ./pie.out | grep main
daje tylko przesunięcie:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
Wyłączając ASLR (za pomocą jednego randomize_va_space
lub set disable-randomization off
drugiego), GDB zawsze podaje main
adres 0x5555555547a9
:, 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
i łączy się i działa dobrze z:
as -o main.o main.S
ld -o main.out main.o
./main.out
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? ):
ld --no-dynamic-linker -pie -o main.out main.o
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ł adres msg
względny w stosunku do bieżącego adresu komputera ze względu na rip
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ą:
objdump -S main.o
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 adres msg
zakodowany jako aktualny adres + 0x19.
mov
Wersja jednak ustawił adres, na 00 00 00 00
, co oznacza, że przeniesienie będą tam wykonywane: Czego łączniki zrobić? Tajemniczy R_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:
readelf -r main.o
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óra ld
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.