Гайд Undetected LoadLibrary aka LdrLoadDll rebuild

Модератор форума
Модератор
Статус
Оффлайн
Регистрация
19 Май 2018
Сообщения
954
Реакции[?]
1,067
Поинты[?]
20K
Название кликбейт, почти. Данный способ инжекта будет обходить лишь хук LdrLoadDll, длл все также будет находиться в списке модулей и т.д.

Ни для кого не секрет, что вызов LoadLibraryA (как и любой другой функции LoadLibrary) приведет в конце к вызову LdrLoadDll, следовательно, чтобы отследить простой инжект, нужно поставить хук на LdrLoadDll
В этом гайде мы обойдем данный хук
Взглянув на LdrLoadDll изнутри, можно увидеть вызовы недокументированных функций, которых нет и в экспортах. Нас интересует лишь этот блок кода:
А именно LdrpLoadDll. Данной функции нет в экспортах, соответственно через GetProcAddress ее адрес не получить
Есть несколько способов достать адрес этой функции:
  1. Прибавить некоторое значение к адресу LdrLoadDll (получив его через GetProcAddress), это некоторое значение - смещение LdrpLoadDll относительно LdrLoadDll. Данный метод не совсем практичен на мой взгляд, поэтому воспользуемся следующим
  2. Найти сигнатуру LdrpLoadDll и найти адрес через скан. Это будет явно надежнее, чем прибавлять оффсет

Функция LdrpLoadDll имеет не 2 аргумента, как может показаться на первый взгляд, а 4
Если просто закинуть ntdll.dll в IDA и открыть LdrLoadDll, мы увидим:
Тогда с чего я взял, что аргумента 4?
Запустим x86 приложение, подключим к нему дебаггер IDA и перейдем в модуль ntdll.dll в функцию LdrLoadDll
Увидим это:
И убедимся, что аргументов и правда 4
С таким вызовом нам не нужно определять прототип функции, ее можно вызвать как на скриншоте выше, заменив ntdll_LdrpLoadDll адресом этой функции
Перейдем к коду
Создадим DLL, которая при присоединении будет писать в консоль, выводя адрес загрузки. Сделаем копию этой DLL
C++:
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

MODULEINFO NtDllInfo;
GetModuleInformation(GetCurrentProcess(), hNtdll, &NtDllInfo, sizeof(MODULEINFO));

printf("ntdll.dll module base is 0x%p\nntdll.dll module size is 0x%X\n", hNtdll, NtDllInfo.SizeOfImage);

oLdrpLoadDll = (LPVOID)FindPattern((UINT64)hNtdll, (UINT64)NtDllInfo.SizeOfImage, (BYTE*)"\x8B\xFF\x55\x8B\xEC\x83\xE4\xF8\x81\xEC\x00\x00\x00\x00\xA1\x00\x00\x00\x00\x33\xC4\x89\x84\x24\x00\x00\x00\x00\x53\x8B\x5D\x0C\x56", (char*)"xxxxxxxxxx????x????xxxxx????xxxxx"); // LPVOID oLdrpLoadDll - определение вне main() функции
printf("LdrpLoadDll address is 0x%p\n", oLdrpLoadDll);
В данной части кода мы нашли адрес и размер ntdll.dll, а также адрес LdrpLoadDll

C++:
DWORD handle_out = 0;
UNICODE_STRING path;

RtlInitUnicodeString(&path, L"C:\\Users\\catahustle\\source\\repos\\ldrloaddll rebuild\\Release\\test_dll.dll");

LdrLoadDllRebuild(&path, &handle_out);

printf("LdrLoadDllRebuild is done\nHandle to module is 0x%X\n", handle_out);
Здесь мы инициализировали UNICODE_STRING и вызвали функцию LdrLoadDllRebuild. Перейдем к ней

C++:
NTSTATUS NTAPI LdrLoadDllRebuild(PUNICODE_STRING path, DWORD* handle)
{
    signed int result;
    int v7;
    char v9;

    if (*(WORD*)(__readfsdword(24) + 4042) & 0x2000)
    {
        result = -1073740004;
    }
    else
    {
        result = ((int(__fastcall*)(int, char*, int, int*))oLdrpLoadDll)((int)path, &v9, 0, &v7);
        if (result >= 0)
        {
            *handle = *(DWORD*)(v7 + 24);
        }
    }

    return result;
}
Некоторые части оригинального кода были убраны за ненадобностью. LdrpLoadDll загрузит наш модуль в процесс, при этом не вызвав LdrLoadDll, чтобы убедиться в этом, предлагаю поставить хук на LdrLoadDll и загрузить сначала 1 DLL с помощью LoadLibraryA, а затем следующую с помощью только что написанной функции. Для хука воспользуемся библиотекой MinHook
Определим прототип LdrLoadDll и напишем хук функцию:
C++:
typedef NTSTATUS(NTAPI* fnLdrLoadDll)(
    IN PWCHAR               PathToFile OPTIONAL,
    IN ULONG                Flags OPTIONAL,
    IN PUNICODE_STRING      ModuleFileName,
    OUT PHANDLE             ModuleHandle);

fnLdrLoadDll oLdrLoadDll = nullptr;

NTSTATUS NTAPI hkLdrLoadDll(IN PWCHAR               PathToFile OPTIONAL,
    IN ULONG                Flags OPTIONAL,
    IN PUNICODE_STRING      ModuleFileName,
    OUT PHANDLE             ModuleHandle)
{
    printf("\nLdrLoadDll has been called, DLL name: %ls\n", ModuleFileName->Buffer);

    return oLdrLoadDll(PathToFile, Flags, ModuleFileName, ModuleHandle);
}
При вызове LdrLoadDll нам выдаст сообщение в консоль с именем загружаемого модуля
Перейдем к настройке MinHook:
C++:
if (MH_Initialize() != MH_OK) // инициализация MinHook
{
    return 1;
}

HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

LPVOID LdrLoadDll_addr = GetProcAddress(hNtdll, "LdrLoadDll");
printf("LdrLoadDll address is 0x%p\n", LdrLoadDll_addr);

//создание хука
if (MH_CreateHook(LdrLoadDll_addr, &hkLdrLoadDll, reinterpret_cast<LPVOID*>(&oLdrLoadDll)) != MH_OK)
{
    printf("Failed to hook LdrLoadDll\nExiting");
    system("pause > nul");
    return 1;
}

printf("Original LdrLoadDll address is 0x%p\n", oLdrLoadDll);

//активация
if (MH_EnableHook(LdrLoadDll_addr) != MH_OK)
{
    printf("Failed to enable LdrLoadDll hook\nExiting");
    system("pause > nul");
    return 1;
}

printf("Starting loading DLL with LoadLibrary\n");

LoadLibraryA("C:\\Users\\catahustle\\source\\repos\\ldrloaddll rebuild\\Release\\test_dll2.dll");

printf("\nStarting loading another DLL with LdrLoadDll rebuild\n");

DWORD handle_out = 0;
UNICODE_STRING path;

RtlInitUnicodeString(&path, L"C:\\Users\\catahustle\\source\\repos\\ldrloaddll rebuild\\Release\\test_dll.dll");

LdrLoadDllRebuild(&path, &handle_out);

printf("LdrLoadDllRebuild is done\nHandle to module is 0x%X\n", handle_out);

// деактивация
if (MH_DisableHook(LdrLoadDll_addr) != MH_OK)
{
    printf("Failed to disable LdrLoadDll hook\nExiting");
    system("pause > nul");
    return 1;
}

// завершение работы MinHook
if (MH_Uninitialize() != MH_OK)
{
    return 1;
}
Запустим и увидим следующее:
В первом случае, при загрузке обычным LoadLibraryA, нам написало из LdrLoadDll о том, что модуль загружается в процесс и затем сам модуль вывел сообщение о своей загрузке. Во втором случае, мы не увидели сообщение из LdrLoadDll о загрузке модуля, только из самого модуля, что означает отсутствие вызова LdrLoadDll

Данный метод безопаснее простого LoadLibrary, так как обходит хук LdrLoadDll. Вряд-ли в том же VAC будет стоять хук LdrpLoadDll или функций внутри, так как они не документированы и, если загуглить LdrpLoadDll, практически на каждом сайте будет разная информация о прототипе, из чего следует, что на каждой версии Windows и ntdll.dll, ее поиск будет отличаться

Кредиты: Microsoft и их дебаг символы, IDA

Протестировано на Windows 10 x64 1903, на x86 приложении

Если в чем-то не прав, прошу поправить

Исходный код проекта -
Пожалуйста, авторизуйтесь для просмотра ссылки.
 
Последнее редактирование модератором:
alive forever
Забаненный
Статус
Оффлайн
Регистрация
21 Июн 2017
Сообщения
663
Реакции[?]
400
Поинты[?]
0
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Прокнет ли хуйня,если вместо LLA функци в ммар способе заменить ее LDRP?
 
Забаненный
Статус
Оффлайн
Регистрация
14 Сен 2018
Сообщения
37
Реакции[?]
33
Поинты[?]
0
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Хорошая работа, Артем
 
Псевдоадмин
Администратор
Статус
Оффлайн
Регистрация
17 Май 2018
Сообщения
183
Реакции[?]
635
Поинты[?]
78K
Пара замечаний к терду:
  • Если софту необходим доступ к интернету, то для получения адреса на LdrpLoadDll можно использовать символы, вместо не стабильного паттерн скана.
  • Даже при использовании символов, данный метод обхода детектов загрузки библиотеки не может гарантировать стабильность из за использования не документированного API, который может меняться от системы к системе.
  • Весь процесс загрузки модулей в процесс Well Known и любые крупные системы фильтрации могут мониторить эту функцию.
  • Вариант обхода хуков при помощи ребилда необходимой тебе функции штука довольно не стабильная сама по себе, даже при условии, что API документирован.
Для обхода техник перехвата управления, которые модифицируют перехватываемую функцию можно использовать более стабильный метод. Все, что тебе нужно сделать так, это загрузить оригинальный файл с диска и сравнить нужную тебе функцию для нахождения разницы в оригинальной/модифицированной функции.

К примеру, если был использован трамплин для перехвата управления, то типичный пролог у winapi в виде
Код:
prologue:
    mov edi, edi
    push ebp
    mov ebp, esp
code:
    mov eax, 0
    ret
будет заменен на безусловный переход на наш хук.
Код:
trampoline:
    jmp our_hook
code:
    mov eax, 0
ret
После определения разницы оригинальной и хукнутой функции необходимо будет сгенерировать прокси блок который будет содержать украденные оригинальные байты и безусловный переход на не тронутый хуком код.
Код:
mov edi, edi
push ebp
mov ebp, esp
jmp code
И вызывать функцию используя нашу прокси функцию.

В качестве недостатков данного метода можно отметить следующее:
  • Сложность реализации. ( правда сложность ребилда тоже варьируется от функции к функции)
    • Для правильной и стабильной реализации необходимо использовать дизассемблер.
    • Нужно учитывать много параметров при генерации прокси функции (инструкции использующие relative адресацию, влияющие на eip, релоки, etc)
  • Не поддерживает техники перехвата управления, которые не используют патчинг и для поддержки оных нужно писать дополнительные методы. ( iat, eat, vmt, page guard, hwbp, etc )
 
Сверху Снизу