Odczytywanie pamięci wskaźnikami generycznymi

Ostatania modyfikacja: 28 września 2017, kategoria: C++

Czy zastanawiałeś się kiedyś do czego przydają się wskaźniki generyczne? Jednym z ciekawych zastosowań wskaźników generycznych jest odczytywanie pamięci. W połączeniu z dll injection uzyskujemy na prawdę wygodne narzędzie. Możliwe, że ten artykuł nie zmieni Twojego życia, jednak postanowiłem opisać w nim tę prostą metodę.

W tym artykule przeczytasz o tym, jak odczytywać pamięć procesu po wstrzyknięciu do niego biblioteki DLL (dll injection).

Co to jest wskaźnik generyczny?

Zmienna wskaźnika (czyli wskaźnik) to zmienna wskazująca na jakiś obszar pamięci. Wskaźnik musi być tego samego typu jakiego jest zmienna na którą wskazuje. Oprócz zmiennych, wskaźniki mogą wskazywać na różne obiekty.

Wskaźnikiem generycznym nazywamy wskaźnik typu void. Oznacza to, że wskazuje on na jakiś obszar pamięci z góry nie określony. Wskaźniki generyczne można bardzo łatwo rzutować na wszelkie inne typy. Rzutowanie wskaźnika generycznego jest jedynym sposobem w jaki można wyświetlić wskazywaną przez niego wartość.

int pesel = 1234;
void *wsk = &pesel;

*(int*)wsk = 4321;              //rzutowanie na *int
cout << *(int*)wsk << endl;     //rzutowanie na *int

Więcej o wskaźnikach dowiesz się z artykułu wskaźniki C++.

 Odczytywanie pamięci i DLL Injection

Jeżeli interesujesz się tematem inżynierii wstecznej zapewne nie raz korzystałeś z dll injection. Jest to technika przydatna szczególnie podczas pisania różnych trainerów do gier lub zwiększania funkcjonalności programów.

Po wstrzyknięciu biblioteki DLL, nie musisz używać ReadProcessMemory aby odczytać pamięć z procesu, ponieważ już się w nim znajdujesz (w odpalonym wewnątrz procesu wątku). Posiadając adres konkretnej zmiennej w pamięci, możesz ją odczytać lub nadpisać, tworząc właśnie wskaźnik generyczny.

Posłużmy się gotowymi adresami pamięci z artykułu odczytywanie pamięci procesów C++. Odczytywaliśmy w nim wartości zmiennych liczbowych całkowitych z gry Saper (Saper z Windows XP!).

  • Adres ilości min: 0x1005194

Posiadamy adres oraz szkielet pustej DLLki. Bibliotekę DLL będę wstrzykiwał za pomocą programu Winject. Obecnie wygląda ona następująco:

extern "C" BOOL __stdcall DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
    switch(Reason)
    {
        case DLL_PROCESS_ATTACH:

        break;
    }
    return TRUE;
}

Utwórzmy teraz wskaźnik generyczny na nasz adres.

Możemy odczytywać zarówno zmienne liczbowe jak i tekstowe. W przypadku odczytywania zmiennych liczbowych warto rzutować na BYTE* lub unsigned char*. Na końcu trzeba także rzutować niejawnie na int aby nie dostać znaczka tylko liczbę. W przypadku zmiennych tekstowych oczywiście rzutujemy na char*.

Kompletny kod wygląda następująco:

#include <fstream>
#include <string>

using namespace std;

extern "C" BOOL __stdcall DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
    switch(Reason)
    {
        case DLL_PROCESS_ATTACH:

            void* wsk = (void*)0x1005194;
            int miny = (int)*(unsigned char*)wsk;   // odczytujemy wartosc min;

            miny = miny * 2;    // mnozymy razy 2

            *(char*)wsk = miny;     // zapisujemy nowa wartosc zmiennej

        break;
    }
    return TRUE;
}

Aby efekt był widoczny w oknie sapera wystarczy oznaczyć flagę prawym przyciskiem myszy w dowolnym miejscu.

Dzięki takiemu rozwiązaniu oszczędzamy czas oraz kod staje się krótszy. Nie musimy zdobywać uchwytu ani identyfikatora procesu.

Użytkownik BonusBGC napisał:

08 maja 2014


Artykuł przydatny (pierwszy raz widzę tworzenie wskaźników do zmiennych :D )

przydałby się jeszcze poradnik o szukaniu w hexach. struktur , funkcji itd, bo to podstawa do zabawy w DLL injection :> .
nastepny ewentualnie o pakietach internetowych (może pisanie BOTa do gry? :)
np. – sterowanie postacią bez używania klienta

Użytkownik Magdalena napisał:

03 września 2019


Ten void-pointer nie jest w tym artykule kompletnie potrzebny.
Odczyt/zapis arbitralnego adresu można zrobić wskaźnikiem każdego typu.
Nie trzeba też rzutować na int aby uzyskać liczbę zamiast znaku.
Podczas zapisów do pamięci mamy do czynienia z liczbami.
Typ char to tylko liczba rozmiaru jednego bajta.
Typ jest interpretowany jako znak podczas użycia przykładowo cout lub printf z formaterem %c.
Trzeba jedynie uważać na rozmiar typu bazowego aby pisać/czytać bajty w kolejności zgodniej z CPU-endianness.

Ten kod może wyglądać tak:

switch(Reason)
    {
        case DLL_PROCESS_ATTACH:

            unsigned char* miny = (unsigned char*)0x1005194;
            *miny *= 2;

        break;
    }

				
		
				

		

Zachęcam Cię do zostawienia komentarza!

Ilość znaków: 0