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

Комментарии (2)


  1. CatAssa
    28.04.2024 07:49
    +2

    Спасибо: понятно, полезно.


  1. qw1
    28.04.2024 07:49

    Непонятно, где это может пригодиться.

    Обычно, цель выполнить свой произвольный код, не привлекая внимания SIEM. Но мы уже выполняем свой код в контексте DllMain. Если нужно отложенно, можно создать таймер, система вызовет нашу функцию через некоторое время.

    Как способ ставить хуки на функции другой DLL вообще не подходит. Статические иморты сразу отпадают. Допустим даже, нам повезло, приложение грузит OrigDll через LoadLibrary/GetProcAddress. Но если наша DLL-ка загрузилась раеньше, то OrigDll ещё нет в списке загрузчика, и патчить нечего. Если наша DLL-ка загрузилась позже, вероятно приложение уже сделало GetProcAddress и себе куда-то сохранило указатели на функции - надо их искать и патчить. Но тогда и в PEB лезть не надо.