Предельная техника. Разборка с сисколами
Что такое системные вызовы (Syscalls)
Системные вызовы Windows или syscalls служат интерфейсом для взаимодействия программ с системой, позволяя им запрашивать определенные услуги, такие как чтение или запись в файл, создание нового процесса или выделение памяти.
Помните из вводных модулей, что syscalls - это API, которые выполняют действия, когда вызывается функция WinAPI. Например, системный вызов NtAllocateVirtualMemory активируется при вызове функций WinAPI VirtualAlloc или VirtualAllocEx. Затем этот системный вызов перемещает параметры, предоставленные пользователем в предыдущем вызове функции, в ядро Windows, выполняет запрошенное действие и возвращает результат программе.
Все системные вызовы возвращают значение NTSTATUS, которое указывает код ошибки. STATUS_SUCCESS (ноль) возвращается, если системный вызов успешно выполняет операцию.
Большинство системных вызовов не документированы Microsoft, поэтому модули syscall будут ссылаться на приведенную ниже документацию.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Большинство системных вызовов экспортируется из DLL ntdll.dll.
Почему нужно использовать Syscalls
Использование системных вызовов обеспечивает низкоуровневый доступ к операционной системе, что может быть полезно для выполнения действий, которые недоступны или сложнее выполнять с помощью стандартных WinAPI. Например, системный вызов NtCreateUserProcess предоставляет дополнительные опции при создании процессов, которые WinAPI CreateProcess не может.
Кроме того, системные вызовы могут быть использованы для обхода хостовых решений по безопасности, о чем будет рассказано в этой статье.
Системные вызовы Zw против Nt
Существует два типа системных вызовов: те, что начинаются с Nt, и другие с Zw.
Системные вызовы NT - это основной интерфейс для программ в режиме пользователя. Это системные вызовы, которые обычно используют большинство программ Windows.
Системные вызовы Zw, с другой стороны, представляют собой низкоуровневый интерфейс в режиме ядра к операционной системе. Они обычно используются драйверами устройств и другим кодом в режиме ядра, которому требуется прямой доступ к функциональности операционной системы.
Подводя итог, системные вызовы Zw используются в режиме ядра при разработке драйверов устройств, в то время как системные вызовы Nt выполняются из программ в режиме пользователя. Хотя возможно использовать оба из программ в режиме пользователя и все равно получить одинаковый результат. Это можно заметить на приведенных ниже изображениях, где обе версии одного и того же системного вызова имеют один и тот же адрес функции.
Для простоты в этом курсе будут использоваться только системные вызовы Nt.
Номер службы системного вызова
У каждого системного вызова есть специальный номер системного вызова, который известен как номер системной службы или SSN. Эти номера системных вызовов используются ядром для различения системных вызовов между собой. Например, у системного вызова NtAllocateVirtualMemory будет SSN равный 24, тогда как у NtProtectVirtualMemory будет SSN равный 80, эти номера используются ядром для различения NtAllocateVirtualMemory от NtProtectVirtualMemory.
Разные SSN в зависимости от ОС
Важно знать, что SSN будут различаться в зависимости от ОС (например, Windows 10 против 11) и внутри самой версии (например, Windows 11 21h2 против Windows 11 22h2). Используя вышеупомянутый пример, NtAllocateVirtualMemory может иметь SSN равный 24 в одной версии Windows, тогда как в другой версии он будет 34. То же самое относится к NtProtectVirtualMemory, а также ко всем остальным системным вызовам.
Системные вызовы в памяти
Внутри машины SSN не являются абсолютно произвольными и имеют отношение друг к другу. Каждый номер системного вызова в памяти равен предыдущему SSN + 1. Например, SSN системного вызова B равен SSN системного вызова A плюс один. Это также верно, когда подходите к системному вызову с другой стороны, где SSN системного вызова C будет тем из системного вызова D минус один.
Это отношение показано на следующем изображении, где SSN ZwAxxessCheck равен 0, а SSN следующего системного вызова, NtWorkerFactoryWorkerReady, равен 1 и так далее.
Понимание того, что системные вызовы имеют отношение друг к другу, пригодится для обхода систем безопасности далее.
Структура системного вызова
Структура системного вызова, как правило, одинакова и будет выглядеть как показанный ниже фрагмент.
Код:
mov r10, rcx
mov eax, SSN
syscall
Например, NtAllocateVirtualMemory на 64-битной системе показан ниже.
И NtProtectVirtualMemory в качестве примера
Объяснение инструкций системного вызова
Первая строка системного вызова перемещает первое значение параметра, сохраненное в RCX, в регистр R10. Затем SSN системного вызова перемещается в регистр EAX. Наконец, выполняется специальная инструкция системного вызова.
Инструкции syscall на 64-битных системах или sysenter на 32-битных системах - это инструкции, которые инициируют системный вызов. Выполнение инструкции syscall приведет к тому, что программа передаст управление из режима пользователя в режим ядра. Затем ядро выполнит запрошенное действие и вернет управление программе в режиме пользователя по завершении.
Инструкции Test & Jne
Инструкции test и jne предназначены для целей WoW64, которые позволяют 32-битным процессам работать на 64-битной машине. Эти инструкции не влияют на поток выполнения, когда процесс является 64-битным процессом.
Не все NtAPI являются системными вызовами
Важно отметить, что хотя некоторые NtAPI возвращают NTSTATUS, они не обязательно являются системными вызовами. Эти NtAPI могут быть вместо этого функциями более низкого уровня, которые используются WinAPI или системными вызовами. Причина, по которой некоторые NtAPI не классифицируются как системные вызовы, заключается в их несоответствии структуре системного вызова, например, отсутствии номера системного вызова или отсутствии обычной инструкции mov r10, rcx в начале. Пример NtAPI, которые не являются системными вызовами, показан ниже.
LdrLoadDll - Это используется LoadLibrary WinAPI для загрузки образа в вызывающий процесс.
SystemFunction032 и SystemFunction033 - Эти NtAPI были представлены ранее и выполняют операции шифрования/дешифрования RC4.
RtlCreateProcessParametersEx - Это используется CreateProcess WinAPI для создания аргументов процесса.
Инструкции LdrLoadDll показаны ниже. Обратите внимание, как оно не соответствует типичной структуре системного вызова.
Syscalls - Перехват в пользовательском режиме
Введение
Решения по безопасности на стороне хоста часто используют перехват API на системных вызовах для анализа и мониторинга программ в реальном времени. Например, перехватывая системный вызов NtProtectVirtualMemory, решение безопасности может обнаружить более высокоуровневые вызовы WinAPI, такие как VirtualProtect, даже если они скрыты от таблицы адресов импорта бинарного файла. Кроме того, решения безопасности могут получить доступ к любому участку памяти, установленному как исполняемый, и сканировать его в поисках сигнатур. Перехватчики в пользовательском режиме обычно устанавливаются перед инструкцией системного вызова, которая является последним шагом для функции системного вызова в пользовательском режиме.
Перехватчики в режиме ядра могут быть реализованы после выполнения, после передачи управления ядру. Однако Windows Patch Guard и другие средства защиты делают сложной или даже невозможной задачу изменения памяти ядра сторонними приложениями. Установка перехватчиков в режиме ядра может также привести к проблемам стабильности и вызвать неожиданное поведение, поэтому она редко используется.
Пример подключения в пользовательском режиме
В этом разделе используется файл DLL, который при внедрении в процесс использует библиотеку Minhook для установки перехвата на NtProtectVirtualMemory с целью получения информации о действиях EDR относительно перехвата системного вызова. Установленный перехватчик обладает возможностью вывода содержимого памяти на экран в консоль, если он установлен как исполняемый (RX или RWX). Кроме того, процесс будет завершен, если обнаружена область памяти RWX.
Демонстрация перехвата EDR
Это просто демонстрация, как перехват NtProtectVirtualMemory может защитить от инъекции, код пока не приводится.
Рекомендую потренироваться самому.
Как устанавливать перехват можно почитать здесь:Кунгфу-2.Изучаем API Hooking | Цикл статей "Изучение вредоносных программ"
Этот раздел демонстрирует, как EDR может блокировать выполнение определенного полезного кода с помощью перехвата системного вызова. В этой демонстрации вредоносным бинарным файлом будет код внедрения APC (Изучаем технику APC Injection).
Запуск программы без перехвата NtProtectVirtualMemory.
Внедрение MalDevEdr.dll в ApcInjection.exe, с использованием Process Hacker. В MalDevEdr.dll установлен перехватчик NtProtectVirtualMemory.
DLL внедрена, и она обнаруживает RX (это связано с внедрением DLL).
При нажатии клавиши Enter в консоли ApcInjection.exe вызывается NtProtectVirtualMemory, устанавливая 0x0000025041080000 как память RWX, этот адрес памяти затем выводится на экран в консоль.
Выведенное содержимое - это полезная нагрузка Msfvenom calc.
Объяснение
Когда ApcInjection.exe использует VirtualProtect с аргументом PAGE_EXECUTE_READWRITE, он перехватывается MalDevEdr.dll. MalDevEdr.dll будет использовать базовый адрес, переданный VirtualProtect, для сброса содержимого этой области памяти. Так как область памяти изменяется на RWX, MalDevEdr.dll завершает программу и блокирует выполнение полезной нагрузки, чего не смог сделать Windows Defender Antivirus.
Этот концептуальный пример демонстрирует мощь перехвата API в обнаружении и мониторинге программы в реальном времени. В реальных условиях EDR обычно перехватывает более широкий диапазон системных вызовов, улучшая свою способность обнаруживать вредоносные действия.
Обход перехватчиков системных вызовов в пользовательском режиме
Использование системных вызовов напрямую является одним из способов обхода перехватчиков в пользовательском режиме. Например, использование NtAllocateVirtualMemory вместо VirtualAlloc/Ex WinAPI при выделении памяти для полезной нагрузки. Есть несколько других способов, которыми системные вызовы могут быть вызваны незаметно:
Использование прямых системных вызовов.
Использование косвенных системных вызовов.
Снятие перехвата.
Прямые системные вызовы
Обход перехвата системных вызовов в пользовательском режиме можно достичь, получив версию функции системного вызова, записанную на языке ассемблера, и вызвав этот созданный системный вызов напрямую из файла ассемблера. Задача заключается в определении номера службы системного вызова (SSN), так как этот номер меняется от одной системы к другой. Чтобы преодолеть это, SSN может быть либо жестко закодирован в файле ассемблера, либо вычислен динамически во время выполнения.
Образец созданного системного вызова в файле ассемблера (.asm) представлен ниже.
Код:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
syscall
ret
NtAllocateVirtualMemory ENDP
NtProtectVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtProtectVirtualMemory)
syscall
ret
NtProtectVirtualMemory ENDP
// другие системные вызовы...
Вместо вызова NtAllocateVirtualMemory с использованием GetProcAddress и GetModuleHandle, как это было сделано ранее в этом курсе, нижеследующую функцию ассемблера можно использовать для получения того же результата. Это исключает необходимость вызывать NtAllocateVirtualMemory из адресного пространства NTDLL, где установлены перехватчики, тем самым избегая этих перехватчиков.
Этот метод используется в инструментах, таких как
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Косвенные системные вызовы
Косвенные системные вызовы реализуются аналогично прямым системным вызовам, где файлы на языке ассемблера должны быть созданы вручную. Различие заключается в отсутствии инструкции системного вызова внутри функции на языке ассемблера, к которой вместо этого происходит переход.
Визуальное представление представлено ниже.
Функции на языке ассемблера для NtAllocateVirtualMemory и NtProtectVirtualMemory представлены ниже.
C:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
jmp (address of a syscall instruction)
ret
NtAllocateVirtualMemory ENDP
NtProtectVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtProtectVirtualMemory)
jmp (address of a syscall instruction)
ret
NtProtectVirtualMemory ENDP
// другие системные вызовы...
Преимущества косвенных системных вызовов
Преимущество выполнения косвенных системных вызовов перед прямыми системными вызовами заключается в том, что решения безопасности будут искать системные вызовы, вызываемые из-за пределов адресного пространства NTDLL, и считать их подозрительными. С косвенными системными вызовами инструкция системного вызова выполняется из адресного пространства NTDLL, как это должно быть у нормальных системных вызовов. Поэтому косвенные системные вызовы более вероятно пройдут мимо решений безопасности, чем прямые системные вызовы.
Снятие перехвата
Снятие перехвата - это еще один способ обхода перехватчиков, при котором перехваченная библиотека NTDLL, загруженная в память, заменяется на версию без перехвата. Неперехваченная версия может быть получена из нескольких мест, но одним из общих подходов является ее загрузка непосредственно с диска. Таким образом, удаляются все перехватчики, размещенные в библиотеке NTDLL.
Прямой вызов системных вызовов при помощи
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Введение
SysWhispers - это инструмент на питоне, который генерирует код на си и ассемблере, который позволяет обойти перехват системных вызовов через прямые системные вызовы. Существует несколько версий SysWhispers, имеющих разные функции. Различия между версиями будут рассмотрены в этом модуле.
SysWhispers:
SysWhispers создает заголовочные/ASM файлы имплантов для активации прямых системных вызовов на 64-битных системах. Он поддерживает системные вызовы от Windows XP до Windows 10 19042 (20H2). Поддерживаемые версии Windows ограничены, поскольку номер системного вызова (SSN) может изменяться с каждым обновлением Windows. Таким образом, прямая реализация системного вызова для конкретного системного вызова на Windows 10 1903 может быть несовместима с тем же системным вызовом на Windows 10 1909 и наоборот.
Поскольку одни и те же системные вызовы могут иметь разные SSN на разных версиях Windows, SysWhispers проверяет версию Windows целевой системы во время выполнения и вручную устанавливает SSN для соответствующей версии.
SysWhispers - Пример NtMapViewOfSection:
SysWhispers использует скрипт на Python для генерации двух файлов (пример). SSN берутся из
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Выходной пример SysWhispers:
Ниже приведены функции сборки, полученные при использовании SysWhispers для генерации прямых системных вызовов для NtMapViewOfSection.
Код:
// ...
NtMapViewOfSection PROC
mov rax, gs:[60h] ; Load PEB into RAX.
NtMapViewOfSection_Check_X_X_XXXX: ; Check major version.
cmp dword ptr [rax+118h], 5
je NtMapViewOfSection_SystemCall_5_X_XXXX
cmp dword ptr [rax+118h], 6
je NtMapViewOfSection_Check_6_X_XXXX
cmp dword ptr [rax+118h], 10
je NtMapViewOfSection_Check_10_0_XXXX
jmp NtMapViewOfSection_SystemCall_Unknown
NtMapViewOfSection_Check_6_X_XXXX: ; Check minor version for Windows Vista/7/8.
cmp dword ptr [rax+11ch], 0
je NtMapViewOfSection_Check_6_0_XXXX
cmp dword ptr [rax+11ch], 1
je NtMapViewOfSection_Check_6_1_XXXX
cmp dword ptr [rax+11ch], 2
je NtMapViewOfSection_SystemCall_6_2_XXXX
cmp dword ptr [rax+11ch], 2
je NtMapViewOfSection_SystemCall_6_3_XXXX
jmp NtMapViewOfSection_SystemCall_Unknown
NtMapViewOfSection_Check_6_0_XXXX: ; Check build number for Windows Vista.
cmp dword ptr [rax+120h], 6000
je NtMapViewOfSection_SystemCall_6_0_6000
cmp dword ptr [rax+120h], 6001
je NtMapViewOfSection_SystemCall_6_0_6001
cmp dword ptr [rax+120h], 6002
je NtMapViewOfSection_SystemCall_6_0_6002
jmp NtMapViewOfSection_SystemCall_Unknown
NtMapViewOfSection_Check_6_1_XXXX: ; Check build number for Windows 7.
cmp dword ptr [rax+120h], 7600
je NtMapViewOfSection_SystemCall_6_1_7600
cmp dword ptr [rax+120h], 7601
je NtMapViewOfSection_SystemCall_6_1_7601
jmp NtMapViewOfSection_SystemCall_Unknown
NtMapViewOfSection_Check_10_0_XXXX: ; Check build number for Windows 10.
cmp dword ptr [rax+120h], 10240
je NtMapViewOfSection_SystemCall_10_0_10240
cmp dword ptr [rax+120h], 10586
je NtMapViewOfSection_SystemCall_10_0_10586
cmp dword ptr [rax+120h], 14393
je NtMapViewOfSection_SystemCall_10_0_14393
cmp dword ptr [rax+120h], 15063
je NtMapViewOfSection_SystemCall_10_0_15063
cmp dword ptr [rax+120h], 16299
je NtMapViewOfSection_SystemCall_10_0_16299
cmp dword ptr [rax+120h], 17134
je NtMapViewOfSection_SystemCall_10_0_17134
cmp dword ptr [rax+120h], 17763
je NtMapViewOfSection_SystemCall_10_0_17763
cmp dword ptr [rax+120h], 18362
je NtMapViewOfSection_SystemCall_10_0_18362
cmp dword ptr [rax+120h], 18363
je NtMapViewOfSection_SystemCall_10_0_18363
jmp NtMapViewOfSection_SystemCall_Unknown
NtMapViewOfSection_SystemCall_5_X_XXXX: ; Windows XP and Server 2003
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_0_6000: ; Windows Vista SP0
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_0_6001: ; Windows Vista SP1 and Server 2008 SP0
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_0_6002: ; Windows Vista SP2 and Server 2008 SP2
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_1_7600: ; Windows 7 SP0
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_1_7601: ; Windows 7 SP1 and Server 2008 R2 SP0
mov eax, 0025h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_2_XXXX: ; Windows 8 and Server 2012
mov eax, 0026h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_6_3_XXXX: ; Windows 8.1 and Server 2012 R2
mov eax, 0027h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_10240: ; Windows 10.0.10240 (1507)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_10586: ; Windows 10.0.10586 (1511)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_14393: ; Windows 10.0.14393 (1607)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_15063: ; Windows 10.0.15063 (1703)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_16299: ; Windows 10.0.16299 (1709)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_17134: ; Windows 10.0.17134 (1803)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_17763: ; Windows 10.0.17763 (1809)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_18362: ; Windows 10.0.18362 (1903)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_18363: ; Windows 10.0.18363 (1909)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_Unknown: ; Unknown/unsupported version.
ret
NtMapViewOfSection_Epilogue:
mov r10, rcx
syscall
ret
NtMapViewOfSection ENDP
// ...
Объяснение:
Структура PEB содержит три элемента, которые можно использовать для определения версии Windows OS:
- OSBuildNumber
- OSMajorVersion
- OSMinorVersion
- Поскольку основной элемент версии PEB равен 10, выполняется метка NtMapViewOfSection_Check_10_0_XXXX.
- Эта метка затем проверяет номер сборки системы. В этом примере этот номер равен 1809, что заставляет его перейти на метку NtMapViewOfSection_SystemCall_10_0_17763.
- Затем SSN устанавливается в 0028h.
- Затем происходит финальный переход на метку NtMapViewOfSection_Epilogue, где выполняются оставшиеся инструкции системного вызова. Напомним, что функция системного вызова имеет следующий формат:
Код:
mov r10, rcx
mov eax, SSN
syscall
ret
SysWhispers2:
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Это потому, что SysWhispers2 больше не полагается на Таблицу системных Вызовов Windows X86-64 для SSN, а вместо этого использует метод, называемый сортировка по адресу системного вызова. Этот метод исключает необходимость в ручном выборе SSN на этапе выполнения, что приводит к меньшему размеру оболочек системных вызовов.
Сортировка по адресу системного вызова:
Сортировка по адресу системного вызова - это метод для получения SSN системного вызова во время выполнения. Это делается путем поиска всех системных вызовов, начинающихся с Zw, затем сохранения их адреса в массиве и сортировки их в возрастающем порядке (от меньшего к большему адресу). SSN станет индексом системного вызова, сохраненного в массиве.
Реализация SysWhispers2:
Сортировка по адресу системного вызова осуществляется через функцию SW2_PopulateSyscallList Syswhispers2, которая извлекает базовый адрес NTDLL и его экспортный каталог. Используя эту информацию, она рассчитывает RVA экспортируемых функций (адреса, имена, порядок).
Далее SysWhispers2 проверяет имена экспортируемых функций на предмет префиксов с Zw. Эти имена функций хешируются и сохраняются в массиве вместе с их адресами. После этого SW2_PopulateSyscallList сортирует собранные адреса в возрастающем порядке.
Чтобы найти SSN системного вызова, функция SW2_GetSyscallNumber принимает хеш имени целевого системного вызова и возвращает индекс, где этот хеш системного вызова найден в массиве.
Значение индекса - это SSN системного вызова.
Визуальный пример реализации показан ниже.
SysWhispers3:
Помните, что системный вызов отвечает за изменение потока выполнения из режима пользователя в режим ядра. Легитимные инструкции системного вызова всегда должны выполняться из адресного пространства ntdll.dll. Поэтому, когда инструкция системного вызова включена в двоичный файл, как это было с SysWhispers и SysWhispers2, инструкция системного вызова происходит извне этого адресного пространства. Таким образом, двоичный файл, выполняющий инструкцию системного вызова, может быть индикатором злонамеренных намерений.
Обновления в Syswhispers3 можно найти в блоге
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Сам исходник есть на гитхабе:
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Изменения в SysWhispers3:
Вместо того чтобы напрямую вызывать инструкцию системного вызова из функций сборки, SysWhispers3 будет искать инструкцию системного вызова в адресном пространстве ntdll.dll, выполнять инструкцию прыжка туда и выполнять инструкцию системного вызова. Этот метод использует технику косвенного системного вызова.
Кроме того, Syswhispers3 поставляется с опцией jumper_randomized, которая будет выполнять прыжок к инструкции системного вызова, которая принадлежит случайной функции. Например, при вызове NtAllocateVirtualMemory с этой опцией инструкция системного вызова, к которой будет произведен прыжок, не принадлежит NtAllocateVirtualMemory в ntdll.dll. Вместо этого инструкция принадлежит другому системному вызову, такому как функция NtTestAlert.
Как и в предыдущей версии, Syswhispers3 использует метод сортировки по адресу системного вызова, чтобы найти системный вызов.
Выходной пример SysWhispers3:
SysWhispers3 используется для генерации заглушки вызова системного вызова для функции NtMapViewOfSection. Выход Syswhispers3 выглядит аналогично Syswhispers2 с основным отличием в дополнительных вызовах функций SW3_GetRandomSyscallAddress и SW3_GetSyscallNumber, которые показаны и объяснены ниже.
Модуль Syscalls-asm.x64.asm
C:
Syscalls-asm.x64.asm
.code
EXTERN SW3_GetSyscallNumber: PROC
EXTERN SW3_GetRandomSyscallAddress: PROC
NtMapViewOfSection PROC
mov [rsp +8], rcx ; Сохранить регистры.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, 01A80161Bh ; Загрузить хеш функции в ECX.
call SW3_GetRandomSyscallAddress ; Получить смещение системного вызова из другого API.
mov r15, rax ; Сохранить адрес системного вызова {поскольку SW3_GetRandomSyscallAddress вернет адрес инструкции 'syscall' в регистре rax}
mov ecx, 01A80161Bh ; Загрузить хеш функции снова в ECX (необязательно).
call SW3_GetSyscallNumber ; Преобразовать хеш функции в номер системного вызова. {Теперь, eax содержит SSN}
add rsp, 28h
mov rcx, [rsp+8] ; Восстановить регистры.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
jmp r15 ; Перейти к -> Вызов системного вызова. {r15 - это адрес случайной инструкции 'syscall' в ntdll.dll}
NtMapViewOfSection ENDP
end
Модуль Syscalls.c
SW3_GetSyscallNumber и SW3_GetRandomSyscallAddress:
Функция SW3_GetSyscallNumber находит системный вызов, а SW3_GetRandomSyscallAddress извлекает адрес инструкции системного вызова случайного системного вызова внутри ntdll.dll, потому что была использована, опция jumper_randomized.
C:
EXTERN_C DWORD SW3_GetSyscallNumber(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
if (!SW3_PopulateSyscallList()) return -1;
for (DWORD i = 0; i < SW3_SyscallList.Count; i++)
{
if (FunctionHash == SW3_SyscallList.Entries[i].Hash)
{
return i;
}
}
return -1;
}
EXTERN_C PVOID SW3_GetRandomSyscallAddress(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
if (!SW3_PopulateSyscallList()) return NULL;
DWORD index = ((DWORD) rand()) % SW3_SyscallList.Count;
while (FunctionHash == SW3_SyscallList.Entries[index].Hash){
// Spoofing the syscall return address
index = ((DWORD) rand()) % SW3_SyscallList.Count;
}
return SW3_SyscallList.Entries[index].SyscallAddress;
}
Вызов системных вызовов при помощи Hell's Gate
Введение
Помните, что использование прямых системных вызовов (syscalls) — это способ обойти хуки в пользовательском режиме, выполняя сборку инструкций системного вызова вручную.
"Врата Ада" (Hell's Gate) - это другая техника, используемая для выполнения прямых системных вызовов. Читая ntdll.dll, "Врата Ада" могут динамически находить системные вызовы и затем выполнять их из двоичного кода.
Статья о "Вратах Ада" доступна
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Как работают "Врата Ада"
В статье выше мы демонстрировали прямые системные вызовы с помощью SysWhispers. SSN был либо жёстко закодирован, либо найден с использованием метода сортировки по адресу системного вызова, чтобы определить SSN во время выполнения. "Врата Ада", с другой стороны, используют другой подход к поиску SSN.
Подход "Врат Ада" заключается в поиске SSN внутри опкодов перехваченного системного вызова, которые затем вызываются в его функциях сборки.
Разбор "Врат Ада"
Сложность кода требует разбиения объяснения на более мелкие подразделы для более лёгкого понимания.
Структура системного вызова
Код "Врат Ада" начинается с определения структуры VX_TABLE_ENTRY. Эта структура представляет собой системный вызов и содержит адрес, хеш-значение имени системного вызова и SSN. Структура показана ниже.
C:
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress; // Адрес функции системного вызова
DWORD64 dwHash; // Хеш-значение имени системного вызова
WORD wSystemCall; // SSN системного вызова
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
Например, NtAllocateVirtualMemory будет представлен как VX_TABLE_ENTRY NtAllocateVirtualMemory.
Таблица системных вызовов
Используемые системные вызовы хранятся внутри другой структуры, VX_TABLE. Поскольку каждый элемент в VX_TABLE является системным вызовом, каждый элемент будет типа VX_TABLE_ENTRY.
C:
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;
Главная функция
Главная функция начинается с вызова функции RtlGetThreadEnvironmentBlock, которая используется для получения TEB. Это требуется для получения базового адреса ntdll.dll через PEB (помните, что PEB находится в TEB). Затем экспортный каталог ntdll.dll извлекается с использованием GetImageExportDirectory. Экспортный каталог находится путем анализа заголовков DOS и Nt, как было показано в предыдущих модулях.
Затем для каждого системного вызова элемент dwHash инициализируется (например, NtAllocateVirtualMemory.dwHash) его соответствующим хеш-значением. При каждой инициализации вызывается функция GetVxTableEntry, которая показана ниже. Функция разделена на несколько частей для упрощения процесса объяснения.
GetVxTableEntry - Часть 1
C:
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
// ...
}
}
return TRUE;
}
Первая часть функции ищет значение хеша Djb2, равное хешу системного вызова, pVxTableEntry->dwHash. Как только найдено совпадение, адрес системного вызова будет сохранен в pVxTableEntry->pAddress. Вторая часть функции — это место, где находится трюк "Врат Ада".
GetVxTableEntry - Часть 2
C:
// Быстрый и грязный исправление в случае, если функция была перехвачена
WORD cw = 0;
while (TRUE) {
// проверить, является ли это системным вызовом, в этом случае мы слишком далеко
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// проверить, является ли это инструкцией возврата, в этом случае мы, вероятно, тоже слишком далеко
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// Первые опкоды должны быть:
// MOV R10, RCX
// MOV RCX, <системный вызов>
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
cw++;
};
Вторая часть начинается с цикла while после поиска адреса системного вызова, pFunctionAddress. Цикл while ищет байты 0x4c, 0x8b, 0xd1, 0xb8, которые являются опкодами для команд mov r10, rcx и mov rcx, ssn, являясь началом неизмененного системного вызова.
В случае, если системный вызов перехвачен, опкоды могут не совпадать из-за добавления хука безопасностными решениями до инструкции системного вызова. Чтобы устранить это, "Врата Ада" пытаются сопоставить опкоды, и если совпадение не найдено, переменная cw инкрементируется, что добавляет к адресу системного вызова на последующей итерации цикла. Этот процесс продолжается, перемещаясь на один байт за раз, пока не достигнуты инструкции mov r10, rcx и mov rcx, ssn. Ниже показано, как "Врата Ада" находят опкоды, переходя через хук.
Проверка границы
Чтобы предотвратить дальний поиск и получение различного SSN для другого системного вызова, в начале цикла while сделаны два условия для проверки инструкций системного вызова и возврата, расположенных в конце системного вызова. Если поиск достигает одной из этих инструкций и опкоды 0x4c, 0x8b, 0xd1, 0xb8 не были определены, поиск SSN не удастся.
C:
// проверить, является ли это системным вызовом, в этом случае мы слишком далеко
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// проверить, является ли это инструкцией возврата, в этом случае мы, вероятно, тоже слишком далеко
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
Вычисление и сохранение SSN
С другой стороны, если найдено успешное совпадение для опкодов, "Врата Ада" будут вычислять номер системного вызова и сохранять его в pVxTableEntry->wSystemCall. Не обязательно понимать вычисление, которое требует знания побитовых операторов, однако те, кто знаком с этим понятием, могут продолжить чтение этого раздела.
Функция сначала использует оператор сдвига влево (<<) для сдвига битов переменной high влево на 8 раз. Затем она использует побитовый оператор OR (|) для сравнения каждого бита первого операнда (являющегося high << 8) с соответствующим битом второго операнда (являющегося low).
C:
pVxTableEntry->wSystemCall = (high << 8) | low;
Для лучшего понимания приведен пример использования системного вызова NtProtectVirtualMemory для демонстрации подхода "Врат Ада" к вычислению SSN.
На изображении выше показано, как "Врата Ада" находят опкоды, переходя через хук. Это изображение упрощено до следующего фрагмента:
00007FFCC42C4570 | 4C:8BD1 | mov r10,rcx |
00007FFCC42C4573 | B8 50000000 | mov eax,50 | 50:'P'
00007FFCC42C4582 | 0F05 | syscall |
00007FFCC42C4584 | C3 | ret |
Байты C4C:8BD1 B8 50000000 соответствуют следующим смещениям:
4C имеет смещение 0, 8B имеет смещение 1 и D1 имеет смещение 2, B8 имеет смещение 3, 50 имеет смещение 4, 00 имеет смещение 5 и так далее.
Функция GetVxTableEntry указывает, что переменные high и low имеют смещение 5 и 4 соответственно.
C:
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); // Смещение 5
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); // Смещение 4
Проверка значения смещения 5 показывает, что это 0x00, в то время как смещение 4 равно 0x50. Это означает, что значение high равно 0x00, а значение low равно 0x50. Таким образом, SSN равно (0x00 << 8) | 0x50.
Результат побитовой операции совпадает с номером SSN системного вызова NtProtectVirtualMemory, который равен 50 в шестнадцатеричной системе.
Вызов системного вызова
Теперь, когда "Врата Ада" полностью инициализировали структуру VX_TABLE_ENTRY целевого системного вызова, они могут вызвать его. Для этого "Врата Ада" используют две функции сборки 64-бит: HellsGate и HellDescent, показанные в файле hellsgate.asm.
Код:
data
wSystemCall DWORD 000h ; это глобальная переменная, используемая для сохранения SSN системного вызова
.code
HellsGate PROC
mov wSystemCall, 000h
mov wSystemCall, ecx ; обновление переменной 'wSystemCall' входным аргументом (значением регистра ecx)
ret
HellsGate ENDP
HellDescent PROC
mov r10, rcx
mov eax, wSystemCall ; `wSystemCall` - это SSN вызываемого системного вызова
syscall
ret
HellDescent ENDP
end
Чтобы вызвать системный вызов, сначала нужно передать номер системного вызова функции HellsGate. Это сохраняет его в глобальной переменной wSystemCall для будущего использования. Затем используется HellDescent для вызова системного вызова, передавая параметры системного вызова. Это демонстрируется в функции Payload.
Заключение
Было показано, что обход хуков в пользовательском режиме возможен с помощью прямых системных вызовов, инструмента SysWhispers и техники "Врата Ада".
В следующей статьи ранее реализованные техники инъекции процессов будут изменены для использования системных вызовов вместо WinAPI.
В следующей статьи будет подробный разбор использования SysWhispers3 и HellsGate.