AUTOR: Tajiri. Analiza metod ochrony oprogramowania przed łamaniem zabezpieczeń.



Podobne dokumenty
Modułowy programowalny przekaźnik czasowy firmy Aniro.

Programowanie Niskopoziomowe

Działanie systemu operacyjnego

Działanie systemu operacyjnego

Algorytm. a programowanie -

1.1 Definicja procesu

Metody wykrywania debuggerów

UNIX: architektura i implementacja mechanizmów bezpieczeństwa. Wojciech A. Koszek dunstan@freebsd.czest.pl Krajowy Fundusz na Rzecz Dzieci

PROBLEMY TECHNICZNE. Co zrobić, gdy natrafię na problemy związane z użytkowaniem programu DYSONANS

Instrukcja do laboratorium Systemów Operacyjnych. (semestr drugi)

Mikroprocesor Operacje wejścia / wyjścia

Programowanie Strukturalne i Obiektowe Słownik podstawowych pojęć 1 z 5 Opracował Jan T. Biernat

Spis treści. 1 Moduł RFID (APA) 3

Trojan bankowy Emotet w wersji DGA

dr inż. Jarosław Forenc

INSTRUKCJA INSTALACJI OPROGRAMOWANIA MICROSOFT LYNC 2010 ATTENDEE ORAZ KORZYTANIA Z WYKŁADÓW SYNCHRONICZNYCH

Rysunek 1: Okno z lista

Działanie systemu operacyjnego

Diagnostyka pamięci RAM

Włączanie/wyłączanie paska menu

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Mikroinformatyka. Wielozadaniowość

SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE

Programowanie w Javie

Jak używać funkcji prostego udostępniania plików do udostępniania plików w systemie Windows XP

Struktury systemów operacyjnych Usługi, funkcje, programy. mgr inż. Krzysztof Szałajko

Reverse Engineerging. Dawid Zarzycki

16MB - 2GB 2MB - 128MB

4. Procesy pojęcia podstawowe

Programowanie na poziomie sprzętu. Programowanie w Windows API

Ustawienia personalne

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Prezentacja systemu RTLinux

1. Pamięć wirtualna. 2. Optymalizacja pliku pamięci wirtualnej

Na chwilę obecną biblioteka ElzabObsluga.dll współpracuje tylko ze sprawdzarkami RSowymi.

Zadanie polega na stworzeniu bazy danych w pamięci zapewniającej efektywny dostęp do danych baza osób.

Obiektowy PHP. Czym jest obiekt? Definicja klasy. Składowe klasy pola i metody

1 Moduł Inteligentnego Głośnika

Instrukcja użytkownika ARSoft-WZ1

Rozdział 5. Administracja kontami użytkowników

Wprowadzenie do projektu QualitySpy

SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE

1 Moduł Inteligentnego Głośnika 3

Wykład 4. Tablice. Pliki

Układy VLSI Bramki 1.0

Co nowego w systemie Kancelaris 3.31 STD/3.41 PLUS

Działanie systemu operacyjnego

Pracownia internetowa w każdej szkole (edycja Jesień 2007)

PROGRAMY REZYDENTNE Terminate and State Resident, TSR

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Projekt Hurtownia, realizacja rejestracji dostaw produktów

Programowanie na poziomie sprzętu. Tryb chroniony cz. 1

Budowa systemów komputerowych

Dodatek B. Zasady komunikacji z otoczeniem w typowych systemach komputerowych

Windows 10 - Jak uruchomić system w trybie

Tytuły Wykonawcze. Opis systemu tworzenia dokumentacji TW-1

Instrukcja obsługi Multiconverter 2.0

Narzędzie konfiguracji rozruchu

Zbigniew Sołtys - Komputerowa Analiza Obrazu Mikroskopowego 2015 część 13

1 Moduł Modbus ASCII/RTU 3

Zasady programowania Dokumentacja

Systemy operacyjne i sieci komputerowe. 1 SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE. Etapy uruchamiania systemu

Laboratorium Komputerowe Systemy Pomiarowe

Struktura i działanie jednostki centralnej

Przewodnik użytkownika (instrukcja) AutoMagicTest

S P I S T R E Ś C I. Instrukcja obsługi

Ćwiczenie Nr 6 Przegląd pozostałych najważniejszych mechanizmów systemu operacyjnego Windows

Dodawanie stron do zakładek

SERWER AKTUALIZACJI UpServ

Kontrola topto. 1. Informacje ogólne. 2. Wymagania sprzętowe i programowe aplikacji. 3. Przykładowa instalacja topto. 4. Komunikacja.

Instrukcja integratora - obsługa dużych plików w epuap2

Analiza i projektowanie oprogramowania. Analiza i projektowanie oprogramowania 1/32

SYSTEMY OPERACYJNE: STRUKTURY I FUNKCJE (opracowano na podstawie skryptu PP: Królikowski Z., Sajkowski M. 1992: Użytkowanie systemu operacyjnego UNIX)

SecureFile. Podręcznik użytkownika

Komputery przemysłowe i systemy wbudowane

Roger Access Control System. Aplikacja RCP Point. Wersja oprogramowania : 1.0.x Wersja dokumentu: Rev. C

Analiza malware Remote Administration Tool (RAT) DarkComet

Adam Kotynia, Łukasz Kowalczyk

9. System wykrywania i blokowania włamań ASQ (IPS)

Temat: Administracja kontami użytkowników

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

Podstawy programowania. Wykład: 9. Łańcuchy znaków. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Użytkowanie Web Catalog

Makra programu Microsoft Access.

Antywirusy. Marcin Talarczyk. 2 czerwca Marcin Talarczyk Antywirusy 2 czerwca / 36

1 Moduł Modbus ASCII/RTU

MOŻLIWOŚCI PROGRAMOWE MIKROPROCESORÓW

Lab 9 Podstawy Programowania

2014 Electronics For Imaging. Informacje zawarte w niniejszej publikacji podlegają postanowieniom opisanym w dokumencie Uwagi prawne dotyczącym tego

Konfiguracja parametrów pozycjonowania GPS /5

Archiwum DG 2016 PL-SOFT

Na komputerach z systemem Windows XP zdarzenia są rejestrowane w trzech następujących dziennikach: Dziennik aplikacji

s FAQ: /PL Data: 29/08/2014

10.2. Udostępnianie zasobów

Opis konfiguracji ST do współpracy z kolektorem DENSO BHT 8000

Ćw. I. Środowisko sieciowe, połączenie internetowe, opcje internetowe

Wątek - definicja. Wykorzystanie kilku rdzeni procesora jednocześnie Zrównoleglenie obliczeń Jednoczesna obsługa ekranu i procesu obliczeniowego

Wirusy w systemie Linux. Konrad Olczak

Instrukcja pierwszego logowania do Serwisu BRe Brokers!

Transkrypt:

Analiza metod ochrony oprogramowania przed łamaniem zabezpieczeń. AUTOR: Tajiri (2013r.) Ostrzegam, że jest to wersja składana z dwóch moich prac, dlatego posiada błędy w numeracji bibliografii i rysunków, jakieś fragmenty mogą nie pasować stylistycznie do całości i takie tam ;) Kontakt: http://4programmers.net/profile/57542

Spis treści_ Słowniczek pojęć... 4 1. Metody antydebuggingowe... 6 1.1. Generyczne techniki wykrywania debuggerów... 6 Funkcja IsDebuggerPresent... 6 Odczytanie pola BeginDebugged ze struktury PEB procesu... 6 Odczytanie pola NtGlobalFlag ze struktury PEB procesu... 7 Odczytanie pola HeapFlag ze struktury PEB.ProcessHeap procesu... 8 Odczytanie pola ForceFlag ze struktury PEB.ProcessHeap procesu... 8 Funkcja OutputDebugString system Windows XP... 9 Funkcja CheckRemoteDebuggerPresent... 10 Funkcja NtQueryInformationProcess... 10 Obsługa przerwań INT 3, INT 2D oraz instrukcji $F1... 11 Błędne zwalnianie zasobów... 12 Wyjątek przy zamykaniu niepoprawnego uchwytu... 12 Token SeDebugPrivilege... 13 Funkcja NtQueryObject... 14 Obsługa prefiksów... 15 Funkcja FindWindow... 17 Przerwania int 3(2B)... 18 RaiseException... 18 BlockInput... 19 DbgBreakPoint... 20 Ldr_module... 20 NtSetInformationThread... 21 RtlQueryProcessDebugInformation... 22 RtlQueryProcessHeapInformation... 23 CreateToolhelp32Snapshot-p... 24 NtQueryInformationProcess-p... 25 NtQuerySystemInformation-p... 26 Self-debugging... 29 1.2. Wykrywanie śledzenia wykonywanego kodu... 31 Znaczniki czasu... 32 Push ss/pop ss... 34 2

Flaga Trap... 35 Selektor segmentu GS... 36 1.3. Wykrywanie punktów wstrzymania... 38 Programowe punkty wstrzymania... 38 Punkty wstrzymania na dostęp do pamięci... 40 Sprzętowe punkty wstrzymania... 41 1.4. Wykrywanie dodatków do debuggerów... 43 2. Metody ochrony kodu programu... 50 Erase DOS/PE Header... 51 Size of image... 52 Junk code... 53 Pasywne SMC... 54 Aktywne SMC... 55 Nanomity... 56 Stolen bytes... 59 Wirtualizacja kodu... 60 3. Testy... 62 Dostępność w systemach... 62 Skuteczność metod antydebuggingowych... 66 Skuteczność wykrywania dodatków... 70 Literatura... 75 3

Słowniczek pojęć cracking - szereg działań prowadzących do usunięcia bądź ominięcia zabezpieczenia. cracker - specjalista od zabezpieczeń, który zajmuje się crackingiem. inżynieria wsteczna/odwrotna (ang. reverse engineering) - proces, który ma na celu poznanie mechanizmów oraz sposobu działania skompilowanych aplikacji. analiza behawioralna - jedna z metod wykorzystywana w inżynierii odwrotnej, w której analizowana jest uruchomiona aplikacja. analiza statyczna - jedna z metod wykorzystywana w inżynierii odwrotnej, w której analizowany jest plik wykonywalny znajdujący się na dysku. debugger - program umożliwiający przeprowadzenie analizy behawioralnej, udostępniający szereg mechanizmów ułatwiających inżynierię wsteczną. disassembler - program umożliwiający przeprowadzenie analizy statycznej, poprzez zamianę kodu maszynowego na kod assemblera. deassemblacja - proces zamiany kodu maszynowego na kod assemblera. metody antydebugginowe - zbiór technik pozwalających na ochronę aplikacji przed zastosowaniem debuggera. metody anty-antydebuginggowe - zbiór metod wykorzystywanych przez debuggery, pozwalających na unieszkodliwienie metod antydebugginowych. zaciemnianie kodu - szereg technik mających na celu utrudnienie analizy statycznej. słabo udokumentowana funkcja/struktura - funkcja/struktura, której częściowy opis udostępniony jest przez Microsoft. nieudokumentowana funkcja/struktura - funkcja/struktura, której opis nie został udostępniony przez Microsoft. pliki PE (ang. Portable Executable) format plików wykonywalnych używany w systemie Windows. nagłówek DOS - stary nagłówek plików wykonywalnych przechowujący podstawowe informacje potrzebne do uruchomienia pliku, zachowany w nowszych systemach w celu zapewnienia kompatybilności wstecznej. nagłówek PE - nowy nagłówek plików wykonywalnych przechowujący wiele informacji potrzebnych do uruchomienia pliku m.in. rozmiar kodu oraz adres pierwszej instrukcji kodu, adres tablicy IAT, adres tablicy EAT, dane dotyczące sekcji pliku. IAT (ang. Import Address Table) - tablica znajdująca się w aplikacji, w której zawarte są adresy importowanych funkcji. 4

EAT (ang. Export Address Table) - tablica znajdująca się bibliotece DLL (rzadziej w pliku EXE), w której udostępnione są nazwy oraz adresy eksportowanych funkcji. struktura PEB - słabo udokumentowana struktura przechowująca wiele informacji o uruchomionym procesie. przerwanie (ang. interrupt) - sygnał powodujący zmianę przepływu sterowania. punkt wstrzymania (ang. breakpoint)- przerwanie umożliwiające zatrzymanie wykonywania kodu programu w miejscu (adresie) postawienia takiego punktu. WinAPI - zestaw standardowych bibliotek programistycznych udostępnionych przez Microsoft, na bazie których tworzone jest oprogramowanie. ring poziom uprzywilejowania określający dostęp do danych oraz interfejsów udostępnianych przez system operacyjny. ring 0 - tryb jądra - pierścień posiadający największy poziom uprzywilejowania w systemie (wykorzystywany przez sterowniki). ring 3 - tryb użytkownika - pierścień posiadający ograniczone uprawnienia (wykorzystywany przez wszystkie pliki EXE oraz biblioteki DLL) oryginalny kod/funkcja/program - kod/funkcja/program, który jest poddawany procesowi dodawania zabezpieczeń. debugger chroniący - debugger wykorzystywany do ochrony aplikacji przed innymi debuggerami (więcej informacji znajduje się w rozdziale poświęconym metodzie self-debuggingu oraz nanomites). PID (ang. Process Identifier) - unikalny identyfikator procesu w systemie. praca krokowa (ang. single step)- jeden z trybów pracy debuggera, który pozwala na wykonywanie kodu programu linijka po linijce. Windows z serii NT - seria początkowo przeznaczona dla klientów korporacyjnych, jednak od systemu Windows XP została połączona z systemami dla odbiorców indywidualnych. COM (ang. Component Object Model) standard definiowania oraz tworzenia interfejsów programistycznych na poziomie binarnym, w oderwaniu od konkretnego narzędzia projektowego czy języka programowania. Rootkit narzędzie służące do ukrywania niebezpiecznych plików/programów w systemie operacyjnym. 5

1. Metody antydebuggingowe 1.1. Generyczne techniki wykrywania debuggerów Pierwsza grupa metod antydebuggingowych pozwala na wykrycie obecności każdego debuggera, który próbuje dokonać analizy behawioralnej programu. Lista wszystkich zaimplementowanych technik jest widoczna na poniższym zrzucie ekranu. Funkcja IsDebuggerPresent System Windows w udostępnionym interfejsie API oferuje funkcję IsDebuggerPresent, która ma za zadanie zwrócić wartość 1 lub 0 w zależności od tego czy program jest debuggowany czy nie. Rysunek 4 Prezentacja użycia funkcji IsDebuggerPresent [źródło własne] Odczytanie pola BeginDebugged ze struktury PEB procesu Zamiast korzystać z gotowej funkcji API - IsDebuggerPresent można odczytać odpowiednią wartość bezpośrednio ze struktury PEB (ang. process environment block), która przechowuje wiele wewnętrznych informacji o uruchomionym procesie. Jedno z pól w tej strukturze zawiera informacje dotyczące debuggowania programu. 6

Rysunek 5 Prezentacja odczytu pola BeginDebugged ze struktury PEB [źródło własne] Odczytanie pola NtGlobalFlag ze struktury PEB procesu Jako że na początku struktura ta była słabo udokumentowana w wyniku przeprowadzonej analizy odkryto kolejne pola, które można z powodzeniem użyć do wykrywania obecności debuggerów. Jednym z takich pól jest NtGlobalFlag. Jeśli program został uruchomiony w normalny sposób (bez debuggera) to pole to powinno zawierać wartość 0. W przeciwnym wypadku znajduje się tam suma trzech flag: FLG_HEAP_ENABLE_TAIL_CHECK = $10 FLG_HEAP_ENABLE_FREE_CHECK = $20 FLG_HEAP_VALIDATE_PARAMETERS = $40 Rysunek 6 Prezentacja odczytu pola NtGlobaFlag ze struktury PEB [źródło własne] 7

W systemie 64 bitowym pole NtGlobaFlag znajduje się pod adresem [eax+$bch]. Odczytanie pola HeapFlag ze struktury PEB.ProcessHeap procesu Kolejnym polem wykorzystywanym do detekcji debuggerów jest pole HeapFlag znajdujące się w podstrukturze ProcessHeap struktury PEB. Zawiera ono informacje dotyczące stery uruchomionego programu i podczas normalnego działania przechowuje tylko jedną flagę (HEAP_GROWABLE = $2). Natomiast jeśli program znajduje się pod kontrolą debuggera to najczęściej dodawane są dwie dodatkowe flagi: HEAP_TAIL_CHECKING_ENABLED = $20 oraz HEAP_FREE_CHECKING_ENABLED = $40. Rysunek 7 Prezentacja odczytu pola HeapFlag ze struktury PEB [źródło własne] W systemie Windows XP pole HeapFlag znajduje się pod adresem [eax+$0ch]. Odczytanie pola ForceFlag ze struktury PEB.ProcessHeap procesu Ostatnie z pól, które może zostać użyte do wykrycia debuggera również znajduje się w podstrukturze ProcessHeap i zawiera informacje o stercie procesu. Domyślnie 8

przyjmuje ono wartość zero, jednak jeśli aplikacja została uruchomiona w trybie debuggowania to pole ForceFlag zawiera liczbę różną od zera. Rysunek 8 Prezentacja odczytu pola ForceFlag ze struktury PEB [źródło własne] W systemie Windows XP pole ForceFlag znajduje się pod adresem [eax+$10h]. Funkcja OutputDebugString system Windows XP Procedura OutputDebugString pozwala na wyświetlenie zadanego komunikatu w oknie debuggera. Jeśli wykona się ona poprawnie to funkcja GetLastError zwróci wartość zero, co oznacza że aplikacja jest debuggowana. W przeciwnym razie GetLastError zwróci numer błędu. Niestety od systemu Windows Vista niezależnie od tego czy program jest debuggowany czy nie, procedura OutputDebugString wykona się poprawnie i błąd nie zostanie zwrócony. Rysunek 9 Prezentacja użycia funkcji OutputDebugString [źródło własne] 9

Funkcja CheckRemoteDebuggerPresent Podobnie jak w przypadku metody 1 interfejs API systemu Windows udostępnia funkcję do sprawdzenia czy program jest podłączony do zdalnego debuggera. W tym przypadku słowo zdalny oznacza inny proces, który nie musi działać na innym komputerze. Aby otrzymać wynik wystarczy podać uchwyt do interesującego nas procesu, przy czym wartość $FFFFFFFF zarezerwowana jest dla programu wywołującego funkcję. Rysunek 10 Prezentacja użycia funkcji CheckRemoteDebuggerPresent [źródło własne] Funkcja NtQueryInformationProcess Kolejną funkcją systemu Windows, którą można wykorzystać do wykrywania debuggerów jest NtQueryInformationProcess. Funkcja ta zwraca bardzo dużą ilość informacji o zadanym procesie, używając do tego wywołań jądra systemu. Pierwszym parametrem tej funkcji jest uchwyt do badanego procesu, następnie podawany jest numer żądanej informacji, kolejne wartości to zwracany wynik, spodziewany rozmiar danych wyjściowych oraz ilość zwróconych danych. Poprzednia z opisanych metod w rzeczywistości wywołuje tą funkcję z parametrem $07, który oznacza ProcessDebugPort. Inne wartości służące do wykrycia debuggera to ProcessDebugObjectHandle = $1E oraz ProcessDebugFlags = $1F. 10

Rysunek 11 Prezentacja użycia funkcji NtQueryInformationProcess [źródło własne] Obsługa przerwań INT 3, INT 2D oraz instrukcji $F1 Każdy debugger w trybie użytkownika wykorzystuje instrukcję $CC, co odpowiada przerwaniu INT 3, do ustawiania punktów wstrzymania programu (ang. breakpoints). Jeżeli program natrafi na tą komendę to zostanie wywołany wyjątek. W przypadku gdy dana aplikacja znajduję się pod kontrolą debuggera wyjątek ten zostanie obsłużony i program będzie kontynuował swoje działanie. Podobne działanie ma przerwanie INT 2D oraz instrukcja $F1. Rysunek 12 Prezentacja obsługi instrukcji $CC [źródło własne] 11

Błędne zwalnianie zasobów Kolejna metoda antydebuggingowa opiera się na błędnym działaniu mechanizmu zwalniania zasobów przez debuggowany program. Jeżeli zostanie załadowany dowolny istniejący plik funkcją LoadLibrary, a następnie zasób ten zostanie zwolniony, na przykład za pomocą funkcji FreeLibrary, to przy próbie otwarcia tego pliku w trybie edycji zostanie wywołany błąd. Aby otworzyć plik można użyć dwóch funkcji: BeginUpdateResource, która zwróci błąd ERROR_OPEN_FAILED CreateFile, która zwróci błąd ERROR_SHARING_VIOLATION Rysunek 13 Prezentacja wykorzystania błędnego zwalniania zasobów do detekcji debuggera [źródło własne] Wyjątek przy zamykaniu niepoprawnego uchwytu Ta metoda pozwala na sprawdzenie czy program jest debuggowany poprzez wywołanie funkcji CloseHandle z błędnym parametrem. Jeśli program jest kontrolowany przez debugger zostanie wywołany wyjątek. W przeciwnym wypadku funkcja CloseHandle zwróci kod błędu i program będzie kontynuował swoje działanie. 12

Rysunek 14 Prezentacja użycia funkcji CloseHandle z niepoprawnym parametrem [źródło własne] Token SeDebugPrivilege Kiedy program jest uruchamiany pod kontrolą debuggera, bądź gdy debugger jest podłączany do już uruchomionego procesu, aplikacji przydzielany jest token SeDebugPrivilege. Token ten nie jest przydzielany w żadnym innym przypadku, dlatego może posłużyć do wykrycia debuggera. Aplikacja posiadająca token SeDebugPrivilege może uzyskać uchwyt dowolnego procesu w systemie. Natomiast podczas normalnego wykonywania programu próba otworzenia procesu systemowego np. csrss.exe ze wszystkimi prawami dostępu zakończy się zwróceniem wartości 0. 13

Rysunek 15 Wykorzystanie tokena SeDebugPrivilege do detekcji debuggera [źródło własne] Funkcja NtQueryObject Wszystkie do tej pory opisywane metody potrafiły wykryć czy dany program znajduje się pod kontrolą debuggera. Ta technika daje większe możliwości detekcji, ponieważ pozwala określić czy jakiś debugger jest uruchomiony w systemie. Opisywany sposób polega na wyszukaniu obiektu DebugObject znajdującego się w jądrze systemu, a następnie sprawdzeniu pola ObjectCount. Jeżeli zawartość tego pola jest większa od zera to debugger jest uruchomiony. Aby tego dokonać należy wywołać funkcję NtQueryObject z parametrem ObjectAllTypeInforamtion, która zwróci wskaźnik na pierwszy obiekt oraz liczbę wszystkich elementów. 14

Rysunek 16 Przykład użycia funkcji NtQueryObject do wykrycia debuggera uruchomionego w systemie [źródło własne] Obsługa prefiksów Procesor udostępnia instrukcję (REP - $F364), która pozwala na kilkukrotne wywołanie kolejnej instrukcji. Część debuggerów nie potrafi poprawnie wykonać 15

rozkazu powtórzenia i zamiast tego omija następną komendę. To specyficzne zachowanie można wykorzystać do wykrycia debuggera. Jeżeli za instrukcją REP znajdzie się komenda wywołująca wyjątek, to w przypadku gdy program ją ominie wyjątek nie zostanie wywołany. W tej sytuacji będzie to oznaczać że program jest debuggowany. Rysunek 17 Wykorzystanie prefiksów jako techniki antydebuggingowej [źródło własne] 16

Funkcja FindWindow System Windows w udostępnionym interfejsie API oferuje funkcję FindWindow [8], która ma za zadanie zwrócić uchwyt okna, którego nazwa (lub nazwa jego klasy) została podana jako jeden z parametrów funkcji. Aby wykryć debugger należy przeszukać okna pod kątem obecności nazw: OllyDbg, WinDbgFrameClass, ID, Zeta Debugger, Rock Debugger, ObsidianGUI, IDA, SoftICE, Syser. Listing 1 Prezentacja użycia funkcji FindWindow do wykrycia debuggera [źródło własne] 17

Przerwania int 3(2B) Przerwania [5] są wykorzystywane przez debuggery do kontrolowania przebiegu programu. Dzięki nim możliwe jest zatrzymanie działania aplikacji w dowolnym momencie wykonywania kodu przez co analiza oprogramowania staje się dużo prostsza. Najczęściej wykorzystywanymi typami przerwań są: int 3 (bajt $CC), int F1 (bajt $F1), int 2D (bajt $2D). Istnieje jeszcze jedno mniej znane przerwanie int 3 o rozmiarze dwóch bajtów ($CD03), które także może zostać wykorzystane do wykrycia obecności debuggera. Gdy procesor natrafi na jedną z wyżej wymienionych instrukcji to zostanie wywołany wyjątek. Jednak jeśli aplikacja jest kontrolowana przez debugger wyjątek ten zostanie obsłużony i program będzie kontynuował swoje działanie. Listing 2 Prezentacja użycia dwubajtowego przerwania do wykrycia debuggera [źródło własne] RaiseException Funkcja RaiseException [17] umożliwia wywołanie wyjątku o zadanym kodzie w dowolnym momencie wykonywania programu. Obsługa wyjątków odbywa się według ściśle określonego schematu. W pierwszej kolejności system sprawdza czy do programu jest podłączony debugger. Jeśli nie lub debugger nie obsługuje tego typu wyjątku to system szuka funkcji obsługi wyjątków zdefiniowanej przez użytkownika. W przypadku gdy taka funkcja nie istnieje, system ponownie informuje debugger o wystąpieniu wyjątku. Dopiero po niepowodzeniu drugiej próby system przekazuje 18

wyjątek do domyślnej funkcji obsługi przerwań, która zazwyczaj powoduje zamknięcie programu. Natomiast jeśli aplikacja jest kontrolowana przez debugger i wyjątek zostanie obsłużony to program będzie kontynuował swoje działanie. Kody wyjątków, które mogą zostać użyte to wykrycia obecności debuggera znajdują się w tabeli poniżej. Opis wyjątku Kod STATUS_BREAKPOINT $80000003 STATUS_SINGLE_STEP $80000004 DBG_PRINTEXCEPTION_C $40010006 DBGRIPEXCEPTION $40010007 DBG_CONTROL_C $40010005 DBG_CONTROL_BREAK $40010008 DBG_COMMAND_EXCEPTION $40010009 ASSERTION_FAILURE $C0000420 STATUS_GUARD_PAGE_VIOLATION $80000001 SEGMENT_NOTIFICATION $40000005 EXCEPTION_WX86_SINGLE_STEP $4000001E EXCEPTION_WX86_BREAKPOINT $4000001F Tabela 1 Kody wyjątków, które mogą zostać użyte do wykrycia debuggera [17] Listing 3 Prezentacja użycia wyjątków do wykrycia debuggera [źródło własne] BlockInput Funkcja BlockInput [14] pozwala na zablokowanie/odblokowanie komunikatów wysyłanych przez myszkę i klawiaturę w zależności od parametru z jakim zostanie wywołana. Prawidłowe działanie funkcji nie pozwala na podwójne zablokowanie komunikatów - druga próba zwróci wartość FALSE. Niektóre debuggery modyfikują 19

wynik zwracany przez tę funkcję, przez co możliwe jest podwójne zablokowanie komunikatów, a w konsekwencji wykrycie obecności debuggera. Listing 4 Prezentacja użycia funkcji BlockInput do wykrycia debuggera [źródło własne] DbgBreakPoint DbgBreakPoint [15] jest jedną z funkcji eksportowana przez bibliotekę ntdll.dll, która służy do wywoływania przerwania int 3 (bajt $CC) przez sterownik działający w jądrze systemu (ring 0). Możliwe jest jednak wywołanie tej funkcji z trybu użytkownika (ring 3), w którym powoduje ona zatrzymanie programu (tak jak przerwanie int 3) jeśli program jest debuggowany lub wywołanie wyjątku w przeciwnym przypadku. Listing 5 Prezentacja użycia funkcji DbgBreakPoint do wykrycia debuggera [źródło własne] Ldr_module Struktura PEB [4], [6] (ang. process environment block) przechowuje wiele informacji o uruchomionym procesie. Jednym z pól, które można z powodzeniem użyć do wykrywania obecności debuggerów jest pole PEB_LDR_DATA / PEB_LDR_MODULE zawierające informacje o modułach (bibliotekach dll) załadowanych do pamięci oraz stertach utworzonych przez proces. Jeśli program został 20

uruchomiony pod kontrolną debuggera to program stworzył sterty w oparciu o trzy dodatkowe flagi: FLG_HEAP_ENABLE_TAIL_CHECK = $10 (znacznik $ABABABAB) FLG_HEAP_ENABLE_FREE_CHECK = $20 (znacznik $FEEEFEEE) FLG_HEAP_VALIDATE_PARAMETERS = $40 Służą one przede wszystkim do kontroli błędów występujących na stercie. Wykorzystanie dwóch z wyżej wymienionych flag w procesie tworzenia stert powoduje pojawienie się w pamięci specjalnych znaczników, które mogą zostać użyte do wykrycia debuggera. Listing 6 Prezentacja funkcji wyszukującej znacznik $FEEEFEEE w pamięci procesu NtSetInformationThread Jest to kolejna słabo udokumentowana funkcja [19], która nie jest dostępna przy użyciu zalecanych standardowych bibliotek WinAPI. Funkcja ta umożliwia zmianę wielu informacji o wątku takich jak: priorytet, procesor na którym dany wątek może zostać 21

uruchomiony (affinity mask), adres tablicy TLS (ang. Thread Local Storage) oraz widoczność wątku dla debuggera. W momencie gdy w programie zostanie wywołana funkcja z parametrami wskazującymi na główny wątek aplikacji oraz zmienną ThreadInforamtionClass ustawianą na ThreadHideFromDebugger, debugger podłączony do programu straci nad nim kontrolę. Mierząc czas wykonania instrukcji (np. przerwania int 3) przed wywołaniem funkcji oraz po jej wywołaniu można wykryć, czy aplikacja była debuggowana czy nie. Listing 7 Prezentacja wykorzystania funkcji NtSetInformationThread do wykrycia debuggera [źródło własne] RtlQueryProcessDebugInformation Jak już zostało wcześniej wspomniane przy omawianiu metody Ldr_module, gdy program uruchamiany jest pod kontrolą debuggera, do parametru flag procedury tworzącej sterty poza standardową flagą (HEAP_GROWABLE = $2) dodawane są dwie dodatkowe flagi: HEAP_TAIL_CHECKING_ENABLED = $20 oraz HEAP_FREE_CHECKING_ENABLED = $40. Aby odczytać pole z flagami można 22

wykorzystać nieudokumentowaną funkcję RtlQueryProcessDebugInformation [22]. Jako parametry przyjmuje ona uchwyt do procesu, flagi: PDI_HEAP oraz PDI_HEAPS_BLOCK i adres wcześniej zainicjowanej struktury, która zawierać będzie informacje o uruchomionym procesie w tym m.in. informacje o stercie programu. Jeśli pole ForceFlag struktury HeapInformation zawiera jakieś inne flagi poza HEAP_GROWABLE to znaczy, że program jest debuggowany. Listing 8 Prezentacja użycia funkcji RtlQueryProcessDebugInformation do wykrycia debuggera [źródło własne] RtlQueryProcessHeapInformation Jak się okazuje poprzednio opisana funkcja RtlQueryProcessDebugInformation wywołana z flagami PDI_HEAP oraz PDI_HEAPS_BLOCK tak naprawdę pośrednio wykorzystuje funkcję RtlQueryProcessHeapInformation [21] do otrzymania informacji o stercie programu. Nic nie stoi na przeszkodzie, żeby wywołać tę funkcję bezpośrednio w aplikacji i sprawdzić czy program jest debuggowany. 23

Listing 9 Prezentacja użycia funkcji RtlQueryProcessHeapInformation do wykrycia debuggera [źródło własne] CreateToolhelp32Snapshot-p Metody z sufiksem -p oznaczają, że bazują one na sprawdzeniu czy identyfikator PID (ang. Process identifier) rodzica chronionej aplikacji wskazuje na proces Explorer.exe. Jeśli program jest uruchomiony pod kontrolą debuggera to PID rodzica będzie wskazywał na PID debuggera. Metody te pełnią jedynie funkcję pomocniczą w procesie wykrywania debuggerów, ponieważ PID rodzica chronionego programu nie zawsze musi wskazywać na proces Explorer.exe, a zatem możliwe jest generowanie fałszywych alarmów (ang. false positives). Metodą zalecaną przez Microsoft do pobierania informacji o uruchomionych procesach jest wykorzystanie funkcji CreateToolhelp32Snapshot [16] do stworzenia migawki, która może zostać wykorzystana do wyszukiwania kolejnych struktur zawierających informacje o procesach. Za pobieranie danych z migawki odpowiadają funkcje: Process32First oraz Process32Next. 24

Listing 10 Prezentacja użycia funkcji CreateToolhelp32Snapshot do wykrycia debuggera [źródło własne] NtQueryInformationProcess-p Znacznie prostszą metodą na pobranie PID rodzica jest wykorzystanie funkcji systemu NtQueryInformationProcess [8]. Jak sama nazwa wskazuje funkcja ta zwraca informacje o zadanym procesie. Jeśli jako parametry przekażemy uchwyt do naszego programu oraz strukturę ProcessBasicInformation to funkcja zwróci wszystkie niezbędne dane. Aby odczytać identyfikator PID rodzica procesu należy odczytać nieudokumentowane pole o nazwie Reserved3. 25

Listing 11 Prezentacja użycia funkcji NtQueryInformationProcess do wykrycia debuggera [źródło własne] NtQuerySystemInformation-p Kolejną z wewnętrznych funkcji systemu jest NtQuerySystemInformation [23], [24]. Pozwala ona na uzyskanie dużej ilości informacji o systemie oraz procesach w nim uruchomionych. Z punktu widzenia zabezpieczenia aplikacji szczególnie interesujące są listy struktur: SystemSessionProcessesInformation oraz SystemProcessesAndThreadsInformation, które umożliwiają pobranie informacji o procesach w zdefiniowanej sesji (pierwsza lista) oraz całym systemie operacyjnym (druga lista). Rozmiar list zmienia się w zależności od ilości uruchomionych procesów, dlatego aby określić ich rozmiar należy kilkukrotnie wywołać funkcję NtQuerySystemInformation. Gdy funkcja zwróci wartość STATUS_SUCCESS bufor zwrotny będzie zawierał adres pierwszego elementu listy procesów. Przechodząc po kolejnych elementach listy możemy odnaleźć nazwę naszej aplikacji, a następnie odczytać wartość pola InheritedFromProcessID, które zawiera identyfikator PID rodzica. 26

Listing 12 Prezentacja użycia funkcji NtQuerySystemInformation z parametrem SystemSessionProcessesInformation do wykrycia debuggera [źródło własne] 27

Listing 13 Prezentacja użycia funkcji NtQuerySystemInformation z parametrem SystemProcessesAndThreadsInformation do wykrycia debuggera [źródło własne] 28

Self-debugging Jest to jedyna metoda [12], która nie służy do wykrycia obecności debuggera, ale ma za zadanie znacząco utrudnić analizę działania aplikacji. Polega ona na stworzeniu kopii uruchomionego programu dziecka, która jest debuggowana przez proces rodzica (proces tworzący kopię). Proces rodzica (debugger) komunikaty Proces dziecka Rysunek 1 Przepływ komunikatów między chronionym programem, a debuggerem ochronnym. [źródło własne] Rozwiązanie to wymaga zaimplementowania prostego debuggera, który będzie przechwytywał komunikaty wysyłane przez proces dziecka. Analiza tak stworzonego programu jest bardzo trudna, ponieważ część kodu aplikacji może być wykonywana zarówno po stronie rodzica jak i dziecka. Aby poprawnie przeanalizować tak skonstruowany program należy wykorzystać kilka zaawansowanych technik debuggowania takich jak: użycie dwóch debuggerów, jeden do kontrolowania procesu rodzica oraz drugi do kontrolowania procesu dziecka podłączenie debuggera do procesu dziecka, który jest już debuggowany przez rodzica. Aby tego dokonać trzeba wyczyścić pole DebugPort w strukturze EPROCESS, co wymaga użycia sterownika działającego w trybie jądra systemu filtrowanie komunikatów w taki sposób, aby nie zakłócić działania aplikacji 29

Listing 14 Zawartość funkcji obsługującej komunikaty wysyłane przez chronioną apliakcję [źródło własne] Listing 15 Funkcja uruchamiająca debugger [źródło własne] 30

1.2. Wykrywanie śledzenia wykonywanego kodu Druga grupa metod antydebuggingowych pozwala na wykrycie jednej z podstawowych funkcji debuggerów jaką jest praca krokowa (ang single step). Dzięki niej możliwa jest dokładna analiza fragmentu programu. 31

Znaczniki czasu Znaczniki czasu [13], [18] są to wszystkie funkcje, które pozwalają na odmierzanie upływającego czasu. Analiza programu przez debugger za pomocą trybu pracy krokowej (ang. single step) powoduje, że czas wykonania fragmentu kodu znacząco rośnie. Aby wykryć obecność debuggera należy dokonać odcinkowego pomiaru czasu. Procedura detekcji może wglądać następująco: Pierwszy pomiar czasu Wykonanie kilku instrukcji Drugi pomiar czasu Obliczenie różnicy pomiędzy dwoma wcześniejszymi pomiarami. Jeśli różnica jest większa od ustalonego progu to program jest debuggowany. Wykorzystując tę metodę należy poświęcić szczególną uwagę na odpowiednie dobranie progu detekcji, ponieważ za mały próg może powodować występowanie fałszywych alarmów (przy mocno obciążonym procesorze). Z kolei za duża wartość progu spowoduje, że debugger nie zostanie wykryty. Pluginy do debuggerów pozwalają na oszukanie części najpopularniejszych funkcji wykorzystywanych w tej metodzie, dlatego warto korzystać z możliwie jak największej ilości różnych funkcji, których w systemie Windows jest pod dostatkiem: funkcja GetTickCount biblioteka: kernel32.dll funkcja GetLocalTime - biblioteka: kernel32.dll funkcja GetSystemTime - biblioteka: kernel32.dll funkcja GetTickCount64 - biblioteka: kernel32.dll funkcja GetSystemTimeAsFileTime - biblioteka: kernel32.dll funkcja QueryUnbiasedInterruptTime - biblioteka: kernel32.dll funkcja QueryPerformanceCounter - biblioteka: kernel32.dll funkcja timegettime - biblioteka: winmm.dll instrukcja RDTSC przerwanie INT 2A - KiGetTickCount funkcja NtGetTickCount - biblioteka: ntdll.dll funkcja NtQuerySystemTime - biblioteka: ntdll.dll 32

funkcja GetTickCount - biblioteka: kernelbase.dll funkcja GetTickCount64 - biblioteka: kernelbase.dll funkcja GetTickCount - biblioteka: api-ms-win-core-sysinfo-l1-1-0.dll funkcja GetTickCount64 - biblioteka: api-ms-win-core-sysinfo-l1-1-0.dll Listing 16 Wykorzystanie znaczników czasu do wykrycia pracy krokowej [źródło własne] W pamięci uruchomionego procesu pod adresem $7FFFFE0000 znajduje się strona pamięci o nazwie User Shared Data. Jest to współdzielony (pomiędzy aplikacją, a sterownikami) nieudokumentowany fragment pamięci, w którym znajduje się m.in. aktualny czas z dokładnością do milisekund, co idealnie nadaje się do wykrycia pracy krokowej. Listing 17 Wykorzystanie nowej metody do pobrania aktualnego czasu [źródło własne] Istnieje jeszcze jeden, sposób na pobranie aktualnego czasu. Polega on na bezpośrednim odczytaniu kilku wartości systemowych na podstawie których możliwe 33

jest obliczenie czasu z dokładnością do milisekund, co idealnie nadaje się do wykrycia pracy krokowej. Rysunek 20 Wykorzystanie ww. metody do pobrania aktualnego czasu [źródło własne] Push ss/pop ss Kolejna metoda wykrywająca pracę krokową wykorzystuje fakt, że niektóre instrukcje powodują chwilowe wyłącznie obsługi przerwań [19]. W tym przypadku jest to polecenie załadowania rejestru SS (ang. Stack Segment) pop ss, które wyłącza wszystkie przerwania dla jednej instrukcji. Takie zachowanie ma umożliwiać zmianę zawartości rejestru ESP bez obawy o zniszczenie stosu aplikacji. Aby wykorzystać tę metodę do wykrycia pracy krokowej wystarczy zamiast zmiany rejestru ESP, wykonać modyfikację flagi Trap odpowiedzialnej za przechodzenie po kolejnych instrukcjach (podczas debuggowania programu). Debugger nie wykryje takiej zmiany, ponieważ jego działanie opiera się o wywoływanie i obsługę przerwań, które chwilowo są wyłączone. 34

Listing 18 Wykorzystanie rejestru SS do wykrycia pracy krokowej[źródło własne] Flaga Trap Kolejna metoda wykrywająca pracę krokową wykorzystuje flagę Trap z rejestru EFLAGS procesora. Jeżeli aplikacja nie działa pod kontrolą debuggera to w momencie ustawienia tej flagi wystąpi wyjątek i dalsze wykonywanie programu zostanie przeniesione do funkcji obsługującej wyjątki. Takie zachowanie pozwala na detekcję pracy krokowej poprzez zapamiętanie wartości z rejestru EFLAGS oraz ustawienie flagi Trap, a następnie przywrócenie poprawnej wartości rejestru EFLAGS. Oczywiście ostatnia część algorytmu (przywrócenie flag) zostanie wykonana tylko w przypadku użycia debuggera w trybie pracy krokowej. 35

Rysunek 21 Wykorzystanie flagi Trap do wykrycia pracy krokowej [źródło własne] Selektor segmentu GS Ostania technika pozwalająca wykryć pracę krokową korzysta z właściwości selektora segmentu GS. Podczas normalnej pracy programu próba zmiany adresu selektora GS nie jest możliwa tzn. system automatycznie przywróci poprzednią wartość. Jednak zanim poprawna wartość zostanie przywrócona musi upłynąć bardzo krótka chwila (poniżej milisekundy), dlatego jeśli program pracuje w trybie krokowym to system zdąży przywrócić poprawny adres selektora GS. 36

Rysunek 22 Wykorzystanie selektora GS do wykrycia pracy krokowej [źródło własne] 37

1.3. Wykrywanie punktów wstrzymania Programowe punkty wstrzymania Programowe punkty wstrzymania [12], [20] są podstawą działania każdego debuggera. Powodują one zatrzymanie wykonywania programu w miejscu w którym zostały postawione. Sposób ich działania wymaga podmiany pierwszego bajtu oryginalnej instrukcji na przerwanie int 3 (wartość $CC). Zmieniony bajt jest zapamiętywany w tablicy tak, aby w momencie zatrzymania wykonywania procesu debugger był w stanie przywrócić oryginalną instrukcję. Ochrona przed tego typu punktami wstrzymania sprowadza się do sprawdzenia czy w testowanym obszarze pamięci znajduje się instrukcja $CC lub czy badany obszar uległ jakimś modyfikacjom. Metody te stwarzają niebezpieczeństwo pojawienia się fałszywych alarmów, ponieważ bajt o wartości $CC może być częścią dłuższej instrukcji, a zbyt duży obszar pamięci może być modyfikowany przez sam program. Dlatego aby uniknąć takich sytuacji można przed zastosowaniem tych technik przeszukać zadany obszar pamięci pod kątem występowania wartości $CC lub ewentualnych modyfikacji wygenerowanych przez chronioną aplikację. 38

Listing 19 Sprawdzenie CRC z wybranego obszaru pamięci w celu wykrycia punktów wstrzymania [źródło własne] Listing 20 Wykorzystanie funkcji Toolhelp32ReadProcessMemory do poszukiwania punktów wstrzymania [źródło własne] 39

Listing 21 Wykorzystanie instrukcji rep movsb do poszukiwania punktów wstrzymania [źródło własne] Punkty wstrzymania na dostęp do pamięci (ang. breakpoints on memory access) Ustawienie tego rodzaju punktów wstrzymania powoduje zmianę w prawach dostępu do fragmentu pamięci. Podczas normalnego wykonywania programu wszystkie instrukcje znajdują się w obszarze pamięci, który ma prawa PAGE_EXECUTE. Jeżeli debugger ustawił taki punkt wstrzymania to w zależności od debuggera ustawione zostną uprawnienia: PAGE_GUARD lub PAGE_NOACCESS. Gdy program spróbuje wykonać instrukcję spod adresu, który nie będzie posiadał prawa PAGE_EXECUTE zostanie zwrócony wyjątek i aplikacja zatrzyma swoje działanie. Metoda ochrony przed tymi breakpointami polega na sprawdzaniu praw dostępu do określonego obszaru pamięci. Do pobrania uprawnień można wykorzystać dwie funkcje: VirtualQuery oraz VirtualProctect. Jeżeli w trakcie działania programu utracimy prawo do wykonania (PAGE_EXECUTE) to oznaczać będzie, że został ustawiony punkt wstrzymania na dostęp do pamieci. 40

Rysunek 25 Metoda ochrony aplikacji przed punktami wstrzymania na dostęp do pamięci [źródło własne] Sprzętowe punkty wstrzymania Każdy procesor udostępnia kilka rejestrów [19], które wykorzystywane są do wstrzymania działania procesora. W momencie gdy adres przechowywany w rejestrze zgadza się z adresem wykonywanej instrukcji to procesor generuje przerwanie INT 1, które powoduje zatrzymanie pracy programu. W obecnych procesorach na sprzętowe punkty wstrzymania zostały przeznaczone cztery rejestry podstawowe: DR0, DR1, DR2, DR3, które służą do przechowywania adresów oraz dwa dodatkowe: DR6 i DR7 zawierające flagi określające warunki zatrzymania procesora np. przy dostępnie do jednego z rejestrów podstawowych. Aby ochronić się przed tego typu punktami wstrzymania wystarczy, że w trakcie wykonywania programu sprawdzana będzie struktura CONTEXT zawierająca opisywane rejestry. Domyśle wartości tych rejestrów są ustawione na zero, dlatego jeżeli jeden z nich będzie miał inną wartość to oznacza, że został ustawiony sprzętowy punkt wstrzymania. 41

Listing 22 Wykorzystanie sztucznego wygenerowania wyjątku do sprawdzenia rejestrów procesora [źródło własne] Rysunek 26 Metoda ochrony aplikacji przed sprzętowymi punktami wstrzymania [źródło własne] 42

1.4. Wykrywanie dodatków do debuggerów Jedną z technik, która wykorzystywana jest w dodatkach służących do obchodzenia wyżej opisywanych metod wykrywania debuggerów jest tworzenie punktów zaczepienia (ang. hooking) w interfejsie WinAPI [7], [10], [11]. Punkty te pozwalają na przechwycenie wywołań funkcji co umożliwia podmianę parametrów wejściowych oraz filtrowanie zwracanych wyników. Aby tego dokonać można wykorzystać jedną z trzech najpopularniejszych metod: Przekierowanie IAT (ang. IAT redirection / IAT hooking) kiedy aplikacja ładowana jest do pamięci, tablica IAT (ang. Import Address Table) uzupełniana jest adresami funkcji, które zawarte są w zewnętrznych bibliotekach DLL. Przekierowanie IAT polega na nadpisaniu adresu oryginalnej funkcji, adresem punktu zaczepienia. Wadą tego rozwiązania jest to, że takie przekierowanie działa tylko dla funkcji importowanych (tzw. ładowanie statyczne funkcji) przez aplikację oraz zawęża działanie do jednego modułu (każdy moduł posiada odrębną tablicę IAT). Normalne wywołanie funkcji Test.exe IAT user32.messagebox XXXXX user32.dll EAT MessageBox XXXXX... XXXXX: mov edi,edi Rysunek 2 Normalne wywołanie funkcji z biblioteki DLL [źródło własne] 43

Test.exe IAT user32.messagebox YYYYY user32.dll EAT MessageBox XXXXX... XXXXX: mov edi,edi Wywołanie podmienionej funkcji HOOK.dll YYYYY: xor eax,eax... Rysunek 3 Wywołanie podmienionej funkcji poprzez modyfikację tablicy IAT [źródło własne] przekierowanie EAT (ang. EAT redirection / EAT hooking) Tablica EAT (ang. Export Address Table) jest bardzo podobna do tablicy IAT za wyjątkiem kierunku działania. Kiedy moduł eksportuje funkcję, tak aby mogła zostać użyta przez inną aplikację, jej adres zostaje zapisany w tablicy EAT. Podczas tworzenia przekierowania EAT ważny jest czas modyfikacji tablicy, ponieważ adresy funkcji z tablicy EAT są odczytywane tylko w dwóch momentach: - załadowanie aplikacji do pamięci (ładowanie statyczne), uzupełnienie tablicy IAT adresami udostępnianymi w tablicy EAT poszczególnych modułów - wykorzystanie funkcji GetProcAddress (ładowanie dynamiczne), odczytanie adresu funkcji w czasie działania aplikacji Jeśli zależy nam na przechwyceniu wszystkich wywołań funkcji, to podmiany adresów należy dokonać już w momencie startu aplikacji. W przeciwnym wypadku przekierowane zostaną tylko wywołania wykorzystujące funkcję GetProcAddress. 44

Test.exe IAT user32.messagebox YYYYY user32.dll EAT MessageBox YYYYY... XXXXX: mov edi,edi Wywołanie podmienionej funkcji HOOK.dll YYYYY: xor eax,eax... Rysunek 4 Wywołanie podmienionej funkcji poprzez modyfikację tablicy EAT [źródło własne] modyfikowanie kodu (ang. inline hooking / jmp patch) najtrudniejsza do wykonania, ale zarazem najczęściej wykorzystywana metoda tworzenia punktów zaczepienia. Polega ona na nadpisaniu pierwszych pięciu bajtów funkcji, która ma zostać przechwycona, adresem skoku bezwarunkowego (instrukcja jmp). Aby możliwe było wywołanie oryginalnej funkcji konieczne jest zapamiętanie nadpisywanych instrukcji. Zaletami tej metody jest przede wszystkim możliwość przechwycenia wszystkich wywołań funkcji niezależnie od modułu, który te funkcje wykorzystuje oraz od czasu utworzenia przekierowania. Normalne wywołanie funkcji Test.exe Call user32.messagebox user32.dll Jmp mhook... Powrót do wykonywania oryginalnej funkcji Przekierowanie wykonywania do podstawionej funkcji HOOK.dll xor eax,eax Jmp back... Rysunek 5 Wywołanie podmienionej funkcji poprzez modyfikację kodu oryginalnej funkcji [źródło własne] 45

Z racji tego, że tworzenie punktów zaczepienia często wykorzystywane jest przez rootkity do ukrywania swojego szkodliwego działania w systemie, to firmy antywirusowe jako pierwsze opracowały metody pozwalające na wykrycie modyfikacji funkcji. Dla każdej z wyżej opisanych metod powstały różne techniki detekcji: wykrycie przekierowań w IAT procedura tworzenia przekierowań w IAT jest dość prosta, dlatego też metody jej wykrycia nie są skomplikowane. Jedną z technik wykrywania czy adres funkcji w tablicy IAT nie został zmodyfikowany jest odczytanie oryginalnego adresu bezpośrednio z tablicy EAT modułu, który taką funkcję udostępnia oraz z tablicy IAT aplikacji. Następnie należy porównać oba adresy i jeśli oba są takie same to oznacza, że funkcja nie została przekierowana za pomocą zmiany adresu w IAT. Kolejna metoda umożliwiająca wykrycie modyfikacji w tablicy IAT została opisana w punkcie dotyczącym przekierowań EAT. Listing 23 Metoda wykrywająca punkty zaczepienia w tablicy IAT [źródło własne] wykrycie przekierowań w EAT podobnie jak w poprzednim przypadku wykrycie modyfikacji w tablicy EAT nie jest skomplikowane. Wszystkie adresy funkcji eksportowanych przez daną bibliotekę muszą należeć do jej przestrzeni adresowej (adres funkcji musi znajdować się w wnętrzu biblioteki udostępniającej tę funkcję.). Jeśli jakiś adres wskazuje na obszar pamięci z poza sprawdzanego modułu to znaczy, że adres funkcji został podmieniony. 46

Listing 24 Metoda wykrywająca punkty zaczepienia w tablicy EAT [źródło własne] wykrycie modyfikacji kodu technika wykorzystywana do detekcji modyfikacji w kodzie funkcji została nazwa walidacją krzyżową. Polega ona na sprawdzeniu czy wszystkie bajty w sprawdzanej funkcji znajdujące się w pamięci są identyczne z bajtami w pliku. Metoda ta jest skuteczna jednak posiada dwie wady: - do odczytania wartości poszczególnych bajtów z pliku konieczne jest skorzystanie z funkcji WinAPI, których wynik może zostać zmieniony w celu ukrycia modyfikacji - badana biblioteka sama modyfikuje swoją zawartość w trakcie wykonywania przez co plik na dysku i obraz w pamięci nigdy nie będą takie same. Rozwiązaniem pierwszego problemu jest skorzystanie z nieudokumentowanych funkcji służących do odczytu plików z dysku np. NtReadFile biblioteka ntdll.dll lub stworzenie sterownika działającego w jądrze systemu, który może odczytać zawartość pliku bezpośrednio z systemu plików pomijając przy tym wykorzystanie funkcji WinAPI. Drugi problem jest rzadziej spotykany, ponieważ żadna z systemowych bibliotek (WinAPI) nie modyfikuje kodu swoich funkcji po załadowaniu do pamięci. Istnieją jednak biblioteki dostarczane przez zewnętrzne firmy, których pliki na dysku 47

są zaszyfrowane, a ich deszyfracja następuje dopiero po załadowaniu modułu do pamięci. Listing 25 Metoda wykrywająca punkty zaczepienia w kodzie funkcji [źródło własne] 48

wykrycie podmiany funkcji np. GetTickCount walidację krzyżową można także wykorzystać do testowania wyników zwracanych przez funkcję. Jeśli jesteśmy w stanie oszacować jaki powinien być prawidłowy wynik, to możemy z dużym prawdopodobieństwem określić czy funkcja została podmieniona. W poniższym przykładzie sprawdzana jest funkcja GetTickCount, która zwraca aktualny czas z dokładnością do milisekund. Do przetestowania jej działania zostały wykorzystane dwa testy sprawdzające: funkcja sleep z czasem ustawionym na 50 milisekund oraz pętla, której czas wykonania jest dużo większy od 10 milisekund. W momencie gdy funkcja GetTickCount zostanie podmieniona, a czas przez nią zwracany nie będzie prawidłowy to co najmniej jeden z testów pozwoli wykryć podmianę sprawdzanej funkcji. Listing 26 Wykorzystanie walidacji krzyżowej do wykrycia podmiany funkcji GetTickCount [źródło własne] 49

2. Metody ochrony kodu programu Uzupełnieniem metod chroniących przed debuggerami są techniki ochrony kodu programu. W wyniku procesu deassemblacji atakujący z kodu maszynowego otrzymuje kod assemblera, który następnie poddawany jest dokładnej analizie. 50

Erase DOS/PE Header Wszystkie pliki wykonywalne posiadają specjalne nagłówki, które zawierają podstawowe informacje potrzebne do uruchomienia pliku. W starszych systemach Windows wykorzystywano nagłówek DOS, natomiast w nowszych (seria NT) zaistniała potrzeba znacznego rozszerzenia ilości przechowywanych informacji, dlatego zdecydowano dodać pole wskazujące na nowy nagłówek PE (ang. Portable Executable) [2] do istniejącego już nagłówka DOS. Jako że dane z nagłówków wykorzystywane są jedynie do wczytania pliku wykonywalnego z dysku, po załadowaniu programu do pamięci można je bezpiecznie usunąć. Dzięki temu prostemu zabiegowi część programów wykonujących zrzuty pamięci będzie wymagała ręcznego wpisania zakresów adresów pod którymi znajduje się uruchomiony program, ponieważ automatyczne odczytanie potrzebnych informacji z nagłówka PE będzie niemożliwe. Listing 27 Funkcja usuwająca dane z nagłówka PE [źródło własne] Rysunek 6 Informacje zawarte w nagłówku PE przed skasowaniu ich wartości [źródło własne] 51

Rysunek 7 Nagłówek PE po użyciu funkcji wymazującej dane [źródło własne] Size of image Kolejnym miejscem, w którym znajduje się wiele informacji o uruchomionej aplikacji jest szereg struktur, których adresy zawarte są w strukturze PEB. Po usunięciu nagłówków DOS oraz PE, niektóre programy wykonujące zrzuty z pamięci szukają informacji o załadowanych modułach w oparciu o standardowe funkcje WinAPI np. GetModuleInformation, CreateToolhelp32Snapshot. Po dokładnym zbadaniu tych funkcji okazuje się, że dane, które są przez nie zwracane są odczytywane ze struktury PEB [3]. Aby uniemożliwić zrobienie zrzutu pamięci z programu można usunąć bądź zmodyfikować informacje przechowywane w tej strukturze. Rysunek 8 Oryginalny rozmiar obrazu pliku w pamięci [źródło własne] Rysunek 9 Podmieniony rozmiar obrazu pliku w pamięci [źródło własne] 52

Listing 28 Funkcja umożliwiająca podmianę rozmiaru obrazu pliku w pamięci [źródło własne] Junk code Jedną z podstawowych technik ochrony kodu jest wstawianie całych bloków instrukcji, których jedynym zadaniem jest utrudnienie odczytania oryginalnego kodu [1]. Metoda ta jest bardzo skuteczna, ponieważ można wymyślić dowolną ilość unikalnych bloków instrukcji zaciemniających, których analiza może być bardzo czasochłonna. Mechanizmem znacząco ułatwiającym korzystanie z tej techniki są makra. Na etapie produkcyjnym, kiedy programista pisze i odpluskwia program, kod źródłowy powinien być w czytelnej formie. Dlatego dodanie kilku linijek z makrami nie utrudni pracy z kodem źródłowym aplikacji, natomiast analiza skompilowanego programu będzie bardzo trudna. 53

Listing 29 Przykładowy blok instrukcji zaciemniających [źródło własne] Pasywne SMC Kod programu otrzymany w wyniku deassemblacji może różnić się w zależności od użytego dekompilatora. Zdecydowana większość obecnie dostępnych deassemblerów dokonuje analizy na podstawie znajomości trzech informacji: adresu pierwszej instrukcji kodu (pole AddressOfEntryPoint nagłówek PE), rozmiaru kodu (pole SizeOfCode nagłówek PE) oraz długości poszczególnych instrukcji assemblera. Taki sposób działania spowodował powstanie techniki nazwanej pasywne SMC (ang. Self Modyfying Code) [1]. Z racji tego, że dekompilatory nie interpretują odczytywanych poleceń możliwe jest oszukanie procesu analizy statycznej przez odpowiednie zastosowanie instrukcji skoku (jmp). Jeżeli dekompilator napotka instrukcję skoku to znając jej długość będzie w stanie określić adres kolejnego polecenia. Jednak jeśli celem skoku będzie ominięcie następnego bajtu to dekompilator nie będzie w stanie tego wykryć, a zatem wszystkie instrukcje występujące po skoku będą nieprawidłowe. Rysunek 10 Kod funkcji przed zastosowaniem SMC [źródło własne] 54

Rysunek 11 Kod funkcji po zastosowaniu SMC [źródło własne] Aktywne SMC Metoda ta różni się od poprzedniej tym, że oryginalny kod programu przez większą część czasu znajduje się w postaci zaszyfrowanej [1]. Dopiero na chwilę przed użyciem następuje odszyfrowanie małego fragmentu kodu, który po wykonaniu jest ponownie szyfrowany. Takie postępowanie całkowicie uniemożliwia analizę statyczną, ponieważ w pliku na dysku znajduje się zaszyfrowany kod programu oraz funkcja deszyfrująca. Dopiero wykorzystanie debuggera pozwala na częściową analizę odszyfrowanych fragmentów kodu. Rysunek 12 Kod funkcji przed zastosowaniem SMC [źródło własne] 55

Rysunek 13 Kod funkcji po zastosowaniu SMC [źródło własne] Nanomity Po raz pierwszy metoda ta została użyta w Armadillo [9] - komercyjnym oprogramowaniu służącym do ochrony innych aplikacji. Jedną z podstawowych metod zabezpieczających zastosowaną w tym rozwiązaniu była technika self-debuggingu. Jednak crakerzy szybko zaleźli sposób na obejście tego zabezpieczenia, poprzez wyłączenie chroniącego debuggera. Aby przeciwdziałać takim zdarzeniom programiści Armadillo opracowali metodę obrony, którą nazwali Nanomity. W chronionej aplikacji część instrukcji skoków warunkowych (jz, jnz, jle etc.) zostaje przeniesiona do zaszyfrowanej tablicy skoków znajdującej się w debuggerze chroniącym, a w ich miejsce wstawiane są przerwania int 3 (nanomit). Kiedy program natrafi na taką instrukcję zostanie wywołany wyjątek. Debugger chroniący na podstawie tablicy skoków określi, czy wyjątek ten jest spowodowany przez nanomit. Jeśli tak to odczytując flagi procesora oraz typ wykonywanego skoku określi pod jaki adres należy przekazać dalsze wykonywanie programu. Dzięki temu że debugger chroniący steruje wykonywaniem programu nie może zostać odłączony od chronionej aplikacji. Listing 30 Kod funkcji przed zastosowaniem nanomitów [źródło własne] 56

Listing 31 Kod funkcji po zastosowaniu nanomitów [źródło własne] Znacznie uproszczona wersja nanomitów, do sterowania wykonywanymi instrukcjami zamiast debuggera wykorzystuje sam mechanizm wywoływania i obsługi wyjątków. Podobnie do oryginalnej metody instrukcje skoków zamieniane są na instrukcje generujące wyjątki. Jeśli skok powinien zostać wykonany to zostaje wywoływany wyjątek, a funkcja go obsługująca analizując wcześniej wygenerowaną tablicę przekazuje wykonywanie do odpowiedniego fragmentu kodu. Jest to technika zdecydowanie mniej skuteczna niż jej oryginalna wersja, ale znacznie prostsza w implementacji. 57

Listing 32 Sterowanie wykonywaniem kodu za pomocą wyjątków [źródło własne] 58

Stolen bytes Kolejnym sposobem ochrony kodu, który został opracowany przez firmę zajmującą się tworzeniem zabezpieczeń jest metoda o nazwie Stolen Bytes [9]. Jak sama nazwa wskazuje główną ideą tej techniki jest przeniesienie (ukradzenie) części bajtów z oryginalnych funkcji do tymczasowej lokalizacji. W ich miejsce wstawiana jest instrukcja skoku, której adres ustalany jest w momencie wykonywania programu. Dodatkowo do przenoszonych instrukcji dodawane są różne mechanizmy zabezpieczające utrudniające analizę statyczną. Skuteczność tej metody zależy od inwencji jej twórcy, ponieważ każda implementacja może zawierać inne dodatkowe środki ochrony, a także sposób tworzenia tymczasowej lokalizacji może zostać wykonany na kilka sposobów. Rysunek 14 Kod funkcji przed zastosowaniem metody Stolen Bytes [źródło własne] Rysunek 15 Kod funkcji po zastosowaniu metody Stolen Bytes [źródło własne] Rysunek 16 Nowa lokalizacja przeniesionego kodu funkcji [źródło własne] 59

Wirtualizacja kodu Wirtualizacja jest obecnie jedną z najczęściej wykorzystywanych oraz najbardziej skutecznych metod ochrony kodu [25]. Niestety do jej implementacji wymagana jest doskonała znajomość procesu dekompilacji oraz modelu programowego procesora (ang. ISA Instruction Set Architecture) na który składają się: lista rozkazów procesora, typy danych, dostępne tryby adresowania, zestaw rejestrów dostępnych dla programisty, zasady obsługi wyjątków i przerwań. Technika ta polega na stworzeniu wirtualnej maszyny, która będzie posiadać własny zestaw instrukcji, rejestry i adresację. W oryginalnej aplikacji wyznaczane są funkcje, które mają zostać poddane ochronie. Następnie w procesie deassemblacji odtwarzane są instrukcje tych funkcji, które finalnie konwertowane są na zestaw rozkazów procesora stworzonej wirtualnej maszyny. Aby prawidłowo wykonać tak spreparowany kod na jego początku umieszcza się odwołanie do funkcji udostępnianej przez rdzeń wirtualnej maszyny. Sam rdzeń odpowiadający za wykonywanie zwirtualizowanego kodu musi zostać stworzony z wykorzystaniem różnych technik ochrony przed debuggerami oraz analizą statyczną przez co dokładne poznanie jego działania będzie znacznie utrudnione. Rysunek 17 Struktura pliku PE przed zastosowaniem wirtualizacji [źródło własne] 60