piątek, 23 października 2009

Analiza stosu i mapa pliku

Ten post miał pójść w piątek (23 X), ale niestety w piątek rano zostałem poinformowany o terminie obrony, tj środa 28 X. W sumie to stety, bo od wczoraj jestem magister inżynier informatyki specjalizacja inżynieria oprogramowana i systemy sieciowe.

Sama obrona to w sumie dość sympatyczne przeżycie, gdyby nie jeden drobny detal, OGROMNY STRES!

A teraz do rzeczy. Jak ustalić adres aktualnie wykonywanej funkcji ? Należy przeanalizować zawartość stosu. Czym jest stos, mam nadzieję wszyscy wiedzą. Nie wszyscy natomiast muszą wiedzieć co w praktyce dzieje się ze stosem i rejestrami w momencie wywołania funkcji. Na początku wykonywany jest kawałek kodu nazywany prologiem funkcji, ma on na celu przygotowanie na stosie tzw ramki:

push ebp
mov ebp, esp
sub esp, 0xXX

W tym momencie stos wygląda tak:
-0xXX ... <- ESP
-0x4 ...
0x0 oldBP <- EBP

Czyli EBP wskazuje na poprzednią zawartość rejestru EBP. ESP wskazuje gdzieś, przy czym gdzieś nie jest gdziekolwiek, ale zależy od rozmiaru zmiennych lokalnych funkcji, 0xXX to właśnie ten rozmiar w bajtach. Warto tu zwrócić szczególną uwagę, iż wszystkie zmienne lokalne funkcji mają adresy od EBP - 4, aż do EBP - 0xXX. Takie rozwiązanie pozwala m.in na prostą organizację rekurencji.
Po wykonaniu prologu wykonywane jest ciało funkcji. Gdy funkcja się kończy rejestr ESP jest odpowiednio modyfikowany o wartość 0xXX, a EBP popowane. Pamięć użyta pod zmienne lokalne nie jest w żaden sposób czyszczona, ale jest traktowana jako wolna przestrzeń.

To dzieje się po wywołaniu funkcji. Zanim jednak do tego dojdzie trzeba ją jakoś wywołać.

mov eax, [ebp-4]
push eax
call test
add esp, 4

Stos:

0x0 oldBP <- tutaj będzie wskazywać EBP po wykonaniu prologu
0x4 retAddr
0x8 [ebp-4]

Na początek do EAX ładowana jest pamięć spod adresu EBP - 4, czyli wartość zmiennej lokalnej. Następnie wartość ta jest odkładana na stos, jest to parametr wywoływanej funkcji. Wywołanie wykonuje się instrukcją call. Instrukcja ta robi dwie rzeczy, odkłada na stos rejestr EIP oraz wykonuje skok pod podany adresy. Ponieważ rejestr EIP zawsze zawiera adres następnej instrukcji do wykonania, odłożona wartość jest adresem pod jaki należy powrócić gdy funkcja się zakończy, w tym wypadku jest to usunięcie ze stosu przekazanego parametru.

Wyciągamy ze stosu adres powrotu, [EBP + 4] i odczytujemy co siedzi w pamięci:

e8 zz yy xx ww 83 blah blah
Dokładnie pod adresem powrotnym jest wartość 83, która nie jest szczególnie ciekawa, bo to opcode dodawania. Interesujące są natomiast cztery poprzednie bajty, czyli wartość wwyyxxzz, jest to offset skoku. Adres pod jaki wykonany został skok, czyli teoretyczny adres funkcji, uzyskuje się po dodaniu offsetu do wartości retAddr.

Tak to właśnie wygląda w teorii, w praktyce kompilatory oszukują :) Otóż MSVC 2008 Debug umieszcza instrukcję JMP pod adresem dla CALL. Czyli mamy tak: CALL -> JMP -> Wywoływana funkcja.

JMP w pamięci wygląda tak: e9 zz yy xx ww. Podobnie jak przy callu jest to offset, tym razem należy dodać adres jmp'a powiększony o 5 aby uzyskać oczekiwany adres funkcji.

No na razie o stosie wystarczy, teraz parę słów o mapowaniu PE do pamięci wirtualnej.

Ostatnio napisałem, że w pliku z mapą są wszystkie informacje potrzebne do identyfikacji funkcji na podstawie jej adresu. No więc nie jest to do końca prawda. Wraz z systemem Vista Beta 2 wprowadzono technologię Address space layout randomization. Robi ona dokładnie to na co wskazuje nazwa, czyli miesza przestrzeń pamięci, aby utrudnić życie piszącym wszelkiej maści exploity.

Przykładowy wpis z mapy:
" 0002:00000540 ?test@@YAXAAVFrame@@@Z 00411540 f TestApp.obj".

Listing disasemblera:
"void test(Frame & frame)
{
013C1540 push ebp
013C1541 mov ebp,esp"

Jest problem, adres 0x013C1540 ma się nijak do 0x00411540 (znaczy końcówkę mają wspólną). Rozwiązania znalazłem dwa:
1) przełączyć flagę /DYNAMICBASE. Wyłączy to ASLR i adresy będą się pokrywać. Sprawdzone, działa pod 7.
2) znaleźć adres pod jaki windows załadował exeka i na tej podstawie ustalić jaka jest wartość w mapie. Również sprawdzone, działa pod 7.

W celu ustalenia adresu bazowego procesu można posłużyć się funkcją GetModuleHandleA. Zwraca ona wartość typu HMODULE, a przyjmuje łańcuch znakowy z nazwą poszukiwanego modułu. Przekazanie jej wartości NULL sprawi, że poszuka ona adresu bazowego procesu wywołującego. Zwrócony HMODULE to poszukiwany przez nas adres. Nie wiem niestety jak to wygląda w przypadku aplikacji 64 bitowych. Zakładam, że HMODULE jest wtedy typem 64 bitowym, ale ...

Uzyskany w ten sposób adres bazowy, w tym wypadku była to wartość 0x13B0000, odejmujemy od adresów pod jakie wykonywane są skoki. Wynikiem jest offset, czyli dla funkcji test 0x11540. Offset ten należy zwiększyć o adres bazowy podany w mapie, 0x400000, aby uzyskać dokładny adres funkcji. Ostatecznie otrzymamy 0x00411540.

wtorek, 20 października 2009

Logger

Miała miejsce długa przerwa, ale nie jest źle. Ostatecznie to będzie drugi wpis w tym miesiącu ;)

Oddałem pracę i dostałem lenia. Kompletnie nic mi się nie chciało. Chyba musiałem trochę odreagować. Powoli jednak zbieram się do roboty, choć nieco straszy mnie obrona. Kochana uczelnia raczyła mnie postawić w bardzo nieprzyjemnej sytuacji. Termin egzaminu ma być za tydzień, czyli 26 - 27 X, ale na dzień dzisiejszy nikt nie wie kiedy dokładnie. Co za tym idzie nie jest znany skład komisji. Boję się, że termin wypadnie na poniedziałek, a poinformują mnie w piątek, oczywiście po południu. I jak ja niby mam się przygotować ?

Od dawna zastanawiałem się nad poprawieniem loggera. To co stworzyłem na początku projektu było skuteczne, poza tym miało same wady. Rozwiązanie to jest oparte o wzorzec singletona, do tego bardzo zależało mi na łatwym formatowaniu a'la strumienie (operator <<) oraz na śledzeniu przebiegu wywołań funkcji. Wyszło rozwiązanie paskudne w użyciu i co znacznie gorsze, nie spełniające do końca moich oczekiwań.
Problemem okazało się zbieranie informacji o stosie. Automatyczna zamiana adresu funkcji na jej nazwę nie jest zadaniem trywialnym. Coś takiego można znaleźć na blogu Gynvaela Coldwinda: w tym wpisie. Sam wymyśliłem nieco inne rozwiązanie, również oparte o makra. W skrócie mniej asemblera, więcej C++. Tylko czy ja mam ochotę przejrzeć 37 linii kodu i pozamieniać definicje wszystkich funkcji w makra ? Wymyśliłem więc, że trzeba ręcznie opatrzyć każdy zapis do logu informacją o tym skąd pochodzi. Sprawa jest dosyć prosta, posłużyłem się standardowym makrem __FUNCTION__. Nieco kłopotliwa jest nazwa klasy w przypadku metod. Rozwiązałem to tak:
#define __CLASS__ "Nazwa klasy"
#define HERE __CLASS__ << "::" << __FUNCTION__

No i działa. Brakuje jednak danych stosu, czyli informacji o stanie aplikacji gdy doszło do powstania wpisu. Fajnie by też było mieć zarejestrowane trochę danych o tym co działo się wcześniej.

Zadałem sobie jednak fundamentalne pytanie, czy umieszczanie nazw funkcji i klas w kodzie ma sens ? No i wyszło mi, że nie. Wracamy do punktu wyjścia, jak zamienić adres funkcji na nazwę ? Linker z Visual Studio ma taki fajny przełącznik /MAP. W efekcie powstaje plik tekstowy chyba ze wszystkim co nam do szczęścia potrzebne, tj adres w pliku, adres w obrazie, nazwa funkcji, atrybuty, plik z którego dana funkcja pochodzi.

Na koniec zabawny błąd:
Logger LoggerInstance;
std::fstream logFile;
Logger::Logger()
{

logFile.open("log.txt", std::fstream::out);
...
}

Otwarcie pliku kończy się wyjątkiem przy próbie odczytu spod adresu 0. Czemu? Bo w momencie wywołania tego konstruktora obiekt logFile jeszcze nie powstał ;) Trzeba zamienić kolejność.
std::fstream logFile;
Logger LoggerInstance;

wtorek, 6 października 2009

Precompiled headers, Visual oraz smart pointers

Po lekturze komentarzy pod Postem Precompiled header. Postanowiłem wypróbować radę raffimoni. W skrócie warto. Raz, że upraszcza to kod. Dwa, że czas budowania spadł z 107250 ms na 91016, czyli 15%.

Mój silniczek ma błędy z którymi nie mogę sobie poradzić przy użyciu Eclipse'a. Przeniosłem, więc projekt do Visual Studio 2008 ze względu na ichni debugger. No i wyszło parę ciekawostek:
* Generalnie kod się kompilował, konieczne były drobne poprawki, np wstawki asm.
* Visual wrzuca wszystkie pliki obiektowe do jednego katalogu. Jeżeli istnieją dwa pliki o tej samej nazwie to je radośnie nadpisuje. Trzeba ręcznie zmienić nazwę pliku obiektowego dla danego cpp.
* Pomieszanie new i delete w wersji z i bez [] powoduje podniesienie wyjątku systemowego.

Ten ostatni punkt jest dość ciekawy. Całkiem możliwe, że to właśnie to jest winne temu, że Eclipse wychwytywał błędy. Tak czy siak postanowiłem się zabezpieczyć i wprowadzić inteligentne wskaźniki, które zajmą się prawidłowym zwolnieniem pamięci. Powstały, więc dwa szablony:
* SmartPtr,
* SmartArray
Kodu nie podam, bo nic odkrywczego nie opracowałem. Jeżeli ktoś poszukuje jakiegoś wzorca to polecam boost i "C++ Szablony: vademecum profesjonalisty" David Vandevoorde i Nicolai M. Josuttis.