Odczytywanie atrybutów obszaru pamięci(VirtualQuery)

W pierwotnym założeniu artykuł ten miał stanowic część większego AntiCrack FAQ, które obejmowałoby gros zagadnień dotyczących zabezpieczenia programu przed crackerskim atakiem, niestety brak czasu i dobrych chęci uniemożliwił mi zrealizowanie tego projektu(nie tylko tego zresztą). Stąd też postanowiłem przerobic nieco istniejące już fragmenty pracy tak aby mogły funkcjonować jako oddzielne i samodzielne teksty. Jeden z nich właśnie macie przed sobą i jak sam tytuł wskazuje opisuje on krok po kroku odczytywanie charakterystyki danego regionu swojej pamięci przez program. Jeśli interesuje Was cel tej operacji to zapraszam do dalszej lektury. Opisywana metoda nie wiąże się bezpośrednio z wykrywaniem określonego debuggera, ponieważ odczytując charakterystykę żądanego regionu pamięci stanowi uniwersalny sposób na wykrycie podglądania pracy programu. Na czym się opiera? Otóż specyfika działalności crackera wymusza stałe monitorowanie odczytu/zapisu obszarów pamięci szczególnie ważnych ze względu na ich przydatność w zarejestrowaniu lub zapatchowaniu aplikacji. Są to oczywiście adresy textów z podanymi: nazwą użytkownika, jego numerem seryjnym i ewentualnymi dodatkowymi danymi. W przypadku, gdy proces przeliczania tych informacji jest dość rozrzucony po kodzie programu jedyną rozsądną metodą, pozwalającą na szybkie dojście do celu, jest założenie pułapek typu BPM(BreakPoint on Memory) na ww zmienne i obserwowanie wyłącznie kodu bezpośrednio odnoszącego się do mielenia istotnych dla crackera informacji. Dzięki tym breakpointom Softice zatrzymuje wykonanie programu w chwili, gdy nastąpi odwołanie do podanych fragmentów pamięci. Postawmy więc pytanie - jak debugger wykrywa operacje związane z dostępem do danego regionu pamięci? Sprawa jest prosta - każdy adres wirtualny ma swoje atrybuty, które określają prawo dostępu do niego dla macierzystego procesu i innych aplikacji. Element ten może mieć następujące wartości: PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS, PAGE_NOCACHE Nazwy mówią same za siebie, więc tłumaczenia są zbędne. Nas interesować będzie jedynie PAGE_NOACCESS. Jak się zapewne domyślacie to ten sposób protekcji jest używany przez SI podczas stawiania pułapki na dane programu. W momencie próby odczytu lub zapisu adresu z takim atrybutem wygenerowany zostanie wyjątek, którego obsługa przekazana będzie do debuggera. W ten sposób program zatrzyma się dokładnie na instrukcji wywołującej wyjątek. Uwaga istotne jest, że SI nie odróznia pułapek na odczyt od tych na zapis, atrybut PAGE_NOACCESS jest stosowany bezwzględnie w każdej sytuacji. Wiemy już jak SI ustawia BPMy, czas teraz zabrać się za ich wykrywanie. Wbrew pozorom nie jest to trudne, a przede wszystkim nie ma nic wspólnego z niczym co trzeba byłoby zrobić w Ring 0. Windows ma specjalną rodzinę funkcji zarządzających pamięcią (Memory Management Functions), z których kilka idealnie pasuje do naszych planów. Zainteresowanie budzą VirtualQuery i VirtualQueryEx, które pobierają atrybuty zadanego obszaru danych. Różnica pomiędzy nimi jest jedna - rozszerzona wersja funkcji może odczytywać charakterystykę pamięci innego procesu posiadającego flagę PROCESS_QUERY_INFORMATION co nas raczej nie interesuje, zajmijmy się więc jej zwykłą postacią. Składnia VirtualQuery wygląda tak:

DWORD VirtualQuery( LPCVOID lpAddress, // adres obszaru pamięci
PMEMORY_BASIC_INFORMATION lpBuffer, // adres struktury uzupełnianej zwracanymi danymi
DWORD dwLength // size of buffer//rozmiar tej struktury
);


Z powyższego wynika, że pierwszym parametrem funkcji jest adres zmiennej z danymi użytkownika, następnie odkładany jest offset struktury MEMORY_BASIC_INFORMATION i jej rozmiar. Poniżej umieściłem schemat jej budowy:

typedef struct _MEMORY_BASIC_INFORMATION { // mbi
PVOID BaseAddress; // nasz adres
PVOID AllocationBase; // pierwszy adres bloku pamięci, w którym znajduje się nasza zmienna
DWORD AllocationProtect; // charakterystyka obszaru pamięci ustawiona w momencie alokacji(utworzenia)
DWORD RegionSize; // rozmiar obszaru w bajtach
DWORD State; // status zakresu pamięci pod kątem obsługi RAM
DWORD Protect; // obecna charakterystyka regionu
DWORD Type; // sposób umieszczenia i współuzytkowania danego zakresu pamięci
} MEMORY_BASIC_INFORMATION;


Z opisu wynika, że nas najbardziej będą interesować pola Protect i RegionSize. Wiadomo dlaczego Protect, ale po co RegionSize? Już wyjaśniam na prostym przykładzie. Załóżmy, że nasza zmienna znajduje się pod adresem 402010h i zajmuje 10h bajtów. Cracker może zastawić pułapkę zarówno na całą 10h bajtową wartość(BPR) albo tylko na jej część używająć BPM, BPMW lub BPMD. Nie jest też zobligowany do ustawienia breakpointu na początek zmiennej, bez problemu założy ją na dowolnym fragmencie danych(no, nie do końca, adres musi być wyalignowany do DWORDa). Przyjmijmy, że zrobił to na adresie 402018. Następnie program testuje obszar spod 402010h i otrzymuje informację, że charakterystyka regionu jest OK. Czyli bez sprawdzania jego rozmiaru nie jest w stanie stwierdzić, że pułapka została istotnie zastawiona. Jeśli natomiast sprawdzi rozmiar sprawdzanej pamięci wówczas funkcja zwróci w polu RegionSize wartość 8h ponieważ tylko te 8 bajtów ma taką samą charakterystykę. za nimi, czyli pod 402018h zaczyna się obszar o charakterystyce PAGE_NOACCESS. Jeśli teraz porówna wartość pola z długością zmiennej (10h) okaże się, że coś jest nie tak i działalność crackera zostanie wykryta. Oki zastanówmy się teraz jak to zaimplementować w kodzie programu. Wszystkie niezbędne wywołania są funkcjami API, a więc w zasadzie schemat zabezpieczenia będzie taki sam we wszystkich językach wysokiego poziomu i w assemblerze. Głównym naszym zadaniem jest znalezienie miejsca, z którego wywoływana byłaby funkcja VirtualQuery. Pierwsza myśl jaka przychodzi do głowy to oczywiście wstawienie tej procedury do kawałka kodu wywoływanego w momencie tworzenia dialogu rejestracyjnego. Wówczas to potencjalny użytkownik zaczyna wprowadzać dane licencyjne, a cracker przeprowadza rozpoznanie terenu. Ideałem byłoby zostawienie pętli, gdzie wywoływana byłaby nasza funkcja, niestety z oczywistych względów nie jest to możliwe. Możliwe jest natomiast wywołanie oddzielnego wątku i umieszczenie w nim sekwencji kodu powtarzanej co jakiś czas w celu upewniania się, że adresy zmiennych są wolne od pułapek. Z pewnością wielu programistów Delphi czy BC++ zada mi pytanie co to jest ten wątek. Kuriozalna kwestia, szczególnie, że to właśnie oni mają z multitaskingiem najwięcej do czynienia. Ale bez tych dygresji - mówiąc w dużym skrócie wątek jest fragmentem procesu wykorzystującym część czasu procesora na pracę niezależną od głównego wątku programu(pomijając oczywiście funkcje synchronizacyjne i komunikacyjne, ich zastosowanie jest wiadome). Wątek jest procedurą posiadającą własny stos, własne ustawienia rejestrów i co ważne, a oczym już wspomniałem własny czas procesora. Wątek tworzymy wykorzystując do tego funkcję CreateThread.

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // istotne tylko w WinNT
DWORD dwStackSize, // rozmiar stosu, jeśli NULL to równy rozmiarowi stosu głównego wątku
LPTHREAD_START_ROUTINE lpStartAddress, // adres procedury wątku
LPVOID lpParameter, // parametr dla niej
DWORD dwCreationFlags, // flagi wątku
LPDWORD lpThreadId // wskaźnik na zwracana identyfikator wątku
);
Jednak kolejnym problemem jest częstotliwość wywołania funkcji VirtualQuery, przecież ma być tylko dodatkiem a nie pożeraczem większości cykli CPU. Użyjemy funkcji Sleep aby uciszać wątek na jakiś czas, uzyskamy wtedy zadawalającą wydajność i brak zakłóceń w pracy ważniejszych elementów programu. Jedynym parametrem tej funkcji API jest czas uśpienia podawany w milisekundach. Ostatnim problemem pozostaje zamknięcie wątku, nie stanowi to wielkiego problemu wystarczy użyć funkcji TerminateThread. Oczywiście uprzednio trzeba zachować uchwyt naszego wątku, stworzonego przez CreateThread. Zdaję sobie sprawę, że taki suchy opis niewiele wyjaśni przeciętnemu pogromcy crackerów dlatego do tegóż tekstu dołączam źródełko w MASMie skrajnie uproszczone bez sprawdzania pola RegionSize struktury MEMORY_BASIC_INFORMATION. Tyle na pierwszy raz, jeśli będzie zainteresowanie tekstem to napiszę drugą część poświęconą aktywnemu zwalczaniu tego typu crackerskich zagrywek i dorzucę parę innych bajerów. Ptasiek ptasiek@park.px.pl