Uruchamianie kodu w pamięci programu

Temat artykułu może wydawać się dziwny, wszak wszystkie programy uruchamiane są w pamięci. W tym przypadku jednak chodzi o inne zagadnienie – jak uruchomić kod innego programu w naszym programie, Xzibit pewnie byłby szczerze zachwycony takim rozwiązaniem. Oprócz całkiem niezłego szpanu na dzielnicy, zyskujemy coś więcej – możliwość przemycenia (zaszyfrowanego) kodu i uruchomienie go niezauważalnie dla użytkownika, jak i również dla programów antywirusowych (nie wszystkich, ale dla wystarczającej większości). Przerażające? Miało być. Trudne? Dość.

Na początek określmy, co dokładnie ma się dziać: program A (nazwijmy go „runner”), ma za zadanie uruchomić program B – mając jedynie jego kod wyciągnięty z pliku EXE. W jakiś sposób należy umieścić kod w pamięci, a następnie go wykonać, oczywiście w Windows API nie ma funkcji RunCodeFromMemory (dobra funkcja do siania zamieszania), dlatego w inny sposób należy zmusić system do wykonania przemyconego kodu.

Najprostszym i pewnie najlepszym rozwiązaniem jest stworzenie w runnerze nowego procesu – „dummy” – i umiejętne wpakowanie do niego kodu z programu B. W ten sposób system wykona wszczepiony kod bez zająknięcia.

Mając nakreślony już ogólny wygląd programu, można zapisać co zatem runner ma robić:

  1. Stwórz nowy proces przy pomocy CreateProcess, może być nawet na podstawie runnera. Ważne, żeby stworzyć go z flagą CREATE_SUSPENDED, dzięki czemu uruchomienie będzie wstrzymane dając nam czas na „poprawki”.
  2. Użyj funkcji GetThreadContext, aby otrzymać informacje o kontekście wątku. Z tych danych określ:
    1. Adres bazowy (Base Address)
    2. Adres wejściowy programu (Entry Point)
  3. Wczytaj do pamięci kod do wszczepienia. Można użyć windowsowego ReadFile, ale na pewno system nie będzie miał nic przeciwko strumieniom czy poczciwemu fread.
  4. Ten punkt będzie najtrudniejszy. Musimy określić jak skonstruowany jest wczytany kod, tzn. rozmiar nagłówka, rozmiar kodu. Te dane można pobrać automatycznie z nagłówka pliku EXE. W artykule zrobię to jednak ręcznie, aby dokładniej pokazać jak to wygląda w pamięci.
  5. Kolejny trudny punkt, należy określić adres pod jakim znajduje się sekcja kodu wczytanego procesu.
  6. Mając powyższe dane, należy użyć funkcji WriteProcessMemory aby wpisać do pamięci procesu nowy kod. Odpowiednio w sekcji nagłówka wpisać zawartość nagłówka kodu, a w sekcji kodu – kod.
  7. Ustawić nowy adres wejściowy procesu na ten pozyskany z wszczepianego kodu.
  8. Użyć funkcji ResumeThread aby wznowić proces.

1. Tworzenie procesu „nosiciela”

W tym punkcie akurat nie o czym się rozpisywać. Korzystamy z funkcji CreateProcess aby stworzyć nowy proces.

dummy.exe jest to dowolny plik EXE, który posłuży nam za nosiciela, może nim być nawet sam runner.

Tak stworzony proces jest wstrzymany i w tym momencie możemy z nim zrobić co chcemy, a chcemy dużo, zatem…

2. Pobieranie kontekstu wątku

Kontekst wątku dostarczy nam informacji o zawartości rejestrów procesora, ponieważ wątek jest wstrzymany już na samym początku, rejestry zawierać będą informacje startowe, czyli to co jest dla nas szczególnie interesujące. Skupimy się na dwóch rejestrach:

Korzystając z odpowiednich źródeł, stwierdzamy, że ImageBaseAddress (adres bazowy kodu) znajduje się w PEB pod offsetem 8, a ponieważ adres PEB jest zapisany w rejestrze EBX, musimy pobrać wartość spod adresu EBX+8. Do tego celu posłuży nam znana (a może i nie) funkcja ReadProcessMemory.

Adres bazowy to adres w pamięci pod którym trzymany jest, podzielony na sekcje, kod programu. Na samym początku znajduje się nagłówek, następnie kod i pozostałe dane.

Warto mieć na uwadze, że to co znajduje się wewnątrz pliku EXE, nie jest dokładnym odwzorowaniem tego, co będzie w pamięci. Sprawi to trochę kłopotów w późniejszym etapie.

3. Wczytywanie kodu

Przyszła pora na wczytanie kodu, który ma zostać uruchomiony. U mnie będzie to mały program „Hello World”, napisany dodatkowo w Assemblerze. Nie wynika to z mojego zboczenia związanego z tym językiem, ale z ułatwienia pracy. Program napisałem stosując model pamięci flat, dzięki czemu w pamięci figuruje jedynie jedna sekcja – kod, znacznie ułatwia to wszczepienie oraz (mi) wytłumaczenie tego zawiłego tematu.

Program robiący identycznie to samo, ale napisany w C, składa się z 8 sekcji, gdy zbudowany w trybie release, oraz 14 z wykorzystaniem trybu debug. W liczbach nie uwzględniłem nagłówka, który jako tako sekcją nie jest.

Na dole strony można pobrać skompilowany „hello.exe”, albo zrobić to samemu w FASM:

Trzymając się konwencji, że (prawie) wszystko zostało napisane w oparciu o funkcje WinAPI, wczytywanie również tak zostanie wykonane. Do tego celu potrzebujemy funkcji CreateFile – otwarcie pliku, oraz ReadFile – czytanie z pliku. Bufor do przechowania zawartości pliku stworzymy przy pomocy LocalAlloc.

4. i 5. Gromadzenie informacji o plikach

Do tego możemy wykorzystać OllyDBG albo PEditor. W artykule użyję tego drugiego, ponieważ ma bardziej przejrzysty interfejs dla osób niemających wcześniej styczności z Olly.

Nosiciel

Najpierw zgromadzimy potrzebne informacje o procesie do którego będziemy wstrzykiwać. Otwieramy PEditor a następnie upuszczamy na niego EXE’k nosiciela.

Notujemy:

  • Image Base – 0x00400000 – Pod jakim adresem w pamięci rozpoczyna się program
  • Section Alignment – 0x00001000 – Mówi o rozmieszczeniu sekcji, coś jak klastry na dysku. Jeżeli rozmiar sekcji jest mniejszy niż SA, to i tak dalej jej rozmiar jest 0x1000, ale jeżeli jej rozmiar jest większy, np. 0x1400, to będzie liczony jako 0x2000.
  • Base of Code – 0x00001000 – Gdzie się rozpoczyna sekcja kodu

Teraz zerknijmy na sekcje, w tym celu należy kliknąć przycisk sections.

Ohoho, sporo tego, ale nas interesuje głównie sekcja .text zawierająca kod, właśnie do niej będziemy wstrzykiwać.

Dzięki temu, że nasz „hello.exe” (to co wstrzykujemy) składa się tylko z jednej sekcji, cała operacja ograniczy się do odpowiedniego przekopiowania jej zawartości do nosiciela. No i oczywiście jeszcze podmiana nagłówka. Gdyby jednak tych sekcji było więcej, każdą należałoby wstawić w odpowiednie miejsce – dużo roboty.

Z odczytanych danych wnioskujemy: program w pamięci zaczyna się pod wirtualnym adresem 0x400000, w tym miejscu znajduje się nagłówek, którego wirtualny rozmiar jest 0x1000, wynika to z Section Alignment. Zaraz po nim znajduje się sekcja .text zawierająca kod.

Program wstrzykiwany

Otwieramy go tak samo jak poprzedni.

Notujemy:

  • Entry Point – 0x00001000 – Punkt wejściowy programu
  • Size of Headers – 0x00000200 – Rozmiar nagłówków

I z tabeli sekcji:

  • Raw Offset – 0x00000200 – Pozycja sekcji (w pliku!)
  • Raw Size – 0x00000200 – Rozmiar sekcji (w pliku!)

I co nam to daje? Dość dużo:

Wiedząc, że konstrukcja pliku EXE to mniej więcej: [{nagłówek} {pozostałe sekcje}], oraz dodając do tego informacje o ich rozmiarach (obie mają rozmiar 0x200 czyli 512 bajtów) wiemy ile i skąd mamy kopiować.

Podsumowanie

Z pliku „hello.exe kopiujemy pierwsze 512 bajtów nagłówka pod adres 0x00400000 nosiciela. Następnie kopiujemy pozostałe 512 bajtów kodu (sekcja .flat) pod adres 0x00401000.

Prawda, że proste ;)? A pomyślcie ile jest zabawy przy kilku sekcjach.

Należy zapamiętać, że wirtualny adres gdzie rozpoczyna się program, może być zupełnie inny w zależności od systemu i programu. Dlatego w naszym programie zapisaliśmy sobie pod zmienną adr adres bazowy.

No dobra, a co jeżeli „hello.exe” byłby większy niż nosiciel? Jeżeli przyjrzymy się informacjom ze screenów, widać jeszcze wartość Size of Image, jest to rozmiar jaki zajmuje program w pamięci, dla hello jest to 0x2000 (0x1000 nagłówka i 0x1000 kodu), nosiciel ma dużo więcej. Ważnym jest, żeby nosiciel był zawsze w pamięci obszerniejszy, wszak nic się nie stanie, gdy nie wypełnimy całej jego pamięci, problem jest, gdy nie mamy na to miejsca.

Trochę was okłamałem z tym „zawsze”, nosiciel może być mniejszy, ale wiąże się to z dodatkowymi operacjami, a mianowicie należy zarezerwować dostateczną ilość pamięci i podczepić ją do procesu. Jak nabiorę chęci to dopiszę do artykułu punkt opisujący to zagadnienie.

6. Wstrzykiwanie

Wreszcie przyszła pora na najważniejszą część artykułu, wbrew pozorom nie będzie to trudna rzecz, pod warunkiem, że poprawnie zanotowaliśmy dane we wcześniejszym punkcie.

Od razu podam zbiór literatury:

Procedura postępowania jest prosta:

  1. Zezwól na zapis w danym fragmencie pamięci
  2. Zapisz odpowiednie dane
  3. Przywróć dawne uprawnienia

Te operacje wykonamy na sekcji nagłówka oraz kodu.

Wszystkie te liczby i przesunięcia jasno wynikają ze zgromadzonych wcześniej informacji i jak najbardziej jest możliwe zautomatyzowanie tego procesu, wystarczy zapoznać się ze strukturą nagłówka PE i napisać kod, który będzie te informacje wyciągał.

7. Ustawianie nowego punktu wejściowego programu

Wszystko jest już na swoim miejscu, pozostało nam zaktualizowania punktu wejściowego programu (EP), czyli miejsca, od którego będzie on wykonywany. Gdy proces został wstrzymany zaraz na starcie, rejestr EAX zawiera adres EP, więc wystarczy go nadpisać, nie ma w tym żadnej większej filozofii.

Aby podmienić zawartość rejestru, należy zmodyfikować kontekst wątku, który wcześniej pozyskaliśmy, a następnie ustawić go przy pomocy SetThreadContext.

W punkcie „4. i 5.” zanotowaliśmy, że EP programu „hello.exe” wynosi 0x1000. Tak de facto nie jest to adres, a przesunięcie względem adresu bazowego, dlatego posiłkując się wyższą matematyką, obliczamy faktyczny adres: 0x00400000 + 0x00001000 = 0x00401000. Taką właśnie wartość wpiszemy do rejestru EAX.

Już? Tak, już :).

8. Wznawianie wątku

I ostatnia czynność – wznowienie wątku, czyli ożywiamy Frankensteina.

 9. Iii…

Działający program

I jest „Hello World!”, co oznacza że cały proces się powiódł.

Epilog

Jak sami widzieliście, roboty było dużo, głównie przy zbieraniu informacji o plikach, dlatego szczerze zachęcam do napisania aplikacji, która sama wszystkie niezbędne rzeczy wyciągnie sama. Zdaję sobie sprawę, że nie jest to łatwe, ale z pewnością będzie bardziej uniwersalne niż wpisanie wszystkich wartości „na twardo” .

Łatwiej będzie zrozumieć, jeżeli wyobrazicie sobie, że to co robiliśmy, to jest prawie to samo co robi system, gdy uruchamia program EXE. Najpierw czyta informacje z nagłówka, rezerwuje pamięć, rozmieszcza w niej odpowiednio odczytane sekcje, a następnie ustawia się na punkcie wejściowym i rozpoczyna wykonywanie.

Obiecany download

run_in_memory.7z – Pliki i źródła

[guest@itachi.pl:~]$
    Pants

    Geniusz ! Ciut trudniejsze od wstrzykiwania DLL heh, ale sposob dobry a artykul majstersztyk. czy dobrze zrozumialem ze sposob z artykulu dziala odwrotnie?
    Zamiast wstrzykiwac kod do glownej aplikacji – co jest wyczulone przez antywirusy, to program z kodem „hello word” tworzy proces w ktorym wstrzykiwana jest glowna aplikacja? 😀

    Prosilbym dokonczenie watku z .exe a ladowaniem go do pamieci :).
    Dziekuje za artykul, jest miazga.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *