Драйвер режима ядра что это
Если ваш компьютер имеет 64-битную архитектуру и поддерживает технологии виртуализации Intel VT-X или AMD-v (поддерживаются большинством современных процессоров), то в Windows 10 вам доступны дополнительные функции безопасности на базе виртуализации.
Одна из таких функций называется “Изоляция ядра” (Core Isolation). Она использует аппаратную виртуализацию для изоляции критически важных частей ядра операционной системы от пользовательских драйверов и программного обеспечения, запущенного на компьютере. Изоляция ядра позволяет предотвратить доступ вредоносных программ и эксплойтов к защищенным зонам ядра и заблокировать попытки обхода контроля безопасности, инъекции вредоносных программ и другое потенциально опасное поведение.
Функция под названием “Целостность памяти” (Memory integrity) является подмножеством изоляции ядра. Она защищает от внедрения вредоносного кода в память при вредоносной атаке.
Целостность памяти — это функция Windows, которая гарантирует надежность кода, работающего в ядре Windows. Она использует аппаратную виртуализацию и Hyper-V для защиты процессов режима ядра Windows от инъекции и выполнения вредоносного или непроверенного кода. Целостность кода, который работает в Windows, проверяется с помощью целостности памяти, что позволяет Windows эффективно противостоять атакам вредоносных программ.
“Целостность памяти” могла блокировать драйверы
При включении Memory Integrity, функция блокирует компьютер и может вызывать проблемы с загрузкой или работой драйверов.
В новом документе поддержки Microsoft пояснила, что ошибки или обычно неопасные уязвимости драйверов могут приводить к тому, что “Целостность памяти” блокирует их загрузку.
В таких ситуациях Microsoft рекомендует проверить доступность обновленного драйвера, в котором уязвимость уже может быть исправлена.
Если данный вариант не сработал, то рекомендуется отключить функцию Memory Integrity, чтобы драйвер мог корректно загрузиться.
Для отключения “Целостности памяти”, выполните следующие шаги:
- Перейдите в Параметры > Обновление и безопасность > Безопасность Windows > Безопасность устройства и в секции Изоляция ядра кликните ссылку Сведения об изоляции ядра
В качестве альтернативы можно кликнуть по ссылке windowsdefender://coreisolation/ в Windows 10, чтобы открыть необходимую страницу.
- Когда откроется страница Изоляция ядра, установите переключатель Целостность памяти в неактивное положение. Windows 10 запросит перезагрузку компьютера.
- Выполните перезагрузку, и Целостность памяти будет отключена.
После этого, проверьте, остались ли проблемы с загрузкой драйверов. Если проблема сохранилась, то вам лучше получить помощь у производителя устройства и уточнить, когда станет доступен обновленный драйвер.
Процессорные архитектуры x86 и x64 имеют четыре кольца защиты, из которых в Windows по факту используются всего два — это ring 3 (режим пользователя) и ring 0 (режим ядра). Бытует мнение, что код режима ядра — самый привилегированный и «ниже» ничего нет. На самом деле архитектура x86/x64 позволяет опускаться еще ниже: это технология виртуализации (hypervisor mode), которая считается кольцом −1 (ring −1), и режим системного управления (System Management Mode, SMM), считающийся кольцом −2 (ring −2), которому доступна память режима ядра и гипервизора.
Итак, мы решили писать собственный драйвер. Начнем с выбора инструментария. Я советую использовать Microsoft Visual Studio, как наиболее user-friendly IDE. Также необходимо будет установить Windows SDK и Windows Driver Kit (WDK) для твоей версии ОС. Кроме того, я крайне рекомендую запастись такими утилитами, как DebugView (просмотр отладочного вывода), DriverView (позволяет получить список всех установленных драйверов) и KmdManager (удобный загрузчик драйверов).
Драйверы в Windows начиная с Vista могут быть как режима пользователя (User-Mode Driver Framework, UMDF), так и режима ядра (Kernel-Mode Driver Framework, KMDF). Более ранние драйверы Windows Driver Model (WDM) появились в Windows 98 и сейчас считаются устаревшими.
Драйверы UMDF имеют намного более ограниченные права, чем KMDF, однако они используются, например, для управления устройствами, подключенными по USB. Помимо ограничений, у них есть очевидные плюсы: их намного проще отлаживать, а ошибка в их написании не вызовет глобальный системный сбой и синий экран смерти. Такие драйверы имеют расширение dll.
Что до драйверов режима ядра (KMDF), то им дозволено куда больше, а расширение файлов, закрепленное за ними, — это sys. В этой статье мы научимся писать простые драйверы режима ядра, напишем драйвер для скрытия процессов методом DKOM (Direct Kernel Object Manipulation) и его загрузчик.
Создание драйвера KMDF
После того как ты создашь проект драйвера, Visual Studio автоматически настроит некоторые параметры. Проект будет компилироваться в бинарный файл в соответствии с тем, какая выбрана подсистема. Наш вариант — это NATIVE, подсистема низкого уровня, как раз для того, чтобы писать драйверы.
Точка входа в драйвер
Строго говоря, точка входа в драйвер может быть любой — мы можем сами ее определить, добавив к параметрам компоновки проекта -entry:[DriverEntry] , где [DriverEntry] — название функции, которую мы хотим сделать стартовой. Если в обычных приложениях основная функция обычно называется main, то в драйверах точку входа принято называть DriverEntry.
Выглядеть это будет так:
Давай пройдемся по параметрам, которые передаются DriverEntry . pDriverObject имеет тип PDRIVER_OBJECT , это значит, что это указатель на структуру DRIVER_OBJECT , которая содержит информацию о нашем драйвере. Мы можем менять некоторые поля этой структуры, тем самым меняя свойства драйвера. Второй параметр имеет тип PUNICODE_STRING , который означает указатель на строку типа UNICODE . Она, в свою очередь, указывает, где в системном реестре хранится информация о нашем драйвере.
WARNING
Любая ошибка в драйвере может вызвать общесистемный сбой и BSOD. Вероятна потеря данных и повреждение системы. Все эксперименты я рекомендую проводить в виртуальной машине.
Interrupt Request Level (IRQL)
IRQL — это своеобразный «приоритет» для драйверов. Чем выше IRQL, тем меньшее число других драйверов будут прерывать выполнение нашего кода. Существует несколько уровней IRQL: Passive, APC, Dispatch и DIRQL. Если открыть документацию MSDN по функциям WinAPI, то можно увидеть примечания, которые регламентируют уровень IRQL, который требуется для обращения к каждой функции. Чем выше этот уровень, тем меньше WinAPI нам доступно для использования. Первые три уровня IRQL используются для синхронизации программных частей ОС, уровень DIRQL считается аппаратным и самым высоким по сравнению с программными уровнями.
Пакеты запроса ввода-вывода (Input/Output Request Packet)
IRP — это запросы, которые поступают к драйверу. Именно при помощи IRP один драйвер может «попросить» сделать что-то другой драйвер либо получить запрос от программы, которая им управляет. IRP используются диспетчером ввода-вывода ОС. Чтобы научить программу воспринимать наши IRP, мы должны зарегистрировать функцию обратного вызова и настроить на нее массив указателей на функции. Код весьма прост:
А вот код функции-заглушки, которая всегда возвращает статусный код STATUS_SUCCESS . В этой функции мы обрабатываем запрос IRP.
Теперь любой запрос к нашему драйверу вызовет функцию-заглушку, которая всегда возвращает STATUS_SUCCESS . Но что, если нам нужно попросить драйвер сделать что-то конкретное, например вызвать определенную функцию? Для этого регистрируем управляющую процедуру:
Здесь мы объявили процедуру с именем IRP_MY_FUNC и ее кодом — 0x801 . Чтобы драйвер ее обработал, мы должны настроить на нее ссылку, создав таким образом дополнительную точку входа в драйвер:
После этого нам нужно получить указатель на стек IRP, который мы будем обрабатывать. Это делается при помощи функции IoGetCurrentIrpStackLocation , на вход которой подается указатель на пакет. Кроме этого, необходимо будет получить от диспетчера ввода-вывода размеры буферов ввода-вывода, чтобы иметь возможность передавать и получать данные от пользовательского приложения. Шаблонный код каркаса обработчика управляющей процедуры:
Продолжение доступно только участникам
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вот и пришло время второй статьи из цикла о написании драйверов под Windows. Сейчас я тебя немного расстрою: в предыдущей статье я обещала, что в этой мы приступим собственно к практике. Но данная статья это, скорее, "полупрактика". Глупо "с места в карьер" бросаться разрабатывать драйвера режима ядра
(так как, если ты не забыл, в данном цикле мы разбираем именно этот тип драйверов), хотя бы поверхностно не изучив особенности и приёмы программирования в режиме ядра, что мы и сделаем в первой части этой статьи. Ну а во второй мы наконец - то разберём структуру настоящего драйвера под Windows
(Legacy и немного WDM) : его основные функции, их взаимодействие, а также параметры, принимаемые ими. Так что к третьей статье этого цикла ты уже, надо думать, будешь основательно подготовлен к написанию своего первого драйвера. Начинаем.
Особенности и приёмы программирования в режиме ядра
Программирование в режиме ядра имеет массу особенностей, для прикладников очень непривычных, а для новичков довольно сложных. Во-первых, у режима ядра своё, отличное от такового в пользовательском режиме, API. Кроме того, для кода, выполняющегося в режиме ядра, имеет очень большое значение его уровень IRQL, так как приложениям, выполняющимся на высоких уровнях IRQL, недоступны многие функции, к которым имеют доступ приложения низких IRQL уровней, и наоборот. Всё это необходимо учитывать.
Во-вторых, в режиме ядра есть свои дополнительные описатели типов. Полный их список можно найти в заголовочном файле ntdef.h. Его содержание примерно таково:
typedef unsigned char USHAR
typedef unsigned short USHORT
typedef unsigned long ULONG
.
Зачем это нужно? Ну, во-первых, для красоты. тьфу, для унификации стиля классических C - типов данных и нововведённых - таких, как WCHAR
(двухбайтный Unicode символ), LARGE_INTEGER (который, на самом деле, является объединением) и т.д. А также для унификации исходников для 32 - разрядных платформ и надвигающихся 64 - разрядных.
В исходниках драйверов часто встречаются макроопределения
IN, OUT, OPTIONAL. Что они означают? А ровным счётом ничего, и введены они только для повышения удобочитаемости исходника. OPTIONAL обозначает необязательные параметры, IN - параметры, передаваемые внутрь функции, например, OUT - соответственно, наоборот. А вот IN OUT означает, что параметр передаётся внутрь функции, а затем возвращается из неё обратно.
Функции драйвера, за исключением DriverEntry (главная процедура драйвера, подробнее см. во второй части статьи), могут называться как угодно, тем не менее, существуют определённые "правила хорошего тона" при разработке драйверов, в том числе и для именования процедур: например, все функции, относящиеся к HAL, желательно предварять префиксом HAL и т.д. Сама Microsoft практически постоянно следует этому правилу. А имена типов данных и макроопределения в листингах DDK написаны сплошь заглавными буквами. Советую тебе поступать также при разработке своих драйверов. Это и в самом деле во много раз повышает удобство работы с листингом.
А теперь, чтобы тебе стали более или менее понятны основные различия между программированием в пользовательском и системном режимах, расскажу об основных функциях для работы с памятью и реестром в режиме ядра.
Начнём с функций для работы с памятью, а для начала поговорим собственно об устройстве и работе с памятью в Windows. Единое 4-х гигабайтное адресное пространство памяти Windows
(я имею в виду 32-х разрядные версии Windows) делится на две части: 2 гигабайта для пользовательского пространства и 2 гигабайта для системного. 2 гигабайта системного пространства доступны для всех потоков режима ядра. Системное адресное пространство делится на следующие части:
Видов адресов в режиме ядра три: физические
(реально указывающие на область физической памяти), виртуальные
(которые перед использованием транслируются в физические), и логические
(используемые HAL уровнем при общении с устройствами; он же и отвечает за работу с такими адресами). Функции режима ядра, отвечающие за выделение и освобождение виртуальной памяти, отличаются от таковых в пользовательском режиме. Также, находясь на уровне режима ядра, становится возможным использовать функции выделения и освобождения физически непрерывной памяти. Разберём все эти функции поподробнее.
1) PVOID ExAllocatePool (уровень IRQL, на котором может выполняться эта функция - < DISPATCH_LEVEL) - выделяет область памяти. Принимает два параметра: параметр
(POOL_TYPE) , в котором содержится значение, означающее, какого типа область памяти нужно выделить: PagedPool - страничная, NonPagedPool - нестраничная
(в этом случае функцию можно вызвать с любого IRQL уровня). Второй параметр
(ULONG) - размер запрашиваемой области памяти. Функция возвращает указатель на выделенную область памяти, и NULL, если выделить память не удалось.
2) VOID ExFreePool (IRQL<DISPATCH_LEVEL) - освобождает область памяти. Принимает параметр
(PVOID) - указатель на освобождаемую область памяти. Если высвобождается нестраничная память, то данная функция может быть вызвана с
DISPATCH_LEVEL. Возвращаемое значение - void.
3) PVOID MmAllocateContiguousMemory (IRQL==PASSIVE_LEVEL) - выделяет физически непрерывную область памяти. Принимает два параметра. Первый параметр
(ULONG) - размер запрашиваемой области памяти, второй - параметр
(PHYSICAL_ADDRESS), означающий верхний предел адресов для запрашиваемой области. Возвращаемое значение: виртуальный адрес выделенной области памяти или NULL
(при неудаче).
4) VOID MmFreeContiguousMemory (IRQL==PASSIVE_LEVEL) - освобождает физически непрерывную область памяти. Принимает единственный параметр
(PVOID) - указатель на область памяти, выделенную ранее с использованием функции MmAllocateContiguousMemory. Возвращаемое значение -
void.
5) BOOLEAN MmIsAddressValid (IRQL<=DISPATCH_LEVEL) - делает проверку виртуального адреса. Принимает параметр
(PVOID) - виртуальный адрес, нуждающийся в проверке. Функция возвращает TRUE, если адрес "валидный"
(т.е. присутствует в виртуальной памяти), и FALSE - в противном случае.
6) PHYSICAL_ADDRESS MmGetPhysicalAddress (IRQL - любой) - определяет физический адрес по виртуальному. Принимает параметр
(PVOID), содержащий анализируемый виртуальный адрес. Возвращаемое значение - полученный физический адрес.
Основные функции для работы с памятью рассмотрели, перейдём к таковым для работы с реестром. Сначала поговорим о функциях доступа к реестру, предоставляемых диспетчером ввода - вывода, потом о драйверных функциях прямого доступа к реестру, а затем о самом богатом по возможностям и удобству семействе функций для работы с реестром -
Zw
Драйверные функции, предоставляемые диспетчером ввода - вывода.
2) IoGetDeviceProperty - данная функция запрашивает из реестра установочную информацию об устройстве.
3) IoOpenDeviceRegistryKey - возвращает дескриптор доступа к подразделу реестра для драйвера или устройства по указателю на его объект.
Драйверные функции для прямого доступа к реестру.
1) RtlCheckRegistryKey - проверяет, существует ли указанный
подраздел внутри подраздела, переданного первым параметром. Что и каким
образом передавать в первом параметре - в рамках статьи всё не перечислить, отсылаю к ntddk.h и wdm.h. Если существует - возвращается
STATUS_SUCCESS.
2) RtlCreateRegistryKey - создаёт подраздел внутри раздела реестра, указанного вторым параметром. Далее - всё то же самое, что и у
RtlCheckRegistryKey.
3) RtlWriteRegistryValue - записывает значение параметра реестра. Первый параметр - куда пишем, второй - в какой подраздел
(если его нет, то он будет создан), а третий - какой параметр создаём.
4) RtlDeleteRegistryValue - удаляет параметр из подраздела. Параметры те же самые, что и у RtlWriteRegistryValue
(только с необходимыми поправками, конечно).
5) RtlQueryRegistryValues - данная функция позволяет за один вызов получить значения сразу нескольких параметров указанного подраздела.
И напоследок функции для работы с реестром семейства
Zw
1) ZwCreateKey - открывает доступ к подразделу реестра. Если такового нет - создаёт новый. Возвращает дескриптор открытого объекта.
2) ZwOpenKey - открывает доступ к существующему подразделу реестра.
3) ZwQueryKey - возвращает информацию о подразделе.
4) ZwEnumerateKey - возвращает информацию о вложенных подразделах уже открытого ранее подраздела.
5) ZwEnumerateValueKey - возвращает информацию о параметрах и их значениях открытого ранее подраздела.
6) ZwQueryValueKey - возвращает информацию о значении параметра в открытом ранее разделе реестра. Полнота возвращаемой информации определяется третьим параметром, передаваемым функции, который может принимать следующие значения
(дополнительные разъяснения не требуются, так как они имеют "говорящие" имена):
KeyValueBasicInformation, KeyValuePartialInformation и KeyValueFullInformation.
7) ZwSetValueKey - создаёт или изменяет значение параметра в открытом ранее подразделе реестра.
8) ZwFlushKey - принудительно сохраняет на диск изменения, сделанные в открытых функциями ZwCreateKey и ZwSetValueKey подразделах.
9) ZwDeleteKey - удаляет открытый подраздел из реестра.
10) ZwClose - закрывает дескриптор открытого ранее подраздела реестра, предварительно сохранив сделанные изменения на диске.
Практически все вышеперечисленные функции для работы с реестром должны вызываться с уровня IRQL
PASSIVE_LEVEL.
Думаю, пока достаточно. Конечно, у всех вышеперечисленных функций есть масса нюансов в применении. Да и вообще функций режима ядра - великое множество, их ничуть не меньше, чем в пользовательском режиме. Но моя задача была не рассказать обо всех API - функциях режима ядра
(что даже в рамках цикла невозможно сделать), а продемонстрировать отличия функций режима ядра, от таковых в пользовательском режиме, и хоть немного рассказать о нюансах их применения
(взять, к примеру, то, что в пользовательском режиме не имеет значения, в потоке какого приоритета будет выполняться приложение: оно будет иметь такой же полный доступ ко всем API функциям пользовательского режима, как и любые другие приложения; на уровне ядра, как ты только что, убедился, это не так). Ну а за более или менее полным списком и описанием всех этих API - функций советую обратиться к библии Гарри Нэббета.
Ну вот и всё, теперь ты готов к разговору о структуре драйвера, который мы сейчас и начнём.
Структура драйвера
Я уже говорила, что драйвер фактически можно представить как довольно-таки обычную dll-ку уровня ядра. Таким образом, далее можно представить драйвер просто как набор процедур, периодически вызываемых внешними программами. Несмотря на то, что процедуры драйверов для разных устройств сильно отличаются, есть общая структура и общие функции для всех драйверов. Главные из них мы сейчас и рассмотрим.
1) Поле DriverUnload - для регистрации собственной функции Unload, вызываемой при выгрузке драйвера. Подробнее о ней.
Эта функция вызывается только при динамической выгрузке драйвера
(т.е. происшедшей не в результате завершения работы системы). Legacy драйвера в этой функции выполняют полное освобождение всех занятых драйвером системных ресурсов. WDM драйвера выполняют такое освобождение в функции RemoveDevice при удалении каждого устройства
(если драйвер обслуживает несколько устройств). Функция Unload вызывается с уровня IRQL PASSIVE_LEVEL, принимает единственный параметр
(PDRIVER_OBJECT) - указатель на объект драйвера, и возвращает
void.
2) Поле DriverStartIo - для регистрации собственной функции
StartIo. Вкратце, регистрация функции StartIo нужна для участия в System Queuing - создании очередей необработанных запросов системными средствами, в отличие от DriverQueuing - когда то же самое реализуется средствами самого драйвера.
3) Поле AddDevice в подструктуре DriverExtension - для регистрации WDM драйвером своей процедуры
AddDevice.
4) Поле MajorFunction - для регистрации драйвером точек входа в свои рабочие процедуры.
Если выгрузка драйвера происходит в результате завершения работы системы, то функция Unload не вызывается. Это понятно: при выключении системы можно не заботиться об освобождении памяти и т.д. Поэтому вызывается функция Shutdown, которая просто предоставляет драйверу возможность оставить устройство в приемлемом состоянии покоя.
Может случиться так, что твоему драйверу необходимо будет получить управление при крахе системы. В этом случае ему нужно зарегистрировать callback процедуру Bugcheck. Если драйвер правильно выполнил регистрацию этой функции, то он будет вызван во время исполнения crash процесса.
Я перечислила основные функции драйвера для его загрузки и выгрузки. Теперь перейдём к рабочим процедурам драйвера.
Поговорим о следующих рабочих процедурах: обслуживания ввода - вывода
(включающих в себя процедуры передачи данных и обслуживания прерываний), callback процедуры для синхронизации доступа к объектам и некоторые другие.
Все драйверы должны иметь обработчик CreateDispatch, обрабатывающий пользовательский запрос CreateFile. Если драйверу нужно обрабатывать пользовательский запрос CloseHandle, то он должен иметь обработчик
CloseDispatch.
Перейдём к процедурам передачи данных. Это обработчики пользовательских запросов ReadFile, WriteFile и
DeviceIoControl.
Процедуру StartIo я уже рассмотрела, поэтому перейдём к процедуре обслуживания прерываний
(ISR - Interrupt Service Routine, напоминаю на всякий случай). Данная процедура вызвается диспетчером прерываний ядра
(Kernel`s Interrupt Dispatcher) при каждой генерации прерывания устройством, и она обязана полностью обслужить это прерывание.
Теперь о callback процедурах сихронизации доступа к объектам. Для начала разберёмся, в чём различия принципов сихронизации доступа к объектам в пользовательском и ядерном режимах. Например, в пользовательском режиме, если какой - либо поток обратился к объекту, уже занятому другим потоком, то он
(первый поток) запросто может быть заблокирован до лучших времён. Как ты сам понимаешь, в режиме ядра такая внеплановая "заморозка" потоков неприемлема, поэтому и применяется другая технология сихронизации. И заключается она в следующем. Когда какой-либо поток обращается к объекту, уже занятому другим потоком, то он оставляет свой запрос в очереди запросов. Если драйвер предварительно зарегистрировал особую callback функцию, то диспетчер ввода - вывода при освобождении
требуемого ресурса, уведомит об этом драйвер, вызвав callback - функцию. Таким образом, обеспечивается гарантия ответа на любой запрос к ресурсу, даже если он
(ответ) будет состоять только в том, чтобы уведомить о задержке в обработке и помещении запроса в очередь. Функции, это реализующие: IoAllocateController
(использующаяся для синхронизации доступа к контроллеру), AdapterControl
(использующаяся для синхронизации доступа к DMA каналам
(чаще всего)) и SynchCritSection (использующаяся для корректного обращения к ресурсам; точнее - эта функция позволяет коду с низким уровнем IRQL сделать работу при уровне DIRQL устройства без опасения возникновения конфликтов с ISR).
Также можно упомянуть ещё таймерные процедуры
(нужные для драйверов, выполняющих точный отсчёт временных интервалов; обычно реализуется с использованием IoTimer
(но не всегда)), процедуру IoCompletion (позволяющую WDM драйверу, работающему внутри многослойной драйверной структуры, получать уведомление о завершении обработки IRP запроса, направленного к драйверу нижнего уровня) и CancelRoutine
(если драйвер зарегистрирует эту callback процедуру при помощи вызова IoSetCancelRoutine, то диспетчер ввода - вывода сможет уведомить его об удалении IRP запросов, находящихся в ожидании обработки, что может понадобиться диспетчеру, если пользовательское приложение, инициировавшее эти IRP запросы, неожиданно завершит свою работу после снятия задачи диспетчером задач
(прошу прощения за необходимую тавтологию)).
Ну вот и всё, мы закончили обзор структуры драйвера и основных его процедур. Примеры их практического применения
(а также некоторых других функций) ты увидишь в третьей, заключительной статье этого цикла, в которой мы напишем свой полноценный Legacy драйвер.
Уфф, наконец-то мы покончили со скучной теорией. В этой статье ты узнал про некоторые приёмы программирования в режиме ядра, изучил структуру драйвера и его основные функции. Теперь ты полностью подготовлен
(на сей раз уже окончательно) к написанию своего первого
(или двадцать первого - не знаю) полноценного Legacy драйвера под Windows. Это мы проделаем в заключительной части моего рассказа о программировании драйверов под Windows. А пока что слегка отдохнём. Да не облысеют твои пятки!
Мы защищаем некоторые из грядущих игр Riot с помощью новых систем для борьбы с жульничеством.
Внимание! Эта статья переполнена техническими подробностями, и в ней рассказывается об инструментах противодействия жульничеству, которые будут не только в League of Legends. В других играх (вроде "Проекта A") они появятся даже раньше, чем в Лиге.
Потратив на исследования около 8 лет и 20 миллионов долларов из государственного бюджета, ведущие ученые смогли установить, что жульничество было изобретено в период между 3,5 миллиардами лет до н. э. и 20 ноября 1985 года. Его точное происхождение до сих пор не определено, но одно известно наверняка: жулики будут всегда.
За последние 20 лет разработка инструментов для взлома и защиты от них переросла из благородной борьбы за контроль над памятью игрового клиента в технологии, которые модифицируют операционную систему – и даже "железо" – компьютера жулика. Современные методы жульничества могут помешать античитам получать необходимые данные, а если последним приходится работать в пользовательском режиме, ситуация еще больше усугубляется.
Что такое пользовательский режим?
Это самый ограниченный уровень привилегий в операционной системе, на котором может запускаться программное обеспечение. Браузер, честно купленный WinRAR и ваши любимые игры работают именно в пользовательском режиме. Такие программы не могут напрямую заглянуть во "внешний мир" вокруг себя – как правило, для чтения и записи данных вне собственных процессов их код должен полагаться на стандартные API операционной системы. Попробуем объяснить с помощью сравнения: если мы (в пользовательском режиме) хотим узнать, что именно было добавлено в наш говяжий гуляш (League of Legends), нам нужно обратиться к поварам на кухне (Microsoft Windows).
Если какой-то доморощенный гений заявит вам: "Лол, мой чит работает на 0-м уровне, его не засечь" – знайте, что он говорит именно об этом. И вот-вот улетит в бан.
В последние годы разработчики читов начали использовать уязвимости или взламывать инструменты проверки подписей Windows, чтобы запускать свои приложения (или их фрагменты) на уровне ядра. И у нас возникает проблема: код, выполняемый на этом уровне, может перехватывать системные вызовы, которые мы используем для получения данных. ПО жуликов меняет их таким образом, чтобы они казались нормальными, а нам сложно было что-то обнаружить. Мы даже видели специализированное аппаратное обеспечение, которое использует DMA 1 для чтения и обработки системной памяти – и если все сделано правильно, обнаружить такое вмешательство в пользовательском режиме невозможно 2 .
И хотя большинство игроков откажутся ставить модифицированную сборку Windows, практика показывает, что пугающее количество жуликов без раздумий готовы стать частью ботнета какого-то парня в обмен на способность атаковать на бегу. В итоге множество современных читов работают на более высоком уровне привилегий, чем наш античит. Вернемся к замечательной кухонной аналогии: когда вы спрашиваете шеф-повара о происхождении мяса в гуляше, какой-то мутный чувак убеждает руководство ресторана, что он "во всем разберется", и отвечает на наш вопрос: "Прямо с фермы, ешьте на здоровье".
1 DMA расшифровывается как "Direct Memory Access", то есть "прямой доступ к памяти". Это метод, с помощью которого аппаратное обеспечение может, как вы наверняка догадались, получить доступ сразу к памяти, минуя Windows API. Некоторые из самых продвинутых сообществ жуликов использовали его, чтобы перенаправлять память на отдельный компьютер для ее обработки и получения преимуществ в игре.
2 Мы тут же наняли того парня, который разработал методы обнаружения такого рода жульничества.
Зачем вы мне это рассказываете?
Исторически ваша любимая команда по противодействию жульничеству была вынуждена играть в эту игру на пользовательском уровне – и все равно не оставляла от жуликов мокрого места. Нам не приходилось идти на крайние меры благодаря преимуществу в виде стабильной зарплаты и отсутствию необходимости ложиться спать, когда скажет мама. Но как бы нам ни нравилась идея набирающей обороты войны с подростками на поле безопасности, мы теперь входим в мультиигровую вселенную, где линейность времени и какая-никакая потребность в сне поставят крест на этой стратегии.
Поэтому некоторые из будущих игр от Riot будут защищены драйвером режима ядра.
Мне уже начинать паниковать?
Есть целый ряд причин не делать этого.
- Стресс может привести к облысению, а мы не хотим, чтобы у вас мерзла голова.
- Это не дает нам никаких возможностей для слежки, которых не было раньше. Если бы нам нужен был секретный рецепт рождественской запеканки вашей бабушки, мы без проблем могли бы заполучить его и в пользовательском режиме, а затем продать в какое-нибудь кулинарное шоу. Цель этого обновления – следить за целостностью системы (чтобы мы могли доверять получаемым данным) и не давать жуликам вмешиваться в наши игры (чтобы вы не винили вражеское автоприцеливание в собственных неудачах).
- Это не новая технология. Некоторые сторонние системы для противодействия жульничеству – вроде EasyAntiCheat, Battleye и Xigncode3 – уже используют драйвер режима ядра для защиты ваших любимых ААА-игр. Мы просто устанавливаем собственного помощника шеф-повара на кухню Windows, чтобы, задавая вопрос "а это блюдо точно без глютена?", мы были уверены, что получим честный ответ.
- Теперь создавать читы, которые невозможно обнаружить, будет гораздо сложнее. Так что мы сможем защитить вас от автоприцеливания, себя от постов на Reddit, а жуликов от них же самих.
Античит – одна из важнейших составляющих многопользовательской сетевой игры, и мы хотим, чтобы вы играли в мире, в котором не придется ставить под сомнение способности ваших соперников. От жульнической лихорадки нет лекарства, но мы продолжим делать все возможное, чтобы обеспечить вам максимальное качество игры.
Передача завершена, но примерно через четыре мегасекунды я вернусь с продолжением нашего бестселлера "Борьба с жуликами в LoL", в котором расскажу о ботах в ваших играх ARAM.
Читайте также: