User login

SecIT na Facebooku

SecIT.sk na Facebooku

Útok záměnou argumentu, aneb vážná chyba v proaktivní ochraně bezpečnostních aplikací

3
Average: 3 (4 votes)
Your rating: None

Cílem tohoto článku je popsat techniku útoku na moduly proaktivní ochrany a monitorování chování aplikací, které se nachází v mnoha bezpečnostních aplikacích patřících do kategorie osobních firewallů a systémů detekce útoku na počítač (Host Intrusion Detecton System – HIPS). Technika se česky nazývá útok záměnou argumentu a je prakticky použitelná.

Výzkum probíhal v rámci skupiny matousec.com a jeho výsledky naleznete v [1], ze kterého tento článek vychází.

Předchozí výzkumy

Jak jsme zjistili krátce po publikování [1], výzkumů na podobné téma bylo v průběhu let publikováno hned několik. V roce 1996 publikovali Matt Bishop a Michael Dilger dokument [2], který se zabývá útokem záměnou argumentu, který pojmenovali TOCTTOU (time-of-check-to-time-of-use) převážně z teoretického hlediska a zaměřuje se převážně na operační systémy UNIX.

V roce 2003 se na emailové konferenci Bugtraq objevil příspěvek [3] od Andreye Kolishaka zabývající se stejným problémem již z praktičtějšího hlediska. Autor článku se zaměřuje na operační systémy Windows NT, ale nepopisuje problém do všech důsledků.

V roce 2007 prezentovali výzkumníci s přezdívkami twiz a sgrakkyu na konferenci Chaos Communication Congress své závěry ([11]) ohledně zneužití TOCTTOU proti aplikacím modifikujícím kód systémových služeb Windows.

Ve stejném roce na univerzitě v Cambridge vznikl dokument [12], který popisuje zranitelnosti několika frameworků na monitorování systémových volání na operačních systémech Solaris, BSD a UNIX a pokouší se navrhnout několik způsobů, jak tyto zranitelnosti řešit.

Pozadí problému

Již od počátečních dob většina uživatelů Windows pracuje pod účtem s oprávněním administrátora, což je pro software zajišťující bezpečnost proti útokům zvenčí velký problém, protože nelze jednoduše využít bezpečnostního modelu vestavěného do samotného operačního systému. Nad faktem, že to tak úplně není chyba uživatelů, můžeme vést dlouhé debaty, které ale nejsou cílem tohoto článku, a tedy svůj názor na toto téma v něm vyjadřovat nebudu. Každopádně, ať už to je chyba uživatelů, nebo ne, bezpečnostní aplikace si pro ochranu systému a dalších prostředků (souborů, klíčů registru, procesů, vláken) vytvořily vlastní bezpečnostní mechanismy.

Bohužel, na starších verzích Windows (NT/2000/XP) neexistují dokumentované způsoby, jak monitorovat přístup k mnoha druhům objektů, případně jej blokovat. Mezi takové objekty patří procesy, vlákna, synchronizační primitiva či paměťově mapované soubory. A i tam, kde takové mechanismy i ve starších Windows existovaly, je ne všechny bezpečnostní aplikace použily.

Tvůrci bezpečnostního software si tedy pomohli sami – modifikovali kód systémových knihoven či samotného jádra, aby mohli zajistit ochranu důležitých entit. Jak ale ukazuje útok záměnou argumentů, provést tyto modifikace bezchybně vůbec není jednoduché.

Bezpečnostní aplikace například modifikují kód rutin, které se používají pro získání přístupu k různým objektům (souborům, procesům, semaforům...). Taková rutina (nazývejme ji přístupová rutina) obvykle jako jeden z parametrů požaduje jméno či jiný typ identifikace cílového objektu, a pokud má volající dostatečná oprávnění, vrátí mu identifikátor (handle) umožňující voláním dalších podprogramů provádět požadované operace.

Bezpečnostní aplikace často modifikují přístupovou rutinu tak, aby její sémantika odpovídala následujícím krokům:

  1. Zjistit, zda volající nežádá přístup k chráněnému objektu.
  2. Pokud ano, vrátit chybový kód, například ve významu „přístup odepřen.“
  3. Pokud ne, předat řízení původní přístupové rutině a vrátit její výsledek.

Tento scénář je zranitelný vůči útoku záměnou argumentu. Nezáleží příliš na tom, zda je modifikace provedena na rutině ležící v části paměti přístupné z uživatelského režimu, nebo v samotném jádře operačního systému.

V následujících odstavcích si ukážeme útok záměnou argumentu na proaktivní ochranu, která dodržuje výše popsaný scénář a je založená na modifikaci tabulky systémových volání (System Service Descriptor Table). Obcházení modifikací v uživatelském režimu pro nás z hlediska útoku záměnou argumentu není příliš zajímavé, protože existují mnohem jednodušší techniky, jak v takovém případě dosáhnout kýženého cíle. Nejprve je však třeba krátce vysvětlit pár detailů kolem systémových volání.

Systémová volání

Jádro Windows za účelem podpory aplikací běžících v uživatelském režimu definuje sadu několika set funkcí. Pomocí nich mohou běžné programy pracovat se soubory, kreslit na obrazovku a provádět další činnosti, na které jsme zvyklí. Proto jsou tyto rutiny velmi důležité; jejich monitorováním získá bezpečnostní produkt kontrolu nad akcemi obyčejných aplikací. Nazývají se též systémové služby (system services). Jejich adresy jsou uloženy v tabulce systémových volání (System Service Descriptor Table). Mechanismus sloužící k volání těchto služeb z uživatelského režimu nese název systémové volání.

Mnohé bezpečnostní aplikace si „zvykly“ adresy v tabulce systémových volání upravovat, aby odkazovaly na rutiny implementované v jejich ovladačích. Tím efektivně dosáhnou přesměrování systémového volání na svůj kód, který může provést různé bezpečnostní kontroly a případně předat řízení i originální systémové službě. Jinak řečeno, taková implementace ochrany se přesně drží výše popsaného scénáře.

Systémová volání akceptují tři druhy parametrů: ukazatele na bloky paměti procesu přístupné z uživatelského režimu, handle k objektům jádra, GDI a USER a hodnoty s přímým významem – příznaky, bitové masky přístupových práv atd. Třetí typ nelze pro útok záměnou argumentů zneužít. Ukazatele a handle však ano, jak si ukážeme v dalších částech článku.

Provedení útoku záměnou argumentu si ukážeme na systémových službách NtLoadDriver a NtTerminateProcess. Budeme vždy předpokládat, že bezpečnostní aplikace modifikuje tabulku systémových volání tak, aby místo nich byly volány její rutiny (NewNtLoadDriver a NewNtTerminateProcess), které provedou potřebné kontroly a případně volání zablokují.


Proti útoku může být zranitelná implementace ochrany založená na všech systémových službách, které akceptují jako parametry handle nebo ukazatele. NtLoadDriver a NtTerminateProcess slouží pouze jako názorný příklad.

Případ s ukazateli

NtLoadDriver je systémové volání, které aplikace mohu využít pro načtení nového ovladače do jádra. Funkce jako parametr akceptuje název klíče s informacemi o cílovém ovladači. Klíč se musí nacházet v HKLM\SYSTEM\CurrentControlSet\Services. Pokud si tedy aplikace přeje načíst nějaký ovladač do jádra, zapíše informace o něm například pod klíč Ovladac do příslušného umístění (formát je stejný jako u obyčejných služeb) a zavolá NtLoadDriver s parametrem „\Registry\Machine\SYSTEM\CurrentControlSet\Services\Ovladac“. Pro úspěch operace je nutné oprávnění SeLoadDriverPrivilege, kterým však administrátoři standardně disponují.

Některé bezpečnostní aplikace hlídají volání NtLoadDriver, aby mohly zablokovat případné pokusy proniknout do jádra. Změní adresu v tabulce systémových volání na svou vlastní rutinu, která provede potřebné bezpečnostní kontroly a případně předá řízení kódu originální systémové služby, která zajistí načtení ovladače. Příklad takové rutiny ukazuje výpis 1.

Výpis 1: Kontrolní rutina pro systémové volání NtLoadDriver

Rutina provádí bezpečnostní kontroly pouze v případě, že volání pochází z uživatelského režimu, kdy volání funkce KeGetPreviousMode (řádek 4) vrátí hodnotu UserMode. Následně je zkontrolována platnost argumentu pomocí funkce ProbeForRead a v případě úspěchu je zkopírován do paměti jádra (řádky 6–21). Následně dojde k extrahování názvu klíče ovladače a jeho porovnání s řetězcem „Ovladac“ – načtení ovladačů s názvem Ovladac totiž tato bezpečnostní rutina blokuje.

Blok try-except je potřebný zejména kvůli ověřování platnosti argumentu pomocí volání ProbeForRead a následnému kopírování do paměti jádra. Výjimka může vzniknout při obou těchto operacích, pokouší-li se volající aplikace o zákeřné manipulace s argumentem, nebo o předání neplatné hodnoty. Před několika lety tyto kontroly platnosti parametry rozhodně nebyly samozřejmostí, jak se můžete dočíst v [4].

Z hlediska útoku záměnou argumentu je velmi důležitý řádek 55, který volá původní systémovou službu. Na tento řádek procesor dospěje pouze tehdy, je-li načtení ovladače povoleno bezpečnostní aplikací. Originálnímu kódu NtLoadDriver je předána původní hodnota argumentu, tedy ukazatel do paměti přístupné z uživatelského režimu. Důvodem je odlišné chování systémových služeb při volání z uživatelského režimu a z režimu jádra – při volání jádrem se neprovádí například kontrola, zda má uživatel pro danou operaci dostatečná oprávnění.

Pokud chce aplikace obejít bezpečnostní kontroly, musí zajistit, aby nová implementace NtLoadDriver až do řádku 55 předpokládala, že účelem volání je načíst neškodný ovladač, a následně jeho jméno změnit na takové, které bezpečnostní rutina blokuje. Jedná se o změnu hodnoty argumentu v pravý čas a na správném místě, proto byla tato technika nazvána útok záměnou argumentu.

Scénář útoku může vypadat například takto:

  1. Vytvoří se vlákna A a B.
  2. Vlákno A zavolá NtLoadDriver s názvem ovladače, který úspěšně projde bezpečnostní kontrolou, například „HKLM\SYSTEM\CurrentControlSet\services\kmixer“.
  3. Systémové volání provedené vláknem A se dostane na řádek 55 výpisu 1.
  4. Vlákno B změní obsah řetězce na „\Registry\Machine\SYSTEM\CurrentControlSet\services\ovladac“.
  5. Vlákno A dokončí načtení ovladače voláním originálního kódu systémové služby.

Úspěch útoku na jednoprocesorových strojích velmi závisí na plánovači vláken, protože je nutné těsně před krokem 4 změnit kontext na vlákno B. Jelikož je samotné provedení jednoho pokusu velmi rychlé (kroky 25 vyžadují pouze jedno systémové volání, a to ještě nedokončené), může být opakován mnohokrát za sekundu, čímž se pravděpodobnost tohoto magického přeplánování zvyšuje.

Experimenty provedené na víceprocesorových strojích dokazují, že při správném nastavení priorit obou vláken se úspěch ve většině případů dostaví nejvýše po několika málo iteracích. Na takových konfiguracích totiž není nutné žádné přeplánování, protože každé vlákno může běžet na svém vlastním procesoru.

Zápletka s handle

Většina bezpečnostních aplikací obsahuje i ochranný modul, který malware znemožňuje jejich snadné vypnutí a tím pádem převzetí kontroly nad celým počítačem. Takový modul zabraňuje ukončení chráněných procesů a vláken, případně blokuje přístup k souborům a klíčům registru, které bezpečnostní aplikaci náleží.

Takovou funkcionalitu lze implementovat i pomocí modifikace tabulky systémových volání. Na výpisu 2 vidíte rutinu NewNtTerminateProcess, která je díky změně v tabulce volána místo systémové služby NtTerminateProcess a blokuje každý pokus o násilné ukončení procesu, jehož PID obsahuje proměnná protected_process_id.

Výpis 2: Kontrolní rutina pro systémové volání NtTerminateProcess

Stejně jako u rutiny NewNtLoadDriver, i NewNtTerminateProcess kontroluje pouze volání pocházející od programů běžících v uživatelském režimu, nikoliv z jádra systému. Na rozdíl od předchozího případu však není nutné použít try-except blok ani volání procedury ProbeForRead – nepracuje se s pamětí přístupnou z uživatelského režimu. Kontrolu pravosti handle zajistí funkce ZwQueryInformationProcess, která v případě úspěchu získá PID cílového procesu, jež porovná s identifikátorem chráněné aplikace uloženém v proměnné protected_process_id. Rovnost těchto hodnot implikuje pokus o ukončení chráněné aplikace, který musí být samozřejmě zablokován.

Pokud se hodnoty nerovnají, nejedná se o snahu narušit běh bezpečnostní aplikace a rutina NewNtTerminateProcess předá řízení originální systémové službě.

Handle sice neodkazují na paměť přístupnou z uživatelského režimu, ale i tak nejsou proti útoku záměnou argumentu odolná. Můžeme je chápat jako ukazatele na instance objektů. A tento nepřímý ukazatel je možné v nestřeženém okamžiku „přesměrovat.“ Scénář je podobný scénáři pro načítání zakázaných ovladačů:

  1. Vytvoříme vlákna A a B.
  2. Vlákno A získá handle k našemu procesu, ale bez oprávnění PROCESS_TERMINATE. Toto opatření je nutné z toho důvodu, že případný neúspěch, který nelze nikdy vyloučit, by vedl k ukončení našeho procesu.
  3. Vlákno A zavolá NtTerminateProcess, který je přesměrován na NewNtTerminateProcess.
  4. NewNtTerminateProcess provede kontrolu, která skončí povolením a pomyslné ukazovátko aktuálně vykonávaného kódu se dostane na řádek 17.
  5. Vlákno B zruší handle procesu použité vláknem A a pokusí se získat nové handle k chráněnému procesu, které ale bude mít s vysokou pravděpodobností stejnou hodnotu, Protože naše demonstrační bezpečnostní aplikace chrání pouze násilné ukončení, tato operace se podaří.
  6. Vlákno A vykoná originální kód systémové služby NtTerminateProcess. Předávaná hodnota handle však má jiný význam než při průchodu bezpečnostní kontrolou – odkazuje na chráněný proces, který bude tedy násilně ukončen navzdory ochraně.

Zajímavé důsledky

Myslím, že útok záměnou argumentu byl dostatečně zdokumentován, tudíž nastal čas na vyvozování důsledků.

Předně bych rád zopakoval, že proti útoku záměnou argumentu je zranitelná implementace proaktivní ochrany, nikoliv antivirové technologie jako detekce na základě signatur či heuristické analýzy. Popsaná zranitelnost tedy (v případě komplexních bezpečnostních balíků) neotvírá malware dveře do systému přímo, ale její zneužití může být pouze článkem v celém řetězu operací, které dohromady vytvoří reálný útok. Scénář by mohl vypadat například takto:

  1. Uživatel otevře nevinně vypadající PDF dokument, který je ale infikovaný, jak je dnes relativně běžné.
  2. PDF dokument zneužije chyby či vlastnosti (viz například [5] a [6]) prohlížeče a spustí na cílovém počítači vlastní kód.
  3. Tento kód může určit, který bezpečnostní produkt je na počítači nainstalován a provést na něho útok záměnou argumentů.
  4. Jakmile je bezpečnostní produkt deaktivován, škodlivý kód může do počítače bez větších problémů stáhnout další havěť.

Tento odstavec je také reakcí na některá tvrzení uvedená na [7], [8] a [9].

Všechny ukázky útoku záměnou argumentu uvedené v tomto článku předpokládaly implementaci ochrany na základě modifikací tabulky systémových volání. Útok však nevyužívá žádné specifické vlastnosti této datové struktury, tudíž jej lze aplikovat i na jiných místech.

Navíc, útok v obecném případě ani nevyžaduje administrátorská oprávnění, tudíž může být proveden i z běžného uživatelského účtu. To neznamená, že by byl primárně ohrožen bezpečnostní model Windows, ale i v ovladačích jako win32k.sys byly nalezeny chyby, které umožňují využít útok záměnou argumentu k provedení akce, na kterou obyčejný uživatel nemá dostatečná oprávnění. Více se dočtete v [10].

Dokument [3] se též zabývá útokem záměnou argumentu, ale zkoumá jeho praktické uplatnění pouze na jednoprocesorových počítačích, které byly v době provádění výzkumu mezi uživateli velmi rozšířené. Pro úspěšný útok je potřeba přepnutí kontextu vlákna na správném místě, což je nepravděpodobné. Kvůli tomuto faktu jej autor pokládá za nepraktický. Jak ukazují provedené testy, už na dvouprocesorových strojích lze úspěchu dosáhnout během několika málo pokusů. Žádné magické přeplánování totiž není potřeba, protože obě útočící vlákna běží simultánně.

Ani dvojúrovňová ochrana nemusí být tím správným řešením. Uvažme bezpečnostní aplikaci, která blokuje neautorizovaný přístup ke svým procesům pomocí přesměrování systémových volání NtOpenProcess a NtTerminateProcess. Pro násilné ukončení chráněného procesu je tedy nutné obejít obě úrovně ochrany. Jak ukazuje následující scénář, i takovou ochranu lze prolomit pomocí útoku záměnou argumentu, tentokrát v takzvané kombinované variantě:

  1. Vytvoříme vlákna A, B a C.
  2. Vlákno A získá handle bez oprávnění PROCESS_TERMINATE k našemu procesu.
  3. Vlákno A se pomocí tohoto handle pokusí proces násilně ukončit. Pomyslný ukazatel aktuálně vykonávaného kódu dospěje na řádek 17 výpisu 2.
  4. Vlákno B zruší handle použité při volání NtTerminateProcess a pokusí se získat nové handle (opět bez oprávnění PROCESS_TERMINATE) k našemu procesu pomocí systémové služby NtOpenProcess, která je také přesměrována.
  5. Bezpečnostní aplikace provede kontroly a povolí získání nového handle k našemu procesu. Ukazatel aktuálně vykonávaného kódu se právě nachází na volání originální rutiny NtOpenProcess.
  6. Vlákno C změní PID cílového procesu ve struktuře CLIENT_ID, jejíž adresu předávalo vlákno B do volání NtOpenProcess na PID chráněné aplikace.
  7. Vlákno B dokončí volání NtOpenProcess vykonáním originálního kódu systémové služby, čímž získá handle na chráněné aplikace.
  8. Nově získané handle dostane stejnou hodnotu jako handle zrušené v kroku 4.
  9. Vlákno A dokončí volání NtTerminateProcess, čímž násilně ukončí chráněný proces.

Ač se to zdá nepravděpodobné, i takto složitý útok může relativně často uspět v krátkém čase.

Součástí výzkumu bylo i testování řady známých bezpečnostních produktů. Výsledky najdete v tabulce v [1].

Použité zdroje

[7] ESET blog
[11] Robert N. M. Watson: Exploiting Concurrency Vulnerabilities in System Call Wrappers

Sledujte aktuality zo sveta IT bezpečnosti!

Links

SecIT.sk fórum

IRC kanál: #secit
IRC server: irc.secit.sk
Porty (SSL): 6696, 9998
Porty (bez SSL): 6667, 6670

Freeweb by WebSupport.sk

© SecIT.sk - info(at)secit.sk - Všetky práva vyhradené. Žiaden obsah umiestnený na našom portáli a fóre sa nesmie kopírovať!