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