Kod samo–modyfikujący się (x64)
🕰 ✒️ Dawid Farbaniec 📄 1190 słów0x01. Słowem wstępu
Program, 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.
0x02. 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.
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. write ).
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:
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.
0x03. 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 MASM64Skrypt budowania kodu MASM64 do pliku wykonywalnego EXE (build.bat)
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.
Na wideo poniżej przedstawiono uruchomienie kodu z poprzedniego listingu 3.1. pod narzędziem typu debugger (x64dbg).
0x04. 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()
).
0x05. 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.
Wykaz literatury (bibliografia)
[1] Advanced Micro Devices Inc., 2017 – AMD64 Architecture Programmer's Manual[2] Intel Corporation, 2019 – Intel 64 and IA-32 Architectures Software Developer's Manual
[3] Microsoft Corporation, 2019 – https://docs.microsoft.com/pl-pl/cpp/assembler/masm/masm-for-x64-ml64-exe [dostęp: 28.07.2020 r.]