Jak utrudnić życie crackerowi. Zabezpieczenia antypirackie dla programistów

Sejf

Metody ochrony oprogramowania przed złamaniem, w tym zabezpieczenia antypirackie i triki antydebug pozwalające na wykrywanie popularnych narzędzi crackerskich.

Zastanawialiście się kiedyś, dlaczego w internecie można znaleźć cracki do prawie każdego programu jaki tylko pojawi się na rynku? Może sami jesteście programistami i już doświadczyliście tego na własnej skórze? Jeśli tak jest lub jeśli po prostu interesują Was zabezpieczenia antypirackie i chcecie wiedzieć jak się przed tym ochronić, zachęcam do dalszej lektury.

Skąd się biorą cracki?

Publikowane w internecie cracki, to zazwyczaj małe programy, które modyfikują aplikację, sprawiając, że przykładowo „znika” ograniczenie czasowe, co do użytkowania programu, lub dzięki zastosowaniu cracka można zarejestrować aplikację na dowolne dane.

Skąd biorą się cracki czy np. generatory numerów seryjnych? Są one tworem crackerów, czyli ludzi, którzy zajmują się usuwaniem zabezpieczeń z programów komputerowych, co odróżnia ich od hakerów, którzy przełamują zabezpieczenia w systemach sieciowych.

Debugger

Aby cracker mógł przełamać zabezpieczenie danego programu, najczęściej musi zlokalizować punkt w kodzie programu, gdzie wywoływane są np. procedury weryfikujące poprawność wprowadzonych danych rejestracyjnych.

Do tego celu najczęściej wykorzystywanym narzędziem jest debugger, czyli program pozwalający śledzić wykonywanie kodu programu na poziomie instrukcji asemblera.

Cracker śledząc kod programu, jest w stanie przechwycić (stosując tzw. pułapki – z ang. breakpoint) wywoływanie funkcji systemowych, odpowiedzialnych przykładowo za odczytanie numeru seryjnego z okna edycyjnego programu. Następnie śledząc dalsze wykonywanie programu, cracker może zlokalizować miejsce, gdzie występuje sprawdzenie wprowadzonego numeru seryjnego z oryginalnym, dzięki czemu może uzyskać poprawny numer seryjny.

Okno debuggera OllyDbg
Okno debuggera OllyDbg.

Jak się bronić przed złamaniem zabezpieczeń?

Mówi się, że każde zabezpieczenie można przełamać i to tylko kwestia czasu. Skoro tak, to dobrym pomysłem jest, aby tak utrudnić życie crackera, żeby musiał poświecić dużo więcej czasu, na jego złamanie, co z kolei może spowodować, że nie będzie w stanie złamać naszego programu i po prostu zrezygnuje z niego, na rzecz innego, słabiej zabezpieczonego programu.

Najczęściej stosowaną metodą pozwalającą utrudnić analizę programu, są tzw. triki antydebug. Polega to na tym, że zanim uruchomimy nasz program, sprawdzamy obecność narzędzi wykorzystywanych przez crackera i w przypadku ich wykrycia, możemy przykładowo zawiesić wykonywanie programu. Można zastosować także bardziej wyrafinowane metody, przykładowo, w razie wykrycia debuggera, pozwalamy programowi na normalne działanie, ale np. ustawiamy timer, który spowoduje, że program zawiesi się lub wyłączy po pięciu minutach działania.

Tego typu metody są bardzo skuteczne, gdyż wymagają dogłębnej analizy programu, w celu wykrycia nieprawidłowości. Najgorszym rozwiązaniem jest wyświetlanie informacji, że np. został wykryty debugger, to jeden z najczęściej popełnianych błędów, cracker, który zna treść tej wiadomości z łatwością może znaleźć kod w programie, gdzie jest ona wywoływana i usunąć kod odpowiedzialny za sprawdzanie obecności debuggera.

Należy nadmienić także, że metody antydebug można podzielić na dwie kategorie, te, które korzystają z udokumentowanych funkcji systemu Windows, oraz wykorzystujące luki w systemie operacyjnym. W tym artykule opiszę metody udokumentowane, gdyż gwarantują one poprawne działanie programu, oraz nie ograniczają ich zastosowań w różnych wersjach systemu Windows.

Wykrywanie debuggera

Obecnie najpopularniejszym debuggerem jest SoftIce firmy Numega, oprócz tego istnieje cała masa innych narzędzi jak TRW, OllyDbg itd. Program debuggera zazwyczaj działa na wyższym poziomie uprzywilejowania (debuggery systemowe) niż normalne programy, w takim wypadku program debuggera jest dostarczony w formie sterownika systemowego. System Windows umożliwia komunikację normalnych programów ze sterownikami, wykorzystując funkcję DeviceIoControl(), ale zanim będzie można komunikować się ze sterownikiem, należy do niego uzyskać dostęp. Robi się to poprzez wywołanie funkcji WinApi CreateFile() z nazwą sterownika, podaną w specjalnej postaci \\.\STEROWNIK. Dzięki tej funkcji możemy wykryć sterownik debuggera, przykładowy kod:

///////////////////////////////////////////////////////////////////////////////
//
// HANDLE CheckDriver(char *lpszDriver)
//
// funkcja probuje otworzyc dostep do podanego sterownika systemowego
//
// na wyjsciu:
// uchwyt sterownika lub INVALID_HANDLE_VALUE w przypadku braku sterownika
//
///////////////////////////////////////////////////////////////////////////////

HANDLE CheckDriver(char *lpszDriver)
{
    return ( CreateFile(lpszDriver, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL) );
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsSice()
//
// funkcja sprawdza obecnosc debuggera SoftIce w wersji dla systemow
// Windows 9x
//
// na wyjsciu:
// jesli debugger SoftIce jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsSice()
{
    return ( CheckDriver("\\\\.\\SICE") != INVALID_HANDLE_VALUE ? TRUE : FALSE);
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsNtice()
//
// funkcja sprawdza obecnosc debuggera SoftIce w wersji dla systemow
// Windows NT/2K/XP
//
// na wyjsciu:
// jesli debugger SoftIce jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsNtice()
{
    return ( CheckDriver("\\\\.\\NTICE") != INVALID_HANDLE_VALUE ? TRUE : FALSE);
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsSiwdebug()
//
// funkcja sprawdza obecnosc dodatkowego sterownika zainstalowanego
// wraz z debuggerem SoftIce dla systemow Windows 9x, obecnosc tego
// sterownika w systemie, swiadczy o obecnosci debuggera
//
// na wyjsciu:
// jesli debugger SoftIce jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsSiwdebug()
{
    // otworz pomocniczy sterownik
    CheckDriver("\\\\.\\SIWDEBUG");

    return ( GetLastError() != ERROR_FILE_NOT_FOUND ? TRUE : FALSE);
}

Wykrywanie debuggera SoftIce poprzez sprawdzanie jego sterownika, obecnie nie jest zbyt skuteczną metodą, gdyż crackerzy modyfikują swoje narzędzia pracy, tak aby nie była możliwa ich detekcja.

Nieco inną metodą pozwalającą stwierdzić, że w systemie zainstalowany jest debugger, jest sprawdzanie kluczy rejestru Windows, na wstępowanie tam wpisów jakie pozostawił np. program instalacyjny debuggera lub kluczy zawierających ustawienia konfiguracyjne debuggera.

Wpisy debuggera w rejestrze Windows
Wpisy debuggera w rejestrze Windows.

Metoda ta jednak ma pewną wadę, nie pozwala na 100% stwierdzić, że debugger jest aktywny, bo wpisy w rejestrze mogły pozostać po poprzedniej instalacji. Przykładowy kod:

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsReg()
//
// funkcja sprawdza obecnosc galezi rejestru Windows
//
// na wyjsciu:
// jesli w rejestrze Windows znajduje sie podana galaz, funkcja zwroci
// TRUE w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsReg(char *lpszReg)
{
    HKEY phkResult;
    DWORD iResult;

    // otworz galaz rejestru Windows w HKEY_LOCAL_MACHINE
    iResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, lpszReg, 0, KEY_ALL_ACCESS, &phkResult);

    // zamknij uchwyt
    RegCloseKey(phkResult);

    return (iResult == ERROR_SUCCESS ? TRUE : FALSE);
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsSiceReg1()
//
// funkcja sprawdza obecnosc galezi rejestru wykorzystywanych przez
// debugger SoftIce do trzymania informacji potrzebnych przy
// deinstalacji debuggera
//
// na wyjsciu:
// jesli w rejestrze Windows znajduje sie galaz debuggera, funkcja zwroci
// TRUE w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsSiceReg1()
{
    return IsReg("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\SoftICE");
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsSiceReg2()
//
// funkcja sprawdza obecnosc galezi rejestru wykorzystywanych przez
// debugger SoftIce do trzymania informacji konfiguracyjnych
//
// na wyjsciu:
// jesli w rejestrze Windows znajduje sie galaz debuggera, funkcja zwroci
// TRUE w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsSiceReg2()
{
    return IsReg("SOFTWARE\\NuMega\\SoftICE");
}

Wykrywanie debuggera można także zrealizować analizując wpisy w pliku „autoexec.bat”, gdzie znajduje się ścieżka do programu debuggera uruchamianego podczas startu systemu.

SET SOUND=C:\PROGRA~1\CREATIVE\CTSND
SET MIDI=SYNTH:1 MAP:E MODE:0
SET BLASTER=A220 I5 D1 H5 P330 E620 T6
SET Path=C:\WINDOWS;C:\WINDOWS\COMMAND;E:\DEV\MASM\;E:\DEV\MASM\BIN;E:\DEV\TASM;
mode con codepage prepare=((852) C:\WINDOWS\COMMAND\ega.cpi)
mode con codepage select=852
keyb pl,,C:\WINDOWS\COMMAND\keybrd4.sys
C:\PROGRA~1\Numega\Softice\winice.exe

Ostatnia linijka służy do ładowania debbugera. Sprawdzając zawartość pliku „autoexec.bat” możemy stwierdzić obecność debuggera:

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsAutoexec(char *lpString)
//
// funkcja szuka w pliku autoexec.bat podanych wpisow
//
// na wyjsciu:
// jesli znaleziono podany wpis, funkcja zwroci TRUE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsAutoexec(char *lpString)
{
    BOOL bResult = FALSE;
    int iSize, i;
    char *lpMap,*lpAutoexec, lpFile[256];
    HANDLE hFile, hMap;

    // odczytaj sciezke katalogu Windows
    GetWindowsDirectory(lpFile, 256);

    // zbuduj sciezke do pliku autoexec.bat
    strcpy(&lpFile[3], "autoexec.bat");

    // otworz plik
    hFile = CreateFile(lpFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        // pobierz rozmiar pliku
        iSize = GetFileSize(hFile, NULL);

        // jesli rozmiar 0 wyjdz
        if (iSize)
        {
            // utworz mape pliku
            hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);

            if (hMap)
            {
                // mapuj plik do pamieci
                lpMap = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

                if (lpMap != 0)
                {
                    lpAutoexec = lpMap;

                    // skanuj zawartosc pliku autoexec.bat
                    while (iSize--)
                    {
                        if (strnicmp(lpAutoexec++, lpString, strlen(lpString)) == 0)
                        {
                            bResult = TRUE;
                            break;
                        }
                    }
                }

                UnmapViewOfFile(lpMap);
            }

            CloseHandle(hMap);
        }

        CloseHandle(hFile);
    }

    return bResult;
}

Następnie w programie należy sprawdzić, czy podane wpisy są obecne w pliku:

// wykrywanie wpisow debuggera w autoexec.bat
if (IsAutoexec("nume") || IsAutoexec("softic"))
{
    ExitProcess(-1);
}

Wykrywanie pułapek debuggera

Na początku artykułu wspomniałem o pułapkach debuggera, crakerzy stosują je, aby przykładowo przechwycić wywołanie funkcji odczytującej numer seryjny z okna edycyjnego. Pułapki debuggera działają na tej zasadzie, iż pierwszy bajt procedury, na którą zastawiona jest pułapka, jest nadpisywany specjalną instrukcją asemblera int 3. Gdy debugger natrafi na instrukcję int 3, zatrzymuje działanie programu i przekazuje sterowanie w ręce crackera. Reprezentacja heksadecymalna tej instrukcji to wartość 0xCC, aby wykryć aktywną pułapkę wystarczy sprawdzić, czy pierwszy bajt procedury jest równy właśnie 0xCC.

Przykładowo, jeśli w procedurze rejestracyjnej wykorzystujemy funkcję WinApi GetDlgItemText() można przed jej wywołaniem sprawdzić, czy na jej adresie cracker zastawił pułapkę i w razie jej wykrycia, można zakończyć działanie aplikacji.

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsBpx(char *lpszModule,char *lpszFunction)
//
// funkcja sprawdza, czy na adresie podanej funkcji zastawiona
// pulapka debuggera
//
// na wyjsciu:
// jesli pulapka jest zastawiona, funkcja zwraca TRUE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsBpx(char *lpszModule,char *lpszFunction)
{
    HINSTANCE hLibrary;
    DWORD lpProc;

    // sprawdz czy biblioteka jest juz zaladowana
    hLibrary = GetModuleHandle(lpszModule);

    // zaladuj biblioteke
    if (hLibrary == NULL)
    {
        hLibrary = LoadLibrary(lpszModule);
    }

    if (hLibrary != NULL)
    {
        // pobierz adres procedury
        lpProc = (DWORD)GetProcAddress(hLibrary, lpszFunction);

        if (lpProc != 0)
        {
            // sprawdz pierwszy bajt procedury na
            // obecnosc pulapki
            if (*(BYTE *)lpProc == 0xCC) return TRUE;
        }

    }

    return FALSE;
}

...

// w procedurze rejestracyjnej sprawdzamy, czy została założona pułapka
if (IsBpx("USER32.dll", "GetDlgItemTextA") == TRUE)
{
    ExitProcess(-1);
}

Jeśli np. nasze zabezpieczenie opiera się na pliku kluczu, zamiast sprawdzać funkcję GetDlgItemTextA, należy sprawdzić funkcję, którą wykorzystujemy do otwarcia pliku klucza i w razie wykrycia pułapki, można wprowadzić program w nieskończoną pętle np.:

// powtarzaj wykonywanie kodu, dopóki zastawiona pułapka
while(IsBpx("KERNEL32.dll", "CreateFileA") == TRUE);

Korzystając z funkcji IsBpx można także wykryć aktywnego debuggera SoftIce w systemach Windows NT, 2K i XP. Debugger SoftIce pod tymi systemami, domyślnie zakłada pułapkę (na własne potrzeby) na funkcję WinApi SetUnhandledExceptionFilter(), wykrycie pułapki świadczy o obecności debuggera w systemie.

// sprawdź obecność debuggera SoftIce NT
if (IsBpx("KERNEL32.dll", "SetUnhandledExceptionFilter") == TRUE)
{
    ExitProcess(-1);
}

Monitory systemowe

Crackerzy oprócz debuggerów często używają wielu pomocniczych narzędzi, do tych bardziej popularnych można zaliczyć monitory systemowe. Są to programy, które pozwalają zobaczyć, jaki program np. próbował odczytać klucze z rejestru Windows lub pozwala zanalizować, jakie pliki próbował otworzyć.

Analizując dane z monitorów można z łatwością zauważyć, że przykładowo aplikacja próbowała otworzyć klucz w rejestrze Windows, gdzie powinny być zapisane dane rejestracyjne. Obecnie najpopularniejszymi narzędziami tego typu są monitory Filemon i Regmon firmy Sysinternals.

Monitor rejestru Regmon w akcji
Monitor rejestru Regmon w akcji.

Działanie monitorów po części opiera się także na wykorzystaniu sterowników i do ich wykrycia, można skutecznie użyć metod, które skutkowały w wykrywaniu sterowników debuggera.

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsFilemon()
//
// funkcja sprawdza obecnosc sterownika wykorzystanego w programie
// sluzacym do sledzenia odwolan do plikow (filemon)
//
// na wyjsciu:
// jesli monitor plikow jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsFilemon()
{
    return ( CheckDriver("\\\\.\\FILEVXD") != INVALID_HANDLE_VALUE ? TRUE : FALSE);
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsRegmon()
//
// funkcja sprawdza obecnosc sterownika wykorzystanego w programie
// sluzacym do sledzenia odwolan do rejestru windows (regmon)
//
// na wyjsciu:
// jesli monitor rejestru jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsRegmon()
{
    return ( CheckDriver("\\\\.\\REGVXD") != INVALID_HANDLE_VALUE ? TRUE : FALSE);
}

Programy Filemon i Regmon posiadają okienkowy interfejs użytkownika, dzięki nazwie głównego okna możliwe jest wykrycie, czy program monitora jest aktywny lub nie. Do tego celu wykorzystuje się funkcję WinApi FindWindowEx().

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsFilemonWnd()
//
// funkcja sprawdza, czy obecnie otwarte jest okno Filemon'a
//
// na wyjsciu:
// jesli monitor plikow jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsFilemonWnd()
{
    return ( FindWindowEx(NULL, NULL, NULL, "File Monitor - Sysinternals: www.sysinternals.com") != NULL ? TRUE : FALSE);
}

///////////////////////////////////////////////////////////////////////////////
//
// BOOL IsRegmonWnd()
//
// funkcja sprawdza, czy obecnie otwarte jest okno Regmon'a
//
// na wyjsciu:
// jesli monitor rejestru jest aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL IsRegmonWnd()
{
    return ( FindWindowEx(NULL, NULL, NULL, "Registry Monitor - Sysinternals: www.sysinternals.com") != NULL ? TRUE : FALSE);
}

Dzięki FindWindowEx() możemy z łatwością sprawdzić, czy uruchomione są monitory i podjąć odpowiednie kroki, np. wyłączyć naszą aplikacje, ale możliwe jest także wyłączenie samego monitora. Proszę zauważyć, że funkcja FindWindowEx() w przypadku znalezienia okna, zwraca jego uchwyt. Mając uchwyt okna, można wysłać do niego komunikat WM_CLOSE, który wysyłany jest, gdy użytkownik zamyka okno aplikacji. Wysłanie tego komunikatu spowoduje wyłączenie monitora.

///////////////////////////////////////////////////////////////////////////////
//
// BOOL KillRegmonWnd()
//
// funkcja w razie wykrycia okna Regmona zamyka je
//
// na wyjsciu:
// jesli monitor rejestru byl aktywny, funkcja zwraca TRUE,
// w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL KillRegmonWnd()
{
    HANDLE hWindow;

    // znajdz okno Regmona
    hWindow = FindWindowEx(NULL, NULL, NULL, "Registry Monitor - Sysinternals: www.sysinternals.com");

    // w razie znalezienia okna
    if (hWindow != NULL)
    {
        SendMessage(hWindow, WM_CLOSE, 0, 0);
    }

    return ( hWindow != 0? TRUE : FALSE);
}

Może się zdarzyć, że aplikacja nie będzie reagowała na komunikat WM_CLOSE, można również spróbować wysłać wiadomość WM_ENDSESSION, która symuluje zamykanie systemu Windows. Jeśli jednak i to nie pomoże, warto zastosować nieco ostrzejsze metody. Mając uchwyt okna, otwieramy proces, do którego należy okno i zamykamy go wykorzystując funkcje TerminateProcess().

///////////////////////////////////////////////////////////////////////////////
//
// BOOL KillFilemonWnd()
//
// funkcja w razie wykrycia okna Filemona zamyka je
//
// na wyjsciu:
// jesli monitor rejestru byl aktywny i zostal zamkniety, funkcja
// zwraca TRUE, w przeciwnym wypadku FALSE
//
///////////////////////////////////////////////////////////////////////////////

BOOL KillFilemonWnd()
{
    HANDLE hWindow, hProcess;
    DWORD dwPid;

    // znajdz okno Filemona
    hWindow = FindWindowEx(NULL, NULL, NULL, "Registry Monitor - Sysinternals: www.sysinternals.com");

    // w razie znalezienia okna
    if (hWindow != NULL)
    {
        // pobierz indentyfikator procesu, do ktorego nalezy okno
        if (!GetWindowThreadProcessId(hWindow, &dwPid))
        {
            // otworz proces z flagami pozwalajacymi wylaczyc go
            hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwPid);

            if (hProcess != NULL)
            {
                // zakoncz proces
                if ( TerminateProcess(hProcess, -1) != 0) return TRUE;
            }
        }
    }

    return FALSE;
}

Zabezpieczanie pliku aplikacji

Nasz program, uzbrojony w metody antydebug może już sprawiać pewne kłopoty, ale nadal istnieje możliwość, że cracker złamie go i opublikuje w internecie cracka.

Najlepszym sposobem, aby tego uniknąć jest dodatkowe zabezpieczenie pliku. Zabezpieczenie opiera się na tej zasadzie, iż plik wykonywalny naszej aplikacji, przed jej publikacją, dodatkowo jest szyfrowany i kompresowany.

Po takim zabezpieczeniu pliku, jakakolwiek jego modyfikacja, np. spowodowana użyciem cracka, powoduje, że plik nie może być uruchomiony. Do zabezpieczania plików wykorzystuje się specjalne programy tzw. exe-protektory.

Po zabezpieczeniu pliku wykonywalnego, do jego struktury dodawany jest specjalny kod, który odpowiedzialny jest m.in. za deszyfrowanie i dekompresję danych, a następnie uruchomienie aplikacji.

Oprócz tego, kod dodawany do zabezpieczanych plików zawiera szereg metod antydebug, dzięki którym nie jest możliwe uruchomienie aplikacji w obecności debuggerów lub innych narzędzi wykorzystywanych przez crackera.

Zakończenie

Przedstawione metody, to tylko skrawek wszystkich możliwości, które pozwolą ochronić własny program przed złamaniem. Mam nadzieję, że mój artykuł pozwolił Wam dostrzec, że publikowane oprogramowanie, wcale nie jest tak bezpieczne jakby się to wydawało na pierwszy rzut oka, ale także chciałem pokazać, że w jakiś sposób można się bronić.

Odnośniki

https://www.pelock.comprotektor i system kluczy licencyjnych PELock
https://docs.microsoft.com/en-us/sysinternals/monitory Filemon i Regmon
https://en.wikipedia.org/wiki/DevPartnerstrona producenta debuggera SoftIce
http://www.knlsoft.comdebugger TRW
http://www.ollydbg.de/darmowy debugger OllyDbg
http://upx.sourceforge.netkompresor plików wykonywalnych UPX
https://bitsum.com/kompresor plików wykonywalnych PECompact