Syringe – moja mała strzykawka (kodu)

Wydobywając kod firmware z kodu procesu Sealogic (SaeLog #4), w jednym z możliwości związanych z hookowaniem transmisji USB, wspomniałem o technice wstrzykiwania kodu do uruchomionego procesu, wykorzystującej popularną metodę ze zdalnym wątkiem (CreateRemoteThread). Sugerując przy tym wykorzystanie dostępnych w sieci injectorów lub napisanie czegoś własnego. Od tego czasu, w wolnych chwilach, próbowałem okiełznać i uporządkować mój kod, jaki używałem do tego typu zabaw. W istocie zrodził się projekt syringe, jako uniwersalnego narzędzia związanego z wstrzykiwaniem kodu i nie tylko… i tak zacząłem pisać stary/nowy kod…

Właśnie wrzuciłem kod na githuba, repozytorium już dawno założyłem, gdy tylko pomyślałem o tym małym narzędziu (czyli gdzieś w końcówce kwietnia, po opublikowaniu raportu z SeaLog #4) Od tego czasu w wolnych chwilach coś dłubię, bazując na starych kodach i jakiś wstawkach. Obecna wersja jest czysto deweloperska, miejscami może przypominać bardziej proof-of-concept, zawiera dużo dziwnego i brzydkiego kodu. Ale z chwilowego braku czasu i blokowania innych rzeczy postanowiłem go opublikować. Mam nadzieję, że sukcesywnie będę go poprawiał i dalej rozwijał, oczywiście w miarę możliwości techniczno-czasowych. Jest wiele potencjalnych nowych komend oraz opcji jakie bym widział w tym narzędziu, ale to wszystko wymaga czasu.

Mimo wersji deweloperskiej, kod jest jak najbardziej użyteczny. Dostępne są 3 proste i podstawowe polecenia, niejako bazowe w tego typu narzędziach:

Available commands:
  code    Inject code into process
  dll     Inject (load) dll into process
  exec    Execute code

Wszystkie bazują na wspomnianej technice z tworzeniem wątku będacego jak entry-point wstrzykiwanego kodu. Metoda jest najbardziej znaną techniką wstrzykiwania kodu lub modułów DLL do innych procesów. Wstrzykiwany kod, po zapisaniu w przestrzeni adresowej danego procesu, traktowany jest jako ciało funkcji nowo tworzonego wątku. Można tego dokonać na własnym procesie (nic ciekawego) lub na dowolnym innym (bardziej ciekawe), systemowe API Windowsa zawiera niezbędne funkcje umożliwiające pracę na zdalnych (remote) procesach i wątkach.

Metoda ta jest szeroko znana i opisana w Internecie, dlatego, aby się nie powtarzać polecam odnośniki do źródeł, gdzie prosto i konkretnie opisano cały mechanizm. Na Infosec Institute w zasobach polecam artykuł „Using CreateRemoteThread for DLL Injection on Windows„, a jako uzupełnienie na blogu Open Security Research wpis autora pt. „Windows DLL Injection Basics„. Są to podstawy, o których warto wiedzieć przed dalszym czytaniem tekstu (niekoniecznie) lub kodu.

W przypadku wstrzykiwania modułu DLL, najprostszym sposobem tej metody, którego również używałem w pierwszych wersjach kodu syringe, było użycie LoadLibrary jako wejścia dla wątku utworzonego przez CreateRemoteThread. Funkcja ta przyjmuje jeden parametr, który jest nazwa biblioteki (modułu) do załadowania, co się dobrze składa, bo funkcje uruchamiane w ramach wątku, mogą przyjąć tylko jeden parametr. Wykorzystanie bezpośrednio tandemu CreateRemoteThread + LoadLibraryA jest najczęściej spotykanym trickiem do wstrzykiwania modułu DLL:

FARPROC LoadLibraryAddress = ::GetProcAddress(::GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
 
::WriteProcessMemory(proc.get(), mem.get(), dll.c_str(), dll.size() + 1, nullptr);
 
Handle thread(
	::CreateRemoteThread(proc.get(), nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(LoadLibraryAddress), mem.get(), 0, nullptr)
);
 
::WaitForSingleObject(thread.get(), INFINITE);
 
DWORD excode = 0;
::GetExitCodeThread(thread.get(), &excode);
 
std::cout << "Thread finish, exit with code: " << excode << std::endl;

Kodem wyjścia wątku jest wartość zwracana przez funkcję uruchamianą w jego przestrzeni, co w tym przypadku będzie uchwytem do załadowanego modułu lub w przypadku błędu wartość NULL. Kod ten można pobrać poprzez funkcję GetExitCodeThread po zakończeniu działania wątku.

W przypadku błędu nie mamy żadnej informacji dlaczego załadowanie biblioteki się nie powidło. Nie ma możliwości pobrania wartości LastError z wątku już po jego zakończeniu. Choć ciekawe, czy przed zwolnieniem struktur wątkowych nie da się pobrać tej wartości bezpośrednio z TEB-a. Dlatego w kolejnej poprawce zmodyfikowałem mechanizm na bardziej rozbudowany, ale dający niezbędne informacje w przypadku problemów z załadowaniem biblioteki DLL.

Zamiast bezpośrednio uruchamiać LoadLibrary, lepiej będzie wstrzyknąć kod funkcji, która przejmie kontrolę nad ładowaniem modułu i w razie potrzeby pobierze niezbędne dane z wątku/procesu. Kod ten nie będzie niezwykły, prosty call po adresie funkcji do LoadLibrary i GetLastError:

void RemoteLoadDllFunction(LoadDllThreadData* data) {
 
	data->ModuleHandle = data->LoadLibrary(data->DllName);
	data->LastError = data->GetLastError();
 
}

Dane przekazywane do funkcji, zaalokowane w przestrzeni adresowej procesu docelowego pośredniczą w komunikacji. Zwierają dane wejściowe określające adresy funkcji i nazwę ładowanego modułu, a zwracane dane wyjściowe uzupełnione w czasie wykonania funkcji – handler załadowanego modułu oraz ewentualny kod błędu:

typedef DWORD (WINAPI *GetLastErrorFunc)(VOID);
typedef HMODULE (WINAPI *LoadLibraryFunc)(LPCSTR lpLibFileName);
 
struct LoadDllThreadData {
 
	// input
	LoadLibraryFunc LoadLibrary;
	GetLastErrorFunc GetLastError;
	CHAR* DllName;
 
	// output
	HMODULE ModuleHandle;
	DWORD LastError;
 
};

Wszystkie dane powinny znaleźć się w przestrzeni adresowej procesu, więc DllName wskazuje w miejsce za strukturą gdzie znajduje się zawartość stringa. Adresy funkcji pobrano z procesu źródłowego – strzykawki. Obecnie mimo róznych technik ASLR i randomizowania adresów bazowych ładowanych modułów w Windowsie, adresy te są stałe między reboot-ami. Ale trzeba mieć na uwadze, że kiedyś może się to zmienić i zacznie działać w bardziej odpowiedni (bezpieczniejszy) sposób. Wtedy będę musiał pomyśleć o nieco innej implementacji.

Kolejnym ciekawym trickiem jest determinacja rozmiaru kodu funkcji RemoteLoadDllFunction. W wykorzystuje ona fakt, że dla elementów oznaczonych jako static, bez różnych specyficznych opcji kompilacji i linkowania (incremental linking, etc.) funkcje te zostaną umieszczone w takiej kolejnościw kodzie wynikowym, w jakiej znajdują się w kodzie źródłowym.

static void RemoteLoadDllFunction(LoadDllThreadData* data) {
	[...]
}
 
// This is just a dummy function used to determine size of RemoteLoadDllFunction code
static void RemoteLoadDllFunctionEnd() {
	return;
}

Zatem wystarczy odjąć poszczególne adresy funkcji, aby otrzymać rozmiar kodu wynikowego, jaki należy wstrzyknąć do procesu:

const size_t sizeCode = reinterpret_cast<size_t>(RemoteLoadDllFunctionEnd) - reinterpret_cast<size_t>(RemoteLoadDllFunction);

Zachowanie to może się zmienić, zależne jest od kompilatora i linkera, zatem docelowo chciałbym to kiedyś zamienić na typowy opcode/shellcode przechowywany w tablicy. Jakieś TODO odnosnie tego znajduje się w kodzie.

W przypadku wstrzykiwania shellcodu przez polecenie code, sytuacja wygląda podobnie. Zamiast kodu funkcji stub ładującej dany moduł DLL, w zaalokowane miejsce w przestrzeni procesu zapisywany jest kod podany przez użytkownika, a nastepnie odpalany w nowo utworzonym wątku.

Zastanawiam się, czy zamiast uruchamiania nowego wątku z entry-point na wstrzyknięty kod, dorzucić lepiej kolejnego stuba dla odpalania kodu w try-catch SEH-a:

void RemoteExecFunction(void* code) {
 
	__try {
 
		FARPROC func = reinterpret_cast<FARPROC>(code);
		func();
 
	} __except (EXCEPTION_EXECUTE_HANDLER) {
		//...
	}
}

Może to zapobiec wywaleniu się procesu, gdy wstrzykiwany kod coś za bardzo popsuje. Ale sam nie jestem pewny, czy ma to jakiś większy sens.

Syringe bazuje obecnie tylko na przedstawionej wyżej metodzie opartej na remote-thread, ale oczywiście istnieje jeszcze wiele innych metod wstrzykiwania kodu do procesów. Niektóre wykorzystują całkiem zmyślne mechanizmy. Na pewno będę chciał rozszerzyć funkcjonalność syringe o inne metody injectingu, jak chociażby poprzez manipulacje kontekstem wątków, czy wykorzystanie bezpośrednio funkcji z ntdll. Ale to temat na inny czas.

Pomijając kwestie wstrzykiwania różnych rzeczy, kod projektu jest bardzo elastyczny i w łatwy sposób rozszerzalny. Bez problemu można dodawać nowe polecenia. W ciekawy, dosyć nie oczywisty sposób udało mi się zmusić Boost.Program_options do odpowiedniej współpracy, ale to zasługa kilku podpowiedzi na jakie trafiłem w sieci. Oprócz Boosta nie ma żadnych innych powiązań i zależności z innymi bibliotekami. Kod zgodny z C++11, powinien bez problemu dać się skompilować na innych kompilatorach, developowany na VC2k13.

Oprócz samej funkcjonalności kręcącej się wokół injectowania kodu, mam kilka innych pomysłów na przydatne polecenia i funkcje. Manipulacja pamięci dowolnego procesu, czyli odczyt i zapis danych w przestrzeni procesu przydaje się w wielu sytuacjach, szczególnie bez dostępu do debuggera lub chęci jego uruchamiania. Inną funkcją mogą być typowe informacyjne polecenia, które oprócz listowania informacji o procesach, mogą wyświetlać listę modułów, wątków, uchwytów etc. API jest bogate, a nie zawsze chce się na szybko pisać kawałki kodu, łatwiej uzyć gotowego narzędzia dostępnego pod ręką. Pewnie prędzej czy później pojawi się opcja z patchowaniem, oczywiście zgodna z kodem generowanym przez IDA, ale nie tylko.

Mam nadzieję, że syringe przerodzi się w zbiór prostych, ale i nie tylko, przydatnych i użytecznych narzędzi i poleceń. W wolnych chwilach, których niestety mało, będę starał się rozwijać projekt. Głównie na własny użytek, ale jeśli komuś również się przyda, to będzie to miły akcent.

Windows Internals: LastErrorToBreakOn

W kodzie Windowsa można znaleźć czasem ciekawe fragmenty, które skrywają pomocne mechanizmy i sposoby ułatwiające debugowanie różnych elementów, nie tylko systemowych. Część, jeśli nie wszystkie, takie wstawki nie są nigdzie udokumentowane. Zatem nic dziwnego, że dziś znów trafiłem na kolejny taki trick w kodzie systemowym. Mowa tutaj o conditional breakpoint przy ustawianiu kodu błędu, co […]

Portfel akcji 2Q 2015

Mija kolejny kwartał, a mojej aktywności na rynku brak. Chociaż początek był bardzo dobry, wydawało się, że nic nie zakłóci mojego planu, no ale jak nie Ukraina to Grecja, albo wielka przecena Energii. A tymczasem przedstawiam małe podsumowanie mijających dziś 3 miesięcy. Sytuacja na GPW Po rosnących indeksach w kwietniu mogłoby się wydawać, że wreszcie […]

Zabezpieczanie przed ryzykiem walutowym

Od drugiej połowy zeszłego roku dolar sukcesywnie się umacniał, dopiero od niedawna następuje lekkie odwrócenie trendu. To zmusiło mnie do zastanowienia się jak zabezpieczyć sobie transakcje dolarowe przed zmianami kursu, aby przyszłe rozliczenia dokonywać właśnie po tym wysokim (obecnie) kursie. Czyli niejako zamrozić sobie kurs waluty na jakiś okres i z niego korzystać. Jest to […]

SaeLog #4: Firmware i jego ekstrakcja

Chcąc zajrzeć do kodu firmware zaszytego w urządzeniu Saleae Logic, trzeba go najpierw jakoś zdobyć. Kod ten w typowej aplikacji USB-FX2 ładowany jest do urządzenia po uprzednim jego wykryciu przez oprogramowanie zainstalowane na komputerze, wprost poprzez port USB. Idąc tym tropem można być pewnym, że znajdzie się go gdzieś w paczce z aplikacją lub w […]

Portfel akcji 1Q 2015

Jak szybko ten czas leci, już 3 miesiące mineło, nie tak dawno Nowego Roku, a ja ciągle wspominam swoje rozważania nad zeszłorocznym raportem dotyczącym mojej aktywności giełdowej, a tutaj nadaża się już kolejna okazja do kwartalnego podsumowania mojej udręki z inwestycjami giełdowymi. Ten pierwszy kwartał roku 2015 nie wypadł wcale najgorzej. Mogę nawet powiedzieć, że […]

SaeLog #3: Fake EEPROM

Już od jakiegoś czasu przyglądałem się kodowi zaszytemu w chipie oraz starałem przypomnieć sobie asemblera 8051. Robiąc sobie małą przerwę od analizy firmware, całkiem przypadkiem trafiłem na małą niespodziankę. Po odłączeniu pamięci EEPROM (wyjęcie zworki) już po wykryciu i załadowaniu oprogramowania do układu, nie stwarza żadnych widocznych problemów z softem. Aplikacja się nie wywala i […]

Muzeum Techniki i Przemysłu

W miniony weekend wybrałem się z dziewczyną do Muzeum Techniki i Przemysłu działającego w Warszawie. Na 2 piętrach muzeum zebrano pokaźny zbiór eksponatów i sprzętu dotyczącego historii polskiej techniki i nie tylko. Ponadto oprócz wystaw stałych, muzeum organizuje wystawy czasowe, często z różnych okazji i rocznic. Naprawdę ciekawe, fajne rzeczy można tam zobaczyć, a każdy […]

SaeLog #2: Komunikacja z pamięcią EEPROM

Jedną z pierwszych rzeczy niezbędnych do rozwiązania problemu, a może tylko do poznania mechanizmu działania komunikacji programu z pamięcią EEPROM, jest analiza funkcji, które za to odpowiadają. Obiekt LogicAnalyzerDevice udostępnia dwie metody do służące do tego celu, są to ReadEeprom i WriteEeprom. Wspominałem już o nich w poprzedniej części, ale dopiero niedawno udało mi się […]

SaeLog #1: Intro

Nowe wersje oprogramowania dla analizatora logicznego Saleae Logic mają problemy z obsługą nieoryginalnego sprzętu opartego na prostej aplikacji kostki CY7C68013A, w którym zastosowano większą pamięć adresowaną wordem. Napomknąłem o tym problemie w poprzedniej notce przy okazji testów płytki deweloperskiej z układem Cypressa w roli analizatora logicznego. Obiecałem sobie również, że spróbuje rozwiązać ten problem programowo, […]