Sociálne siete

SecIT.sk na Facebooku SecIT.sk na Google+ SecIT.sk na Twitteri

Podporte nás


V prípade, že Vám obsah nášho portálu niekedy nejakým spôsobom pomohol, či bol pre Vás prínosom prosím podporte jeho chod ľubovoľnou čiastkou. Ďakujeme!

Štítky

Vyhľadávanie

You are here

Domov
Upozornenie: Obsah je licenčne chránený a bez písomných súhlasov autora článku a vlastníka webovej stránky nesmie byť v žiadnej forme ďalej kopírovaný a šírený v pôvodnom, či v akokoľvek upravenom stave.

Vývoj ovladačů jádra - 2. díl

V minulém díle jsme si uvedli pár základních faktů o ovladačích jádra a končili jsme ukázkovým kódem na načtení driveru do jádra. V tomto díle si řekneme, jak ovladač z jádra uvolnit. Dále si popíšeme jeden z velmi důležitých mechanismů, na který musíme brát ohled při programování ovladačů, a nakonec se dozvíme jeden způsob komunikace ovladačů s okolím.

 

Uvolnění ovladače z jádra

Uvolnění ovladače z jádra je vcelku podobné jeho načtení. Zjednáme si přístup ke službě, která ovladač reprezentuje, a pošleme jí signál, že se má zastavit.

Var hSCM     : THandle; 
    hService : THandle; 
    SS       : _SERVICE_STATUS;
begin
hSCM:=OpenSCManager(
		Nil, 
		Nil, 
		SC_MANAGER_ALL_ACCESS);
If hSCM > 0 Then
   begin
   hService:=OpenService(
		hSCM,
		PAnsiChar(DriverName),
		SERVICE_ALL_ACCESS);
   If hService > 0 Then
      begin
      ControlService(
		hService,
		SERVICE_CONTROL_STOP,
		SS);
      CloseServiceHandle(hService);
      end;
   CloseServiceHandle(hSCM);
   end;
end;

Proměnná DriverName obsahuje název služby driveru, který chcete uvolnit z jádra. Pokud funkce ControlService vrátí nenulovou hodnotu (booleovsky TRUE), tak se uvolnění driveru podařilo. Pokud vrátí 0, došlo k chybě, jejíž číslo zjistíte přes funkci GetLastError.

Drivery a služby patří ke kritickým součástem operačního systému, tudíž se uvolnění ovladače zdařit nemusí. Vše totiž záleží na ovladači - pokud z jádra být uvolněn nechce, standardními prostředky jej nedonutíte. Existují prostředky nestandardní, leč jejich použití vám nedoporučuji. Driver může být například zavěšen na nějakém systémovém zařízení, může na sebe přesměrovat kód některých funkcí. Pokud ho tvrdě uvolníte, pravděpodobně velmi rychle dojde k modré obrazovce.

Pokud chcete operačnímu systému umožnit uvolnění vašeho ovladače z paměti, musíte ve zdrojovém kódu definovat příslušnou proceduru, která se většinou nazývá DriverUnload. Deklarace vypadá následovně:

VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)

Procedura v parametru dostane odkaz na strukturu DRIVER_OBJECT, která obsahuje většinu důležitých informací, které je nutné při uvolňování složitějších ovladačů znát. Jakmile tato rutina skončí, ovladač je uvolněn. Rutina nijak nemůže zabránit uvolnění ovladače a není k tomuto účelu určena. Měla by obsahovat kód, který po ovladači uklidí (uvolní alokovanou paměť, zruší všechny vytvořené objekty atp.).

Samotná deklarace DriverUnload nestačí. Operační systém se nějak musí dozvědět, že taková rutina ve vašem ovladači existuje. Struktura DRIVER_OBJECT obsahuje položku s názvem DriverUnload, do které se systém podívá při uvolňování ovladače z jádra. Pokud tato položka obsahuje NULL, systém předpokládá, že ovladač uvolněn z jádra být nechce. Pokud je obsah položky adresa, systém na ni předá řízení. Pokud tedy chcete umožnit uvolnění ovladače, musíte do této položky přiřadit svoji rutinu.

NTSTATUS DriverEntry(
		IN PDRIVER_OBJECT DriverObject, 
		IN PUNICODE_STRING RegPath)
{
	...
	DriverObject->DriverUnload 
			= DriverUnload;
	...
}

Mechanismus IRQL

Zatím byste mohli usuzovat, že psát ovladač je stejně těžké, jako psát libovolnou jinou aplikaci. Leč není tomu tak. A jednou z příčin vyšší obtížnosti psaní ovladačů je mechanismus IRQL. IRQL je zkratka ze sousloví "Interrupt ReQuest Level" a jedná se v podstatě o prioritu. Pokud nějaké vlákno vykonává kód, běží s uričtým IRQL. Ale pozor! Narozdíl od priorit vláken a procesů, které jistě dobře znáte, mají IRQL zásadní dopad na vykonávání kódu. Dostatečně vysoké IRQL totiž zabraňuje multitaskingu na procesoru, kde příslušné vlákno běží. Vlákno nemůže být přerušeno vlákny, jejichž IRQL je menší nebo rovno jeho IRQL (výjimkou je nejnižší hodnota IRQL). Z toho také vyplývá, že pomocí IRQL lze maskovat i přerušení. Tato vlastnost má zásadní dopad na programování ovladačů - kód běžící s vysokou prioritou totiž nemůže používat žádné mechanismy, které jsou založeny na vykonávání kódu s prioritou nižší. Čím vyšší IRQL, tím méně toho může kód provádět.

Jak jsem již zmínil, IRQL je specifické pro procesor a vlákno, které je aktuálně aktivní. Pokud má váš počítač například dvoujádrový procesor, může na jednom jádře běžet vlákno s vysokým IRQL a na druhém vlákno s nízkým IRQL. Proto je nutné psát ovladače velmi opatrně a snažit se použít synchronizační primitiva všude tam, kde by mohlo dojít k race condition, jenž má obvykle fatální následky. A právě synchronizace někdy dělá z psaní ovladačů peklo na zemi.

IRQL může mít hodnotu od 0 do 255 (0xFF). V následujícím seznamu jsou uvedena a stručně charakterizována nejdůležitější IRQL:

  • PASSIVE_LEVEL (0) - na tomto IRQL běží všechna vlákna v uživatelském režimu (tedy vlákna normálních procesů). Dochází k multitaskingu a multithreadingu. Kód může využívat stránkovanou paměť a může provádět synchronizaci pomocí klasických mechanismů. Nejsou zde žádná omezení.
  • DISPATCH_LEVEL (2) - Vlákno s touto prioritou nemůže být přerušeno vlákny s prioritou <= DISPATCH_LEVEL. Z toho vyplývá, že nefunguje multitasking a nelze použít synchronizačních objektů (semafory, eventy, mutexy), protože vlákno s touto prioritou nemůže být uspáno ani přeplánováno na jiný procesor. Rovněž mechanismus stránkování není dostupný, protože přerušení, které je voláno při výpadku stránky běží s IRQL 1 (APC_LEVEL). Ačkoliv platí tato strašlivá omezení, stále lze pracovat s nestránkovanou pamětí. Ale libovolný přístup k paměti stránkované může vyvolat modrou obrazovku, pokud se tato paměť nachází v stránkovacím souboru. Protože kód s touto prioritou efektivně blokuje daný procesor, je nutné, aby byl vykonán v co nejkratším čase. Jinak může docházet k zpomalení chodu operačního systému.
  • HIGH_LEVEL (0xFF) - Nejvyšší možné IRQL. Kód s touto prioritou nemůže být přerušen. Platí stejné "zákazy" jako u DISPATCH_LEVEL, navíc není možné alokovat a uvolňovat nestránkovanou paměť (to platí i pro všechna IRQL větší DISPATCH_LEVEL). Kód s tímto IRQL musí proběhnout velmi rychle, aby nezpomaloval chod operačního systému a zbytečně neblokoval procesor. Tato priorita je nastavena, pokud OS zjistí nějakou závažnou chybu a usoudí, že je nutné zastavit běh systému a vyvolat modrou obrazovku.

Z popisu výše vyplývá, že ovladač by měl vykonávat co nejvíce kódu s IRQL PASSIVE_LEVEL, protože takový kód neblokuje procesor a může volat téměř všechny funkce, které mu operační systém poskytuje.

 

S IRQL se blíže sektáme v příštím díle seriálu, který bude věnován synchronizačním objektům, které nám jádro poskytuje.

Komunikace ovladače s okolním světem

Po trošku suchém teoretickém výkladu o IRQL se dostáváme k věcem, jejichž fungování si ukážeme na praktickém příkladu. Bude jím driver s názvem KMemReader, který umožní obyčejným aplikacím číst obsah paměti jádra. Z tohoto důvodu se v této podkapitolce zaměřím na komunikaci ovladače s normálním procesem běžícím v uživatelském režimu.

Existuje několik mechanismů, které může proces a driver využít k předávání zpráv. Jedním z nich jsou například namapované soubory (v terminologii jádra nazývané "Sections"). Těmi se zatím zabývat nebudeme. Ukážeme si jiný způsob komunikace, který lze považovat za ten nejpoužívanější. Tento mechanismus je založen na objektech zvaných Zařízení (Device). Zařízení si můžeme zjednodušeně představit jako rouru, na jejímž jednom konci je proces a na druhém driver. Aplikace přes objekt zařízení posílá zprávy driveru, který na ně odpovídá. Opačně tento princip nefunguje.

Zařízení se chová velmi podobně jako soubor - pomocí funkce CreateFile si k němu aplikace může zjednat přístup a potom používat další rutiny jako ReadFile, WriteFile, DeviceIoControl či CloseHandle.

Jakmile se volání některé z výše uvedených funkcí dostane do jádra, je vytvořen požadavek (IRP - Interrupt Request Packet), ve kterém jsou zakódovány všechny informace potřebné pro driver. Požadavek je poslán na zadané zařízení, které je kontrolováno driverem. Driver si přečte obsah požadavku, zareaguje a případně pošle zpět data (pokud to typ požadavku umožňuje).

My si ukážeme komunikaci pomocí funkce DeviceIoControl. Tato funkce způsobí vygenerování požadavku typu IRP_MJ_DEVICE_CONTROL, jenž obsahuje tyto položky:

  • Vstupní buffer (InputBuffer) - oblast paměti, kde se nachází vstupní data požadavku.
  • Výstupní buffer (OutputBuffer) - oblast paměti, kam driver zapíše výsledky své činnosti. Obsah tohoto bufferu se doručí odesilateli požadavku
  • Kód zprávy (ControlCode) - umožňuje nejen driveru poznat, co to po něm vlastně aplikace chce, ale také určuje charakter vstupního a výstupního bufferu. Je až s podivem, že kód zprávy je 32bitové celé číslo, ačkoliv obsahuje tolik zásadních informací.

Kód zprávy se většinou definuje pomocí makra CTL_CODE, které pohodlně umožňuje nastavit všechny parametry. Zmínili jsme zatím charaketer vstupního a výstupního bufferu a uživatelsky definovaný kód. Dále je nutné definovat, jakému typu zařízení lze zprávu doručit a jaký přístup musí aplikace k zařízení mít, aby mu zprávu s daným kódem mohla poslat.

Charakter vstupního a výstupního bufferu se charakterizuje pomocí konstant METHOD_XXX, které jsou následující:

  • METHOD_BUFFERED - při převádění volání DeviceIoControl na IRP správce vstupně- výstupních operací (I/O Manager) realokuje vstupní a výstupní buffer do nestránkované paměti. Vstupní a výstupní buffer se budou nacházet na stejné adrese (velikost bloku paměti bude rovna větší z velikostí vstupního a výstupního bufferu). Jakmile driver pořadavek zpracuje, výstupní buffer je překopírován na adresu, kterou předala aplikace při volání DeviceIoControl. Protože driver má zajištěno, že buffery jsou v nestránkované paměti, může s nimi pracovat i při vysokém IRQL, protože nemůže dojít k výpadku stránky. Na druhou stranu tato metoda předávání bufferů vyžaduje alokaci nestránkované paměti, s kterou by se mělo šetřit. A je také pomalejší než ostatní metody. Proto se hodí pro předávání menších objemů dat (předávat touto takto například megabajty vyžaduje velké množství nestránkované paměti, která ani nemusí být v aktuálním okamžiku k dispozici).
  • METHOD_NEITHER - driveru jsou předány přímo adresy bufferů, které aplikace specifikuje při volání DeviceIoControl. Adresy bufferů jsou tedy většinou různé. Pokud plánujete použit METHOD_NEITHER, musíte být velmi opatrní - protože ony pointery vůbec nemusí být validní - mohla vám je předat například nějaká jiná, zákeřná aplikace, která by ráda provedla útok typu DoS (Denial of Service). Proto musíte velmi pečlivě zkontrolovat, zda ze vstupního bufferu lze číst a do výstupního zapisovat! Provádí se to pomocí funkcí ProbeForRead a ProbeForWrite. Pokud neuděláte pořádnou kontrolu, může dojít při manipulaci s daty v bufferech k modré obrazovce, což uživatele opravdu nepotěší.
  • METHOD_IN_DIRECT - správce vstupně-výstupních operací realokuje výstupní buffer do nestránkované paměti. Vstupní buffer zůstane tam, kde ho specifikovala aplikace. Používá se, pokud je nutné předat driveru velké množství dat<./li>
  • METHOD_OUT_DIRECT - správce vstupně-výstupních operací realokuje vstupní buffer do nestránkované paměti. Výstupní buffer zůstane tam, kde ho specifikovala aplikace. Metoda se používá, pokud driver potřebuje předat aplikaci velké množství dat.

Přesnou definici kódu zprávy a další informace najdete v nápovědě k DDK. Více do podrobností zabíhat nebudeme a vše si ukážeme na příkladu:

#define FILE_DEV_DRV 0x2A7B
#define IOCTL_KMR_READ	CTL_CODE (
			FILE_DEV_DRV,
			0x01,
			METHOD_BUFFERED,
			FILE_ALL_ACCESS)

Nejprve si zadefinujeme typ našeho zařízení (FILE_DEV_DRV) a poté definujeme vlastní kód zprávy. Bude použita bufferovaná metoda (oba buffery budou realokovány do nestránkované paměti) a naše zpráva bude mít uživatelský kód 1. Aby ji aplikace mohla poslat, musí získat k zařízení přístupová práva FILE_ALL_ACCESS.

Teď už máme mnoho informací o tom, co musí udělat aplikace, aby poslala zprávu ovladači. Ještě by bylo dobré vědět, co musí zařídit ovladač, aby komunikace mohla proběhnout. Teorie už však bylo dost. Proto si ukážeme příklad opravdu použitelného ovladače, který umožní číst paměť jádra. Vše potřebné bude vysvětleno v komentářích ve zdrojovém kódu. Použijeme definice kódu zprávy, jenž je uvedena výše.

Příklad: KMemReader.sys

KMemReader je jednoduchý driver, který umožňuje aplikacím číst paměť jádra. Přesněji řečeno: nestránkovanou paměť jádra vám přečte zaručeně. Bohužel jsem zatím nepřišel na jednoduchý postup, jak poznat nevalidní adresu od adresy paměti, jenž je uložena ve stránkovacím souboru. V ZIP archivu se nachází ovladač i se zdrojovými kódy, které jsem se snažil pečlivě komentovat, a jednoduchá konzolová aplikace, která přečte 2 byty z adresy specifikované parametrem (adresa musí být specifikována jako hexadecimální číslo "$XXXXXXXX"). Aplikace počítá s tím, že driver je již zaveden do jádra - k tomu můžete využít zdrojové kódy z minulého a tohoto dílu. Aplikaci můžete velmi jednoduše modifikovat, aby načítala více bytů či dělala jiné vylomeniny. Například pokud si necháte načíst 2 byty z počáteční adresy hlavního modulu jádra, uvidíte pravděpodobně signaturu jeho PE hlavičky ("MZ").

Odkaz na stažení: KMemReader.zip

Komentáre

Link nefunguje...

Děkuji za upozornění, chyba byla opravena.

Podporte nás


Páčil sa Vám tento článok? Ak áno, prosím podporte nás ľubovoľnou čiastkou. Ďakujeme!


ITC manažer Security-portal.cz Soom.cz
Hoax.cz Antivirove centrum Crypto-world.info