Video of the week

This is a must-watch video about one of us trying to reach the stars :-)

Well done #HRejterzy

Reverse Engineering II

Pamiętasz film "Nieuchwytny cel" z Van Dammem? Jeśli nie to szkoda, ale czytaj dalej. Był w tym filmie pewien człowiek - postać grana przez Arnolda Vosloo. Odpowiadał za tropienie ofiar podczas dość nietypowej zabawy i biznesu zarazem. Ofiara miała czas na ucieczkę a po chwili grupa uzbrojonych bandziorów goniła go z arsenałem broni maszynowej. Ciekawa umiejętność - potrafić odtworzyć drogę ucieczki tak aby dopaść ofiarę.

Dlaczego nie zorganizować takiej 'zabawy' w kontekście Reverse Engineeringu? Wyobraź sobie, że masz program.exe, który wyrzuca jakiś napis na wyjściu - co powiesz na to, żeby odtworzyć funkcję instrukcja po instrukcji, linijka po linijce tak aby można było z 99% pewnością stwierdzić, że nasza wersja funkcji to w 99% funkcja zakodowana w tym execu? Brzmi hardcorowo?

Okey, odpalę ten plik i zobaczę co on w ogóle robi - przy okazji odpalam sobie notatnik bo lubię notować wszystko co może podprowadzić mnie do celu. Cel? - przypomnę, odtworzenie funkcji do poziomu kodu źródłowego.

reverseMe0

Czyli mamy program, który wypisuje komunikat(1) o długości string i wypisuje prawdopodobnie ten właśnie string(2). Pierwsze wnioski - funkcja lub cały program może wyglądać następująco:

  1. int main()
  2. {
  3. fprintf("String length: %d. \n%s3",strlen("ddd"),"ddd");
  4. return 0;
  5. }

Na tym etapie nie mogę jednoznacznie stwierdzić, czy mam rację czy nie - uruchomię program w IDA 5.2 i spróbuję obalić lub potwierdzić tę teorię.

intmain

Hura! Na tym etapie mogę z całą pewnością stwierdzić, że poprzednie założenie co do budowy funkcji / programu było błędne - why? Przed wywołaniem funkcji printf widoczne jest wywołanie innej funkcji - reverseMe. Przed  wywołaniem tej funkcji widać też jakieś manipulacje na stosie - najprawdopodobniej chodzi o argument dla funkcji reverseMe - czyli funkcja przyjmuje argument. Co jeszcze? - zwróć uwagę, że za chwilę wywoływana będzie funkcja printf. Funkcja przyjmuje tutaj dwa argumenty - dlaczego 2? - ponieważ przed call ds:printf widać dwie instrukcje push, które ładują argumenty dla tej funkcji. Jeden z tych argumentów to łańcuch formatujący - drugi argument to liczba lub napis. Skoro program.exe wypisał na wyjściu "ddd" oznacza to, że najprawdopodobniej drugim argumentem funkcji printf jest "ddd" a pierwszym "String length: %s.\n". Co więcej z tak krótkiego programu? "ddd" musiało pojawić się jakoś za pośrednictwem rejestru eax - dlaczego? Funkcja printf wywoływana jest według konwencji __cdecl - to znaczy, że w przypadku np. int funkcja(a,b,c) - argumenty będą pojawiać się na stosie od tyłu - tzn. najpierw na stos trafi c, potem b i na końcu a - a potem adres powrotu, saved_ebp itd. Oznacza to, że w rejestrze EAX pojawi się drugi argument funkcji printf a skoro funkcja wypisała na wyjściu napis - oznacza to, że funkcja reverseMe zwraca wskaźnik na łańcuch znakowy. Dobra rada na przyszłość - wyniku dowolnej działania funkcji spodziewaj się właśnie w rejestrze EAX. Podsumujmy i spróbujmy zbudować prototyp funkcji na podstawie tego co wyczytaliśmy z tych dwóch rysunków:

  1. char* reverseMe( PARAM ) {
  2. // szukamy
  3. return adres_string;
  4. }

Z takiego prototypu i zalążka kodu można dalej wywnioskować - skoro funkcja zwraca adres do łańcucha znaków to oznacza, że na pewno nie jest to tablica znakowa deklarowana i definiowana lokalnie w tej funkcji. Oznacza to, że albo modyfikowany jest i zwracany PARAM (który musi stanowić łańcuch znakowy) albo drugie rozwiązanie - PARAM jest jakimś wzorcem a nowy napis jest tworzony za pośrednictwem sterty - oba rozwiązania są równie prawdopodobne. Ciekawe dochodzenie?

Wracając do screena z programu IDA - 1 oznacza 3 bajty, które umieszczane są gdzieś na stosie - nad ESP + 8 w miejsce etykiety var_4 - domyślamy się (jak już wcześniej wspomniałem), że chodzi o argument dla funkcji reverseMe. Argument ten - "666564h" zgodnie z "nauką" asemblera może oznaczać adres lub daną natychmiastową. Np. mov eax, var_4 oznacza, że pod var_4 kryje się offset - adres do "jakiejś" zmiennej. Inny przykład - mov eax, 10h to już umieszczenie w rejestrze eax danej natychmiastowej czyli nie trzeba odwoływać się do pamięci i szukać tam czegoś bo odrazu w instrukcji wiadomo - co ma się znaleźć w rejestrze eax. Teraz popatrz znowu na ten argument - "666564h" - czy on wygląda jak adres, jakaś etykieta czy bardziej jak dana natychmiastowa? Moim zdaniem to dana natychmiastowa a skoro tak to można ją spróbować odczytać w ten sposób: uruchom aplikację calc.exe i w trybie "naukowy" przełącz się na widok hex i wpisz tam liczbę 666564 - otrzymałeś: 6710628. Dziwna liczba jak na argument funkcji - zgadza się? A może potraktować te bajty jako ASCII? Spróbujmy - znajdź w I-necie tablicę ASCII i sprawdź kolejną co reprezentują 0x66, 0x65 i 0x64 - otrzymałeś zapewne odpowiednio: f - e - d (możesz sprawdzić to tutaj). Okey - teraz wybiegnę trochę przed szereg i napiszę Tobie, dlaczego trzeba było w aplikacji calc.exe wpisać nie 666564 tylko - "646566".

Uruchomię debuger i przejdę parę kroków po funkcji reverseMe i będę się uważnie przyglądać wartościom rejestrów:

stos byte by byte0

 

Okey - rejestr EIP wskazuje, że utknąłem na 9 instrukcji w funkcji reverseMe - wystarczy. Zwróć uwagę na coś innego - nasz parametr funkcji reverseMe to etykieta var_4 - adres (offset) tego parametru to 0012FF7C. Zwróć teraz uwagę na wartość rejestru EDX - 0012FF7D - wskazuje adres drugiego bajtu parametru funkcji. Dalej - EAX - 0012FF7E - var_4+2 - wskazuje na 3ci bajt parametru. Zwróć ponownie uwagę, że var_4 to adres niżej położony niż var_4+1 i var_4+2.

0x0012FF7C < 0x0012FF7D < 0X0012FF7E

Intuicyjnie wydawać by się mogło, że parametr "666564" to w rzeczywistości "fed" przekazane do funkcji reverseMe - ale czy faktycznie? Popatrz teraz na bardziej szczegółowy rozkład stosu w danym momencie funkcji (reverseMe+9):

stos byte by byte

def

Widać tutaj, że pod adresem 0X0012FF7C czyli var_4 kryje się bajt 0x64 czyli "d". Potem dopiero "bajt wyżej"(0X0012FF7D) to "e" i jeszcze bajt wyżej na stosie(0X0012FF7E) to "f". Wniosek - do funkcji reverseMe trafił argument postaci: "def" a nie "fed". Z czego to wynika? - możesz przeczytać tutaj.

Okey - ledwo wbiegliśmy do funkcji a znów możemy podsumować co wiemy:

  1. char* reverseMe( char* PARAM ) {
  2. // szukamy
  3. return adres_string;
  4. }

Uzgodniliśmy, że funkcja przyjmuje w parametrze "def" - oznacza to, że albo przyjmuje jako parametr tablicę znakową albo wskaźnik - w zasadzie jedno i to samo. Użyłem sobie char* ale równie dobrze mógłby być to zapis:

  1. char* reverseMe( char tablica[] ) {
  2. // szukamy
  3. return adres_string;
  4. }

Wbijamy się w funkcję, żeby zobaczyć co tam się dzieje już na samym początku:

reverseMe1

Z drugiego rysunku dowiadujemy się, że EDI zawiera adres na pierwszy bajt bufora ("def") - będę używać terminu bufor jako argumentu funkcji reverseMe. Czyli tutaj - program pakuje adres bufora do rejestru EAX a do rejestru EDX trafia "to" co znajduje się w komórce pamięci o wartości [eax+1] - chodzi o drugi bajt bufora. Dalej mamy etykietę loc_401006 - zwróć uwagę na drugie podświetlenie tej etykiety - jnz short loc_401006. Wygląda jakby pętla - od instrukcji test cl,cl zależy czy będzie kolejna iteracja pętli czy program przejdzie do instrukcji sub eax,edx. Idziemy krok za etykietę - mov cl, [eax] - rejestr EAX zawiera ADRES - natomiast zapis [EAX] oznacza [ADRES_W_EAX] co oznacza np. - jeśli w EAX znajduje się liczba 10, to zapis [eax] oznacza, że do cl będzie skopiowane to, co znajduje się w dziesiątej komórce pamięci operacyjnej procesu. CL to niższe słowo rejestru CX, który jest częścią 32-bitowego ECX. CL oznacza, że z pod [EAX]  skopiowany zostanie jeden bajt czyli "d". Dalej - program inkrementuje eax, czyli teraz w tym rejestrze znajduje się adres drugiego bajtu bufora - "e". Instrukcja test cl, cl polega na wykonaniu operacji iloczynu logicznego na argumentach czyli "CL AND CL" - po co? Jeśli bajt skopiowany do CL jest różny od zera - wynik operacji będzie różny od zera! Jeśli do CL trafił bajt '\0' to wynikiem operacji będzie 0. Kolejna instrukcja skoku zależy właśnie od tego "testu". Jeśli wynikiem operacji nie jest zero - skocz ponownie do instrukcji MOV CL,[EAX]. Brzmi to jak przechodzenie po naszym buforze aż do napotkania bajtu zerowego - kończącego łańcuch znakowy.

Dalej - zakładam, że doszliśmy już do instrukcji sub eax, edx - co to za instrukcja? Przypomnijmy - EAX w tym momencie zawierać będzie adres ze stosu za bajtem zerowym łańcucha. Zwróć uwagę, że eax jest inkrementowane kiedy w CL już znajduje się bajt '\0'. EDX zawiera z kolei adres drugiego bajtu parameru - można to przedstawić w następujący sposób:

STOS: d[e-EDX-0x0012FF7D]f[0][x-EAX-adres>0X0012FF7D] :STOS

sub eax, edx pozwoli obliczyć jaka jest długość bufora przekazanego w argumencie funkcji reverseMe - będzie to długość 3 (adres wyższy - adres niższy = 3 bajty różnicy na stosie). Dalej mamy skok: jz loc_4010A2 - popatrzmy dokąd by nas zabrał, gdyby okazało się, że argument jest pusty:

invalidString

Widać tutaj, że do eax trafia adres innego łańcucha - "Invalid string.". To, że trafia do EAX to nie przypadek, gdyż tak jak wspomniałem wcześniej - wyników działania wszelakich funkcji należy szukać w rejestrze EAX. Potem następuje jeszcze zdjęcie ostatniej - będącej na wierzchu - informacji ze stosu do rejestru ECX i wychodzimy z funkcji. To kolejna cenna informacja, która ponownie pozwala dobudować kod źródłowy naszej funkcji - co powiesz na to:

  1. char* reverseMe( char* PARAM )
  2. {
  3. if( strlen( PARAM ) )
  4. return "Invalid string.";
  5.  
  6. // szukamy
  7.  
  8. return adres_string;
  9. }

Jeśli okaże się, że długość parametru równa jest zero - zwróć "Invalid string." - jak dowiedzieć się, czy łańcuch jest pusty? - może funkcja strlen() będzie pomocna? - w ten oto sposób z pewnością sięgającą 99% odtworzyliśmy logikę funkcji - jestem przekonany, że do tego momentu wygląda ona właśnie tak - lub wykonuje dokładnie to samo co funkcja oryginalna. Być może z dalszej analizy wyjdzie na jaw, że if składa się z miliona dodatkowych warunków połączonych "||" lub "&&" albo, że siedzi tam coś jeszcze - ale - najważniejsza jest logika - tzn. nie ma znaczenie dla mnie to, że przypuśćmy odtworzę kod do tej postaci:

  1. char* reverseMe( char* PARAM )
  2. {
  3. if( strlen( PARAM ) )
  4. return "Invalid string."
  5.  
  6. if( cos_tam )
  7. return "cos_tam";
  8.  
  9. // szukamy
  10.  
  11. return adres_string;
  12. }

kiedy w rzeczywistości kod będzie wyglądał tak:

  1. char* reverseMe( char* PARAM )
  2. {
  3. if( strlen( PARAM ) || (cos_tam == 0) )
  4. return "Invalid string."
  5.  
  6. // szukamy
  7. return adres_string;
  8. }

Logika obu kodów jest identyczna, czyli nie rozchodzi się o to, żeby dokładnie zacytować autora ale o to, żeby dokładnie odzwierciedlić sens jego słów.

Dalej - jestem na adresie 0040100B:

2ndloop

EDI zawiera adres bufora - argumentu, ten adres: eax.

Ten adres trafia teraz do EAX. EDX zawiera przed operacją lea edx, [eax+1] coś - nieważne, ważne co będzie zawierać po tej operacji - EAX zawierać będzie adres 0012FF7C+1 czyli 0012FF7D, który wskazuje na literę "e" - patrz rysunek wyżej. Kolejna instrukcja - ebx jest puste i pozostaje puste (podgląd pod debuggerem). Dalej - podświetliłem loc_401020 i znowu wygląda to na pętlę. EAX zawiera adres do bufora, kolejne bajty trafiać będą do bajtowego rejestru CL i sprawdzane będzie czy w CL siedzi bajt-zero czy coś innego. Czyli znowu mamy przechodzenie po buforze - popatrz, że ponownie też obliczana jest długość bufora - sub eax,edx. Ta długość porównywana jest z wartością 8 - cmp eax, 8 a to oznacza, że funkcja w kolejnym kroku sprawdza, czy bufor jest większy niż 8 - jeśli tak - skocz do loc_4010A2. Ten fragment kodu jest już znany - to wyjście z funkcji (patrz jeden z obrazków wyżej). Okey czyli możemy dopisać nowy kod do funkcji:

  1. char* reverseMe( char* PARAM ) {
  2. if( strlen( PARAM ) )
  3. return "Invalid string."
  4.  
  5. if( strlen( PARAM ) > 8 )
  6. return "Invalid string."
  7.  
  8. // szukamy
  9. return adres_string;
  10. }

Jedziemy dalej - jesteśmy tutaj:

printf

Trzeba przyznać, że do znudzenia przypomina to ponowne obliczanie długości bufora tym razem na potrzeby funkcji printf. Skoro do funkcji printf trafia jako parametr rejestr EAX oznacza to, że w tym rejestrze znajduje się wartość, która stanowi długość PARAM czyli, jeśli wywołam funkcję strlen( PARAM ) to podobnie jak tutaj, otrzymam jej wynik w rejestrze EAX:

  1. char* reverseMe( char* PARAM ) {
  2. if( strlen( PARAM ) )
  3. return "Invalid string."
  4.  
  5. if( strlen( PARAM ) > 8 )
  6. return "Invalid string."
  7.  
  8. printf("String length: %d.\n", strlen( PARAM ));
  9.  
  10. // szukamy
  11. return adres_string;
  12. }

Kolejny fragment kodu:

malloc

I znowu powtórka z rozrywki! Zwracam jeszcze tylko uwagę na operacje czyszczenia stosu - add esp, X - operacja ta jest związana z porządkowaniem "po funkcji" i jest również związana z konwencją wywołania funkcji printf - __cdecl. Nie są to tutaj operacje kluczowe. Pod debuggerem widać, że push ebx wstawia na stos wartość 0. sub eax, edx to długość bufora. EBP zawiera podstawę ramki stosu - nie ma to tutaj kluczowego znaczenia bardzo. Funkcja malloc zwraca adres zajętego obszaru na stercie a jako parametr przyjmuje rozmiar obszaru - czyli tutaj długość PARAM. Popatrz teraz na rejestry, które uaktywniły się po wywołaniu instrukcji call ds:malloc

eflags

Rejestry, których wartości "zaświeciły się" na niebiesko związane są z ostatnią operacją (krokiem debuggowania). W rejestrze EAX pojawił się adres zwrócony przez funkcję malloc.

heap

Dalej już trochę przyspieszę - do dl trafia bajt z adresu w EDI. Potem mamy czyszczenie stosu, zerowanie ebx i skopiowanie adresu na stercie do ebp. Potem program sprawdza, czy bajt który trafił do dl nie jest bajtem zerowym. Po instrukcji push esi w końcu zaczyna się dziać coś ciekawego. Do ESI trafia adres ze stery a do ECX trafia offset bufora - PARAM. Instrukcja sub esi, edi czyli ADRES_HEAP - offset_var_4(PARAM) == 00264304 - odległość pomiędzy obszarem na stercie a PARAM na stosie. Operacja jest trochę nietypowa - zauważ, że za chwilę następuje inna instrukcja mov [esi+ecx], dl czyli skopiowanie pierwszego bajtu z PARAM pod pierwszy zarezerwowany bajt na stercie. Dziwne na pierwszy rzut oka jest to, że zamiast np. mov [esi], dl - inc esi, inc ecx itd kompilator postanowił tak to rozwiązać - zapewne jest to rozwiązanie bardziej optymalne; najważniejsze, że odbywa się tutaj przeniesienie wartości związanej z PARAM do obszaru na stercie. Zauważ jeszcze, że esi+ecx to adres z rejestru EAX czyli sterta zwrócona przez malloc. Zauważ jeszcze jedną ciekawą instrukcję za etykietą loc_401076 - sub dl, bl - w dl znajduje się litera "d", rejestr EBX został wcześniej wyzerowany czyli operacja polega na: 64 - 0 i wynik umieść w dl. Nie jest to jakieś zwyczajne kopiowanie bo wcześniej następuje modyfikacja bajtu - przed umieszczeniem na stercie. Patrzę dalej na czym polega ta modyfikacja:

endp

To jest przysłonięty (i pozostały) fragment z poprzedniego rysunku. Znowu pętla ale zwróć uwagę, że rejestr EBX jest inkrementowany tzn. 64 - [0] ; 65 -[1] ; 66 - [2] - co daje "ddd". Czyli na stertę trafia napis "ddd" zbudowany z PARAM funkcji reverseMe. Wnioski: malloc zwrócił adres na stercie do którego trafiają dane typu char co oznacza, że w funkcji zdefiniowana została zmienna lokalna - najpewniej wskaźnik na char. Paramaterem funkcji malloc był rozmiar PARAM:

  1. char* reverseMe( char* PARAM ) {
  2. if( strlen( PARAM ) )
  3. return "Invalid string."
  4.  
  5. if( strlen( PARAM ) > 8 )
  6. return "Invalid string."
  7.  
  8. printf("String length: %d.\n", strlen( PARAM ));
  9.  
  10. char* heap = (char*)malloc((size_t)strlen( PARAM ));
  11.  
  12. for(int index=0; ;index++)
  13. {
  14. if( PARAM[i] == 0 ) // --> mov dl, [ecx+1] .. test dl,dl .. inc ecx
  15. break;
  16.  
  17. heap[index] = (char)(int (PARAM[i] - 3));
  18. }
  19.  
  20. // szukamy
  21. return adres_string;
  22. }

W pętli for nie dałem warunku kończącego ponieważ w kodzie widać, że program sprawdza bajt zerowy PARAM a nie odlicza iteracje. A oto ostatnia prosta:

eflags2

Ustawiłem się z debuggerem na instrukcji 401090, żeby zaobserwować rejestry po poprzednich 3 instrukcjach. Trochę streszczę bo kod znowu coś przypomina - ponownie program przechadza się po PARAM aż napotka bajt zerowy. Bajt zerowy zostanie umieszczony w al a następnie skopiowany pod [ecx+ebp] -

ecxebp

Z rejestrów wynika, że 0 zostało wpisane pod 00394280+3 czyli po "ddd" na stercie - "ddd\0". Na koniec do eax trafia wartość ebp czyli adres na stercie zwrócony z funkcji malloc. Koniec - jak zatem prezentuje się teraz analizowana funkcja? -

  1. char* reverseMe( char* PARAM ) {
  2. if( strlen( PARAM ) )
  3. return "Invalid string."
  4.  
  5. if( strlen( PARAM ) > 8 )
  6. return "Invalid string."
  7.  
  8. printf("String length: %d.\n", strlen( PARAM ));
  9.  
  10. char* heap = (char*)malloc((size_t)strlen( PARAM ));
  11.  
  12. for(int index=0; ;index++)
  13. {
  14. if( PARAM[index] == 0 ) // --> mov dl, [ecx+1] .. test dl,dl .. inc ecx
  15. break;
  16.  
  17. heap[index] = (char)(int (PARAM[i] - 3));
  18. }
  19.  
  20. heap[strlen( PARAM )] = '\0';
  21. return heap;
  22. }

Nie mam pewności, że oryginalna funkcja jest co do znaku identyczna - mam za to pewność, że odtworzona funkcja działa tak jak oryginalna. Tak oto pozamykany w sobie Windows stał się na moment otwartym kodem.