Сказка для самых маленьких про двух братьев - DLL Hijacking и DLL Side-Loading

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: shqnx
Специально для XSS.is

Вступление

Всем привет, дорогие читатели! В данной статье мы поговорим про DLL Hijacking и DLL Side-Loading и рассмотрим эти техники атак на практическом примере. Статья в первую очередь посвящается новичкам, тут будет довольно простое и разжёванное объяснение всего и вся. Пора приступать.

Теоретическая часть

Начать я хотел бы с небольшого рассказа про DLL Hijacking.

И так, что же такое DLL Hijacking? DLL Hijacking - это техника атаки, при которой задача атакующего сводится к тому, чтобы разместить вредоносный DLL-файл в определённом месте, которое является более приоритетным для поиска DLL-файлов в Windows. Тем самым заставить выполнить вредоносный DLL-файл вместо легитимного. Сейчас всё объясню.

В Windows при запуске программы есть определённая очередь из тех мест, в которых будут искаться необходимые для этой программы DLL-файлы. Первый - самый приоритетный, последний - наименее приоритетный. Ничего сложного. Выглядит очередь примерно так:
1. Проверка. Не загружена ли на данный момент необходимая DLL в памяти?
2. Известные DLL (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
3. Каталог, в котором находится программа
4. C:\Windows\System32\
5. C:\Windows\System\
6. C:\Windows\
7. Рабочий каталог
8. Каталоги в переменной среды PATH

Теперь рассмотрим поэтапно, как именно это работает и в чём суть:
1. Определяется уязвимая программа, которая загружает DLL-файл из определенного места;
2. Атакующий размещает в более приоритетном из очереди для поиска месте вредоносный DLL-файл с тем же именем, что и легитимный;
3. Когда программа загружает DLL-файл, она загружает уже не легитимный файл, а вредоносный;
4. Успех.

Думаю с этим разобрались. Теперь перейдём к DLL Side-Loading.

И так, что же такое DLL Side-Loading? DLL Side-Loading - это техника атаки, при которой задача атакующего сводится к тому, чтобы заменить легитимный DLL-файл на вредоносный, что позволит ему выполнить произвольный код на целевой системе. На данном этапе уже должны отпасть вопросы, почему я обозвал эти две техники братьями. Цель везде одна - манипулировать загружаемыми программой DLL-файлами. Поэтому удобнее всего рассказать о них двух в одной статье.

Практическая часть

И так, настал черёд перейти к практической части. Хотелось бы сказать о том, что практическую часть я буду показывать именно для техники DLL Side-Loading. Основная причина заключается в том, что для вас главным будет научиться работать с программой-жертвой, находить необходимую информацию для создания своего вредоностного DLL-файла и непосредственно правильно создавать его. Для обоих методов этой информации уже будет достаточно на 99%. И я просто не вижу смысла дублировать, грубо говоря, один и тот же код два раза. Для начала я создам легитимный DLL-файл и подопытную программу, загружающую этот самый DLL-файл. Затем мы создадим вредоносный DLL-файл и проверим, что у нас получилось. Поехали:
C: Скопировать в буфер обмена
Код:
#include <windows.h>

__declspec(dllexport) void Pause()
{
    system("pause");
}

__declspec(dllexport) int Sum(int a, int b)
{
    return a + b;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    return TRUE;
}

И так, это код DLL, которая впоследствии будет загружаться программой-жертвой. Он максимально прост. Но для нас главное не это, а то, что в нём присутствуют функции экспорта, как и во встречающихся в живой природе примерах. В данном случае это функция для нахождения суммы двух целых чисел и возвращающая результат, а также функция, которая останавливает выполнение программы с помощью системной команды pause. Нам нужно будет научиться находить функции экспорта и работать с ними в дальнейшем. Но об этом позднее. Сейчас создадим саму программу-жертву, которая будет загружать полученный только что DLL-файл:
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>

typedef int (*Sum)(int, int);

typedef void (*Pause)();


int main() {
    HINSTANCE hExampleLib = LoadLibrary(L"examplelib.dll");
    if (hExampleLib == NULL) {
        return EXIT_FAILURE;
    }

    Pause exPause = (Pause)GetProcAddress(hExampleLib, "Pause");
    if (exPause == NULL) {
        FreeLibrary(hExampleLib);
        return EXIT_FAILURE;
    }

    exPause();

    Sum exSum = (Sum)GetProcAddress(hExampleLib, "Sum");
    if (exSum == NULL) {
        FreeLibrary(hExampleLib);
        return EXIT_FAILURE;
    }

    int sumRes = exSum(5, 3);
    printf("Sum: %i\n", sumRes);

    FreeLibrary(hExampleLib);
    return EXIT_SUCCESS;
}

И так, вот и она. Я специально создаю свои экземпляры, чтобы в них была исключительно нужная информация касаемо данной техники и для новичков это всё легче усваивалось. В общем, что тут происходит? Происходит стандартная загрузка DLL (в нашем случае она называется examplelib.dll). Мы объявляем новые типы (Sum и Pause), которые являются указателями на соответствующие функции. В функции main происходит загрузка библиотеки в адресное пространство текущего процесса при помощи LoadLibrary из Windows API. В случае успешной загрузки, мы получаем хэндл нашей DLL. Иначе программа завершает выполнение с кодом ошибки. Далее получаем адрес нашей экспортированной функции Pause из загруженной DLL. Если не удаётся это сделать, вызывается FreeLibrary для выгрузки DLL из памяти и программа завершает выполнение с кодом ошибки. Вызываем функцию функцию Pause через указатель exPause, полученный на предыдущем шаге. Аналогичные действия проводим и для экспортированной функции Sum. Наконец, выводим результат суммы, выгружаем DLL из памяти и завершаем программу.

И так, перемещаем полученный examplelib.dll к исполняемому файлу нашей программы example.exe для тестов и смотрим, всё ли работает:
1725983731496.png



Поиск необходимых данных

Всё окей, мы получили рабочий экземпляр, с которым теперь будем работать. Представим, что мы ничего не знаем об example.exe. Каким образом мы можем это исправить? Правильно, с помощью соответствующих инструментов для анализа и мониторинга. А если быть точнее, то с помощью API Monitor и Process Monitor. Для начала открываем API Monitor (чувствительна к архитектуре, поэтому убедитесь, что вы открыли версию, соответствующую архитектуре программы-жертвы). Важно указать модули, для которых мы хотим отслеживать вызовы API. Поскольку в исходной программе мы используем Windows API, выберем Kernel32.dll:
1725983745269.png



Далее ставим галочку напротив System Services > Dynamic-Link Libraries, как показано на скриншоте:
1725983762197.png



Выбираем нашу программу-жертву example.exe в соседнем окне Monitored Processes:
1725983774214.png



И нажимаем OK:
1725983783736.png



И так, мы можем видеть вызовы API в нашей программе:
1725983795255.png



Здесь отображается всё, что нам необходимо, а именно: вызов функции LoadLibrary, которая загружает легитимную библиотеку examplelib.dll, вызов функции GetProcAddress для двух экспортируемых функций - Pause и Sum, а также для выгрузки DLL из памяти по завершению выполнения программы. Двигаемся дальше.

Лучший способ определить, есть ли у вас возможность выполнить DLL Side-Loading - сделать это при помощи программы Process Monitor. Открываем её. Для удобства нам нужно создать определённый фильтр. Для этого тыкаем на значок фильтра или нажимаем сочитание клавиш Ctrl+L:
1725983811025.png



В появившемся окне нам необходимо выставить следующие параметры и нажать Add:
1725983822252.png



И так, я еще раз запущу example.exe, дабы в Process Monitor появилась информация о запущенном процессе. Что мы конкретно должны найти для проверки, возможен ли сайдлоадинг - так это операции с DLL, для которых результатом является NAME NOT FOUND, вот пример:
1725983832945.png



Сейчас я перемещу examplelib.dll в другое место, например, в C:\Windows\ и еще раз запущу example.exe. В Process Monitor мы увидим следующее:
1725983843287.png



Это говорит нам о том, что у нас есть возможность выполнить данную атаку. На данном скриншоте также прослеживается информация, по которой виден порядок поиска нашей DLL. О чём я и расписывал в начале статьи про DLL Hijacking. И так, возможность для проведения атаки найдена. Давайте приступать к созданию "злой" DLL.

Создаём нашу "злую" DLL. Проксирование функций.

И так, сначала код, потом объяснение. Приступаем:
C: Скопировать в буфер обмена
Код:
#include <windows.h>

#pragma comment(linker, "/export:Pause=_examplelib.Pause,@1")
#pragma comment(linker, "/export:Sum=_examplelib.Sum,@2")

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxA(NULL, "Evil DllMain", "XSS.is", MB_OK);
    }

    return TRUE;
}

Для того, чтобы example.exe не крашнулась, необходимо либо реплицировать функционал исходных функций экспорта, либо проксировать их. В данном случае показано именно проксирование. Мы объявляем директивы линковщика, которые используются для управления экспортом функций из легитимной DLL, которая в данном случае должна называться _examplelib.dll. Также мы указываем порядковые номера функций в легитимном DLL при помощи @номер. Откуда взять порядковые номера функций? В этом нам поможет dumpbin. Открываем командную строку разработчика и пишем следующую команду:
dumpbin /EXPORTS C:\Windows\examplelib.dll
Нажмите, чтобы раскрыть...

Можем посмотреть на вывод данной команды и увидеть в ней порядковые номера функций:
1725983922476.png



И так, как это вообще работает? Мы экспортируем функции из легитимного DLL-файла, при этом получаем возможность добавить свой вредоносный функционал в DllMain. В данном случае это просто вывод месседж бокса, однако там может быть всё что угодно. Для проверки нам нужно поместить переименованный в _examplelib.dll легитимный DLL-файл, наш перемеименованный в examplelib.dll "злой" DLL-файл и запустить example.exe. Смотрим, что получилось:
1725983938978.png



Выполняется DllMain нашего поддельного examplelib.dll, появляется месседж бокс. После его закрытия example.exe продолжает свою работу как ни в чём не бывало и потом завершает своё выполнение:
1725983949843.png



Всё отработало корректно.

Создаём нашу "злую" DLL. Репликация функций.

Теперь давайте рассмотрим пример с репликацией функционала исходных функций экспорта. Пишем "злую" DLL:
C: Скопировать в буфер обмена
Код:
#include <windows.h>

#pragma comment(linker, "/export:Sum=_examplelib.Sum,@2")

__declspec(dllexport) void Pause()
{
    MessageBoxA(NULL, "Evil Pause", "XSS.is", MB_OK);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    return TRUE;
}

В данном случае мы реплицируем только функцию Pause, а функцию Sum мы по прежнему проксируем. Суть в том, чтобы заменить функционал легитимной функции Pause. Думаю тут опять понятно, что вместо месседж бокса, который я использую для примера, может быть всё, что душе угодно. Проверяем результат:
1725983985432.png



Как мы видим, сначала выполняется наша злобная репликация функции Pause. То есть выводится месседж бокс вместо системной паузы. Затем, как ни в чём не бывало, выполняется функция Sum и программа завершает своё выполнение:
1725983998583.png



Заключение

Всех благодарю за внимание. Я постарался сделать материал максимально полезным и максимально приятным в чтении для новичков. Желаю всем успехов!

Ученье — свет, а неученье — тьма.
 
Сверху Снизу