Как обойти самозащиту Avast

Как обойти самозащиту Avast

В этой статье я покажу обход самозащиты Avast, но остановлюсь не на результате, а на процессе: на том, как я узнал, как реализована функция безопасности, обнаружил новый недокументированный способ перехвата все системные вызовы без гипервизора и PatchGuard вызывали BSOD, и, наконец, на основе полученных знаний реализовали обход. В общем разберем как обойти самозащиту Avast.

Обзор самообороны

Самозащита каждого антивируса (AV) представляет собой проприетарный недокументированный механизм, поэтому официальной документации не существует. Тем не менее, я постараюсь провести вас через наиболее важные общие основные аспекты. Деталей здесь должно быть достаточно, чтобы понять следующие шаги исследования.

Типичная самозащита антивируса представляет собой механизм, аналогичный по назначению Protected Process Light (PPL): разработчики пытаются переместить процессы продукта в собственный домен безопасности, но без использования специальных сертификатов (OID проверки защищенного процесса (light) в EKU), чтобы злоумышленник не мог вмешиваться и завершать свои собственные процессы. То есть самозащита по функциям аналогична PPL, но не является ее частью или расширением — EPROCESS.Protection не содержит флагов, установленных AV, и поэтому RtlTestProtectedAccess не может предотвратить доступ к защищенным объектам. Поэтому разработчикам самостоятельно приходится:

  1. Назначение и управление тэгами доверия процессам (при создании процесса, при совершении подозрительных действий); Перехватывать важные с точки зрения инвазивного воздействия операции операционной системы (ОС) (открытие процессов, потоков, файлов на запись) и проверять, не нарушают ли они правила выбранной политики.

И если с первым пунктом все просто и понятно — какие баги там искать (например, CVE-2021-45339), то второй пункт требует уточнения. Что и как перехватывают антивирусы? Из-за PatchGuard и требований совместимости у разработчиков есть довольно скудные возможности, а именно использовать только ограниченное количество задокументированных хуков. И их не так много, которые могут помочь защитить процесс:

  1. Ob-Callbacks — запретить открытие для записи процесса, потока;
  2. Driver Minifilter — предотвращает запись в файлы продукта;
  3. Некоторые хуки пользовательского режима — другие предупреждения.

Я не буду вдаваться в подробности того, как это работает под капотом, но если вы не знакомы с этими механизмами, я рекомендую вам перейти по ссылкам выше. На этом мягкое введение в самозащиту антивируса считаем законченным и можно переходить к исследованиям.

Проверка самозащиты Avast

Когда вам нужно взаимодействовать с объектами ОС, NtObjectManager — отличный выбор. Это модуль PowerShell, представляет собой мощную оболочку для очень большого количества API-интерфейсов ОС. С его помощью также можно проверить, насколько процессы защищены самозащитой, дают ли механизмы AV-драйвера больше доступа, чем должны. И я начал с простого открытия пользовательского интерфейса Avast, процесса AvastUI.exe:

Open process AvastUI.exe Как обойти самозащиту Avast

На картинке выше видно, что в целом все работает предсказуемо — WRITE-права «урезаны» (1). Немного опасно, что они оставляют право доступа VmRead(2), но эксплуатировать не так просто, поэтому я решил поискать дальше:

Copy handle of process AvastUI.exe Как обойти самозащиту Avast

Я попытался продублировать ограниченный дескриптор с разрешениями до AllAccess (1), и на удивление это сработало, хотя трюк довольно тривиален. Получив дескриптор с правами на запись, в случае реализации самозащиты на основе Ob-Callbacks ничто не ограничивает злоумышленника от выполнения деструктивных действий, направленных на защищаемый процесс. Потому что проверка доступа и Ob-Callbacks происходят только один раз при создании дескриптора и не участвуют в последующих системных вызовах с использованием полученного дескриптора. Здесь можно инжектить, но для теста достаточно просто завершить процесс, что я и сделал. Результат оказался неожиданным — процесс не смог завершиться (2), произошла ошибка доступа, хотя мой хэндл должен был разрешить выполнение запрошенного действия.

Очевидно, что каким-то образом AV мешает завершению процесса и запрещает ему это делать. И делается это не на уровне хэндлов Ob-Callbacks, а уже на вызове API. Это означает, что TerminateProcess где-то перехвачен. Я проверил, был ли это хук пользовательского режима, и оказалось, что это не так.

Исследование хука системного вызова

В первую очередь я изучил существующие способы перехвата системных вызовов. Широко известно, что перехват системных вызовов невозможен в системах x64 с 2005 года из-за PatchGuard. Но очевидно, что Avast перехватывает. Я нашел пару интересных статей (здесь и здесь), но все эти трюки были недокументированы и подтверждали, что в современной Windows перехват системных вызовов не является документированной функцией и формально недоступен даже для антивирусов.

Затем я проследил вышеупомянутый системный вызов (TerminateProcess на AvastUI.exe) и обнаружил, что перед каждым вызовом обработчика системного вызова из SSDT происходит вызов PerfInfoLogSysCallEntry, который подменяет адрес обработчика в стеке (обработчик сохраняется в стеке, затем Вызывается PerfInfoLogSysCallEntry, затем он снимается со стека и выполняется):

Call PerfInfoLogSysCallEntry Как обойти самозащиту Avast

На скриншоте выше видно, что мы находимся в обработчике системного вызова (1), но еще до маршрутизации на конкретный обработчик. Код ядра помещает адрес обработчика завершения процесса (nt!NtTerminateProcess) в стек по смещению @rsp+0x40h(2), затем вызывается PerfInfoLogSysCallEntry(3), после возврата из вызова адрес обработчика выталкивается обратно из стек (4) и обработчик вызывается напрямую (5).

А если следовать коду дальше, то после вызова PerfInfoLogSysCallEntry можно увидеть следующую картину:

Call replaced syscall Как обойти самозащиту Avast

В регистре @rax появляется адрес aswbidsdriver+0x20f0 из драйвера Avast (3), и вместо исходного обработчика происходит переход на него (2).

Этот метод перехвата системных вызовов не похож на упомянутый выше. Но уже сейчас мы видим, что в функции PerfInfoLogSysCallEntry происходит некое «волшебство» и имя этой функции достаточно уникально, чтобы попытаться поискать информацию по ней в гугле.

Первый результат в результатах поиска ведет на проект InfinityHook, который как раз реализует перехваты системных вызовов x64. Какая удача! 😉 Подробно как это работает можно прочитать на странице README.md, а здесь я приведу самое главное:

В +0x28 в структуре _WMI_LOGGER_CONTEXT вы можете увидеть элемент с именем GetCpuClock. Это указатель функции, который может принимать одно из трех значений в зависимости от конфигурации сеанса: EtwGetCycleCount, EtwpGetSystemTime или PpmQueryTime.

Контекст «Circular Kernel Context Logger» ищется по сигнатуре, и в нем заменяется его указатель на GetCpuClock. Но есть одна проблема, а именно: в последних ОС этот код не работает. Почему? В проекте есть проблема, из которой можно понять, что элемент GetCpuClock структуры _WMI_LOGGER_CONTEXT больше не является указателем на функцию, а является обычным флагом. Мы можем проверить это, заглянув в память объекта в Windows 11, и действительно ничего нельзя изменить в этом члене класса. Вместо указателя на функцию мы можем наблюдать беззнаковое 8-битное целое:

GetCpuClock member Как обойти самозащиту Avast

Тогда как они берут на себя управление? Я установил точку останова доступа к данным при изменении адреса системного обработчика внутри PerfInfoLogSysCallEntry (что-то вроде «ba w8 /t @$thread @rsp + 40h»), чтобы увидеть, какой конкретный код заменяет исходный обработчик системного вызова:

Replace original syscall Как обойти самозащиту Avast

На скриншоте выше видно, что код из модуля aswVmm по смещению 0xdfde (1) заменяет адрес обработчика системного вызова в стеке (2) на адрес aswbidsdriver+0x20f0 (3). Если еще раз изменить причину вызова этого кода в EtwpReserveTraceBuffer, мы увидим, что при регистрации события ETW вызывается обработчик nt!HalpPerformanceCounter + 0x70:

HalpPerformanceCounter calls QueryCounter

И соответственно при проверке значения по смещению в этой недокументированной структуре (ходят слухи, что по смещению находится член QueryCounter структуры) можно убедиться, что там стоит символ Аваста:

HalpPerformanceCounter.QueryCounter

Теперь стало понятно, как реализован перехват системных вызовов. Я поискал в Интернете и нашел некоторую публичную информацию об этом виде перехвата здесь и даже код, который реализует этот подход. В этом коде видно, как можно найти приватную структуру nt!HalpPerformanceCounter и если описать ее пошагово, то получится следующее:

  1. Найдите _WMI_LOGGER_CONTEXT поставщика ETW циклического регистратора контекста ядра, выполнив поиск подписи глобальной переменной EtwpDebuggerData в разделе .data образа ядра. Далее используется знание, что после этой переменной идет массив провайдеров и искомый имеет индекс 2;
  2. Затем настраиваются флаги провайдера для ведения журнала системных вызовов. И флаг установлен на использование KeQueryPerformanceCounter, который, в свою очередь, вызовет HalpPerformanceCounter.QueryCounter;
  3. HalpPerformanceCounter.QueryCounter заменяется напрямую. Для этого эту переменную следует найти: дизассемблируется функция KeQueryPerformanceCounter, которая ее использует, и из нее по сигнатуре извлекается адрес переменной. Далее член недокументированной структуры заменяется хуком;
  4. Провайдер запускается, если он был остановлен ранее.

Обход самозащиты

Теперь мы знаем, что Avast реализует самозащиту, перехватывая системные вызовы в ядре, и понимаем, как реализуются эти перехваты. Внутри хуков, очевидно, реализована логика для определения, разрешать ли конкретному процессу выполнять конкретный системный вызов с этими параметрами, например: может ли процесс Maliscious.exe выполнить TerminateProcess с дескриптором для процесса AvastUI.exe. Как мы можем преодолеть эту защиту? Я вижу 3 варианта:

  1. Сами крючки ломаем:
  • Замененный HalpPerformanceCounter.QueryCounter вызывается не только при обработке системных вызовов, но и при других событиях. Так что драйвер Avast как-то различает эти случаи. Можно попробовать вызвать системный вызов таким образом, чтобы драйвер Avast не понял, что это системный вызов, и не заменил его собственной процедурой;
  • Или отключите подключение.

2. Найти ошибку в логике Avast для определения запрещенных операций (например, найти процесс из списка исключений и сымитировать его);

3. Используйте системные вызовы, которые не перехватываются.

Последний вариант кажется самым простым, так как разработчики точно забыли перехватить и запретить какую-то важную функцию. Если этот подход не сработает, то мы можем приложить больше усилий и попытаться реализовать пункт 1 или 2.

Чтобы понять, забыли ли разработчики какую-то функцию, необходимо перечислить названия функций, которые они перехватывают. Если посмотреть на xref на функцию aswbidsdriver+0x20f0, на которую перенаправляется управление вместо оригинального обработчика системного вызова согласно скриншоту выше, то можно увидеть, что его адрес находится в каком-то массиве вместе с именем перехватываемого системного вызова. Это выглядит так:

Hooked API array

Логично предположить, что если пройтись по всем элементам этого массива, то можно получить имена всех перехваченных системных вызовов. Реализуя такой подход, мы получаем следующий список системных вызовов, которые Avast перехватывает, анализирует и, возможно, запрещает вызывать:

NtContinue
NtSetInformationThread
NtSetInformationProcess
NtWriteVirtualMemory
NtMapViewOfSection
NtMapViewOfSectionEx
NtResumeThread
NtCreateEvent
NtCreateMutant
NtCreateSemaphore
NtOpenEvent
NtOpenMutant
NtOpenSemaphore
NtQueryInformationProcess
NtCreateTimer
NtOpenTimer
NtCreateJobObject
NtOpenJobObject
NtCreateMailslotFile
NtCreateNamedPipeFile
NtAddAtom
NtFindAtom
NtAddAtomEx
NtCreateSection
NtOpenSection
NtProtectVirtualMemory
NtOpenThread
NtSuspendThread
NtTerminateThread
NtTerminateProcess
NtSuspendProcess
NtNotifyChangeKey
NtNotifyChangeMultipleKeys

Напомню, что изначально мы хотели обойти самозащиту, и в целях быстрой демонстрации попытались просто убить процесс. Но теперь вернемся к первоначальному замыслу — впрыску. Нам нужно найти способ внедрения, который просто не использует функции, перечисленные выше. Методов инъекций очень много и есть много ресурсов, где они описаны. Я нашел довольно старый, но все еще актуальный список в статье Elastic «Десять методов внедрения процессов: технический обзор распространенных и трендовых методов внедрения процессов» (после завершения этого исследования я нашел еще один интересный пост «Код «Плата о пломо» приемы инъекций/выполнения», очень рекомендую пост и блог). В ОС Windows есть самые популярные техники инъекций. Итак, что из этого можно применить, чтобы оно работало, а самозащита Avast не могла предотвратить внедрение кода?

Из перехваченных системных вызовов видно, что разработчики, похоже, прочитали эту статью и позаботились о смягчении внедрения в процессы. Например, самая первая классическая инъекция «CLASSIC DLL INJECTION VIA CREATEREMOTETHREAD AND LOADLIBRARY» невозможна. Хотя в названии техники есть только CreateRemoteThread и LoadLibrary, но там все равно нужна WriteProcessMemory, а это узкое место в нашем случае — Avast перехватывает NtWriteVirtualMemory, поэтому в исходном виде техника работать не будет. А что, если ничего не писать в удаленный процесс, а использовать существующие в нем строки? У меня возникла следующая идея:

  1. Используя ошибку копирования дескриптора, получить дескриптор полного доступа к процессу AvastUI.exe;
  2. Найти в памяти процесса (дескриптор есть и перехватов таких действий нет) строку, представляющую путь, по которому злоумышленник может записать свой модуль. Мне показалось самым надежным способом поискать в PEB среди переменных окружения строку типа «LOCALAPPDATA=C:\Users\User\AppData\Local», так что этот путь точно доступен для записи и память не будет случайно освобождена при время выполнения, т.е. эксплойт будет надежнее;
  3. Скопируйте модуль для внедрения в C:\Users\User\AppData\Local.dll;
  4. Находим адрес kernel32!LoadLibraryA (для этого, спасибо KnownDlls, даже не нужно читать память, хотя мы можем);
  5. Вызов CreateRemoteThread (не перехватывается) с адресом процедуры LoadLibraryA и аргументом — строкой «C:\Users\User\AppData\Local». Поскольку путь не заканчивается на «.dll», согласно документации, LoadLibraryA сама добавляет постфикс;

Если этот сценарий выразить в коде PowerShell, то получится следующее (помимо упомянутого ранее NtObjectManager, скрипт использует командлет Search-Memory из модуля PSMemory):

$avastUIs = Get-Process -Name AvastUI
$avastUI = $avastUIs[0]
$localAppDataStrings = $avastUI | Search-Memory -Values @{String='LOCALAPPDATA=' + $env:LOCALAPPDATA}
$pathAddress = $localAppDataStrings.Group[0].Address + 'LOCALAPPDATA='.Length  #[1]
Copy-Item -Path .\MessageBoxDll.dll -Destination ($env:LOCALAPPDATA + '.dll') #[2]
$process = Get-NtProcess -ProcessId $avastUI.Id
$process2 = Copy-NtObject -Object $process -DesiredAccess GenericAll #[3]
$kernel32Lib = Import-Win32Module -Path 'kernel32.dll'
$loadLibraryProc = Get-Win32ModuleExport -Module $kernel32Lib -ProcAddress 'LoadLibraryA' #[4]
$thread = New-NtThread -StartRoutine $loadLibraryProc -Argument $pathAddress -Process $process2 #[5]

И если мы запустим этот код, то… Ничего не произойдет. Скорее будет создан поток, он попытается загрузить модуль, но не загрузит его, и самое страшное, что код загрузки, основанный на стеке вызовов в ProcMon, перехватывается драйвером aswSP.sys (Avast Self Защита) и судя по доступу к директориям через CI.dll пытается проверить подпись модуля:

LoadLibrary failed

Avast не только использует недокументированные перехватчики системных вызовов, но также использует недокументированную библиотеку режима ядра CI.dll для проверки подписи в ядре. Это очень смелая и крутая фича, но нам она приносит проблемы: либо надо менять схему закачки на безфайловую, либо теперь искать баг и в механизме проверки подписи. Я выбрал второе.

Кэшированная ошибка подписи

AvastUI.exe — это электронное приложение, поэтому у него есть особая модель процесса — один основной процесс и несколько процессов рендеринга:

AvastUI process model

А дело в том, что в случае неудачной попытки инъекции в предыдущем разделе мы пытались внедрить код в основной процесс, но потом, в процессе раздумий, я попытался перезапустить скрипт, указав в качестве цели дочерние процессы и  инъекция сработала.

AvastUI pwned

И если мы потом попытаемся снова инжектиться в основной процесс, то у нас все получится и никаких проверок подписи производиться не будет:

LoadLibrary succedeed

Странно, но инъекция работает. После загрузки тестовой неподписанной библиотеки процессом визуализации в файл добавляется расширенный атрибут ядра $KERNEL.PURGE.ESBCACHE:

$f = Get-NtFile -Path ($env:LOCALAPPDATA + '.dll') -Win32Path -Access GenericRead -ShareMode Read
$f.GetEa()
Entries                                                     Count
-------                                                     -----
{Name: $KERNEL.PURGE.ESBCACHE - Data Size: 69 - Flags None}     1

Это специальный атрибут, который можно установить только из ядра с помощью функции FsRtlSetKernelEaFile, и он удаляется при каждом изменении файла. CI  хранит в этом атрибуте статус проверки подписи, и если он присутствует, то повторная проверка не происходит, а повторно используется результат предыдущей. Таким образом, очевидно, что при загрузке модуля в процесс рендера происходит ошибка в драйвере самозащиты (вероятно, aswSP.sys) (в данной статье мы не будем разбираться, какая именно, но читатель сам может поискать в ProcMon стек вызовов операции SetEAFile в файле и изучите, почему она вызывается), что приводит к установке расширенного атрибута ядра в неподписанном файле с подтвержденной информацией о подписи для CI. И после этого этот файл можно загрузить в любой другой процесс, использующий результаты предыдущей «проверки подписи». Посмотрим, что написано в атрибуте (здесь нам снова поможет NtObjectManager):

$f.GetCachedSigningLevelFromEa()
Version             : 3
Version2            : 2
USNJournalId        : 133143369490576857
LastBlackListTime   : 4/6/2022 2:40:59 PM
ExtraData           : {Type DGPolicyHash - Algorithm Sha256 - Hash 160348839847BC9E112709549A0739268B21D1380B9D89E0CF7B4EB68CE618A7}
Flags               : 32770
SigningLevel        : DeviceGuard
Thumbprint          :
ThumbprintBytes     : {}
ThumbprintAlgorithm : Unknown

Подпись неподписанного файла помечается уровнем DeviceGuard (DG) как действительная, поэтому понятно, почему основной процесс ее загружает. Кроме того, эта ошибка может позволить выполнять неподписанный код в системе DG. Хотя код должен быть уже выполнен, чтобы вызвать ошибки, эту ошибку можно использовать как этап в цепочке эксплуатации для выполнения произвольного кода в системе DG.

Подводя итог, скрипт обхода самозащиты выше действителен, но применять его нужно не к основному процессу AvastUI, а к одному из дочерних. Но если вы все же хотите инжектить в основной процесс, то достаточно сначала инжектить в любой неосновной AvastUI — это установит Kernel EA неподписанного файла в значение пройденной проверки подписи и после этого уже можно инжектить этот модуль в основной процесс — наличие атрибута будет информировать процесс о том, что файл подписан и будет успешно загружен.

Выводы

В результате проделанной работы у нас есть ошибка копирования дескриптора процесса на текущей последней версии Avast Free Antivirus (22.11.6041 сборка 22.11.7716.762), мы знаем, что Avast использует хук ядра на системных вызовах, мы знаем, как они работают на полностью обновленной Windows 11 22H2, исследовали, какие хуки ставит Avast, разработали инъекцию в обход механизма перехвата, обнаружили проверку подписи в ядре Avast с помощью функций CI.dll, нашли ошибку в установке кэшированного уровня подписи, и используют все это, мы, наконец, можем внедрить код в доверенный процесс AvastUI.exe, защищенный антивирусом.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Click to rate this post!
[Total: 0 Average: 0]

Leave a reply:

Your email address will not be published.