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.
New blog
7 lat temu