haker.info  — Etyczny hacking_
Spreading knowledge like a virus.

 Nanomity — metoda utrudniająca zrzuty pamięci

   Dawid Farbaniec    1190 słów

1. Słowem wstępu

kreślenie nanomit kojarzy się z mikroskopijnym robotem wielkości rzędu nanometrów. Taką też nazwę nadano znanej od długiego czasu, ale wciąż używanej metodzie przeciwko inżynierii odwrotnej kodu (ang. RCE). A dokładnie przeciwko odbudowie kodu analizowanej aplikacji ze zrzutu pamięci procesu. W prostych słowach technika ta polega na wstrzykiwaniu (zamianie) określonych instrukcji (przeważnie skoków — JZ, JNZ, JC etc.) w języku Asembler na rozkazy przerwania INT 3h określane nanomitami. Podczas wykonania programu przy trafieniu na nanomit, sterowanie zostaje przekazywane do specjalnej procedury, która decyduje o dalszej ścieżce wykonywania kodu.

2. Modyfikacje na poziomie kodu maszynowego

Przed przedstawieniem mechanizmu działania tej techniki krótkie wyjaśnienie.

Rozkazy INT 3h są umieszczane na poziomie kodu maszynowego. Przy kompilacji programu np. w C++ poszczególne formy kodu to C++, język Asembler, a na końcu kod maszynowy. Rozkaz w języku Asembler w formie tekstowej (np. INT, MOV, JNE etc.) nazywany jest mnemonikiem. Natomiast wartość liczbowa rozkazu w kodzie maszynowym nazywana jest kodem operacyjnym — w skrócie opkodem.

Załóżmy, że dla przykładu chcemy zmienić w programie (pliku .exe) instrukcję skoku JNE (ang. Jump If Not Equal — skocz jeśli nierówne) na instrukcję JE (ang. Jump If Equal — skocz jeśli równe). Rozkaz JNE w architekturze x64 posiada opkod 75h, a rozkaz JE to wartość 74h. Wartości te można znaleźć w dokumentacji procesorów lub na różnych listach opkodów dostępnych w sieci. Zatem zmiana instrukcji Asemblera w programie typu .exe polega na podmianie wartości opkodu. Dla tego przykładu jest to zmiana 75h na 74h.

Podobnie jest z wprowadzaniem nanomitów do kodu. Program typu protector zamienia wtedy opkod określonej instrukcji na nanomit INT 3h, którego wartość wynosi 0CCh (rysunek 2.1). Pisząc program w języku Asembler MASM możemy użyć zwykłego zapisu int 3h (mnemonik) lub db 0CCh (opkod).

Nanomity
Rysunek 2.1. Nanomit INT 3h powoduje błędne dekodowanie instrukcji przez deasembler

Więcej informacji o podstawach modyfikacji plików wykonywalnych znajdziesz w moim artykule:
https://haker.info/text/podstawy-edycji-plikow-binarnych

3. Mechanizm działania

Implementacje tej techniki mogą być różne. Standardowy schemat działania polega na zamianie instrukcji skoków warunkowych na rozkaz INT 3h (nazywany nanomitem). Program zabezpieczający przy zamianie opkodów zapisuje rodzaj skoku (JE, JNE, JAE etc.), miejsce gdzie była instrukcja skoku (przesunięcie) oraz miejsce do którego prowadzi instrukcja skoku (przesunięcie). Tutaj możliwe jest podzielenie programu na dwa procesy: sterujący oraz proces zabezpieczanej aplikacji. Proces sterujący posiadałby wtedy procedurę obsługującą wykonanie nanomitu. Dla uproszczenia procedura ta może znajdować się w jednym procesie. Jej zadaniem jest rozpoznanie czy przerwanie INT 3h to nanomit czy nie. A następnie odpowiednia obsługa przerwania czyli odczytanie zapisanego wcześniej rodzaju skoku, sprawdzenie stanu rejestru flag i wykonanie skoku, jeśli warunek jest spełniony. Zobacz rysunek 3.1.

Nanomity
Rysunek 3.1. Sterowanie przepływem wykonania obsługiwane jest przez specjalną procedurę i/lub proces

Przechwytywanie obsługi przerwania INT 3h może być wykonane na kilka sposobów. Jako pierwszy, który przychodzi mi na myśl to strukturalna obsługa wyjątków (ang. SEH). Można też wykonywać debugowanie utworzonego procesu (WaitForDebugEvent(...)).

4. Nanomity — przykładowy prototyp w MASM64 (ml64.exe)

Poniżej przedstawiono przykładowy dość prymitywny prototyp zabezpieczenia z użyciem nanomitów.

Program w funkcji głównej (Main) ustawia procedurę obsługi wyjątków funkcją SetUnhandledExceptionFilter(...). Następnie wykonywane jest proste porównanie rejestru RAX z jakąś wartością (cmp rax, 02h). Standardowo po takim porównaniu powinien być skok warunkowy (np. JE, JNE itp.), ale tutaj wstawiony został nanomit 🤖. Wykonane jest to za pomocą makra nanomite_here, które przyjmuje dwa parametry: rodzaj skoku i adres do etykiety docelowej. Makro zapisuje potrzebne informacje do struktury, a następnie wywołuje wyjątek int 3h.

Wywołanie wyjątku przekazuje sterowanie do procedury myExceptionHandler. Sprawdza ona rodzaj skoku, warunek na podstawie zachowanego bajtu z rejestru flag i decyduje czy wykonać skok, czy też może przekazać sterowanie do kolejnej instrukcji po int 3h.

Jak już wspomniałem jest to prosty przykład edukacyjny. W przypadku prawdziwej implementacji tej techniki na pewno należałoby dodać szyfrowanie struktury z informacjami o nanomitach, wydzielić procedurę obsługi do osobnego procesu i połączyć to wszystko z innymi technikami. Należałoby też wybrać jakiś język wyższego poziomu np. Visual C++ i używać tylko elementów w języku Asembler, a nie pisać w nim całą aplikację.

;-------------------------------------------------+ ; Nanomites anti-dump technique example ; ; (general idea, primitive educational prototype) ; ;-------------------------------------------------+ ; Code: Dawid Farbaniec // haker.info A.D. 2020 ; ;-------------------------------------------------+ extrn ExitProcess : proc extrn MessageBoxA : proc extrn SetUnhandledExceptionFilter : proc ;stałe reprezentujące poszczególne rodzaje skoków jumpTypeJZ equ 0 jumpTypeJNZ equ 1 jumpTypeJE equ jumpTypeJZ jumpTypeJNE equ jumpTypeJNZ ;(...) etc. ;struktura z informacjami o skoku NanomiteStruct struct JumpType dq ? SavedFlags db ? JumpAddress dq ? NextInstructionAddress dq ? NanomiteStruct ends ;makro tworzące nanomit w kodzie nanomite_here macro _type, _address lahf ;załadowanie bajtu rejestru flag do rejestru AH mov nano.JumpType, _type ;zapisanie rodzaju skoku (JE, JNE...) mov nano.SavedFlags, ah ;zapisanie flag do zmiennej ;zapisanie adresu skoku dokąd prowadzi mov rax, offset _address mov nano.JumpAddress, rax ;zapisanie adresu instrukcji po nanomicie mov rax, offset @f mov nano.NextInstructionAddress, rax int 3h ;nanomit @@: endm .data szCaption db "haker.info", 0 szTextYes db "Nanomite executed.", 0 szTextNo db "Nanomite NOT executed.", 0 nano NanomiteStruct <0, 0, 0, 0> .code ;procedura obsługi wyjątków (wywołuje ją instrukcja int 3h) myExceptionHandler proc ;sprawdź rodzaj skoku (JZ, JNZ...) cmp nano.JumpType, jumpTypeJZ jne @f jmp _JZ @@: cmp nano.JumpType, jumpTypeJNZ jne _return ;sprawdź czy bit znacznika ZF ;w rejestrze flag równy zero (nieustawiony) _JNZ: mov rcx, nano.JumpAddress mov ah, nano.SavedFlags and ah, 40h cmp ah, 0h jz _go jmp _return ;sprawdź czy bit znacznika ZF ;w rejestrze flag równy jeden (ustawiony) _JZ: mov rcx, nano.JumpAddress mov ah, nano.SavedFlags and ah, 40h cmp ah, 40h jz _go jmp _return ;przejście do adresu gdzie prowadził skok ;(skok warunkowy wykonany) _go: jmp rcx jmp _return ;przejście do kolejnej instrukcji po int 3h ;(skok warunkowy niewykonany) _return: jmp nano.NextInstructionAddress myExceptionHandler endp Main proc ;ustawienie procedury myExceptionHandler, aby obsługiwała wyjątki sub rsp, 28h mov rcx, myExceptionHandler call SetUnhandledExceptionFilter add rsp, 28h ;przykładowe instrukcje porównania wartości mov rax, 07h cmp rax, 02h ;nanomit zamiast skoku JNZ (makro) nanomite_here jumpTypeJNZ, _executed jmp _notexecuted _executed: lea rdx, szTextYes jmp _msgbox _notexecuted: lea rdx, szTextNo _msgbox: sub rsp, 28h xor r9, r9 lea r8, szCaption ;RDX ustawiony wcześniej xor rcx, rcx call MessageBoxA add rsp, 28h _exit: sub rsp, 8h xor rcx, rcx call ExitProcess Main endp end

Kompilacja przykładu w Asemblerze MASM64 (ml64.exe):

ml64.exe prog1.asm /link /entry:Main /subsystem:windows /defaultlib:"kernel32.Lib" /defaultlib:"User32.Lib" /LARGEADDRESSAWARE:NO

5. Przepływ programu sterowany wyjątkami — przykład w Visual C++

Poniżej przedstawiam jeszcze przykład metody podobnej do nanomitów. Polega ona na sterowaniu programem poprzez wyjątki. Rzucane wyjątki mogą być różnego rodzaju. Często łączy się je ze śmieciowym kodem (ang. junk code), który zaciemnia schemat działania podczas analizy.

/* Exception driven program control flow - Visual C++ example (similar to nanomites) https://haker.info/ */ #include <iostream> int main(int argc, char* argv[]) { __try { std::cout << "Blok I wykonany." << std::endl; throw; //rzucenie wyjątku //tutaj dodać jakiś śmieciowy kod //(ang. junk code) } __except(1) { __try { std::cout << "Blok II wykonany." << std::endl; //rzucenie wyjątku int a = 4, b = 0; int c = a / b; //tutaj dodać jakiś junk code } __except (1) { __try { std::cout << "Blok III wykonany." << std::endl; throw; //rzucenie wyjątku //tutaj dodać jakiś junk code } __except (1) { std::cout << "Blok IV wykonany." << std::endl; } } } return EXIT_SUCCESS; }

6. Zakończenie

Dziękuję za czas poświęcony na przeczytanie tego wpisu.

Dawid Farbaniec


Tagi:  reverse-engineering  masm64  visual-cpp  security 

Komentarze czytających

Wszystkie treści umieszczone na tej witrynie są chronione prawem autorskim. Surowo zabronione jest kopiowanie i rozpowszechnianie zawartości tej witryny bez zgody autora. Wszelkie opublikowane tutaj treści (w tym kody źródłowe i inne) służą wyłącznie celom informacyjnym oraz edukacyjnym. Właściciele tej witryny nie ponoszą odpowiedzialności za ewentualne niezgodne z prawem wykorzystanie zasobów dostępnych w witrynie. Użytkownik tej witryny oświadcza, że z zamieszczonych tutaj danych korzysta na własną odpowiedzialność. Wszelkie znaki towarowe i nazwy zastrzeżone zostały użyte jedynie w celach informacyjnych i należą wyłącznie do ich prawnych właścicieli. Korzystając z zasobów witryny haker.info oświadczasz, że akceptujesz powyższe warunki oraz politykę prywatności.