ethical.blue Magazine

// Cybersecurity clarified.

Wnętrzności systemu Windows: Interfejsy API, poziomy uprzywilejowania i wywołania systemowe

13.06.2022   Dawid Farbaniec
...
Interfejs programowania aplikacji (w skrócie API) w Windows to w uproszczeniu zestaw funkcji zawartych w bibliotekach, które są przeważnie częścią systemu. Udostępniane są one programistom, aby mogli ich używać w swoich programach. Istniejące od bardzo dawna Windows API (nazywane też Win32 API) składa się z warstw (podobnie jak cebula czy ogry) i często jedne funkcje są „opakowaniami” na znajdujące się niżej (biorąc pod uwagę poziom abstrakcji) wywołania.

Quote:
„Cebula ma warstwy, ogry mają warstwy – cebula ma warstwy.”
~Shrek (film)

Podstawowe funkcje dostępne są w bibliotekach kernel32.dll oraz user32.dll. Na przykład: Ktoś tworzy program korzystający z Win32 API i potrzebuje utworzyć plik. Może do tego celu skorzystać z funkcji CreateFile. Co jednak dzieje się wewnątrz funkcji? Znajdziemy tam m.in. różne rozkazy procesora, ale wykonanie zmierza do wywołania funkcji NtCreateFile. Funkcja NtCreateFile znajduje się w bibliotece ntdll.dll i jest abstrakcyjnie niżej niż Win32 API, a zbiór funkcji z przedrostkiem Nt tam zawartych określa się jako Native API. Interfejs ten jest mostem pomiędzy aplikacjami użytkownika, a wnętrznościami systemu Windows. Native API zawiera też elementy, które dotyczą trybu jądra (ang. kernel mode, ring 0) takie jak np. wersje funkcji z przedrostkiem Zw (np. ZwCreateFile). Korzysta się z nich podczas tworzenia programów działających w trybie jądra.



Warto zaznaczyć, że od pewnego czasu dawne Win32 API programowane głównie w języku C powoli jest zastępywane przez nowy interfejs nazywany Windows Runtime (w skrócie WinRT).

Poziomy uprzywilejowania (ang. rings)

Architektura systemu Windows zapewnia izolację programów użytkownika od tych działających w trybie jądra systemu. Możliwe jest to dzięki zaimplementowanym w procesorze zabezpieczeniom, które udostępniają cztery poziomy uprzywilejowania (ang. rings) dla wykonywanego kodu.



Najbardziej uprzywilejowany jest poziom zerowy w którym to działają komponenty krytyczne dla systemu operacyjnego jak np. sterowniki. Dwa kolejne poziomy (ring 1 oraz ring 2) są o umiarkowanym poziomie uprzywilejowania. Natomiast poziom użytkownika (ring 3) to tryb w którym działają standardowe programy, które nie mają bezpośredniego dostępu do krytycznych zasobów systemowych czy sprzętowych. W dokumentacji procesorów AMD64 oraz Intel 64 bieżący poziom uprzywilejowania oznaczany jest skrótem CPL od Current Privilege Level. Można go odczytać z rejestru segmentu kodu CS (ang. code segment).



Warstwa abstrakcji sprzętowej (HAL)

Zadaniem HAL jest m.in. ukrycie szczegółów implementacji obsługi sprzętu i urządzeń, co zwiększa przenośność (ang. portability) kodu oraz dostarcza interfejs do komunikacji ze sprzętem oraz usługami jądra systemu Windows.



Biblioteka warstwy abstrakcji sprzętowej ma nazwę HAL.DLL i znajduje się w katalogu:
%WINDOWS%\System32\hal.dll

Funkcje, które udostępnia ta biblioteka mają przedrostek (prefiks) Hal np. HalExamineMBR.

Warto zainteresować się tą biblioteką w przypadku chęci programowania sterowników oraz tworzenia czy analizy kodu działającego w trybie jądra (ang. kernel mode).

Wywołania systemowe (SYSCALL)

Jako, że Native API jest mostem pomiędzy trybem użytkownika (ang. user mode, ring 3), a trybem jądra systemu (ang. kernel mode, ring 0), to powinno zawierać jakiś mechanizm przekazywania wywołań do jądra systemu.

Wykonywane jest to za pomocą rozkazu procesora SYSCALL. A najlepszym wyjaśnieniem będzie podejrzenie wnętrza wybranej funkcji Native API. Przykładowym programem może być systemowy Kalkulator.



Poprzez wydanie polecenia u ntdll!NtCreateFile możliwe jest wyświetlenie wnętrza funkcji NtCreateFile w języku Asembler. Otrzymany kod po deasemblacji przedstawia rysunek.



Polecenie u w narzędziu WinDbg pozwala dokonać deasemblacji, czyli otrzymania listingu w języku Asembler wybranego fragmentu kodu (np. funkcji czy adresu w pamięci). We wnętrzu funkcji NtCreateFile przedstawionym jako kod w Asemblerze na rysunku można zauważyć między innymi:
  • przekazanie argumentu z rejestru RCX do R10 (rozkaz SYSCALL niszczy zawartość rejestru RCX),
  • wpisanie numeru wywołania do rejestru EAX (tutaj 55h),
  • instrukcje sprawdzające (TEST, JNE),
  • wywołanie systemowe poprzez rozkaz SYSCALL,
  • etc.
Dla porównania można wyświetlić wnętrze innej funkcji np. NtQuerySystemInformation. Tutaj numer wywołania SYSCALL w rejestrze EAX to 36h.

      

Na poprzednich rysunkach przedstawiono jak wygląda wywołanie systemowe za pomocą rozkazu procesora SYSCALL na przykładzie wyświetlenia wnętrza dwóch wybranych funkcji Native API.

Należy pamiętać, że numery wywołań (wartość przekazywana przez rejestr EAX) mogą zmieniać się wraz w wersjami systemu Windows, gdyż są to mechanizmy wewnętrzne i nie jest tu wymagane zapewnianie kompatybilności. Dla programistów aplikacji jest Win32 API lub nowsze WinRT, natomiast Native API oraz mechanizm SYSCALL są stosowane przez specjalistyczne programy wymagające tego lub wirusy komputerowe.

Mam nadzieję, że podoba Ci się ten samouczek.

Wykaz literatury

Chris Anley, John Heasman, Felix FX Linder, Gerardo Richarte, 2007 – The Shellcoder’s Handbook, ISBN: 9780470080238
https://docs.microsoft.com/en-us/windows/uwp/get-started/universal-application-platform-guide#how-the-universal-windows-platform-relates-to-windows-runtime-apis [access: 2020-08-23]
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/u--unassemble- [access: 2020-08-31]