Autor: | Wojciech Muła |
---|---|
Dodany: | 3.03.2002 |
Aktualizacja: | 16.04.2002 |
Treść
Uwaga! — poziomy uprzywilejowania na x86 są numerowane „odwrotnie” 0 = najwyższy, 3 = najniższy. Tutaj, pisząc w kontekście poziomów uprzywilejowania, słowa niższy/wyższy mam na myśli możliwości aplikacji.
Aplikacje uruchamiane przez serwer DPMI zawsze są uruchamiane na Ring3, czyli najniższym poziomie uprzywilejowania. Do większości zastosowań w zupełności to wystarcza, ale np. profilowanie kodu przy użyciu instrukcji rdmsr/wrmsr nie będzie możliwe — są to instrukcje uprzywilejowane.
Bieżący poziom uprzywilejowania (CPL) kodu jest zapisany w selektorze.
15 4 3 2 1 +------------------------+--+----+ | INDEX |TI|RPL | +------------------------+--+----+
Gdyby zabezpieczenia były bardzo prymitywne wystarczyłoby utworzyć kopię bieżącego deskryptora kodu, nadać mu DPL=0, stworzyć selektor do niego z RPL=0 i wykonać daleki skok. Jednakże procesor sprawdza czy CPL (ang. Current Privilage Level — bieżący poziom uprzywilejowania) jest dostatecznie wysoki: innymi słowy czy CPL <= RPL. Można wykonać skok z kodu działającego na poziomie 0 na kod ring3, ale w przeciwną stronę nie jest to możliwe.
Zmiany poziomu uprzywilejowania są możliwe w trzech przypadkach:
Bramka wywołania jest specjalnym deskryptorem, który określa adres procedury oraz jej poziom uprzywilejowania który może być wyższy niż CPL. Deskryptor ten ma następujący format:
liczba bitów | zakres bitów | opis |
---|---|---|
16 bitów | 48..63 | offset 16..31 |
1 bit | 47 | P |
2 bity | 45..46 | DPL |
5 bitów | 40..44 | TYPE=01100 |
8 bitów | 32..39 | DWORD COUNT |
16 bitów | 16..31 | code selector |
16 bitów | 0..15 | offset 0..15 |
Interesują nas w zasadzie trzy pola tego deskryptora:
Pole DWORD COUNT określa ile podwójnych słów ma skopiować procesor ze stosu wywołującej aplikacji na nowy stos — w naszym przypadku to będzie zero. Odsyłam do dokumentacji procesora, gdzie zagadnienie to jest szczegółowo wyjaśnione.
Poniższy przykład pokazuje jak używać callgates.
segment. data adress: garbages dd ? ; cokolwiek callgate_selector dw ? ; selektor bramki segment. text call far [adress] ; lub push word [callgate_selector] pop gs call dword [gs:0]
Sterowanie zostanie przekazane do procedury pod adresem zapisanym w deskryptorze bramki, tj. code_selector:offset. CPL zostanie ustawiony na poziom uprzywilejowania bramki.
Właściwie pokazałem już jak wejść na ring0, jednak „problemy” stwarza serwer DPMI — standardowymi funkcjami do zapisywania deskryptorów nie uda się zapisać ani deskryptora kodu na ring0, ani bramki wywołania.
Dlatego trzeba samodzielnie zapisać w odpowiednie miejsca tablicy LDT niedozwolone (niebezpieczne?) deskryptory. Dlaczego w LDT? Bo DPMI pozwala na alokację deskryptorów w LDT, dzięki temu będziemy znać „pewne” adresy, i nie trzeba będzie ryzykować zapisem (i zniszczeniem) przypadkowych deskryptorów.
Problemem jest dostanie się do pamięci przechowującej tablice deskryptorów — obszar tej pamięci na 99% znajduje się poza dostępną aplikacji przestrzenią adresową. Trzeba się więc jakoś do niej dostać, a przepis na to jest nadzwyczaj prosty — stworzyć segment danych obejmujący całe 4GB pamięci, o adresie bazowym 0, z prawami zapisu/odczytu, z DPL=3 — serwer DPMI powinien pozwolić na zapisanie takiego deskryptora — jeśli nie pozwoli — game over.
To nieważne, że obszary pamięci które obejmie ten nowy segment są chronione z poziomu innych deskryptorów — procesor sprawdza prawa dostępu tylko poprzez bieżący deskryptor.
Załóżmy, że mamy już rzeczony 4-gigabajtowy segment — pora dostać się do LDT naszej aplikacji.
segment .data whole_memory dw 0 ; selektor danych 4GB descriptor dw 0 ; deskryptor który chcemy zapisać dw 0 ; dw 0 ; dw 0 ; LDT_selector dw 0 ; oraz jego selektor ; opis Globalnej Tablicy Deskryptorów GDT dw 0 ; GDT limit - rozmiar tablicy dd 0 ; GDT base adres - adres liniowy początku tablicy
Proszę zwrócić uwagę, że adres liniowy GDT nie wymaga żadnych translacji — jest to równocześnie adres logiczny w 4-gigabajtowym segmencie.
segment .text sgdt [GDT] ; rozkaz SGDT nie jest uprzywilejowany - na szczęście :) sldt ax ; ax = selektor LDT
Adres bazowy bieżącej tablicy LDT (w 4GB segmencie) jest równy:
movzx eax, ax ; eax = selektor LDT (jednocześnie offset w GDT) mov esi, [GDT+2] ; esi = GDT base adress add esi, eax ; esi = adres bazowy bieżącego LDT
Z kolei mając selektor w LDT wystarczy dodać go teraz do esi, by móc zapisać żądany deskryptor.
push ds push ds ; pop es ; es = ds push [whole_memory] ; pop ds ; ds = whole_memory mov edi, descriptor ; esi = offset descriptor push esi add esi, [LDT_selector] cld movsd ; przesłanie danych spod es:edi do ds:esi movsd ; pop esi pop ds
Najczęściej będzie tak, że procedura mająca działać na ring0 będzie zapisana w bieżącym segmencie kodu. Aby uzyskać selektor kodu ring0 wystarczy odczytać deskryptor bieżącego kodu, zmodyfikować tylko pole DPL, i rzecz jasna zapisać.
W artykule przedstawiłem wszystko co jest potrzebne by wskoczyć na ring0. Przykładu nie zamieszczam bo w zasadzie wszystko już jest, jedyny co trzeba zrobić to alokować deskryptory, odpowiednio je inicjować (lub modyfikować) i zapisywać.
Oto kolejne kroki:
Funkcje DPMI które będą potrzebne (ich opis można znaleźć w liście przerwań Ralfa Browna):
Powodzenia!