Когда этот эксплойт впервые появился на рубеже апреля и мая, он вызвал у меня интерес, поскольку, несмотря на сильную обфускацию, структура кода казалась хорошо организованной, а код эксплуатации уязвимости достаточно мал, чтобы упростить анализ. Я скачал POC from github и решил, что это хороший кандидат, чтобы заглянуть изнутри. На тот момент уже были опубликованы два анализа: первый от 360 и второй от Касперского. Оба они помогли мне понять, как это работает, но их было недостаточно, чтобы глубоко понять каждый аспект эксплойта. Вот почему я решил проанализировать его самостоятельно и поделиться своими выводами.

Предварительная обработка

Сначала, чтобы удалить обфускацию целых чисел, я использовал подстановку регулярных выражений в скрипте Python:

Что касается запутанных имен, я постепенно переименовал их во время анализа. Этот анализ лучше всего читать с исходным кодом, ссылка на который находится в конце.

Использовать после бесплатного

Уязвимость возникает, когда объект завершается и вызывается пользовательская функция Class_Terminate(). В этой функции ссылка на освобождаемый объект сохраняется в UafArray. Отныне UafArray(i) относится к удаленному объекту.

Также обратите внимание на последнюю строку в Class_Terminate(). Когда мы копируем объект ClassTerminate в UafArray, его счетчик ссылок увеличивается. Чтобы сбалансировать его, мы снова его освобождаем, присвоив FreedObjectArray другое значение. Без этого память объекта не была бы освобождена, несмотря на вызов Class_Terminate, и следующий объект не был бы размещен на ее месте.

Создание и удаление новых объектов повторяется 7 раз в цикле, после чего создается новый объект класса ReuseClass. Он размещается в той же памяти, которая ранее была занята 7 ClassTerminate экземплярами. Чтобы лучше понять это, вот простой WinDbg скрипт, который отслеживает все эти выделения:

bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";

Вот журнал распределения из функции UafTrigger:

Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600

Мы сразу видим, что ReuseClass действительно выделяется в той же памяти, которая была назначена 7 предыдущим экземплярам ClassTerminate. Это повторяется дважды. В итоге мы получаем два объекта, на которые ссылается UafArrays. Ни одна из этих ссылок не отражается в счетчике ссылок объекта. В этом журнале мы также можем заметить, что даже после вызова Class_Terminate есть некоторые манипуляции с объектом, которые изменяют его счетчик ссылок. Вот почему, если бы мы не сбалансировали этот счетчик в Class_Terminate, мы получили бы что-то вроде этого:

Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740

Различные адреса размещения. Эксплойт не сможет создать условие использовать после освобождения.

Введите путаницу

Создав эти два объекта с 7 несчетными ссылками на каждый, мы установили примитив чтения произвольной памяти. Есть два похожих класса ReuseClass и FakeReuseClass. При замене первого класса вторым возникает путаница типов для mem члена.

В SetProp сохраняется функция ReuseClass.mem и вызывается Default Property Get класса ReplacingClass_*, результат этого вызова будет помещен в ReuseClass.mem.

Внутри этого геттера UafArray опустошается путем присвоения 0 каждому элементу. Это вызывает вызов VBScriptClass::Release для объекта ReuseClass, на который ссылается UafArray. Оказывается, на данном этапе выполнения объект ReuseClass имеет счетчик ссылок, равный 7, и, поскольку мы вызываем Release 7 раз, этот объект освобождается. И поскольку эти ссылки были получены в результате использования после освобождения, они не учитываются в счетчике ссылок. Вместо ReuseClass размещается новый объект FakeReuseClass. Теперь, чтобы получить его счетчик ссылок, равный 7, как в случае с ReuseClass, мы назначаем его 7 раз UafArray. Вот схема памяти до и после этой операции.

После этого функция получения вернет значение, которое будет присвоено старой переменной ReuseClass::mem. Как видно из дампов памяти, старое значение было помещено на 0xC байт перед новым. Объекты были специально созданы, чтобы вызвать эту ситуацию, например, путем выбора правильной длины для имен функций. Теперь значение, записанное в ReuseClass::mem, перезапишет заголовок FakeReuseClass::mem, вызывая ситуацию путаницы.

Последняя строка присвоила строку FakeArrayString objectImitatingArray.mem. Заголовок теперь имеет значение VT_BSTR

Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0

Это значение заменило тип objectImitatingArray.mem на VT_ARRAY | VT_VARIANT, и теперь указатель на строку будет интерпретироваться как указатель на структуру SAFEARRAY.

Произвольная память прочитана

В результате мы получаем два объекта FakeReuseClass. Один из них имеет массив элементов mem, адресующий все пространство пользователя (0x00000000 - 0x7fffffff), а другой - элемент типа VT_I4 (4-байтовое целое число) с указателем на пустую 16-байтовую строку. При использовании второго объекта происходит утечка указателя на строку:

some_memory=resueObjectB_int.mem

Позже он будет использоваться как адрес в памяти, доступный для записи. Следующим шагом будет утечка любого адреса внутри vbscript.dll. Здесь используется очень хитрый прием.

Сначала мы определяем, что в случае ошибки скрипт должен просто продолжать обычное выполнение. Затем делается попытка присвоить переменной EmptySub. Это невозможно в VBS, но все же значение помещается в стек до того, как будет сгенерирована ошибка. Следующая инструкция должна присвоить переменной значение null, что она и делает, просто изменяя тип последнего значения из стека на VT_NULL. Теперь emptySub_addr_placeholder содержит указатель на функцию, но с типом VT_NULL.

Затем это значение записывается в нашу доступную для записи память, его тип изменяется на VT_I4, и оно считывается как целое число. Если мы проверим содержимое этого значения, окажется, что это указатель на CScriptEntryPoint, а первый член vftable указывает внутрь vbscript.dll

Для чтения значения с произвольного адреса, в данном случае указателя, возвращенного из LeakVBAddr, используются следующие функции:

Чтение достигается путем записи адреса + 4 в доступную для записи память, затем тип изменяется на VT_BSTR. Теперь адрес + 4 рассматривается как указатель на BSTR. Если мы вызовем LenB по адресу + 4, он вернет значение, на которое указывает адрес. Почему? Из-за того, как определяется BSTR, значению unicode предшествует его длина, и эта длина возвращается LenB.

Теперь, когда произошла утечка адреса внутри vbscript.dll, и после установки произвольного чтения из памяти, необходимо правильно просмотреть PE-заголовок для получения всех необходимых адресов.

Подробности этого здесь не объясняются. В этой статье подробно описывается PE-файл.

Запуск выполнения кода

Окончательное выполнение кода осуществляется в два этапа. Сначала строится цепочка из двух вызовов, но это не ROP-цепочка. NtContinue имеет структуру CONTEXT, которая устанавливает для EIP адрес VirtualProtect, а для ESP - структуру, содержащую параметры VirtualProtect.

Первый адрес шелл-кода получается с использованием ранее описанной техники изменения типа переменной на VT_I4 и чтения указателя. Далее строится структура VirtualProtect, которая содержит все необходимые параметры, такие как адрес шеллкода, размер и защиту RWX. В нем также есть пространство, которое будет использоваться стековыми операциями внутри VirtualProtect. После этого строится структура CONTEXT, в которой EIP установлен на VirtualProtect, а ESP - на его параметры. Эта структура также имеет в качестве первого значения указатель на адрес NtContinue, повторяемый 4 раза. Последний шаг перед запуском этой цепочки - сохранить структуру как строку в памяти.

Затем эта функция используется для запуска цепочки. Сначала он изменяет тип сохраненной структуры на 0x4D, а затем устанавливает его значение на 0, что вызывает вызов VAR::Clear.

И динамический просмотр из отладчика

Хотя это может показаться сложной, эта цепочка выполнения очень проста. Всего два шага. Вызовите NtContinue со структурой CONTEXT, указывающей на VirtualProtect. Затем VirtualProtect отключит DEP на странице памяти, содержащей шелл-код, и после этого он вернется в шелл-код.

Вывод

CVE-2018–8174 - хороший пример объединения нескольких условий использования после освобождения и путаницы типов для достижения выполнения кода очень умным способом. Это отличный пример для изучения и понимания внутренней работы таких эксплойтов.

Полезные ссылки

Прокомментированный код эксплойта

Анализ первопричин Касперского

Анализ 360 °

Очередной анализ Касперского

Анализ CVE-2014–6332, проведенный Trend Micro