Взлом доступа к ядру Windows при помощи драйвера принтера

в 13:00, , рубрики: ruvds_переводы, взлом, обратная разработка, режим ядра, уязвимости, хакерство, ядро Windows

Взлом доступа к ядру Windows при помощи драйвера принтера - 1


В этой статье приводятся подробности CVE-2023-21822 — уязвимости Use-After-Free (UAF) в win32kfull, которая может привести к повышению привилегий. Отчёт о баге отправлен в рамках программы ZDI, а позже она была пропатчена компанией Microsoft.

В ядре Windows есть три API, предназначенные для общего использования драйверами устройств с целью создания растровых изображений (bitmap): EngCreateBitmap, EngCreateDeviceBitmap и EngCreateDeviceSurface. Каждый из этих API возвращает дескриптор растрового изображения. Если вызывающая сторона хочет выполнить какие-то операции рисования на растровом изображении, то она должна сначала заблокировать это изображение, передав его дескриптор функции EngLockSurface. EngLockSurface увеличивает значение эталонного счётчика растрового изображения и возвращает указатель на соответствующую запись SURFOBJ. SURFOBJ — это расположенная в памяти ядра структура, содержащая всю информацию, связанную с растровым изображением, например, его размер, формат пикселей, указатель на пиксельный буфер и так далее. Подробнее структуру SURFOBJ мы рассмотрим позже.

После вызова EngLockSurface полученный указатель на SURFOBJ может передаваться различным API рисования, например, EngLineTo и EngBitBlt. Полный список этих API рисования можно найти в winddi.h. После того, как вызывающая сторона завершит операции рисования, она должна вызывать EngUnlockSurface. На этом этапе эталонный счётчик растрового изображения снова сбрасывается до нуля, и вызывающей стороне больше не разрешается использовать указатель на SURFOBJ. В конце вызывающая сторона может удалить растровое изображение, вызвав для его дескриптора EngDeleteSurface. Типичное использование этих API показано ниже:

// ПРИМЕЧАНИЕ: этот пример кода должен выполняться драйвером режима ядра.
HBITMAP TestBitmap = EngCreateBitmap(...);
// эталонный счётчик растрового изображения равен 0
SURFOBJ* pso = EngLockSurface(TestBitmap);
// эталонный счётчик растрового изображения равен 1
// выполняем операции рисования на растровом изображении…
EngLineTo(pso, ...);
...
// эталонный счётчик растрового изображения равен 1
EngUnlockSurface(pso);
// эталонный счётчик растрового изображения равен 0
EngDeleteSurface(TestBitmap);

Все упомянутые выше API экспортируются из модуля режима ядра win32k.sys. Однако стоит отметить, что функции в win32k.sys — это лишь обёртки, а реализации находятся в win32kbase.sys и win32kfull.sys.

Много лет назад драйверы и дисплеев, и принтеров работали в режиме ядра, но начиная с Windows Vista драйверы принтеров работают только в пользовательском режиме (отсюда и название User-Mode Printer Driver, или UMPD). Из этого изменения следуют два важных факта:

  • Во время операций печати ядро теперь должно выполнять обратные вызовы в пользовательский режим, чтобы вызвать соответствующий драйвер принтера пользовательского режима.
  • Чтобы код драйвера принтера мог выполняться в пользовательском режиме, какие-нибудь API ядра должны быть доступны из этого режима.

В результате этого все описанные выше API ядра имеют аналоги для пользовательского режима, экспортированные из модуля пользовательского режима gdi32.dll. Давайте попробуем исполнить тот же код, показанный выше, но на этот раз из пользовательского режима:

// ПРИМЕЧАНИЕ: этот пример кода должен выполняться драйвером принтера в пользовательском режиме.
HBITMAP TestBitmap = EngCreateBitmap(...);
// эталонный счётчик растрового изображения равен 0
SURFOBJ* pso = EngLockSurface(TestBitmap);
// эталонный счётчик растрового изображения равен 0 !!!
// выполняем операции рисования с растровым изображением...
EngLineTo(pso, ...);
...
// эталонный счётчик растрового изображения равен 0 !!!
EngUnlockSurface(pso);
// эталонный счётчик растрового изображения равен 0
EngDeleteSurface(TestBitmap);

Обратите внимание на значения эталонного счётчика, показанные в комментариях. После блокировки растрового изображения значение остаётся равным нулю. Почему?

Код режима ядра всегда считается надёжным, а коду пользовательского режима система всегда не доверяет. Поэтому теперь, когда драйверы принтера исполняются в пользовательском режиме, они считаются ненадёжными и потенциально зловредными.

Предположим, что вызов EngLockSurface пользовательского режима увеличивал бы эталонный счётчик растрового изображения так же, как версия режима ядра. Нападающий, действующий как драйвер принтера пользовательского режима, много раз вызывал бы в цикле EngLockSurface для растрового изображения, чтобы переполнить эталонный счётчик растрового изображения, и это привело бы к его сбросу в ноль. Тогда растровое изображение можно было бы удалить, что позволило бы использовать уязвимость use-after-free для растрового изображения.

Поэтому в ядре Windows реализован другой подход. От API EngLockSurface ожидается, что он вернёт указатель на запись SURFOBJ растрового изображения, и он это делает. Но в пользовательском режиме это копия «истинной» записи SURFOBJ режима ядра. Мы можем воссоздать эту структуру данных пользовательского режима следующим образом:

typedef struct _UMSO {
  ULONG     magic;        // 0x554D534F = “UMSO” (User-Mode Surface Object?)
  HBITMAP   hsurf;        // дескриптор растрового изображения
  SURFOBJ   so;           // копия записи SURFOBJ режима ядра
} UMSO;

Реализация EngLockSurface пользовательского режима возвращает указатель на поле UMSO.so, которое является копией истинной записи SURFOBJ режима ядра, поэтому всё работает, как и задумано. Внутри вызов EngLockSurface пользовательского режима переходит к своей реализации win32kfull.sys!NtGdiEngLockSurface режима ядра, где запись UMSO пользовательского режима распределяется и заполняется. В режиме ядра выполняется «истинный» вызов EngLockSurface режима ядра к растровому изображению, которому необходим доступ к записи SURFOBJ растрового изображения, чтобы её данные можно было скопировать в поле UMSO.so. Однако затем NtGdiEngLockSurface вызывает EngUnlockSurface режима ядра, который снова сбрасывает эталонный счётчик растрового изображения до нуля. Это объясняет наблюдаемые значения эталонного счётчика.

Вызвав EngLockSurface пользовательского режима, мы можем передать его результат (то есть указатель на скопированные данные SURFOBJ) различным функциям рисования, например, EngLineTo или EngBitBlt. Когда соответствующие вызовы выполняются из режима ядра, это работает очень просто, однако при вызове из пользовательского режима необходим дополнительный слой для преобразования указателей SURFOBJ пользовательского режима в истинные указатели режима ядра. То есть, например, если код пользовательского режима вызывает gdi32.dll!EngLineTo, то будет выполнен переход к обёртке win32kfull.sys!NtGdiEngLineTo режима ядра. Обёртка получит истинную запись растрового изображения SURFOBJ режима ядра, поэтому в конечном итоге можно будет выполнить обработчик рисования win32kfull.sys!EngLineTo режима ядра.

Как ядро получает необходимую запись SURFOBJ режима ядра? Запись SURFOBJ содержит уязвимые данные, например, указатель на пиксельный буфер растрового изображения, поэтому ядро никогда не полагается на содержимое записей SURFOBJ, передаваемых из пользовательского режима. В противном случае возникала бы угроза безопасности со стороны зловредного кода пользовательского режима, который мог бы вмешиваться в содержимое структур UMSO.so. Поэтому вместо этого в функции обёртки (допустим, win32kfull.sys!NtGdiEngLineTo из примера выше) ядро верифицирует значение UMSO.magic, а затем использует значение дескриптора растрового изображения UMSO.hsurf для блокировки растрового изображения вызовом EngLockSurface. Благодаря этому ядро безопасно получает запрашиваемую запись растрового изображения SURFOBJ режима ядра, которую затем может передать соответствующей функции рисования win32kfull.sys!EngXXX режима ядра.

▍ Уязвимость

Функция EngLockSurface пользовательского режима выполняет валидацию переданного дескриптора растрового изображения, то есть этому вызову может быть успешно передан не любой тип растрового изображения (подробнее мы поговорим об этом ниже). Однако зловредный код пользовательского режима может обойти эту проверку одним из следующих способов:

  1. После выполнения вызова EngLockSurface мы можем удалить уже валидированное растровое изображение и создать какое-то другое с тем же значением дескриптора. При этом можно создать растровое изображение, которое нельзя успешно передать функции EngLockSurface.
  2. Выполнив вызов EngLockSurface, мы получаем указатель на запись SURFOBJ пользовательского режима, которая, как мы уже знаем, является частью записи UMSO. То есть мы можем переписать поле UMSO.hsurf, присвоив ему значение дескриптора любого нужного нам растрового изображения. Можно присвоить значение дескриптора растрового изображения, которое не может быть успешно передано функции EngLockSurface.
  3. И самое простое: мы можем подготовить запись UMSO с нуля, не выполняя предварительно никаких вызовов EngLockSurface. Нам достаточно распределить память пользовательского режима, присвоить UMSO.magic значение 0x554D534F, а UMSO.hsurf присвоить дескриптор нужной нам растрового изображения. Оставшуюся часть этой записи (поле UMSO.so, в обычных обстоятельствах содержащее запись SURFOBJ) можно обнулить, потому что её в любом случае ядро проигнорирует.

Каждый из трёх вариантов позволит нам обойти валидацию растровых изображений, выполняемую версией API EngLockSurface пользовательского режима.

Разобравшись, как можно обойти валидацию, нужно задаться вопросом: в чём же цель этой валидации и какие последствия это имеет для безопасности? Чтобы ответить на этот вопрос, мы должны взглянуть на определение записи SURFOBJ. Некоторые поля публично документированы, а другие можно воссоздать, как показано ниже:

typedef struct _SURFOBJ {
  DHSURF    dhsurf;       // важно для нас
  HSURF     hsurf;
  DHPDEV    dhpdev;
  HDEV      hdev;         // важно для нас
  SIZEL     sizlBitmap;
  ULONG     cjBits;
  PVOID     pvBits;
  PVOID     pvScan0;
  LONG      lDelta;
  ULONG     iUniq;
  ULONG     iBitmapFormat;
  USHORT    iType;
  USHORT    fjBitmap;
  ...
  // здесь начинается незадокументированная часть
  ULONG     flags;        // важно для нас
  ULONG     flags2;
  ...
} SURFOBJ;

Поле flags растрового изображения не задокументировано, но известно, что оно содержит несколько задокументированных флагов HOOK_XXX, находящихся в файле заголовка winddi.h. Эти флаги сообщают подсистеме win32k, какие операции рисования должны обрабатываться самой win32k, а какие должны перенаправляться специализированному драйверу устройства. Драйвер устройства указан в поле hdev растрового изображения.

Например, предположим, что мы хотим нарисовать на каком-то растровом изображении линию. Мы вызовем EngLineTo, передав указатель на запись SURFOBJ растрового изображения. Внутри ядро преобразует запрошенную линию в более общую конструкцию рисования под названием «path» (контур) (который может быть последовательностью отрезков и кривых). Затем оно проверит, установлен ли в поле SURFOBJ.flags растрового изображения флаг HOOK_STROKEPATH. Если этого флага нет, то ядро будет использовать обобщённый код рисования («штрихования») контуров, переданных win32kfull. Однако если HOOK_STROKEPATH есть, то ядро направит запрос рисования драйверу устройства, указанному в поле SURFOBJ.hdev. Второй вариант, если это возможно, обеспечивает повышенную производительность, поскольку позволяет отдельным драйверам устройств пользоваться ускорением, предоставляемым оборудованием. Например, графический адаптер может иметь функцию аппаратно ускоренного рисования контуров. Аналогично, устройства принтеров имеют специализированное ускорение для вывода текста.

То есть если мы подготовим растровое изображение, имеющее связанное с экраном значение SURFOBJ.hdev, у которого также задан флаг HOOK_XXX, и передадим его одному из API рисования EngXXX, то есть возможность достичь входной точки специализированного драйвера дисплея, работающего в режиме ядра. В случае использования одного монитора это может быть cdd.dll!DrvXXX, а в случае использования нескольких — win32kfull.sys!MulXXX (однако, как показано в примере выше, не всегда есть простая связь между запрошенной функциональностью и вызываемой входной точкой драйвера). Указатель на запись SURFOBJ растрового изображения будет передан входной точке драйвера как параметр.

Стоит также отметить, что некоторые API EngXXX получают в качестве параметра не одно растровое изображение, а два: исходное и конечное растровые изображения (некоторые опционально получают растровое изображение маски, но нас это не интересует). Примером такого API является EngBitBlt, который копирует прямоугольник пикселей из исходного растрового изображения в конечное. Работающие с двумя растровыми изображениями API для выбора драйвера устройства, который получит вызов, используют значения SURFOBJ.flags и SURFOBJ.hdev конечного растрового изображения. Тем не менее, при вызове входной точки окончательно выбранного драйвера ей передаются и исходное, и конечное растровые изображения.

То есть правильно подготовленное связанное с экраном растровое изображение при передаче какому-нибудь API EngXXX в качестве конечного растрового изображения позволяет нам добраться до драйвера дисплея режима ядра, а также передать в качестве исходного растрового изображения произвольное.

Пока проблема безопасности неочевидна, но давайте ещё раз взглянем на определение записи SURFOBJ. Она содержит поле dhsurf (не путать с рассмотренным выше полем hsurf). Подсистема win32k работает с SURFOBJ.dhsurf как с непрозрачным значением. Оно зарезервировано для отдельных драйверов устройств с целью применения для их внутренних задач. Этому полю можно легко присвоить новое растровое изображение: API создания растровых изображений EngCreateDeviceBitmap и EngCreateDeviceSurface получают в качестве параметра только dhsurf. И Canonical Display Driver (cdd.dll, применяемый для графического вывода на один монитор), и многодисплейный драйвер (win32kfull.sys!MulXXX) ожидают, что будут работать только с собственными растровыми изображениями, значения SURFOBJ.dhsurf которых задаются этим конкретным драйвером, а не с произвольными растровыми изображениями, созданными из пользовательского режима (или другими драйверами). Внутри каждый из этих драйверов использует значение SURFOBJ.dhsurf в качестве указателя на блок в памяти режима ядра, который содержит приватные данные, принадлежащие этому драйверу.

Но мы можем добраться до драйвера дисплея режима ядра, передав правильно подготовленную конечную растровое изображение вызову EngXXX, а также передав какое-нибудь произвольное растровое изображение на свой выбор в качестве исходного растрового изображения тому же вызову EngXXX. Это исходное растровое изображение может быть созданным нами произвольным растровым изображением, а его значение SURFOBJ.dhsurf может указывать на произвольную контролируемую память. Драйвер дисплея режима ядра, например, Canonical Display Driver, будет работать с этим блоком памяти, как если бы это был его собственный блок памяти режима ядра. А это означает «геймовер» для безопасности.

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

▍ Эксплойт

Первым делом нужно отметить, что вызовы EngXXX пользовательского режима предназначены только для применения драйверами принтеров пользовательского режима, поэтому большинство этих API не смогут выполнить задачу, если только их не вызвали во время обратного вызова из режима ядра в пользовательский режим для операции печати. Но это не особо усложняет задачу: часть обратного вызова пользовательского режима реализована как функция gdi32.dll!GdiPrinterThunk, которая является публичным экспортом из gdi32.dll. Этого достаточно, чтобы перехватить или пропатчить эту функцию и выполнить в ней наш основной эксплойт. Эта функция получает четыре параметра (входной буфер, размер входного буфера, выходной буфер и размер выходного буфера), но во время эксплойта нам не нужны эти параметры. (Однако если вам любопытны подробности, то см. Selecting Bitmaps into Mismatched Device Contexts. В частности, изучите разделы «User-Mode Printer Drivers (UMPD)» и «Hooking the UMPD implementation».)

Первым делом нам нужно получить обратный вызов от ядра к перехваченной функции gdi32.dll!GdiPrinterThunk. Чтобы добиться этого, нам нужно инициировать операцию печати. Сначала мы должны найти установленный принтер. На каждой машине с Windows есть как минимум один виртуальный принтер, установленный по умолчанию. Обнаружить установленные принтеры можно при помощи вызова API winspool.drv!EnumPrintersA/W пользовательского режима. Затем мы должны создать связанный с принтером контекст устройства:

CreateDC(“Microsoft XPS Document Writer”, “Microsoft XPS Document Writer”, NULL, NULL);

Этот вызов будет передан режиму ядра, который затем снова выполнит множество обратных вызовов к пользовательскому режиму, то есть можно будет вызвать нашу перехваченную функцию gdi32.dll!GdiPrinterThunk, как мы того и хотели. Здесь начинается наша основная фаза эксплойта.

Сначала нам нужно получить растровое изображение со связанным с экраном значением SURFOBJ.hdev и полезный флаг HOOK_XXX, заданный в её поле SURFOBJ.flags. Для получения такой растрового изображения мы можем создать окно с нужными параметрами, получить контекст устройства окна, а затем взять внутреннее растровое изображение. Полученное растровое изображение будет использоваться в качестве конечного растрового изображения:

HelperWindow = CreateWindowEx(WS_EX_TOOLWINDOW, "BUTTON", NULL,
                              WS_VISIBLE | WS_POPUP | WS_BORDER | WS_DISABLED,
                              0, 0, 50, 50, NULL, NULL, hInstance, NULL);
HelperWindowDCScr = GetWindowDC(HelperWindow);
HelperWindowBitmap = GetCurrentObject(HelperWindowDCScr, OBJ_BITMAP);

Также нам нужно исходное растровое изображение, поле SURFOBJ.dhsurf которого указывает на контролируемую нами память пользовательского режима (наш FakeDhsurfBlock):

sizl.cx = 100;
sizl.cy = 100;
MaliciousBitmap = EngCreateDeviceBitmap(&FakeDhsurfBlock, sizl, BMF_1BPP);

Теперь мы можем подготовить две записи UMSO, одну для конечного, другую для исходного растрового изображения:

FillMemory(&UmsoDest, sizeof(UmsoDest), 0);
UmsoDest.magic = 0x554D534F;
UmsoDest.hsurf = HelperWindowBitmap;
FillMemory(&UmsoSrc, sizeof(UmsoSrc), 0);
UmsoSrc.magic = 0x554D534F;
UmsoSrc.hsurf = MaliciousBitmap;

Теперь у нас есть всё необходимое, чтобы выполнить зловредный вызов EngXXX с нашими растровыми изображениями. У нашего связанного с экраном конечного растрового изображения будут заданы все флаги HOOK_XXX, и мы сможем выбрать любой из API EngXXX, получающих два растровые изображения:

rclDest.left   = 0;
rclDest.top    = 0;
rclDest.right  = 10;
rclDest.bottom = 10;
rclSrc.left    = 0;
rclSrc.top     = 0;
rclSrc.right   = 20;
rclSrc.bottom  = 20;
EngStretchBltROP(&UmsoDest.so, &UmsoSrc.so, NULL, NULL, NULL, NULL, NULL,
                 &rclDest, &rclSrc, NULL, BLACKONWHITE, NULL, 0xCCCC);

Благодаря реверс-инжинирингу внутренностей Canonical Display Driver или многодисплейного драйвера, мы можем узнать, как подготовить FakeDhsurfBlock пользовательского режима так, чтобы драйвер дисплея обеспечил примитивы памяти с возможностью эксплойта.

▍ Патч

Как говорилось выше, каждый из API рисования EngXXX пользовательского режима (например, EngLineTo и EngBitBlt) вызывает соответствующую обёртку win32kfull.sys!NtGdiEngXXX режима ядра, в которой, среди прочего, указатели SURFOBJ пользовательского режима преобразуются в указатели SURFOBJ режима ядра. Затем вызывается конечная точка драйвера win32kfull.sys!EngXXX режима ядра для выполнения запрошенной операции рисования.

Хотя это не связано с нашей уязвимостью, стоит отметить, что во время обратного вызова gdi32.dll!GdiPrinterThunk пользовательского режима ядро хранит отображение известных записей SURFOBJ пользовательского режима на записи SURFOBJ режима ядра. Когда драйвер принтера пользовательского режима передаёт указатель SURFOBJ пользовательского режима какому-то вызову EngXXX пользовательского режима, то ядро пытается использовать это отображение для нахождения соответствующего указателя SURFOBJ режима ядра, чтобы его можно было передать соответствующему вызову EngXXX режима ядра.

Отображение подготавливается до начала обратного вызова GdiPrinterThunk пользовательского режима. Это вызвано тем, что некоторые растровые изображения могут передаваться обратному вызову в качестве параметров (хотя во время нашего эксплойта мы не пользовались входными данными GdiPrinterThunk). Однако это означает, что растровые изображения, «заблокированные» позже, то есть вызовами EngLockSurface, выполненными из обратного вызова, не будут присутствовать в отображении.

Когда какой-либо win32kfull.sys!NtGdiEngXXX получает в качестве параметра указатель SURFOBJ пользовательского режима, но он не может найти его в отображении, то он предполагает, что полученная запись SURFOBJ содержится в записи UMSO (в качестве её поля UMSO.so).

До патча такие случаи направлялись внутренней функции win32kfull.sys!UMPDSURFOBJ::GetLockedSURFOBJ, где значение UMSO.magic сверялось со значением 0x554D534F, а затем выполнялся вызов EngLockSurface режима ядра со значением дескриптора UMSO.hsurf, что, как говорилось выше, позволяло получить нужный указатель на «истинную» SURFOBJ запись режима ядра.

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

После патча имя функции было изменено на GetLockableSURFOBJ. Драйвер принтера пользовательского режима по-прежнему может выполнять все описанные выше манипуляции, но теперь GetLockableSURFOBJ считает полученный дескриптор растрового изображения (UMSO.hsurf) ненадёжным. После использования значения UMSO.hsurf для блокировки растрового изображения в режиме ядра GetLockableSURFOBJ теперь ещё раз выполняет ту же валидацию растрового изображения, которая происходила при вызове API EngLockSurface пользовательского режима. Эта валидация выполняется при помощи вызова win32kfull.sys!IsSurfaceLockable. Благодаря этому связанные с экраном растровые изображения, которые можно было использовать, чтобы добраться до драйвера дисплея режима ядра из драйвера принтера пользовательского режима, теперь отклоняются GetLockableSURFOBJ.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх 🕹️

Автор:
ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js