haker.info | Etyczny hacking |

haker.info  — Etyczny hacking

 Kod samo–modyfikujący się (x64)

02 lutego 2019 godz. 02:33    Dawid Farbaniec    1190 słów

1. Słowem wstępu

rogram, który sam modyfikuje swój kod, gdy jest już uruchomiony na pewno wzbudzi ciekawość podczas analizy w debuggerze. Technika ta polega na zamienianiu przez program swoich instrukcji podczas wykonywania. Używa się jej np. w zabezpieczeniach antypirackich. Otóż tak, modyfikacja własnego kodu przez proces aplikacji pozwala utrudnić osobom niepowołanym rozpracowanie, a następnie np. złamanie zabezpieczeń plików programu. Kod samo–modyfikujący się znajdzie też zastosowanie w innych miejscach. Inny przykład użycia to aplikacje do kompresji plików wykonywalnych. Narzędzie typu packer po uruchomieniu rozpakowuje skompresowany kod w pamięci i dopiero wtedy rozpoczyna się wykonanie go. W tym artykule poznasz właśnie technikę modyfikacji własnego kodu przez program. Miłej lektury.

2. Teoria

Na początek trochę potrzebnej teorii. Kompilator lub jeśli programujemy w Asemblerze to bezpośrednio asembler (z małej litery — narzędzie) buduje kod źródłowy do pliku wykonywalnego (np. *.exe). Instrukcje procesora w pliku *.exe są reprezentowane poprzez kod maszynowy. Rozkazy i ich operandy są zakodowane jako ciąg bajtów.

Sytuacja początkowa przedstawia się tak, że po uruchomieniu programu mamy w pamięci rozkazy procesora jako kod maszynowy (bajty). Sposób tłumaczenia instrukcji w formie mnemoników (np. ADD, MOV czy XOR) na ich kody operacyjne (opkody) jest opisany dokładnie w dokumentacji procesorów x86-64. Modyfikacja kodu aplikacji w pamięci polega w skrócie na zamianie opkodów.
Na przykład instrukcja xor rcx, rcx (zerowanie rejestru licznika RCX) jest zakodowana jako ciąg bajtów 48 33 C9. Chcąc zmodyfikować kod w pamięci należy podmienić te bajty. Np. ustawienie im wartości 90 90 90 spowoduje, że otrzymanymi instrukcjami będą trzy rozkazy NOP.

Poniżej na rysunku 2.1 przedstawiono adresy, opkody (bajty) i mnemoniki instrukcji z fragmentu kodu uruchomionego w debuggerze x64dbg.


Rysunek 2.1. Adresy, kody operacyjne i mnemoniki w debuggerze x64dbg

W tym momencie próba nadpisania instrukcji zakończy się błędem naruszenia ochrony pamięci (Exception Access Violation). Już tłumaczę dlaczego tak jest. Określone bloki pamięci mają ustawione odpowiednie atrybuty. I tak np. sekcji z danymi nie można domyślnie wykonywać (ang. execute). A do sekcji kodu nie można domyślnie pisać (ang. read ).

Zatem kolejnym krokiem będzie poznanie funkcji, która pozwoli zmieniać atrybuty stron pamięci w uruchomionej aplikacji. Jest to funkcja z Windows® API o nazwie VirtualProtect.
Jej składnia przedstawia się następująco:

BOOL VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect );

Funkcja posiada cztery parametry:

  • lpAddress — wskaźnik, który określa początek strony (ze zbioru stron pamięci) od którego będą zmieniane atrybuty dostępu
  • dwSize — rozmiar obszaru w bajtach do którego będą modyfikowane atrybuty dostępu
  • flNewProtect — stała określająca atrybuty dostępu.
    Podstawowe stałe:
    PAGE_EXECUTE (0x10) » zbiór stron może być wykonywany.
    PAGE_EXECUTE_READ (0x20) » zbiór stron może być wykonywany i odczytywany.
    PAGE_EXECUTE_READWRITE (0x40) » zbiór stron może być wykonywany, odczytywany i zapisywany.
    PAGE_READONLY (0x02) » zbiór stron może być odczytywany.
    PAGE_READWRITE (0x04) » zbiór stron może być odczytywany i zapisywany.
    (...)
  • lpflOldProtect — wskaźnik na zmienną o rozmiarze podwójnego słowa, która będzie przechowywać stare atrybuty strony (przed zmianą).

Na koniec tego rozdziału wspomnę o sposobie adresowania w architekturze x86-64 nazywanym RIP-relative addressing, czyli adresowanie względne do wskaźnika rozkazów. Jeśli byłaby potrzeba stworzenia, aby kod był position independent, czyli mógł być uruchomiony w różnych miejscach pamięci, to dzięki tej metodzie możliwe będzie zachowanie poprawnych odwołań do danych poprzez użycie zwykłych etykiet języka Asembler. W architekturze x86 było już wsparcie dla względnego odwoływania się do określonych miejsc w pamięci (np. w instrukcji JMP). Jednak w x86-64 możliwy jest dodatkowo także zapis i odczyt pamięci poprzez adresowanie względne do wskaźnika rozkazów. Może to być użyteczne przy samo–modyfikującym się shellcode.

3. Nadpisywanie kolejnych instrukcji

Rozpocznijmy od mniej złożonego przykładu. Jest to nadpisanie dwóch instrukcji. Kod źródłowy tego przykładu przedstawiono na listingu 3.1. Analiza i opis przykładu znajduje się poniżej listingu 3.1.

Listing 3.1. Prosty przykład samo–modyfikującego się kodu w Asemblerze MASM64
;http://haker.info/ extrn ExitProcess : proc extrn VirtualProtect : proc .const PAGE_EXECUTE_READWRITE equ 040h .data oldProtect dd 0 .code Main proc ;zmiana atrybutów bloku pamięci na: ;wykonanie, odczyt i zapis sub rsp, 28h mov r9, offset oldProtect mov r8, PAGE_EXECUTE_READWRITE mov rdx, 15h mov rcx, offset _mutable call VirtualProtect add rsp, 28h _mutable: ;wpisanie do rejestru RAX adresu etykiety _mutable mov rbx, offset _mutable ;nadpisanie wskazywanych instrukcji czterema rozkazami NOP mov dword ptr [rbx+011h], 090909090h ;poniżej instrukcje do nadpisania xor rcx, rcx ret _exit: sub rsp, 8h xor rcx, rcx call ExitProcess Main endp end

Skrypt budowania kodu MASM64 do pliku wykonywalnego EXE (build.bat)
@echo off ml64.exe prog1.asm /link /entry:Main /subsystem:windows /defaultlib:"kernel32.Lib" /defaultlib:"user32.Lib" /LARGEADDRESSAWARE:NO pause

W funkcji głównej Main na początku jest ustawienie blokowi pamięci atrybutów PAGE_EXECUTE_READWRITE co pozwala, oprócz wykonywania, także dokonywać zapisu i odczytu kodu aplikacji. Od etykiety _mutable rozpoczyna się główny kod, który dokonuje samo–modyfikacji.

Najpierw do rejestru RBX kopiowany jest adres etykiety _mutable. Następnie instrukcja mov dokonuje wpisania czterech bajtów (podwójnego słowa) o wartości 090909090h pod adres rbx+11h. W rejestrze RBX jest adres etykiety _mutable do którego dodawane jest 17 bajtów (011h). Jest tak, gdyż 17 bajtów od etykiety _mutable znajduje się początek kodu, który będzie modyfikowany.

Instrukcje xor rcx, rcx oraz ret po przetłumaczeniu na kod maszynowy (opkody) mają rozmiar 4 bajtów. Z tego powodu wstawiane są cztery rozkazy nop, które mają razem również 4 bajty.

Kod z poprzedniego listingu 3.1. uruchomiony w debuggerze x64dbg przedstawiono wraz z dodatkowym wyjaśnieniem na rysunku 3.1. poniżej.


Rysunek 3.1. Prosty przykład samo–modyfikującego się kodu pod debuggerem x64dbg

Na wideo poniżej przedstawiono uruchomienie kodu z poprzedniego listingu 3.1. pod narzędziem typu debugger (x64dbg).


4. Rozszyfrowanie i uruchomienie kodu w pamięci

Technika modyfikacji własnego kodu może być używana na różne sposoby. W poprzednim rozdziale było to nadpisywanie instrukcji. Tutaj zaprezentowane zostanie rozszyfrowywanie kodu, a następnie uruchomienie go. Artykuł ten nie przedstawia oczywiście wszystkich możliwości. Z innych metod to spotyka się też np. modyfikację wartości argumentów w kodzie, wymazywanie kodu z pamięci po wykonaniu i inne.

W przykładzie zaprezentowanym poniżej na listingu 4.1. zaszyfrowany kod do wykonania znajduje się w tablicy bajtów. Jest to sekcja danych. System Windows® posiada zabezpieczenie o nazwie Data Execution Prevention (DEP), co oznacza w języku polskim zabezpieczenie przed wykonywaniem danych. Z tego powodu dane nie mogą zostać wykonane jako instrukcje. Chyba, że zmieni się im atrybuty pamięci. Zatem tak jak w poprzednim rozdziale zostanie użyta funkcja VirtualProtect.

Przedstawiając w punktach działanie programu z listingu 4.1., lista wyglądałby tak jak poniżej:

  • Rozszyfrowanie kodu z tablicy bajtów w pętli for.
  • Ustawienie pamięci (tablicy) atrybutu wykonywalności (ang. execute) za pomocą funkcji VirtualProtect.
  • Rzutowanie (konwersja) tablicy bajtów na wskaźnik na funkcję.
  • Wywołanie kodu z tablicy bajtów (funkcja func()).
Listing 4.1. Prosty przykład samo–modyfikującego się kodu w Visual C++
#include <Windows.h> BYTE encryptedCode[] = { 0x97, //0x97 xor 7 = 0x90 = NOP opcode 0x97, //j.w. 0x97, //j.w. 0xC4 //0xC4 xor 7 = 0xC3 = RET opcode }; int wmain(int argc, TCHAR* argv[]) { UINT codeLength = sizeof(encryptedCode); DWORD oldProtect = NULL; typedef int (*ExecuteFunc)(); for (auto &byte : encryptedCode) { byte = byte xor 7; } VirtualProtect(encryptedCode, codeLength, PAGE_EXECUTE_READWRITE, &oldProtect); //wywołanie zmodyfikowanego (rozszyfrowanego) kodu ExecuteFunc func = (int(*)())&encryptedCode; func(); #if _DEBUG system("pause"); #endif return EXIT_SUCCESS; }

5. Podsumowanie

Wpis ten jest tylko wprowadzeniem do tworzenia kodu samo–modyfikującego się. Nie muszę chyba pisać, że technika ta daje na prawdę duże możliwości. Dziękuję za czas poświęcony na przeczytanie wpisu i pozdrawiam.

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.