Погружаемся в 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;
}

При обычном ходе выполнения получим ожидаемый результат:

2f600fbf16d99c9516552e4d75e9301b.png

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

Поставим точку останова

4eaef10769588e97c3522b5254add54d.png

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

4470cdcf686feebc0afcf22b1cfc7292.png

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

37efb51254d6b9268e94252c04615b79.png

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

Давайте перейдем к реализации этой функции в ntdll.dll:

735ee4da7a5c3a54031600aee22f8f26.png

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

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

551e5f132fd8e85d6c8218830d08118e.png

Отсюда видно, что функция использует значение 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"))();

}

Получим ожидаемый результат в виде последовательного вызова обеих функций:

9b36b3b568d7906986a9404247df1279.pnga187a0bd5f83d75d1235f4d14bcb7567.png

Теперь приступим к самому интересному.

Попробуем подменить адрес 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:

62f683db6b537038ec694883ab12fffc.png

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

abaf9ee44b90c400b9853db58706b19e.png

Более того, теперь при вызове функции, которая реализована только в вредоносной библиотеке, обращаясь к оригинальной, получим вызов:

#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"))();

}

fbd777f401b96809c57d3a9e7b3058cf.png

Такая подмена DLL может служить альтернативным способом проксирования функций, а так же простейшей защитой собственной библиотеки от динамического анализа, в случае подмены адреса самой библиотеки.

Мы уже описали два способа манипуляций PEB, однако существует еще их огромное количество, о которых мы когда-нибудь обязательно расскажем!

Напоминаем, что хакинг является незаконной деятельностью и допустим только в случае тестирования на проникновение в согласовании с заказчиком.

Подписывайтесь на наш telegram-канал AUTHORITY.

© Habrahabr.ru