Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2015-0057 — Перевод статьи, эксплуатация уязвимости CVE-2015-0057 в 32-разрядных и 64-разрядных системах. Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit (Aaron Adams of NCC) | Kitploit
Инструменты/GitHubGitHub/highandhigh/cve-2015-0057
Криминалистика памятиАнализ уязвимостейЭксплуатацияОбратная инженерияСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubhighandhigh/cve-2015-0057

CVE-2015-0057

Перевод статьи, эксплуатация уязвимости CVE-2015-0057 в 32-разрядных и 64-разрядных системах. Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit (Aaron Adams of NCC)

Репозиторий
89 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Использование уязвимости CVE-2015-0057 в 32-битных и 64-битных системах

Автор: Aaron Adams

Перевод: 55-AA

Примечание переводчика: некоторые части статьи переведены вольно, при возникновении вопросов обращайтесь к оригиналу.

Термины:

  • Примитив (primitive): аналогичен функциональной процедуре, представляет собой набор операций по повреждению (corruption), выполняющий полную и повторно используемую функцию, например, чтение памяти, запись произвольных данных и т.д.

Вместо предисловия

В начале этого года я столкнулся с интересной уязвимостью в win32k.sys (CVE-2015-0057) и реализовал стабильный эксплойт для 32-битных и 64-битных систем, работающий от XP до Windows 8.1 (с некоторыми исключениями). В этой статье подробно описано, как я это сделал на обеих платформах, а в конце добавлены некоторые дополнительные материалы. Также описано использование с низким уровнем привилегий (low integrity) на Windows 8.1 с включенным SMEP.

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

Введение

10 февраля 2015 года Microsoft опубликовала подробности MS15-010. Эта ошибка была впервые обнаружена Уди Яво (Udi Yavo) из enSilo. Уди дал отличный анализ в своём блоге breaking malware: "one bit rule-bypassing windows 10 protections using single bit". Рекомендую внимательно прочитать эту статью для глубокого понимания ошибки, хотя в этой статье я приведу как можно больше деталей, касающихся препятствий, которые необходимо преодолеть при срабатывании уязвимости. Эксплуатация этой уязвимости очень интересна, многие детали взяты из блога Уди, ниже его заявление:

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

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

Раньше я никогда не эксплуатировал уязвимости win32k.sys и не был знаком с пользовательскими обратными вызовами (user-mode callbacks) и многими связанными API, поэтому я также благодарю известных исследователей безопасности, которые предоставили в открытом доступе новые ресурсы, таких как Skywing, Tarjei Mandt, Alex Ionescu и j00ru. Эти люди заслуживают похвалы за предоставление такого количества технической информации. Я активно использовал статью Tarjei Mandt Win32k.sys exploitation paper.

Во время написания этого эксплойта один отличный reverse-инженер реализовал стабильный эксплойт для CVE-2015-1701, где пример кода пользовательского обратного вызова был очень полезен. Спасибо автору.

Стоит отметить, что мой анализ проводился на Windows 7, так как это, похоже, единственная версия, где все структуры в win32k.sys имеют соответствующие символы. Большинство этих символов можно использовать для структур в других версиях Win32k.sys. По непонятным причинам Microsoft удалила эти символы из Windows 8.

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

Ошибка (BUG)

Ниже в дизассемблере win32k!xxxEnableWndSBArrows мы видим, как выглядит эта ошибка – довольно изящная:

Неисправленная версия:

root@kitploit:~
.text:FFFFF97FFF1B157D mov r8d, r13d 
.text:FFFFF97FFF1B1580 mov rdx, r14 
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar    ; вызывает usermode callback 
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
 [...] 
.text:FFFFF97FFF1B1519 mov eax, [rbx]           ; обращение к указателю tagSBINFO без проверки 
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh 
.text:FFFFF97FFF1B1520 xor eax, esi

В приведённом выше коде Win32K!xxxdrawscrollbar может при определённых условиях вызвать callback в пользовательское пространство. В пользовательском коде указатель tagSBINFO может быть освобождён атакующим. При возврате к приведённому выше коду, инструкция по адресу 0xFFFFF97FFF1B1519 будет обращаться к недействительному указателю.

Исправленная версия:

root@kitploit:~
    .text:FFFFF97FFF1D69C3 xor r8d, r8d 
    .text:FFFFF97FFF1D69C6 mov rdx, rbp 
    .text:FFFFF97FFF1D69C9 call xxxDrawScrollBar        ; вызывает usermode callback
    .text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h]          ; проверка, корректен ли указатель tagSBINFO 
 ---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; если корректен, продолжаем обычный поток 
 |  .text:FFFFF97FFF1D69D7 mov rcx, rbp 
 |  .text:FFFFF97FFF1D69DA call _ReleaseDC 
 |  .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958     ; переход на выход из функции 
 |->.text:FFFFF97FFF1D69E4 mov eax, [rbx]               ; безопасно используем корректный указатель tagSBINFO 
    .text:FFFFF97FFF1D69E6 xor eax, r14d

В исправленной версии мы видим проверку указателя tagSBINFO перед использованием. Соответствующая информация о структуре будет приведена позже.

Основы – Этап 1 повреждения памяти (Memory Corruption Phase 1)

При реализации этого эксплойта было выполнено несколько повреждений (corruptions). Уязвимость срабатывает в одном из них.

Техническая первопричина этой ошибки – Use-After-Free (UAF) в куче десктопа (Desktop Heap). Сначала это меня смутило, так как я не был знаком с механизмом пользовательских обратных вызовов win32k.sys и не понимал, как он работает. Я предположил, что это состояние гонки (race condition) с блокировкой, приводящее к UAF. На самом деле блокировка той структуры использовалась правильно, и поток выполнения соответствовал ожиданиям. Короче говоря, реальная причина такова:

  1. Функция win32k!xxxEnableWndSBArrows содержит указатель на tagSBINFO в куче десктопа, полученный из связанной с окном структуры tagWND, описывающей полосу прокрутки.
  2. win32k!xxxEnableWndSBArrows вызывает другую функцию, которая приводит к пользовательскому обратному вызову (эта функция может быть перехвачена (HOOK) в пользовательском режиме).
  3. Как только код выполняется в пользовательском пространстве, структуры в куче десктопа могут быть изменены через другие системные вызовы win32k, включая освобождение структуры tagSBINFO из кучи десктопа.
  4. При возврате в режим ядра win32k!xxxEnableWndSBArrows не обращается к tagSBINFO из tagWND и не проверяет, действителен ли ранее полученный указатель, а напрямую использует старый указатель (который уже освобождён).

Всё. Если не учитывать пользовательские обратные вызовы, этот этап довольно прост.

Понимание того, как мы контролируем поток

Но как именно мы производим повреждение и зачем? Как упоминалось в блоге Уди, вы можете установить или сбросить 2 бита в определённом месте, которое системный код считает полем WSBflags в структуре tagSBINFO. Это нестандартный подход к UAF, но в статье была подсказка, как это сделать, что я объясню в следующих разделах. Сначала разберёмся, как можно управлять этими битами.

Структура tagSBINFO (одинакова для 32 и 64 бит):

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Эта UAF находится в функции win32k!xxxEnableWndSBArrows(), которая включает или выключает стрелки одной или обеих (горизонтальной или вертикальной) полос прокрутки. Полоса прокрутки – это специальное окно для управления скроллингом. Его можно создать с помощью функции CreateWindow() со встроенным классом окна "SCROLLBAR".

Прототип win32k!xxxEnableWndSBArrows():

root@kitploit:~
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);

Параметр WSBflags имеет те же значения, что и в WinUser.h, и указывает, какая полоса прокрутки будет обработана:

root@kitploit:~
#define SB_HORZ 0 
#define SB_VERT 1 
#define SB_CTL  2 
#define SB_BOTH 3

Параметр wArrows указывает, доступны ли стрелки или нет. Если бит установлен, стрелка недоступна; иначе доступна. Младшие два бита wArrows соответствуют горизонтальной полосе, следующие два – вертикальной, остальные биты не имеют отношения к целям данного эксплойта.

Следующий код взят из функции win32k!xxxEnableWndSBArrows() – он устанавливает или сбрасывает соответствующие биты для горизонтальных стрелок, если установлен SB_HORZ или SB_BOTH:

Эта ошибка существует при установке флагов горизонтальной и вертикальной полос прокрутки. После обновления горизонтальной полосы, если соответствующее окно видимо на рабочем столе, функция win32k!xxxEnableWndSBArrows() вызывает win32k!xxxDrawScrollBar(), что, как упоминалось ранее, может привести к потенциальному пользовательскому обратному вызову.

Прежде чем обсуждать пользовательский обратный вызов, продолжим обсуждение того, что происходит после вызова win32k!xxxDrawScrollBar(). По сути, это та же логика, что и для горизонтальной полосы, только с несколькими битами. Если мы выбрали отключение вертикальной полосы и предположим, что мы вызвали UAF, то это приведёт к записи 2 бит в некоторое место в блоке кучи tagSBINFO. Таким образом, если исходное значение было 0x2, то теперь оно станет 0xe. Как показано на рисунке ниже.

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

Суть вышесказанного в том, чтобы одновременно работать и с горизонтальной, и с вертикальной полосами прокрутки, необходимо каким-то образом создать элемент управления полосой прокрутки, имеющий оба этих элемента. Это делается вызовом CreateWindow() с флагами WS_HSCROLL и WS_VSCROLL. Код выглядит так:

root@kitploit:~
g_hSBCtl = CreateWindowEx( 
    0,                  // Нет расширенного стиля 
    "SCROLLBAR",        // класс 
    NULL,               // имя 
    SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // Вертикальная + Горизонтальная 
    10,                 // x 
    10,                 // y 
    100,                // ширина 
    100,                // высота 
    g_hSpray[UAFWND],   // родительское окно без модальности 
    (HMENU)NULL,
    NULL,               // владелец окна 
    NULL                // дополнительные параметры 
    );

Обеспечиваем его видимость (обычно это по умолчанию, но здесь явно вызываем):

root@kitploit:~
result = ShowWindow(g_hSBCtl, SW_SHOW);

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

root@kitploit:~
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);

Вызов уязвимости

Хотя мы описали детали ошибки и то, как вызвать соответствующий код, мы всё ещё упускаем самый важный шаг: перехват пользовательского обратного вызова, инициированного win32k!xxxDrawScrollBar(), чтобы мы могли изменить содержимое кучи до того, как win32k!xxxEnableWndSBArrows() продолжит выполнение. Нам действительно нужно вызвать ошибку, но без понимания тонкостей Win32k.sys и связанных API, как это было у меня вначале, это настоящее приключение.

В предыдущих статьях была хорошая диаграмма стека вызовов, показывающая этот процесс: вызов через win32k!xxxDrawScrollBar(), затем вызов ClientLoadLibrary() и диспетчеризация через KeUserModeCallback(). Нам нужно точно понять, как работает KeUserModeCallback(), чтобы перехватить его в нашем процессе.

Я нашёл несколько хороших работ, где более или менее упоминаются пользовательские обратные вызовы. Особенно полезны части, касающиеся win32k:

  • https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf (работа Tarjei)
  • http://azimuthsecurity.com/resources/recon2012_mandt.pptx (презентация Tarjei с множеством дополнительных сведений)
  • http://www.nynaeve.net/?p=204
  • http://www.cprogramdevelop.com/3825874/
  • http://www.zer0mem.sk/?p=410
  • https://www.reactos.org/wiki/Techwiki:RegisterUserApiHook
  • http://pasotech.altervista.org/windows_internals/Win32KSYS.pdf
  • http://j00ru.vexillium.org/?p=614
  • http://uninformed.org/index.cgi?v=10&a=2#SECTION00042000000000000000

Обычно каждый процесс имеет таблицу указателей на функции пользовательского режима обратного вызова, на которую указывает PEB->KernelCallBackTable. Когда ядро хочет вызвать функцию пользовательского режима, оно передаёт индекс функции в KeUserModeCallBack(). В приведённом примере индекс указывает на пользовательскую функцию __ClientLoadLibrary().

KeUserModeCallBack() находит соответствующую функцию в PEB->KernelCallBackTable по индексу и выполняет её, и в итоге в пользовательском режиме вызывается KiUserModeCallbackDispatch().

Чтобы перехватить указанную точку входа, нужно найти индекс __ClientLoadLibrary() в PEB->KernelCallBackTable и заменить нашу собственную функцию. Стоит отметить, что этот индекс зависит от версии ОС и аппаратной платформы.

Чтобы просмотреть PEB->KernelCallBackTable, с помощью WinDbg можно найти адрес этой таблицы. Сравнение 32- и 64-битных платформ не показывает больших различий.

root@kitploit:~
kd> dt !_PEB @$peb 
ntdll!_PEB 
    +0x000 InheritedAddressSpace    : 0 '' 
    +0x001 ReadImageFileExecOptions : 0 '' 
    +0x002 BeingDebugged            : 0 '' 
    +0x003 BitField                 : 0x8 '' 
    +0x003 ImageUsesLargePages      : 0y0 
[...] 
    +0x02c KernelCallbackTable      : 0x76daf620 Void 
    
kd> dds 0x76daf620 
76daf620 76d96443 user32!__fnCOPYDATA 
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA 
76daf628 76da736b user32!__fnDWORD 
76daf62c 76d9d603 user32!__fnNCDESTROY 
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG 
76daf634 76ddf1be user32!__fnINOUTDRAG 
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS 
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL 
[...] 
76daf724 76da3962 user32!__ClientLoadLibrary 

kd> ?? (0x76daf724-0x76daf620)/4 int 0n65

В приведённом примере индекс __ClientLoadLibrary равен 65 – именно его мы и будем перехватывать. После перехвата я обнаружил, что __ClientLoadLibrary вызывается из кода win32k многократно! Первое, что нужно сделать – уведомить наш перехватчик о том, когда именно произойдёт интересующий нас вызов, чтобы мы знали, что мы действительно перехватили то, что нужно. Поэтому в коде перехватчика используется глобальная переменная-флаг, и действия выполняются только когда этот флаг установлен.

Теперь есть два препятствия:

  1. Если позволить оригинальному __ClientLoadLibrary выполняться нормально, то при срабатывании уязвимости в win32k я обнаружил, что поток не попадает в пользовательский режим. Я не углублялся в это, предположив, что динамическая библиотека, которую нужно загрузить, уже загружена, поэтому вызов загрузчика не требуется. Чтобы принудительно вызвать загрузку, я сделал так, чтобы перехватчик __ClientLoadLibrary каждый раз не возвращал результат, заставляя ядро повторять попытку. Я просто возвращал NULL в структуре параметров – это было сделано путём реверса функции __ClientLoadLibrary() из user32.dll.
  2. Выполнение EnableScrollBar() в конечном итоге вызывает __ClientLoadLibrary, а затем мы попадаем в точку, которую хотим использовать через win32k!xxxDrawScrollBar(). Поэтому необходимо знать, сколько раз этот вызов происходит до появления интересующего нас события. С помощью счётчика я точно определил момент, когда уязвимость срабатывает и управление попадает в мой перехватчик. К счастью, этот счётчик одинаков для всех платформ и версий ОС.

Таким образом, функция-перехватчик выглядит примерно так:

root@kitploit:~
void ClientLoadLibraryHook(void * p) 
{ 
    CHAR Buf[PGSZ]; 
    memset(Buf, 0, sizeof(Buf)); 
    if (g_PwnFlag) 
    { 
        dprintf("[+] __ClientLoadLibrary hook called\n"); 
        if (++g_HookCount == 2) 
        {
            g_PwnFlag = 0;      // выполняется только один раз.. 
            ReplaceScrollBarChunk(NULL); 
        }
    } 
    fpClientLoadLibrary(&Buf); // вызов оригинальной функции
}

Как только мы убедимся, что вызов произошёл из win32k!xxxDrawScrollBar(), мы можем попытаться вызвать уязвимость. Сейчас мы рассматриваем только вызов: достаточно вызвать DestroyWindow(g_hSBCtl). Это приведёт к освобождению структуры tagSBINFO окна. Само окно не освобождается немедленно, так как его счётчик ссылок всё ещё используется оригинальным вызовом, но у tagSBINFO нет такого механизма, поэтому он освобождается сразу.

На этом этапе мы вызвали уязвимость. Хотя мы не перераспределили блок кучи, содержащий tagSBINFO, мы всё равно можем записать два бита (означающие отключение) в уже освобождённую кучу. Следующий шаг – заменить освобождённый блок кучи чем-то, что мы хотим, чтобы мы могли сделать что-то более интересное, чем просто установить несколько бит. Для этого нужно понять некоторые основы кучи десктопа.

Куча десктопа (Desktop Heap)

win32k.sys использует кучу десктопа для хранения GUI-объектов, связанных с данным десктопом. К ним относятся объекты окон и связанные с ними структуры, такие как списки свойств (property lists), тексты окон, полосы прокрутки. В статье Tarjei упоминается об этом, но важно отметить, что куча десктопа на самом деле является упрощённой версией пользовательского бэкенд-аллокатора и использует RtlAllocateHeap() и RtlHeapFree(). Куча десктопа управляется структурой _HEAP, и поскольку нет фронтенд-аллокатора, в ней нет таких вещей, как Low Fragmentation Heap (LFH) или списки обхода (lookaside lists).

При создании каждого десктопа для него выделяется соответствующая куча десктопа. Это означает, что мы можем создать новый десктоп, чтобы получить «чистую» кучу, в которой наши операции будут более предсказуемыми. Однако это не имеет смысла для процессов с низким уровнем привилегий (low integrity), так как таким процессам не разрешается создавать новый десктоп.

Теперь основная задача – отслеживать выделения (подробнее о метаданных и т.д. позже).

Мониторинг выделений в куче десктопа

Для отслеживания выделений и освобождений в куче десктопа я использовал скрипты WinDbg:

Мониторинг 64-битной кучи

root@kitploit:~
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";

Мониторинг 32-битной кучи

root@kitploit:~
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc"; 
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"

Кроме этих скриптов, поскольку куча десктопа является упрощённой формой пользовательского бэкенд-аллокатора, мы действительно можем использовать встроенную команду WinDbg !heap.

Заполнение дыр в куче (Heap Feng Shui)

Чтобы использовать эту уязвимость, нам нужно заменить последний освобождённый блок tagSBINFO, и мы знаем, как использовать типичные ошибки этого типа: повреждать соседние данные. Это предъявляет базовое требование: предварительно выделить блоки кучи рядом со структурой, которую мы хотим повредить. Чтобы предсказать, куда будет помещён новый блок, мы должны управлять компоновкой кучи (как можно полнее). Для этого подходящий метод – выделить как можно больше блоков, чтобы заполнить пустоты, оставшиеся после освобождения других блоков. Таким образом, новые блоки будут располагаться последовательно. Когда нам понадобится дыра, мы можем создать её в предсказуемом месте (освободив уже выделенный блок).

Эта часть – простое понимание факторов, влияющих на распределение. Скрипты WinDbg выше могут помочь. Tarjei в своей презентации о win32k упомянул основные объекты, выделяемые в куче десктопа, и это полностью совпало с тем, что я видел. Это:

  • Окно (Window)
  • Меню (Menu)
  • Хук (Hook)
  • CallProcData
  • Контекст ввода (Input Context)

Куча десктопа весьма интересна: большая часть выделений напрямую связана с объектами окон и управляется через структуру tagWND. Это означает, что если мы хотим выделить блок произвольного размера (так называемую «маленькую дыру» для заполнения), нам нужно сначала выделить связанное окно. Можно считать, что структура окна является интерфейсом для выделения кучи. Ещё один интересный момент: многие блоки, выделенные через операции с окнами, не могут быть немедленно освобождены, пока само окно не будет уничтожено, что, очевидно, влияет на кучу. Наконец, представим, что мы выделяем через окно блок размера N, как в примере выше. Нужно ли нам выделять много блоков размера N? Можно с уверенностью сказать, что выделенные структуры окна не хранятся в связном списке независимо от их размера. Таким образом, каждое окно может контролировать одно выделение размера N. То есть, если вам нужно выделить много блоков размера N, вы должны сначала создать много окон, используя их для помощи в выделении блоков.

В куче десктопа также есть три важных типа данных, которые мы можем косвенно использовать через объекты окон для управления данными в куче. Мы активно используем эти типы данных для реализации эксплуатации и построения heap feng shui. Это:

  1. Структура tagPROPLIST: если выделить достаточно маленький блок, можно заполнить маленькие дыры. Окно содержит tagPROPLIST, который в 32-битной системе имеет размер 0x10 байт, а в 64-битной – 0x18 байт.
  2. Текст окна (Window Text): это строка в Unicode произвольной длины, выделенная в куче десктопа, хранящаяся в структуре _LARGE_UNICODE_STRING, встроенной в tagWND. Обратите внимание, что поле strName является структурой, а не указателем, но эта структура содержит указатель, связанный с блоком кучи текста окна.
  3. Структура tagSBINFO: корень уязвимости, содержит 4 полностью контролируемых поля.

Рисунок 2 иллюстрирует взаимосвязь этих типов данных:

Чтобы инициализировать кучу, я создал множество структур tagWND (созданием окон). Это заполняет многие большие дыры в куче и предоставляет интерфейс для выделения других необходимых блоков. В Win8 и Win8.1 создание нового окна приводит к автоматическому выделению структуры tagPROPLIST (это можно наблюдать с помощью упомянутых скриптов WinDbg). В Win7 и более ранних версиях мы выделяем новый tagPROPLIST сами, чтобы заполнить маленькие дыры.

Здесь все окна, которые мы распыляем (spray), не имеют текста окна, однако при необходимости мы всё равно можем использовать это для выделения или освобождения блоков произвольного размера. После создания вы не можете удалить существующий список свойств (property list), пока окно не будет уничтожено, но мы можем управлять перераспределением этого списка, чтобы вместить новые свойства – этот механизм можно использовать для создания дыр в предварительно занятых местах. Вам нужно только установить новое свойство, которого нет в текущем списке (отличающееся по atomKey).

Проверка компоновки кучи (Feng Shui)

Интересно, что куча десктопа отображается в пользовательское пространство, хотя и только для чтения. Это означает, что мы можем проверить построенную нами компоновку и убедиться, что она работает. Сначала нужно определить, куда отображается куча десктопа в пользовательском режиме. В статье Tarjei о win32k упоминается об этом. С этим связана недокументированная структура Win32ClientInfo в TEB, примерное определение:

root@kitploit:~
typedef struct _CLIENTINFO { 
    ULONG_PTR CI_flags; 
    ULONG_PTR cSpins; 
    DWORD dwExpWinVer; 
    DWORD dwCompatFlags; 
    DWORD dwCompatFlags2; 
    DWORD dwTIFlags; 
    PDESKTOPINFO pDeskInfo; 
    ULONG_PTR ulClientDelta; // неполно. см. reactos 
} CLIENTINFO, *PCLIENTINFO;

Структура PDESKTOPINFO определена так:

root@kitploit:~
typedef struct _DESKTOPINFO { 
    PVOID pvDesktopBase; 
    PVOID pvDesktopLimit;   // неполно. см. reactos  
} DESKTOPINFO, *PDESKTOPINFO;

Первое поле pvDesktopBase указывает на адрес кучи десктопа в пространстве ядра – запомним это. Поле ulClientDelta структуры Win32ClientInfo представляет собой разницу между адресом в ядре и адресом в пользовательском режиме. Используя эту информацию, мы можем получить то, что нам нужно.

Однако мы не хотим самостоятельно разбирать структуру кучи; вместо этого мы хотели бы иметь дескриптор user32, например значение HWND, которое можно преобразовать в адрес в пользовательском отображении, чтобы определить, связано ли оно с другими выделениями кучи. Чтобы найти этот дескриптор, нужно найти структуру под названием gShared, обычно расположенную в user32.dll. Начиная с Windows 7, она экспортируется, поэтому её легко найти.

На большинстве систем эта структура определена так:

root@kitploit:~
kd> dt !tagSHAREDINFO 
win32k!tagSHAREDINFO 
    +0x000 psi                  : Ptr32 tagSERVERINFO 
    +0x004 aheList              : Ptr32 _HANDLEENTRY 
    +0x008 HeEntrySize          : Uint4B 
    +0x00c pDispInfo            : Ptr32 tagDISPLAYINFO 
    +0x010 ulSharedDelta        : Uint4B 
    +0x014 awmControl           : [31] _WNDMSG 
    +0x10c DefWindowMsgs        : _WNDMSG 
    +0x114 DefWindowSpecMsgs    : _WNDMSG

В приведённой структуре aheList указывает на массив _HANDLEENTRY, каждый _HANDLEENTRY содержит дескриптор, указывающий на адрес в пространстве ядра. Мы можем получить доступный адрес в пользовательском пространстве через «разницу между адресом ядра и адресом пользователя». К сожалению, это невозможно в версиях до Windows 7, так как gSharedInfo не экспортируется. В статье Tarjei сказано, что недокументированная функция CsrClientConnectToServer может быть использована для получения копии gSharedInfo, но я не нашёл работающего примера. Досадно, что структура, необходимая для реализации этой функции, имеет разную длину в разных системах, поэтому, исходя из моего опыта, нельзя полностью доверять тому, что вы видите в ReactOS.

Как только мы вычислим расположение отображения, мы сможем построить функцию, которая сообщает нам положение объекта окна в куче десктопа. Затем, если мы хотим узнать, куда был выделен блок, соответствующий списку свойств или тексту окна, достаточно проанализировать структуру в пользовательском режиме.

Замена tagSBINFO на tagPROPLIST

Теперь мы наконец подходим к эксплуатации этой уязвимости. У нас есть метод управления блоками кучи, метод проверки правильности расположения блоков и возможность вызвать ошибку. Теперь мы можем окончательно заменить освобождённый блок tagSBINFO выбранным блоком tagPROPLIST (списком свойств). Обратите внимание, что tagPROPLIST – это всего лишь заголовок большого списка, поэтому мы можем сделать размер списка соответствующим размеру блока полосы прокрутки. Остальная часть tagPROPLIST – это, по сути, массив структур tagPROP, или список свойств; поэтому я не буду различать термины «массив» и «список». Структура tagPROPLIST в 64-битной системе выглядит так:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

Как уже упоминалось, каждый объект окна имеет связанный список свойств. Этот список создаётся функцией SetProp(). Она используется для поиска существующего свойства по atomKey; если свойство не найдено, в списке создаётся новый элемент. Если список свойств вообще отсутствует, создаётся список и прикрепляется к структуре tagWND.

Если мы уже распылили (spray) tagWND с ошибками и создали связанные элементы tagPROPLIST, то итоговая компоновка показана на рисунке 3:

После того, как это настроено, мы можем создать элемент управления полосой прокрутки, который будем использовать. Это приведёт к результату, показанному на рисунке 4:

Затем, манипулируя полосой прокрутки, мы вызываем хук пользовательского обратного вызова. В функции-хуке, пытаясь уничтожить окно, мы освобождаем структуру tagSBINFO. Это приводит к ситуации на рисунке 5:

В 64-битной системе структура tagSBINFO имеет размер 0x28 байт. Элемент массива tagPROPLIST имеет размер 0x18 байт, из которых 0x10 байт – это стандартный tagPROP. Таким образом, список свойств из двух элементов будет иметь размер 0x28 байт (0x8 + 0x10 + 0x10) – настоящее божественное совпадение. Предположим, мы распылили память так, чтобы заполнить дыру. Нам нужно только одно окно со списком свойств, и сразу после освобождения структуры tagSBINFO (как показано на предыдущем рисунке) добавить новый элемент в список свойств этого окна. Процесс: сначала освобождается ранее выделенный блок tagPROPLIST размером 0x18 байт. Поскольку куча уже распылена, рядом нет свободных блоков, поэтому слияния блоков не произойдёт, и не будет достаточно большого пространства для нового блока размером 0x28 байт. Таким образом, будет использована только что освобождённая позиция tagSBINFO (её размер как раз 0x28 байт). Эта ситуация показана на рисунке 6:

После возврата из нашей функции-хука обратного вызова срабатывает UAF, и несколько бит записываются в поле cEntries в tagPROPLIST. Исходное значение cEntries равно 0x2 (мы создали два элемента списка свойств). После перезаписи оно становится 0xe – 3-й и 4-й биты (счёт с 1) устанавливаются в 1.

На этом этапе мы завершили переполнение нового блока и увеличили количество элементов списка свойств до значения больше 0xc. Далее мы переполним соседний блок кучи, что я называю этапом 2 повреждения (Phase 2 Corruption).

Злоупотребление списком свойств (Property List Abuse) – Этап 2 повреждения

В блоге Уди объясняется именно это. До этого момента это называлось «типичное переполнение кучи», однако, по моему опыту, добиться от этой точки произвольного чтения/записи или выполнения кода очень сложно. Посмотрим ещё раз на структуру tagPROPLIST в 64-битной системе:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

Повреждение на этапе 1 дало нам повреждённый массив tagPROPLIST, позволяющий увеличить количество элементов tagPROP. tagPROPLIST имеет только два поля:

  • cEntries: количество имеющихся элементов списка.
  • iFirstFree: индекс первого свободного элемента. Полный список (когда нужно запросить новый) означает iFirstFree == cEntries.

При вставке нового элемента в список сначала вызывается функция, которая сканирует каждый элемент, пока не найдёт подходящее значение iFirstFree. Если не найдено, проверяется, больше ли iFirstFree, чем cEntries. Если элемент с данным atomKey отсутствует в списке, проверяется, не равно ли iFirstFree != cEntries. Если не равно, новый элемент вставляется в позицию с индексом iFirstFree; если равно, выделяется новый блок под список свойств, способный вместить вставляемый элемент, старые элементы копируются, и новый элемент вставляется.

Поле atomKey соответствует LPCTSTR lpString. Как указано в документации MSDN к SetProp(), вызывающий может передать указатель на строку или 16-битное значение atom. При передаче строки она автоматически преобразуется в atom перед сохранением в списке свойств. Поскольку мы можем передать произвольное значение atom в SetProp(), это даёт нам возможность контролировать эти два байта, но с некоторыми ограничениями. А именно, мы не можем повторять значение atomKey при повреждении, иначе при установке нового свойства оно заменит существующее с тем же atom. Кроме того, поле fs не под контролем: его значение 0, если atomKey < 0xBFFF (что соответствует целочисленному atom). Значение 2 означает, что atomKey >= 0xC000.

Ещё одно замечание: tagPROP имеет всего 0xc байт. В 64-битной системе эта структура выравнивается до 0x10 байт, поэтому при вставке элемента tagPROP остаётся 4 дополнительных байта, которые нельзя повредить. Последний важный момент: первые 8 байт блока кучи tagPROPLIST задают размер списка, что означает, что каждый новый элемент tagPROP будет записываться по адресу, выровненному на 8 байт.

Для каждого вставленного tagPROP в 64-битной системе ситуация следующая:

root@kitploit:~
* Смещение 0x0: 8 произвольно контролируемых байт (hData) 
* Смещение 0x8: 2 в основном контролируемых байта (atomKey) 
* Смещение 0xa: 2 неконтролируемых байта (fs) 
* Смещение 0xc: 4 неизменяемых байта (padding)

Эта ситуация гораздо лучше, чем всего 2 бита, но всё ещё не идеальна. Мы ограничены, если не можем перезаписать что-то первыми 8 байтами, которые берутся из полностью контролируемого поля hData. Если нам нужно записать в более глубокие поля соседней структуры, мы не можем избежать неконтролируемого повреждения некоторых значений. Я потратил некоторое время на поиск различных объектов в куче десктопа, и, учитывая вышеуказанные ограничения, единственный способ, который я смог придумать для обхода этих ограничений и достижения произвольного чтения/записи, – это повредить поле strName в tagWND, которое является структурой _LARGE_UNICODE_STRING:

root@kitploit:~
kd> dt !_LARGE_UNICODE_STRING 
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           : Uint4B 
    +0x004 MaximumLength    : Pos 0, 31 Bits 
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B

Если мы сможем повредить поле Buffer этой структуры, то, манипулируя текстом окна, мы сможем читать или записывать MaximumLength байт по заданному адресу. Именно это я и собираюсь сделать. Возможно, вы заметили эту структуру в предыдущих разделах, где описывалось, как создать блок произвольного размера и содержимого в куче десктопа – та же логика применима и здесь.

Теперь мы знаем, как с помощью элементов списка tagPROPLIST повреждать данные, какие части мы можем контролировать и, что более важно, с какими ограничениями мы столкнёмся. В 32- и 64-битных системах есть различия. То, что мы делали в 64-битной системе, не будет работать в 32-битной. Вскоре мы перейдём от этапа 2 повреждения (запись через структуру tagPROP) к другому примитиву повреждения, который позволит нам записывать полностью контролируемые данные; это я называю этапом 3 повреждения (Phase 3 Corruption).

Построение примитива «чтение/запись» (Read/Write Primitive) – Этап 3 повреждения

64-битная система

План состоит в том, чтобы повредить поле strName в соседней структуре tagWND. Мы уже знаем, что это структура _LARGE_UNICODE_STRING, но давайте посмотрим на структуру tagWND подробнее. Выглядит она так:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [SNIPPED FLAGS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [SNIPPED FLAGS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [SNIPPED FLAGS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [SNIPPED FLAGS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN_ +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [SNIPPED FLAGS] +0x120 bChildNoActivate : Pos 11, 1 Bit

Выше приведена структура для 64-битной системы. Видно, что смещение структуры _LARGE_UNICODE_STRING, которую мы хотим перезаписать, равно 0xd8. Вы также заметите важное поле в начале этой структуры. Изначально я надеялся хорошенько с ним поработать, но в _THRDESKHEAD есть множество указателей, которые требуют от нас осторожности, и, к сожалению, мы не можем контролировать место записи — ограничения, о которых мы говорили ранее.

Определение структуры _THRDESKHEAD:

root@kitploit:~
kd> dt !_THRDESKHEAD 
win32k!_THRDESKHEAD 
    +0x000 h        : Ptr64 Void 
    +0x008 cLockObj : Uint4B 
    +0x010 pti      : Ptr64 tagTHREADINFO 
    +0x018 rpdesk   : Ptr64 tagDESKTOP 
    +0x020 pSelf    : Ptr64 UChar

Проблема с _THRDESKHEAD не только вызывает у нас вопросы, но и заставляет пересмотреть ограничения выравнивания. Вне зависимости от смещения нового элемента списка tagPROP наша операция записи будет напрямую перезаписывать начало _LARGE_UNICODE_STRING:

root@kitploit:~
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           <-- hData (полностью контролируемо) перезаписывает это 
    +0x004 MaximumLength    <-- и это 
    +0x004 bAnsi            <-- и это 
    +0x008 Buffer           <-- atomKey и fs (частично контролируемо) перезаписывают это

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

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

В версиях Windows после XP заголовки кучи (то есть _HEAP_ENTRY) для пользовательского бэкенд-аллокатора (например, ядра Desktop Heap) хранятся в куче и располагаются перед фактическим содержимым блока. Сама Desktop Heap управляется через структуру _HEAP, что даёт нам некоторую свободу при эксплуатации этого блока кучи.

Структура _HEAP_ENTRY определяется так:

root@kitploit:~
kd> dt !_HEAP_ENTRY 
ntdll!_HEAP_ENTRY 
    +0x000 PreviousBlockPrivateData : Ptr64 Void 
    +0x008 Size             : Uint2B 
    +0x00a Flags            : UChar 
    +0x00b SmallTagIndex    : UChar 
    +0x00c PreviousSize     : Uint2B 
    +0x00e SegmentOffset    : UChar 
    +0x00f UnusedBytes      : UChar

Заголовок блока кучи занимает 0x10 байт. Первые 8 байт — это PreviousBlockPrivateData; когда запрашиваемый размер превышает обычные 0x10 (меньше 8 байт выравнивается по 8 байт), он содержит данные предыдущего фактического блока. Это кратко описано в записи блога Leviathan, а также в более ранних статьях о пользовательской куче. Size и PreviousSize указывают размер текущего блока и размер предыдущего блока в единицах по 0x10 байт. Flags показывает, свободен ли блок, и т. д. Если в _HEAP включён безопасный режим _HEAP_ENTRY, то SmallTagIndex будет содержать XOR-контрольную сумму данных блока.

Несмотря на то, что ограничения выравнивания работают против нас, они всё же существуют. Если вы вызываете tagPROPLIST, он всегда занимает как минимум 0x18 байт, затем добавляется 0x10 байт для tagPROP. Для tagPROPLIST размером 0x28 байт с двумя элементами он будет помещён в блок кучи размером 0x20 байт, а лишние байты, обозначаемые PreviousBlockPrivateData, будут использовать соседний блок кучи. Это означает, что при добавлении третьего элемента соседний блок кучи повреждается, и контролируемые 8 байт hData перезаписывают верхнюю часть _HEAP_ENTRY.

Мы хотим использовать это, чтобы каким-то образом записать произвольные данные в позицию над Buffer. Сначала мы изменяем расклад кучи так, чтобы приблизиться к повреждённому нами блоку кучи tagPROPLIST. В процессе управления у нас есть небольшой блок кучи, содержащий текстовую строку, связанную с окном; назовём его «перезаписываемым блоком». Рядом с этим «перезаписываемым блоком» мы размещаем tagWND, чтобы повредить этот tagWND. Ниже на рисунке 7 показан этот процесс. Обратите внимание: мы опустили ранее разбросанные блоки кучи для экономии места, поэтому теперь они считаются подразумеваемыми.

Затем мы вставляем третий tagPROP в список tagPROPLIST, который перезаписывает последние 8 байт _HEAP_ENTRY и первые 8 байт «перезаписываемого блока». Таким образом мы можем изменить _HEAP_ENTRY «перезаписываемого блока», сделав его размер больше фактического и достаточным, чтобы вместить соседнюю структуру tagWND.

Теперь освобождаем только что повреждённый «перезаписываемый блок», чтобы диспетчер кучи поместил его в список свободных блоков, соответствующий размеру (каждый список свободных блоков соответствует фиксированному размеру), большему, чем фактический размер «перезаписываемого блока». Затем мы повторно используем этот блок, изменяя текст окна (который мы полностью контролируем). Однако есть небольшая проблема, которую нужно решить. Когда «перезаписываемый блок» освобождается, диспетчер кучи пытается найти предыдущий соседний блок; это зависит от повреждённого поля Size. Диспетчер кучи проверяет, свободен ли этот соседний блок, чтобы объединить их. В любом случае мы хотим контролировать его и установить флаг использования. Слегка изменив расклад кучи, мы можем этого добиться. Для этого мы размещаем фиктивный блок кучи с заголовком, у которого установлен флаг «в использовании», а PreviousSize установлен в значение повреждённого поля Size. Мы можем легко это сделать, просто изменив текст окна другого окна. Новая раскладка кучи показана ниже (рисунок 8):

Теперь мы можем освободить повреждённый «перезаписываемый блок», обновив текстовую строку связанного с ним окна так, чтобы её длина превышала исходные 0x10 байт. Таким образом, повреждённый «перезаписываемый блок» сначала освобождается и помещается в список свободных, но его размер (повреждённый) объявляется больше фактического. Этот размер можно настроить в зависимости от наших реальных потребностей. После этого наши строковые данные записываются в «перезаписываемый блок», и мы используем его для повреждения соседнего tagWND произвольными данными. Это показано ниже (рисунок 9):

Это повреждение для этапа 3. Теперь мы можем перезаписать указатель strName.Buffer любыми нужными нам данными. Однако повреждение других данных tagWND всё ещё представляет некоторые трудности, но это не проблема, поскольку Desktop Heap отображается в пользовательское пространство! Поэтому до повреждения всего мы считываем всё содержимое tagWND, изменяем содержимое структуры strName на то, что нам нужно, и отправляем все данные, изменяя текст окна.

StrName даёт нам не только «примитив» произвольного чтения/записи, но и возможность многократно изменять strName, что допускается механизмом изменения текста окна. Пока длина записываемой строки не превышает значение MaximumLength, можно продолжать использовать тот же блок кучи. Поэтому каждый раз, когда мы хотим изменить адрес strName для чтения какого-либо значения, мы добавляем новую строку и наши данные для обновления «перезаписываемого блока». Это повторное использование показано на рисунке 10. Обратите внимание: я снова увеличил масштаб, чтобы детально показать каждое повреждение.

Это означает, что в итоге нам нужно повредить только две дополнительные вещи (кроме исходных элементов списка tagPROPLIST):

  1. Заголовок «перезаписываемого блока». Мы можем прочитать его до повреждения, поэтому знаем, как восстановить его после использования. Забавно, что мы можем даже исправить блок кучи третьего элемента tagPROPLIST, отправив использованный для повреждения atomKey обратно в исходное место!
  2. Структура strName, которую мы можем легко изменить, записывая текст окна. При операции нужно установить возвращаемое значение в NULL.

Теперь, если мы хотим прочитать несколько байт из какого-то места памяти, мы запрашиваем текст окна через функцию InternalGetWindowText() — там находится повреждённая запись strName. Мы можем прочитать количество байт, указанное в поле Length. Аналогично, если мы хотим записать произвольную позицию в памяти, используем функцию NtUserDefSetText() для обновления повреждённого текста окна, но объём записи не должен превышать значение поля MaximumLength (которое мы тоже можем установить). При этом существующий буфер повторно используется и указывает на нужный адрес памяти.

Кодирование кучи в Windows 8 и 8.1

Хотя начиная с Windows Vista пользовательский бэкенд-аллокатор использует кодирование кучи (heap encoding), Desktop Heap никогда не включал его до Windows 8. Поэтому в системах Windows 8 и новее при перезаписи «перезаписываемого блока» возникает препятствие. Однако на самом деле структура _HEAP, содержащая кучу, содержит этот cookie и использует его для кодирования всего заголовка кучи. Поэтому мы можем прочитать этот cookie из отображённой в пользовательское пространство Desktop Heap, а затем закодировать заголовок «перезаписываемого блока» с его помощью, изучив код аллокатора и имитируя его операции; аллокатор примет такое действие.

32-битные системы

Прежде всего отметим, что в 32-битных системах структура tagPROP занимает 8 байт, а не 0xc, как в 64-битных, и контролируемое нами поле hData имеет всего 4 байта, а не 8, как в 64-битных. Кроме того, нет дополнительных байтов выравнивания (в 64-битных есть 8 байт выравнивания, поэтому вся структура ровно 8 байт). Это означает, что мы не можем полностью повредить соседний заголовок блока кучи, если можем контролировать данные только частично. В некоторых версиях Windows это возможно, потому что мы можем контролировать самые важные поля, но в Windows 8 и 8.1 заголовок кучи кодируется, и в итоге мы можем небезопасно перезаписать часть заголовка через поле fs. Заголовок _HEAP_ENTRY в 32-битных системах выглядит аналогично, но в нём отсутствует поле PreviousBlockPrivateData.

Мы по-прежнему не можем повредить все части tagWND, так как не избежать усечённых указателей. И я всё ещё не нашёл объект, который бы удовлетворял этим требованиям. Учитывая, что _LARGE_UNICODE_STRING хорошо работает в 64-битных системах, я решил использовать его и в 32-битных.

Моя идея заключается в следующем: если мы сможем повредить поле iFirstFree структуры tagPROPLIST (индекс первого освобождённого элемента списка свойств) путём увеличения его значения, то сможем направить его на более дальнюю позицию в куче. Например, мы можем направить его на вершину tagWND.strName. Рисунок 11 иллюстрирует эту идею:

Для ясности процесса мы теперь используем две структуры tagPROPLIST: «список A» для UAF и «список B». Нам нужно точно знать, какие части tagPROP, вставленного в «список A», перезапишут поле iFirstFree «списка B». Мы также должны помнить, что за одну запись можно записать только 8 байт, поэтому мы должны вставить как минимум один лишний tagPROP в «список A»: первое повреждение — соседний заголовок, второе — попадание в поля tagPROPLIST «списка B». Это может варьироваться в зависимости от операционной системы и размера блоков кучи, и в моей эксплуатации должно адаптироваться к различным раскладкам кучи. Рисунок 12 показывает, как мы повреждаем. Обратите внимание: первый tagPROPLIST не разбит на отдельные поля, поэтому tagPROP[0] подразумевается. Однако во втором tagPROPLIST его внутренние члены показаны отдельно, чтобы продемонстрировать процесс повреждения. Вот почему показан tagPROP[0]:

Прежде всего заметим, что если мы записываем 8 байт для каждого tagPROP, это означает, что мы можем лишь частично контролировать перезапись iFirstFree (поскольку она исходит из полей atomKey и fs), а это нас больше всего интересует. Поскольку мы можем полностью контролировать как минимум два ключевых байта через значение atomKey, когда это значение достаточно мало, поле fs становится 0. Поэтому мы используем значение hData для перезаписи cEntries разумным значением, а с помощью atomKey заставляем iFirstFree указывать на tagWND, в котором находится указатель strName.Buffer, который мы хотим перезаписать. Если мы не можем напрямую перезаписать значения Length и MaximumLength, мы можем предварительно выделить строку для целевого окна, чтобы убедиться, что её длина уже установлена в нужное значение.

Давайте посмотрим на 32-битную структуру tagWND, чтобы понять, что можно получить. Обратите внимание: на этот раз я использовал параметр -b, чтобы удобно вычислить смещение Buffer в strName.

root@kitploit:~
kd> dt -b !tagWND 
win32k!tagWND 
    +0x000 head         : _THRDESKHEAD 
        +0x000 h        : Ptr32 
        +0x004 cLockObj : Uint4B 
        +0x008 pti      : Ptr32 
        +0x00c rpdesk   : Ptr32 
        +0x010 pSelf    : Ptr32 
    +0x014 state        : Uint4B 
    +0x014 bHasMeun     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x014 bDestroyed   : Pos 31, 1 Bit 
    +0x018 state2       : Uint4B 
[SNIPPED FLAGS] 
    +0x018 bWMCreateMsgProcessed    : Pos 31, 1 Bit 
    +0x01c ExStyle                  : Uint4B 
    +0x01c bWS_EX_DLGMODALFRAME     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x01c bUIStateFocusRectHidden  : Pos 31, 1 Bit 
    +0x020 style        : Uint4B 
    +0x020 bReserved1   : Pos 0, 16 Bits 
[SNIPPED FLAGS] 
    +0x020 bWS_POPUP    : Pos 31, 1 Bit 
    +0x024 hModule      : Ptr32 
    +0x028 hMod16       : Uint2B 
    +0x02a fnid         : Uint2B 
    +0x02c spwndNext    : Ptr32 
    +0x030 spwndPrev    : Ptr32 
    +0x034 spwndParent  : Ptr32 
    +0x038 spwndChild   : Ptr32 
    +0x03c spwndOwner   : Ptr32 
    +0x040 rcWindow     : tagRECT 
        +0x000 left     : Int4B 
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x050 rcClient     : tagRECT 
        +0x000 left     : Int4B
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x060 lpfnWndProc  : Ptr32 
    +0x064 pcls         : Ptr32 
    +0x068 hrgnUpdate   : Ptr32 
    +0x06c ppropList    : Ptr32 
    +0x070 pSBInfo      : Ptr32 
    +0x074 spmenuSys    : Ptr32 
    +0x078 spmenu       : Ptr32 
    +0x07c hrgnClip     : Ptr32 
    +0x080 hrgnNewFrame : Ptr32 
    +0x084 strName      : _LARGE_UNICODE_STRING 
        +0x000 Length   : Uint4B 
        +0x004 MaximumLength : Pos 0, 31 Bits 
        +0x004 bAnsi    : Pos 31, 1 Bit 
        +0x008 Buffer   : Ptr32 
    +0x090 cbwndExtra   : Int4B 
    +0x094 spwndLastActive  : Ptr32 
    +0x098 hImc         : Ptr32 
    +0x09c dwUserData   : Uint4B 
    +0x0a0 pActCtx      : Ptr32 
    +0x0a4 pTransform   : Ptr32 
    +0x0a8 spwndClipboardListenerNext   : Ptr32 
    +0x0ac ExStyle2                     : Uint4B 
    +0x0ac bClipboardListener           : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x0ac bChildNoActivate             : Pos 11, 1 Bit

Смещение strName равно 0x84, смещение Buffer равно 0x8c. Мы знаем индекс элемента списка tagPROP и то, что можем записать 8 байт. Поэтому легко определить, будет ли iFirstFree указывать на смещение 0x88 окна, где находится MaximumLength. Поскольку мы можем контролировать только два байта Buffer, операция записи невыполнима, и, учитывая, что наша цель — использовать это как «примитив» произвольного чтения/записи, такой результат неприемлем. Если мы запишем следующий индекс, указывающий на 0x90, то перезапишем cbwndExtra, а это не то, что нам нужно.

Вернёмся к тому, что мы можем контролировать при предварительной настройке кучи, и посмотрим, есть ли в tagWND интересные смещения, которые можно контролировать. По смещению 0x70 в tagWND находится поле pSBInfo. Это смещение делится на 8, поэтому мы можем перезаписать этот указатель частью данных hData из ложного tagPROP.

Можем ли мы перезаписать pSBInfo так, чтобы он указывал непосредственно на strName в той же структуре tagWND? Возможно, использовать API полосы прокрутки для повреждения strName для достижения нашей цели.

pSBInfo указывает на структуру tagSBINFO, которая упоминалась в начале процесса UAF.

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Мы помним, что WSBflags не даёт нам большего контроля, но по крайней мере мы знаем, что при включении полосы прокрутки он устанавливается в 1, а при выключении — в 0. Это поле флага нельзя установить в произвольное значение; изучив соответствующие функции, мы обнаружили, что без изменения состояния полосы прокрутки значение этого поля не меняется. Значения в структуре tagSBDATA кажутся более интересными. Если мы прочитаем документацию по SetScrollInfo(), мы хорошо поймём, что означают эти значения. Похоже, мы можем передавать параметры в SetScrollInfo() через структуру SCROLLINFO. Если рядом с нашим целевым окном есть элемент управления полосой прокрутки, мы можем напрямую манипулировать указателем pSBInfo (он будет отправлять специальное сообщение окну, связанному с элементом управления окном). Очевидно, мы можем безусловно контролировать значения posMin и posMax. Поля page и pop немного проблематичны, так как они ограничены определённым диапазоном; сейчас мы постараемся их избежать. Устанавливаем флаг SIF_RANGE в структуре SCROLLINFO, чтобы указать, где мы хотим установить минимальное и максимальное значения.

Мы хотим перезаписать Buffer произвольными данными, то есть хотим, чтобы posMin перезаписал его. Поэтому мы можем заставить pSBInfo указывать на strName.MaximumLength. Пока мы не включаем и не выключаем полосу прокрутки, поле WSBflags не будет перезаписано, что гарантирует целостность strName.MaximumLength. Это означает, что независимо от того, как мы установим posMin (через nMin в SCROLLINFO), это перезапишет Buffer, а posMax будет записан в cbwndExtra. Это не большая проблема; в 64-битных системах мы можем предварительно прочитать это значение и восстановить его позже. Общая идея переполнения показана на рисунке 13:

Теперь на рисунке 14 показан процесс атаки для 32-битных систем. Прежде чем повредить какой-либо адрес, удалённый от UAF, давайте сделаем шаг назад и посмотрим на соответствующие блоки кучи и раскладку на рисунке. Теперь, когда мы знаем больше деталей, дальнейшие действия становятся очевидными.

Затем вставляем два элемента свойств в «список A». Это повредит данные рядом со «списком A» благодаря предыдущему повреждению UAF, а также заставит iFirstFree «списка A» указывать на pSBInfo. Обратите внимание: это также повредит соседнее значение pSBInfo, но мы можем прочитать его заранее, чтобы восстановить после повреждения.

Мы вставляем новый tagPROP в «список B». Идентификатор atom этого tagPROP отличается от уже существующих в списке, поэтому он будет вставлен в следующую свободную индексную позицию. Таким образом, pSBInfo повреждается, указывая на strName.MaximumLength в том же tagWND.

Наконец, обновляем полосу прокрутки, чтобы повредить поле strName.Buffer (рисунок 17):

Важно понимать, что, в отличие от 64-битного случая, мы не можем повредить значения длины strName. Мы можем предварительно выделить текстовую строку окна подходящей длины, чтобы её значение уже использовалось. Впоследствии, когда мы захотим прочитать или записать данные по адресу ядра, нам нужно только вызвать SetScrollInfo() для целевого окна, чтобы обновить значение Buffer, а затем использовать API работы с текстом окна.

Теперь в 32-битных системах у нас есть многократно используемый «примитив» произвольного чтения/записи!

Выполнение кода

Всё, начиная с этого момента, предполагает, что у нас есть «примитив» произвольного чтения/записи. Поэтому, когда я говорю «утечка/чтение какого-то значения» или «перезапись какого-то значения», я имею в виду выполнение «примитива», созданного на предыдущем этапе повреждения. Этот «примитив» в целом одинаков для обеих платформ. Остаётся только перезаписать указатель на функцию и заставить его указывать на полезную нагрузку Shellcode где-то. Обычный метод — перезаписать второй элемент nt!HalDispatchTable, который соответствует функции HalQuerySystemInformation(). Затем в пользовательском режиме вызвать функцию NtQueryInternalProfile(), чтобы запустить её.

Нам нужно знать базовый адрес загрузки модуля ядра, чтобы вычислить адрес nt!HalDispatchTable в ядре. Для этого мы можем вызвать NtQuerySystemInformation() в пользовательском режиме, чтобы получить информацию о модулях, которая содержит базовый адрес модуля.

root@kitploit:~
// Значение перечисления 11 соответствует SystemModuleInformation – это недокументировано... 
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);

После этого загружаем ntoskrnl.exe в пользовательском режиме, чтобы найти смещение nt!HalDispatchTable, и таким образом получаем его адрес в пространстве ядра. Затем с помощью «примитива чтения» считываем адрес HaliQuerySystemInformation() (эта функция не экспортируется), чтобы изменить его, а с помощью «примитива записи» повреждаем указатель на эту функцию, заставляя его указывать на адрес shellcode (либо в пространстве ядра, либо в пользовательском пространстве; подробнее ниже). Количество читаемых/записываемых байт одинаково для 32- и 64-битных систем.

Обход SMEP

Windows 8 и 8.1 ввели поддержку SMEP, и некоторые продукты безопасности могут включать её в Windows 7, поэтому мы предполагаем, что она присутствует. SMEP предотвращает выполнение кода из пользовательского пространства с привилегиями ядра, что делает невозможным изменение элемента nt!HalDispatchTable для указания на адрес в пользовательском пространстве. Поэтому мы хотим, чтобы он указывал на контролируемую область в пространстве ядра, где код может изменить регистр cr4 для отключения SMEP, после чего мы сможем перейти в пользовательское адресное пространство. В статье MWR описан интересный трюк для 64-битных систем: отображение собственных записей таблицы страниц, чтобы получить действительный адрес в пространстве ядра для любого виртуального адреса. Затем с помощью «примитива записи» напрямую изменяем запись таблицы страниц и её биты маски. Я портировал этот трюк на 32-битные системы, но есть различия для систем с PAE и без PAE.

Очевидный метод — отобразить пользовательский адрес в пространство ядра, а затем с помощью «примитива записи» изменить запись таблицы страниц, чтобы она имела права системы, а не пользователя. Это первый шаг. При реализации под Windows 8 я столкнулся с интересной проблемой. Начиная с Windows 8, диспетчер рабочего стола (dwm.exe) периодически сканирует окна на рабочем столе и запрашивает их имена — точную причину я не выяснял. Эта операция не отправляет сообщение окну, но у неё есть соответствующий обработчик окна, который вызывает GetInternalWindowText(). Поэтому проблема в том, что при использовании полей strName окна для перезаписи записи таблицы страниц, содержащей shellcode (память в адресном пространстве нашего собственного процесса), dwm.exe при получении имени окна из ядра проверяет, не равен ли strName.Buffer нулю, и косвенно ссылается на этот адрес; если адрес недействителен, система аварийно завершает работу.

Чтобы удовлетворить запрос dwm.exe, я использовал адрес ядра в качестве полезной нагрузки. Таким образом, независимо от того, какой процесс загружен, запись таблицы страниц, связанная с этим адресом, всегда будет действительна. Я решил разместить её в Desktop Heap, потому что мы можем вычислить её адрес ядра упомянутым ранее способом. Мы по-прежнему используем метод отображения собственных записей таблицы страниц, но на этот раз запись уже помечена как привилегированная, но не исполняемая. Поэтому нам нужно только установить бит выполнения.

Шаги:

  1. Создать буфер текста окна, содержащий полезную нагрузку этапа 1, и вычислить его адрес ядра.
  2. Используя трюк с отображением собственных записей таблицы страниц, вычислить запись таблицы страниц для полученного на этапе 1 базового адреса ядра.
  3. С помощью «примитива записи» установить бит маски выполнения в записи таблицы страниц.
  4. Перезаписать nt!HalDispatchTable, чтобы он указывал на адрес ядра, полученный на этапе 1.
  5. Вызвать NtQueryInternalProfile(), чтобы перейти к полезной нагрузке.
  6. Отключить маску SMEP в cr4 и перейти к полезной нагрузке в пользовательском пространстве этапа 2.
  7. Выполнить полезную нагрузку в пользовательском пространстве этапа 2 для повышения привилегий и вернуться.
  8. Восстановить маску SMEP в cr4, чтобы избежать обнаружения PatchGuard, и выполнить соответствующую очистку, после чего вернуться.

Обход песочницы с низким уровнем целостности

В Windows 8.1 есть ещё одна проблема: NtQuerySystemInformation() проверяет SID низкого уровня целостности, что означает, что только средний уровень целостности или выше может получить базовый адрес ядра. Это легко обойти известным трюком с sidt. Мы сохраняем адрес IDT в пользовательском пространстве (для этого не требуется проверка прав), а затем с помощью «примитива чтения» считываем нужный индекс IDT; он чаще всего указывает на адресное пространство ядра, поэтому мы можем утечь адрес обработчика прерывания в ядре, а затем найти смещение в соответствующем PE-файле модуля ядра.

Как только мы получим базовый адрес загрузки ядра, мы сможем вычислить адрес nt!HalDispatchTable.

Обычный метод — загрузить файл ntoskrnl.exe и интерпретировать смещения его символов, а затем прибавить их к утёкшему базовому адресу загрузки ядра. Однако это не работает для песочницы с повышенным режимом, поскольку существуют ограничения файловой системы: вы не можете прочитать C:\windows\system32\ntoskrnl.exe. Чтобы обойти это ограничение, мы используем наш «примитив утечки» для анализа образа PE ядра в памяти и определения нужных нам смещений символов.

Заключение

На этом всё. Спасибо за чтение. Используя методы, изложенные в этой статье, я смог реализовать стабильную эксплуатацию для 32- и 64-битных систем Windows XP, Vista, 7, 8, 8.1 и Server 2012. Для Windows 2003 и 2008 по умолчанию это не работает, поскольку нельзя перехватывать обратные вызовы пользовательского режима, поэтому эти системы нельзя атаковать, если только не выполнены наши требования. Процесс эксплуатации довольно сложен, есть много препятствий, которые нужно преодолеть, но это также доставило много удовольствия и дало ценные знания. Многие из используемых здесь жизнеспособных методов и результатов исследований уже упоминались в работах других исследователей. Насколько мне известно, единственная мера защиты, которая может предотвратить эксплуатацию win32k.sys, — это то, что использует песочница Google Chrome, которая эффективно блокирует системные вызовы ядра win32k во время выполнения. Я буду рад любым улучшениям и отзывам; если вы укажете на недостатки некоторых предложенных мной методов, сообщите мне, и я обновлю этот документ. Связаться со мной можно через Twitter @fidgetingbits или по электронной почте [email protected].

Скачать инструмент