«У меня обычно сюжетные песни,
а эта песня без сюжета.
Казалось бы, набор слов.
Нет — это песня-беспокойство» В. Высоцкий о песне «Парус»
Отдавая себе полный отчёт в том, что слово «криптоанализ», столь бесцеремонно использованное в заголовке статьи, может повлечь за собой настоящую бурю праведной критики, я всё-таки не спешу с ним расставаться и даже не собираюсь лишать приставки «крипто». Разумеется, сама по себе WinAPI-функция UuidCreate, ставшая предметом данного исследования, никакого отношения к криптографии не имеет. Её скромный удел — создавать уникальные идентификаторы (Universally Unique Identifiers), соответствующие требованиям RFC4122. Однако желающих колоть орехи микроскопом это почему-то не останавливает. Напротив, я всё чаще сталкиваюсь с попытками трудоустроить UuidCreate в качестве генератора псевдослучайных чисел (ГПСЧ).
Причин у такого явления, на мой взгляд, несколько. Во-первых, значения идентификаторов кажутся случайными и независимыми (трижды крестимся). Во-вторых, алгоритм UUID-генератора до сих пор не опубликован и, насколько мне известно, не существует ни одной заслуживающей доверия работы, где были бы описаны его свойства. В-третьих, официальный и полномочный ГПСЧ Windows — функция CryptGenRandom — проигрывает UuidCreate в простоте и доступности (достаточно вспомнить, что использование CryptGenRandom предполагает свидание с CryptoAPI, и далеко не у каждого программиста это свидание окончится бурным романом).
Так, например, встретившись однажды с коллегой из телекоммуникационной компании за кружкой добротного чешского пива, я узнал довольно любопытный факт. Оказывается, PIN-коды для карт экспресс-оплаты услуг этой «дальновидной» коммерческой организации генерируются на MS SQL Server с помощью функции newid (под чьей маской скрывается всё та же UuidCreate), причём никаких дополнительных преобразований над полученными значениями не производится. Решив поначалу, что «афтар жжот», я выразил эмоции в лучших традициях лошади Пржевальского, но уже через секунду об этом пожалел. Недоумение на лице собеседника совершенно точно указывало на искренность его слов и, будь у меня под рукой карманная Библия, уверен — он бы на ней поклялся. Библии не было, поэтому пришлось клясться на свежем номере журнала «MAXIM».
Тогда, к сожалению, мне не удалось найти весомых аргументов в пользу недопустимости использования UuidCreate в подобных целях, и до последней капли пива каждый из нас оставался при своём мнении. Однако откровения коллеги долгое время не давали мне покоя. Чтобы окончательно расставить точки над i, я решил проверить, насколько осуществимы следующие два сценария.
Оператор международной и сотовой связи ОАО «Дайте сала кило», известный после ребрендинга под торговой маркой «Алло, прачечная?», готовится представить своим абонентам новый тарифный план «Глухонемой». Помимо беспрецедентно низкой стоимости одной минуты разговора с коренным населением Земли Франца-Иосифа, тариф даёт возможность бесплатно скачать рингтон «Не слышны в саду даже шорохи».
Компания с оптимизмом смотрит в будущее и рассчитывает увеличить своё присутствие на рынке услуг междугородной связи. Предвидя большой рост числа абонентов, руководство поручает секретному отделу «Белые воротнички», куда входят сотрудники только с безупречной репутацией, выпустить новую серию карт экспресс-оплаты.
На это благое дело отводится специальный физический сервер, подготовкой которого занимается системный администратор Василий Раздолбаев (характер нордический, не женат).
Далее опишем события в хронологическом порядке.
Утро, 14 апреля, понедельник. Василий устанавливает на машину ОС Windows и MS SQL Server 2008.
Вечер 14 апреля, понедельник. Комиссия из числа сотрудников секретного отдела проверяет сервер на наличие шпионского ПО и выносит заключение: зловредного кода на машине не обнаружено. Сервер, не выключая, перемещают в подземный бункер, полностью отрезанный от внешнего мира (свинцовые стены толщиной 2 метра, перехват информации полностью исключён, доступ в помещение имеют только «Белые воротнички»).
Вечер, 15 апреля, вторник. Василий отправляется на самый беспощадный в его жизни корпоратив, устроенный по случаю выхода нового тарифа. Исполняет в караоке похабные песни, танцует голым на столе и пристаёт к Маше Ведро из бухгалтерии. Другими словами, ведёт себя как настоящий джентльмен.
А в это время... Пока системный администратор расслабляется, «Белые воротнички» генерируют PIN-коды новой серии карт экспресс-оплаты. В качестве источника случайных чисел используется T-SQL-функция NEWID (что, как будет показано ниже, равносильно вызову UuidCreate). Полученные таким образом PIN-коды сразу же наносятся на карты оплаты. Когда процедура полностью завершена, сервер под неусыпным контролем вооружённой до зубов охраны отвозят на ближайший металлургический завод, где и сжигают в доменной печи.
Утро, 16 апреля, среда. Новые карты экспресс-оплаты поступают в продажу. Василий Раздолбаев находит себя в мусорном баке за пределами МКАД.
Внимание, вопрос: может ли Василий, не прибегая к ректальному криптоанализу сотрудников секретного отдела, восстановить PIN-коды всей серии карточек, и что ему для этого нужно предпринять?
Предположим, сервер не уничтожили. Более того, системный администратор сумел получить к нему доступ только в среду, когда карточки уже появились в продаже. Остаётся ли у Василия шанс заполучить PIN-коды, учитывая, что они были удалены из базы данных «Белыми воротничками»?
Чтобы более-менее внятно ответить на эти интригующие вопросы, нам предстоит заглянуть под капот UuidCreate.
К сожалению, имеющаяся в нашем распоряжении официальная документация (статья в MSDN [4] и текст RFC4122 [1]) может пролить свет только на структуру возвращаемого значения, так что пользы от неё не больше, чем от ранее упомянутого журнала «MAXIM».
Определение функции (rpcdce.h):
RPCRTAPI RPC_STATUS RPC_ENTRY UuidCreate (OUT UUID RPC_FAR * Uuid); |
Структура возвращаемого параметра Uuid (guiddef.h):
typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[ 8 ]; } GUID; typedef GUID UUID; |
UUID-идентификаторы часто записывают в виде текстовой строки:
{G1G2G3G4-G5G6-G7G8-G9G10-G11G12G13G14G15G16}
где Gx — значение соответствующего байта структуры в шестнадцатеричном представлении:
Data1 = G4G3G2G1
Data2 = G6G5
Data3 = G8G7
Data4 = G9G10G11G12G13G14G15G16
Согласно [1], первые два старших бита G9 определяют формат, в рамках которого необходимо интерпретировать значение идентификатора. В случае UuidCreate, они всегда равны 10, следовательно, формат должен полностью отвечать требованиям RFC4122, что даёт нам право трактовать четыре старших бита G7 как номер версии алгоритма. Наша подопытная выставляет в них последовательность 0100 (версия 4, «The randomly or pseudo-randomly generated version»), поэтому оставшиеся 122 бита структуры должны заполняться случайным образом. Так ли это на самом деле — сейчас узнаем.
Будем препарировать rpcrt4.dll (перечень исследованных версий см. в Приложении В), используя нечеловеческие возможности дизассемблера IDA Pro и отладчика Visual Studio 2005, поскольку именно в этой библиотеке сокрыто от любопытных глаз устройство UUID-генератора.
Начнём, разумеется, с функции UuidCreate.
RPCRTAPI RPC_STATUS RPC_ENTRY UuidCreate ( OUT UUID __RPC_FAR * Uuid ) { RPC_STATUS ret; ret = GenerateRandomNumber((BYTE*)Uuid, sizeof(UUID)); if (ret == RPC_S_OK) { // Устанавливаем версию Uuid->Data3 = (Uuid->Data3 & 0x0fff) | 0x4000; // Клянёмся в верности формату RFC4122 Uuid->Data4[0] = (Uuid->Data4[0] & 0x3F) | 0x80; }; return ret; }; |
Кроме вызова GenerateRandomNumber, тело функции не содержит для нас ничего нового. Режем дальше.
// Состояние экземпляра RC4 typedef struct _RC4_INSTANCE_STATE { BYTE SBOX[256]; // S-блок BYTE i; // Регистр RC4 BYTE j; // Регистр RC4 }RC4_INSTANCE_STATE, *PRC4_INSTANCE_STATE; // Экземпляр RC4 typedef struct _RC4_INSTANCE { // Кол-во байт выхода экземпляра DWORD Accumulator; // Критическая секция для синхронизации потоков CRITICAL_SECTION CS; // Состояние экземпляра RC4 RC4_INSTANCE_STATE State; } RC4_INSTANCE, *pRC4_INSTANCE; // Состояние генератора typedef struct _RC4_CONTEXT { DWORD InstanceCount; // Общее число экземпляров RC4 DWORD AlwaysOne; // Зарезервировано. Всегда равно 1 pRC4_INSTANCE Rc4Ins[8]; // Экземпляры RC4 }RC4_CONTEXT, *pRC4_CONTEXT; // Глобальная переменная, определяющая состояние UUID-генератора RC4_CONTEXT g_rc4SafeCtx; // Глобальная переменная, определяющая выбор экземпляра RC4 DWORD g_rc4TotalRequests = 0; RPC_STATUS GenerateRandomNumber(BYTE* pbData, DWORD cbData) { // Суммарное кол-во байт выхода текущего экземпляра RC4 // с момента его инициализации DWORD Accumulator = 0; // Номер текущего экземпляра RC4 DWORD InstNum; // Буфер для ключа RC4, если потребуется инициализация PVOID RandomKey; rc4_safe_select(&g_rc4SafeCtx, &InstNum, &Accumulator); if (Accumulator >= 500000) { //Инициализация //Генерация псевдо-случайного ключа для RC4 if (!SystemFunction036(RandomKey, 256)) return RPC_S_OUT_OF_MEMORY; // Инициализация экземпляра RC4 rc4_safe_key(&g_rc4SafeCtx, InstNum, 256, (BYTE*)RandomKey); }; // Генерация значения UUID с помощью алгоритма RC4 rc4_safe(&g_rc4SafeCtx, InstNum, cbData, pbData); return RPC_S_OK; }; // Выбор экземпляра RC4 void rc4_safe_select( pRC4_CONTEXT pRC4Ctx, DWORD* pInstNum, DWORD* pAccumulator) { g_rc4TotalRequests++; *pInstNum = g_rc4TotalRequests & (pRC4Ctx->InstanceCount - 1); *pAccumulator = pRC4Ctx->Rc4Ins[*pInstNum]->Accumulator; }; // Потокобезопасная обёртка для вызова RC4 void rc4_safe(pRC4_CONTEXT pRC4Ctx, DWORD InstNum, DWORD cbData, BYTE* pbData) { EnterCriticalSection(&pRC4Ctx->Rc4Ins[InstNum]->CS); rc4(&pRC4Ctx->Rc4Ins[InstNum]->State, cbData, pbData); LeaveCriticalSection(&pRC4Ctx->Rc4Ins[InstNum]->CS); }; // Классическая реализация алгоритма RC4 void rc4(PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE* pbData) { BYTE t = 0; for (int p = 0; p < cbData; p++) { pRC4State->i = (pRC4State->i + 1) & 0x0FF; pRC4State->j = (pRC4State->j + pRC4State->SBOX[pRC4State->i]) & 0x0FF; t = pRC4State->SBOX[pRC4State->i]; pRC4State->SBOX[pRC4State->i] = pRC4State->SBOX[pRC4State->j]; pRC4State->SBOX[pRC4State->j] = t; *(pbData + p) = *(pbData + p) ^ pRC4State->SBOX[(pRC4State->SBOX[pRC4State->i] + pRC4State->SBOX[pRC4State->j]) & 0xFF]; }; }; |
Очевидно, что в алгоритме UUID-генератора отсутствуют какие-либо необратимые преобразования, а в его основу положен потоковый шифр RC4 [6]. Глобальная структура g_rc4SafeCtx, содержащая 8 независимых экземпляров состояний потокового шифра (далее просто экземпляры), и глобальный счётчик g_rc4TotalRequests, на основании которого происходит переключение между ними, полностью описывают состояние всего генератора. Каждый экземпляр имеет свой собственный S-блок и хранит значения двух регистров шифра. При вызове GenerateRandomNumber выбирается следующий по порядку экземпляр RC4 (с индексом (g_rc4TotalRequests + 1) mod 8), что должно обеспечивать равномерную утилизацию S-блоков.
ПРИМЕЧАНИЕ Поскольку rpcrt4.pdb не очень-то разговорчив, особенно когда дело касается глобальных переменных, я присвоил счётчику g_rc4TotalRequests имя, более-менее соответствующее его прямому назначению. |
Формально алгоритм генерации можно представить в виде схемы, изображённой на рисунке 1.
Рисунок 1. Схема генерации UUID в ОС Windows.
Когда требуется создать i-е по счёту UUID-значение (Ui), генератор выполняет следующие действия:
В момент загрузки rpcrt4.dll происходит вызов функции rc4_safe_startup. Она выделяет из кучи целевого процесса необходимый объём памяти под структуру g_rc4SafeCtx, устанавливает значение всех аккумуляторов равным 0xFFFFFFFF и инициализирует критические секции экземпляров RC4. Таким образом, каждый процесс будет обладать своим собственным состоянием Uuid-генератора.
Инициализация S-блока экземпляра выполняется при первом обращении к нему или в случае, когда счётчик байт выхода (Accumulator) у данного экземпляра начинает превышать 500 000 байт. Следовательно, прежде чем весь генератор подвергнется повторной инициализации, с его помощью должно быть создано не менее 250 000 Uuid-значений (8 экземпляров * 500 000 байт = 16 байт * 250 000 Uuid-значений). Немаловажно и то, что состояние генератора можно считать полностью определённым лишь после того, как процесс 8 и более раз вызовет UuidCreate.
RPC_STATUS GenerateRandomNumber(BYTE* pbData, DWORD cbData) { // Суммарное кол-во байт выхода текущего экземпляра RC4 // с момента его инициализации DWORD Accumulator = 0; // Номер текущего экземпляра RC4 DWORD InstNum; // Буфер для ключа RC4, если потребуется инициализация PVOID RandomKey; rc4_safe_select(&g_rc4SafeCtx, &InstNum, &Accumulator); if (Accumulator >= 500000) { //Инициализация //Генерация псевдослучайного ключа для RC4 if (!SystemFunction036(RandomKey, 256)) return RPC_S_OUT_OF_MEMORY; // Инициализация экземпляра RC4 rc4_safe_key(&g_rc4SafeCtx, InstNum, 256, (BYTE*)RandomKey); }; // Полный код этой функции был приведён в предыдущем разделе статьи, так // что здесь мы ограничимся лишь участком, относящимся к инициализации ... }; |
Загадочная функция SystemFunction036 из advapi32.dll имеет ещё одно, более вразумительное название — RtlGenRandom («Георгий Иванович, он же Гога, он же Гоша, он же Юрий, он же Жора, здесь проживает?»). Именно под этой вывеской её можно отыскать в анналах MSDN[7]:
BOOLEAN RtlGenRandom( __out PVOID RandomBuffer, __in ULONG RandomBufferLength ); |
Функция способна напрямую обращаться к ГПСЧ Windows для получения псевдослучайной последовательности байт и, в отличие от CryptGenRandom, не требует указания криптопровайдера. То, что SystemFunction036 является кратчайшим путём к генератору, легко проследить с помощью отладчика (на листинге показан стек вызовов из CryptGenRandom):
> advapi32.dll!_SystemFunction036@8() rsaenh.dll!_FIPS186Gen@28() + 0x5f bytes rsaenh.dll!_CPGenRandom@12() + 0x33 bytes advapi32.dll!_CryptGenRandom@12() + 0x3d bytes |
Руководствуясь народной мудростью «чем дальше в лес, тем толще партизаны», мы не станем (по крайней мере, сейчас) углубляться в дебри устройства ГПСЧ Windows. Во-первых, описывать здесь довольно запутанный алгоритм CryptGenRandom не имеет большого смысла, поскольку предложенная модель атаки никак не связана с генератором псевдослучайных чисел ОС. Во-вторых, исследователи из The Hebrew University of Jerusalem в своей работе «Cryptanalysis of the Random Number Generator of the Windows Operating System»[8] справились с этой задачей куда лучше, разобрав структуру ГПСЧ, что называется, по полочкам. Единственное, на что стоило бы обратить внимание, так это на поразительное сходство обоих генераторов — и UuidCreate и CryptGenRandom поддерживают 8 экземпляров RC4, которые используются ими в round-robin-манере.
Другая функция, rc4_key, непосредственно участвующая в инициализации S-блока экземпляра, вызывается в критической секции rc4_safe_key и представляет собой классическую реализацию RC4 Key Scheduling Algorithm (KSA) [6]. Код, отвечающий за тайлинг ключа (если длина ключа составляет менее 256 байт, ключ, согласно алгоритму KSA, повторяют до тех пор, пока не будет заполнен весь 256-байтный ключевой массив), был опущен в целях экономии места (длина ключа, передаваемого в rc4_key из GenerateRandomNumber всегда равна 256, поэтому отброшенный код никакой роли не играет).
// Потокобезопасная обёртка для вызова KSA void rc4_safe_key( pRC4_CONTEXT pRC4Ctx, DWORD InstNum, DWORD cbData, BYTE* pbData) { EnterCriticalSection(&pRC4Ctx->Rc4Ins[InstNum]->CS); //RC4 Key Scheduling Algorithm (KSA) rc4_key(&pRC4Ctx->Rc4Ins[InstNum]->State, cbData, pbData); LeaveCriticalSection(&pRC4Ctx->Rc4Ins[InstNum]->CS); }; // RC4 Key Scheduling Algorithm (KSA) void rc4_key(PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE* pbData) { BYTE t = 0; for (pRC4State->i = 0; pRC4State->i < 256; pRC4State->i++) pRC4State->SBOX[pRC4State->i] = pRC4State->i; pRC4State->j = 0; for (pRC4State->i = 0; pRC4State->i < 256; pRC4State->i++) { pRC4State->j = ( pRC4State->j + pRC4State->SBOX[pRC4State->i] + (*(pbData + pRC4State->i)) ) & 0x0FF; }; t = pRC4State->SBOX[pRC4State->i]; pRC4State->SBOX[pRC4State->i] = pRC4State->SBOX[pRC4State->j]; pRC4State->SBOX[pRC4State->j] = t; }; |
Таким образом, мы приходим к следующей схеме инициализации экземпляров RC4:
Как бы мне хотелось посмотреть в глаза индусу, который использовал словечко safe в названии функции rc4_safe_select и нисколько не позаботился о синхронизации потоков при обращении к глобальной переменной g_rc4TotalRequests в её теле. То ли склероз молодца одолел, то ли дело клонилось к релизу — вряд ли мы теперь узнаем. Зато можем полюбоваться эффектами.
На рисунке 2 показана ситуация, когда два конкурирующих потока обращаются к rc4_safe_select и при определённом стечении обстоятельств получают одинаковые значения InstNum и Accumulator.
Рисунок 2. Отсутствие синхронизации в теле rc4_safe_select приводит к непредсказуемому результату.
Последствия очевидны:
Описанная ошибка не настолько критична, чтобы удобрять лысину пеплом или окапываться в ближайшей пещере в ожидании апокалипсиса. Однако избавиться от неё вовсе не будет лишним, тем более что это можно сделать лёгким росчерком пера:
// Выбор экземпляра RC4 void rc4_safe_select( pRC4_CONTEXT pRC4Ctx, DWORD* pInstNum, DWORD* pAccumulator) { DWORD l_RC4TotalRequests; // Взаимоблокировка при инкрементировании l_RC4TotalRequests = InterlockedIncrement (&g_rc4TotalRequests); *pInstNum = l_RC4TotalRequests & (pRC4Ctx->InstanceCount - 1); *pAccumulator = pRC4Ctx->Rc4Ins[*pInstNum]->Accumulator; }; |
или то же самое, но для ценителей искусства:
// Выбор экземпляра RC4 void rc4_safe_select( pRC4_CONTEXT pRC4Ctx, DWORD* pInstNum, DWORD* pAccumulator) { DWORD l_RC4TotalRequests; __asm { mov eax, 1 lock xadd g_rc4TotalRequests, eax inc eax mov l_RC4TotalRequests, eax }; *pInstNum = l_RC4TotalRequests & (pRC4Ctx->InstanceCount - 1); *pAccumulator = pRC4Ctx->Rc4Ins[*pInstNum]->Accumulator; }; |
Подробнее с функциями взаимоблокировки вы можете ознакомиться в MSDN [9].
Детерминизм, присущий программной (а в некоторых случаях и аппаратной) реализации любого алгоритма, является настоящей ахиллесовой пятой криптографических систем. И как не прискорбно, именно ошибки реализации дискредитируют защищённую среду куда чаще, нежели уязвимости, обнаруженные в математических моделях. Дело осложняется ещё и тем, что разработчики порой сознательно отказываются использовать стойкие механизмы защиты, объясняя это, например, невозможностью обеспечить максимальное быстродействие в сочетании с приемлемым уровнем безопасности. Причём пунктом об ущемлённой производительности «договор с совестью», как правило, не ограничивается. С не меньшим успехом в ход идут и другие, мешающие танцору, «весомые аргументы». Вспомним хотя бы нашу с Яном Либерманом удалённую атаку на MS SQL Server [10], провести которую нам бы не удалось, не будь безопасность в протоколе TDS принесена в жертву «фиче» обратной совместимости с менее защищенными клиентами. Так что следы компромиссов можно обнаружить практически в любом прикладном решении и результаты исследования UuidCreate лишний раз это подтверждают.
Надёжный в криптографическом смысле генератор псевдослучайных чисел не только должен выдавать последовательность, на первый взгляд кажущуюся случайной, но и обладать двумя важными свойствами — Backward и Forward Security.
Backward security — не позволяет злоумышленнику по текущему состоянию генератора предсказывать состояния, в которых тот будет находиться в будущем. Чтобы уменьшить пагубное влияние детерминизма и хоть как-то выдержать это требование, в алгоритме обычно предусматривают регулярное обновление состояния (rekey) истинно случайной последовательностью бит. И чем чаще оно выполняется, тем более стойким оказывается генератор. Источниками энтропии могут служить показания счётчиков производительности, измерения тепловых датчиков, значения регистров процессора в определённый момент времени и другие величины, остающиеся для злоумышленника тайной за семью печатями (любознательных отсылаю к книге Майкла Ховарда и Дэвида Лебланка «Защищённый код» [11], где авторы, не пожалев бумаги, через запятую перечислили более сотни источников для CryptGenRandom).
Сейчас мы уже знаем, из какого теста сделан Uuid-генератор и с уверенностью можем сказать — испытания на backward security он не выдерживает. Полное обновление состояния у него возникает спустя 4·106 байт выхода (8 экземпляров RC4 * 500 000 байт), поэтому злоумышленник, сумевший заполучить «слепок» всех регистров и S-блоков UuidCreate в определённый момент времени, способен предугадать до 250 000 будущих Uuid-значений. Более того, источником энтропии генератора выступает ГПСЧ Windows в лице SystemFunction036, что, с учётом предсказуемости последнего [8], увеличивает длину скомпрометированного потока данных до запредельных 256·106 байт!
Действительно, CryptGenRandom, как и UuidCreate, состоит из 8 экземпляров RC4. Каждый из них проходит процедуру повторной инициализации, когда счётчик байт выхода начинает превышать 16 Кб. Это почти в 30 раз меньше, чем у Uuid-генератора, но недостаточно, чтобы считать его надёжным. Экземпляры RC4 в CryptGenRandom используются равномерно, следовательно, полное обновление состояния ГПСЧ выполняется только после 128 Кб выхода (8 экземпляров RC4 * 16 Kб).
На одну повторную инициализацию экземпляра в Uuid-генераторе функция rc4_safe_key потребляет 256 байт выхода ГПСЧ Windows, тогда как на полное обновление затрачивается 2 Кб (8 экземпляров * 256 байт ключа). Путём нехитрых арифметических операций легко подсчитать, сколько Uuid-значений сможет предсказать злоумышленник, если ему удастся заполучить текущее состояние CryptGenRandom:
4·106 * (128 Кб / 2 Кб) = 256·106 байт;
256·106 байт / 16 байт в одном Uuid-значении = 16·106 Uuid-значений
Рисунок 3. Иерархия источников энтропии.
Вы спросите, зачем потребовалось вместо источников энтропии ОС использовать суррогат, получаемый из ГПСЧ Windows? Начну издалека. Во-первых, никто в здравом уме на стойкость UuidCreate больших надежд не возлагал (а те, кто возлагал, просто обязаны дочитать эту статью до конца). Сами демиурги вполне внятно обозначили свою позицию в RFC4122[1]: «Do not assume that UUIDs are hard to guess; they should not be used as security capabilities (identifiers whose mere possession grants access), for example. A predictable random number source will exacerbate the situation». Так что перед разработчиками никогда не стояло цели создать безопасный Uuid-генератор. К сожалению, планету Земля все ещё топчут гоблины (не только топчут, но и карты экспресс-оплаты штампуют!), которые к подобным предостережениям относятся без должного трепета.
Во-вторых, за лишнее обращение к ключу системного реестра, где хранится 80-байтовый Seed, или вызов функции драйвера KSecDD придётся расплачиваться дорогостоящими тактами процессорного времени (.NET-чикам не понять ;). Это особенно будет заметно в последнем случае, поскольку требует переключения в режим ядра (вот вам и компромисс между производительностью и безопасностью). В-третьих, создать псевдослучайный ключ для RC4 с помощью SystemFunction036 гораздо проще, нежели усложнять себе жизнь чересчур запутанной конструкцией генератора.
Перейдём теперь ко второму свойству надёжных ГПСЧ — Forward Security. Смысл его в том, что злоумышленник, владеющий текущим состоянием генератора, не должен извлечь из него никакой информации о предшествующем выходе. Другими словами, ГПСЧ нужно проектировать как однонаправленную функцию, пользуясь для этих целей алгоритмами хеширования.
Но и здесь у UuidCreate незачет. Единственные преобразования, которые мы видим в коде, ограничиваются перестановками внутри S-блоков. Злоумышленник без труда может выполнить эти операции в обратном порядке и получить предшествующую гамму генератора:
void rc4_revert(PRC4_INSTANCE_STATE pRC4State, DWORD cbData, BYTE* pbData) { BYTE t = 0; for (int p = cbData - 1; p >= 0; p--) { *(pbData + p) = *(pbData + p) ^ pRC4State->SBOX[(pRC4State->SBOX[pRC4State->i] + pRC4State->SBOX[pRC4State->j]) & 0xFF]; t = pRC4State->SBOX[pRC4State->i]; pRC4State->SBOX[pRC4State->i] = pRC4State->SBOX[pRC4State->j]; pRC4State->SBOX[pRC4State->j] = t; pRC4State->j = (pRC4State->j - pRC4State->SBOX[pRC4State->i]) & 0x0FF; pRC4State->i = (pRC4State->i - 1) & 0x0FF; }; }; |
Итак, предположим, злоумышленник снял «слепок» памяти со структуры g_rc4SafeCtx и запомнил счётчик обращений g_rc4TotalRequests в момент времени t такой, что T1 ? t ? T2, где T1 — время последнего полного обновления генератора, а T2 — время предстоящего обновления. Тогда он может воспроизвести выход генератора в интервале [T1; t) и предугадывать значения, возвращаемые функцией UuidCreate, вплоть до наступления T2.
Существует, правда, один нюанс, который обязательно следует учитывать. Значение буфера Ii, передаваемого в UuidCreate при i-м вызове, не всегда известно злоумышленнику. А согласно схеме, изображённой на рисунке 1, между этим значением и гаммой RC4 выполняется операция XOR. Поэтому, на первый взгляд, кажется, что злоумышленник, не знающий Ii, вряд ли сумеет воспользоваться предложенной моделью атаки. Однако на практике переменную Uuid либо инициализируют нулями, либо она содержит «мусор», находящийся в стеке к моменту выделения памяти. В последнем случае вероятность возникновения корреляций между различными значениями Ii в интервале от T1 до T2 остаётся довольно высокой, чем не преминет воспользоваться атакующий (я продемонстрирую это чуть ниже на примере MS SQL Server).
Теперь, когда вы познакомились с анатомией UuidCreate и освоили теоретическую часть статьи, мне бы хотелось сказать несколько слов о консольной утилите Uuid Big Brother, которую я написал, чтобы автоматизировать процедуру атаки. Утилита умеет снимать дамп состояния Uuid-генератора в любом из процессов ОС и угадывать как предыдущие, так и последующие Uuid-значения (подробное описание параметров UBB см. в Приложении Б).
В качестве разминки предлагаю отточить наше мастерство оракула на простом приложении guidgen.exe, с незапамятных времён входящее в поставку Visual Studio [12]. Эта утилита примечательна тем, что перед вызовом UuidCreate буфер под Uuid-значение инициализируется нулями (то есть Ii == NULL). Вы сами можете в этом убедиться, поскольку исходный код guidgen доступен для скачивания всем желающим [13]:
// Guidgdlg.h void CGuidGenDlg::OnNewguid() { // create random GUID using UuidCreate so that we // can check for more error codes m_guid = GUID_NULL; HRESULT hr = ::UuidCreate(&m_guid); // ... } |
Определённо это шанс поупражняться в прозорливости.
ubb -d3912 "c:\ubb\guidgen.dmp"
|
Результат выполнения команды будет примерно следующим:
Warning: not all of the RC4 instances were initialized. Please, initiate 8 UUIDs generation. State was successfully dumped to file c:\ubb\guidgen.dmp. |
UBB намекает нам, что процесс не успел инициализировать один или несколько экземпляров RC4, поэтому часть предсказанных Uuid-значений окажутся не соответствующими действительности.
ubb -d3912 "c:\ubb\guidgen.dmp"
--------------------------------
State was successfully dumped to file c:\ubb\guidgen.dmp.
|
ubb -g "c:\ubb\guidgen.dmp" -------------------------------- EAC00EFD-68DB-449F-9350-8954C2300DA2 D2DD36ED-6620-4CC9-80BE-9569EFD00FA5 D60CF120-A6DA-4B4B-84F1-2C1BD6EDC071 1F00F7F3-8F0F-4473-80F2-D3B175CC93DB ADA0FB01-85C0-4D67-86D1-925C68373F22 82D781D6-489A-46D6-9F9F-47265F76DB38 F10E5C71-5561-4999-9AF7-16CB1D80B0A0 050E5489-AAB3-4837-A240-92B4B165D588 |
Нажатие любой клавиши в UBB приводит к появлению новых значений. По умолчанию утилита выдаёт их порциями по 8 элементов, но вы можете изменить это число, воспользовавшись ключом –o (подробнее см. в Приложении Б).
Пришло время вспомнить о Василии Раздолбаеве и разобраться, каким образом ему удалось обвести ребят из отдела «Белые воротнички» вокруг пальца. Но сначала убедимся, что T-SQL-функция NEWID() действительно использует UUID-генератор Windows, и что описанная здесь техника атаки применима к сценариям с картами экспресс-оплаты. На листинге показан восстановленный код для MS SQL Server 2008 CTP5 (build 1075):
; sqlservr.exe ; void __fastcall GuidNewid(class CEsExec *, class CXVariant *) ?GuidNewid@@YIXPAVCEsExec@@PAVCXVariant@@@Z proc near mov edi, edi push esi mov esi, edx lea eax, [esi+4] push eax ; pguid call __imp__CoCreateGuid@4 ; CoCreateGuid(x) test eax, eax jnz short loc_1A07597 mov [esi], al loc_1A07597: pop esi retn ?GuidNewid@@YIXPAVCEsExec@@PAVCXVariant@@@Z endp ; ole32.dll ; long __stdcall wCoCreateGuid(struct _GUID *) ?wCoCreateGuid@@YGJPAU_GUID@@@Z: mov edi, edi push ebp mov ebp, esp push esi push [ebp+lp] ; Uuid call ds:__imp__UuidCreate@4 ; UuidCreate(x) mov esi, eax cmp esi, 720h jz short loc_776A5659 test esi, esi jnz loc_776EDF0E loc_776A5659: xor eax, eax loc_776A565B: pop esi pop ebp retn 4 _CoCreateGuid@4 endp |
Следующим шагом необходимо выяснить, чем инициализируется буфер Ii до выполнения UuidCreate. Для этого нам потребуется SQL Internals Profiler — инструмент, разработанный Яном Либерманом и незаменимый в тех случаях, когда нужно собрать статистику по вызовам внутренних функций MS SQL Server. Сейчас, в частности, нас будут интересовать значения, передаваемые по указателю pguid в CoCreateGuid.
-- Включаем перехват функции CoCreateGuid методом сплайсинга
exec xp_SQLInternalsProfiler_AddSource_Splicing 'ole32.dll',
'CoCreateGuid', 'hpg1;-', 1
|
В переводе с либерманского, фраза 'hpg1;-' звучит примерно так: «Первый параметр (1) функции является ссылкой (p) на архиважное значение длиной 16 байт (h), которое нужно перехватить ещё до вызова CoCreateGuid (;-). Сгруппировать результаты по уникальным значениям (g)». Желающим освоить этот язык могу порекомендовать отличный русско-либерманский разговорник [14,15].
Посмотрим, как ведёт себя NEWID, если обращаться к ней периодически:
-- Сбрасываем накопленную статистику exec xp_SQLInternalsProfiler_ClearAll print NEWID() waitfor delay '00:00:05' print NEWID() waitfor delay '00:00:05' print NEWID() waitfor delay '00:00:05' print NEWID() waitfor delay '00:00:05' print NEWID() -- Выводим результат EXECUTE xp_SQLInternalsProfiler_Results 147 |
У меня получилась следующая картина (SQL Server 2008 CTP5):
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid | Before_call | 0xF9E30A013061AE0400000000C061AE04 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A013863AE0400000000C863AE04 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A014065AE0400000000D065AE04 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A014867AE0400000000D867AE04 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A015069AE0400000000E069AE04 | 1 |
Мы видим, что Ii (поле binary_grpup_id) изменялось каждый раз, когда вызывалась функция NEWID, хотя разница в значениях и не столь велика — всего лишь 25% (4 байта из 16, выделены жирным). Сервер не утруждает себя инициализацией *pguid, поэтому в CoCreateGuid постоянно передаётся «мусор». Так, например, двойное слово 0xF9E30A01 представляет собой адрес возврата из CreateExecValSeg:
010AE3F4 call CEsCompValSeg::CreateExecValSeg 010AE3F9 pop edi ... |
Или вот ещё забавная арифметика (вычитаем второе двойное слово из четвёртого):
0x04AE61C0 – 0x04AE6130 = 0x90 0x04AE63C8 – 0x04AE6338 = 0x90 0x04AE65D0 – 0x04AE6540 = 0x90 ... |
Обнаруженная корреляция снижает уровень энтропии Ii вдвое (с 232 до 216), поскольку число независимых неизвестных сократилось. Не стоит сбрасывать со счетов и закономерность «по вертикали» (вычитаем второе двойное слово на i+1-итерации из него же на i-итерации):
0x04AE6338 - 0x04AE6130 = 0x208 0x04AE6540 - 0x04AE6338 = 0x208 0x04AE6748 - 0x04AE6540 = 0x208 ... |
Значит, если мы узнаем Ii, то легко сможем вычислить Ii+1. Причём у любого Ii непредсказуемыми остаются всего 2 байта.
Перезапустим MS SQL Server и повторим эксперимент:
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid | before_call | 0xF9E30A0130E1840600000000C0E18406 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A0138E3840600000000C8E38406 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A0140E5840600000000D0E58406 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A0148E7840600000000D8E78406 | 1 |
ole32.dll CoCreateGuid | Before_call | 0xF9E30A0150E9840600000000E0E98406 | 1 |
Новые результаты свидетельствуют о том, что неизменными остаются 8 байт, а корреляции сохраняются и после перезапуска.
Разумеется, всецело полагаться на эти наблюдения было бы чересчур опрометчиво. Давайте попытаемся смоделировать ситуацию, когда Uuid-идентификаторы не только создаются, но и вставляются в поле таблицы:
create table #pin_codes ( id int identity , pin_code varchar(36) ) go -- Сбрасываем накопленную статистику exec xp_SQLInternalsProfiler_ClearAll declare @i int = 0 while (@i != 10000) begin insert #pin_codes values(newid()) set @i += 1 end -- Выводим результат EXECUTE xp_SQLInternalsProfiler_Results 147 |
Оказывается, любое использование NEWID в циклических операциях (не обязательно при вставке в таблицу) демонстрирует завидное постоянство значений Ii. За 10 000 итераций оно изменилось лишь единожды!
source_name | event_id | binary_grpup_id | count |
---|---|---|---|
ole32.dll CoCreateGuid | Before_call | 0x0000000000000000B4F5C36200000000 | 9999 |
ole32.dll CoCreateGuid | Before_call | 0x00000000FFFFFFFF00000000FA700201 | 1 |
Очень маловероятно, что PIN-коды к картам экспресс-оплаты генерировались «поштучно», как в первом эксперименте. Поэтому асоциальный элемент Раздолбаев по достоинству оценит этот подарок судьбы. Впрочем, совокупная энтропия множества {I1, I2, ... In} в обоих случаях будет одинаковой — 232. Можно было бы упомянуть и о других способах вызова NEWID, но практически все они (есть исключения, врать не стану) сводятся либо к статистике, собранной в эксперименте с wait for, либо к результатам упражнения с циклом. Аналогичные тесты для MS SQL Server 2000/2005 обнаруживают примерно те же корреляции.
При одиночных обращениях к NEWID мы наблюдаем «вертикальную» закономерность, позволяющую нам по Ii рассчитать {Ii+1, Ii+2, ... Ii+n}. А если функция использовалась в цикле, то Ii = Ii+1 = Ii+1 = ... = Ii+n. Следовательно, чтобы восстановить всю последовательность, злоумышленнику необходимо вычислить хотя бы одно значение Ii. И здесь ему на помощь приходит свойство обратимости операции XOR. В алгоритме генератора присутствует этап, где между гаммой RC4 Oi и значением Ii выполняется исключающее ИЛИ:
Ci = Oi XOR Ii => Ii = Oi XOR Ci
Ui[1..6] = Ci[1..6]
Ui[7] = (Ci [7] & 0x0F) | 0x40
Ui[8] = Ci[8]
Ui[9] = (Ci [9] & 0x3F) | 0x80
Ui[10..16] = Ci[10..16]
Введём I’i = Oi XOR Ui, где I’i отличается от Ii несколькими битами в 7 и 9 байтах.
Покажем, что замена Ii на I’i в прямых вычислениях даёт то же самое значение Ui:
C’i = Oi XOR I’i
C’i[1..6] = Ci[1..6] = Ui[1..6]; C’i[8] = Ci[8] = Ui[8]; C’i[10..16] = Ci[10..16] = Ui[10..16];
C’i[7] ? Ci[7]; C’i[9] ? Ci[9], но
U’i[7] = (C’i[7] & 0x0F) | 0x40 = (Ci[7] & 0x0F) | 0x40 = Ui[7]
U’i[9] = (C’i[9] & 0x3F) | 0x80 = (Ci[9] & 0x3F) | 0x80 = Ui[9]
Значит, U’i = Ui и замена I’i на Ii является правомочной.
Предположим, Раздолбаев купил в ближайшем ларьке одну карту оплаты и под защитным слоем нашёл PIN-код Uk. Наши выкладки помогут ему рассчитать Ik, только если он знает, на какой итерации Uuid-генератора был получен данный PIN-код. Другими словами, Раздолбаев должен знать k. Эту проблему можно решить, прибегнув к перебору значений Oi, выдаваемых генератором в интервале между двумя полными обновлениями состояния [T1;T2]. И теперь уже Василию придётся раскошелиться не на одну, а на целых три карты оплаты (PIN-коды Px, Py и Pz).
BOOL InOrder = FALSE; Для i от 1 до 250 000 { Для j от 1 до 250 000 { Если (Oi XOR Px) == (Oj XOR Py) { // Возможно x = i, y = j Для k от 1 до 250 000 { Если (Oi XOR Px) == (Ok XOR Pz) { // x = i, y = j, z = k // Ii = Ij = Ik = (Oi XOR Px) InOrder = TRUE; } } Если (!InOrder) { // x = j, y = i, z = k // Ii = Ij = Ik = (Oi XOR Py) } } } } |
Метод основывается на допущении, что с каждым последующим вызовом UuidCreate значение Ii не изменяется. Однако эту же идею легко адаптировать на случай, когда у Ii остаются постоянными всего несколько байт. Например, для одиночных обращений к NEWID достаточно трактовать знак '=' как равенство 8 любых байт значения из 16. Ограничившись лишь частью Ii, мы по большому счёту ничем не рискуем, поскольку вероятность существования Oi и Oj таких, что (Oi XOR Px)=[8](Oj XOR Px) при i ? j ничтожно мала.
Величины, удовлетворяющие условию (Oi XOR Px) = (Oj XOR Py), будут также удовлетворять (Oi XOR Py) = (Oj XOR Px), причём (Oi XOR Px) ? (Oi XOR Py). Чтобы определить, какой из двух вариантов является верным, необходимо совершить ещё одну утреннюю пробежку по множеству {Oi}:
Поставим следственный эксперимент, где выступим в роли Васи Раздолбаева.
ubb –d1508 "c:\ubb\sqlservr.dmp" |
Эти два шага Раздолбаев должен был проделать 14 апреля, накануне процедуры генерации PIN-кодов.
create table #pin_codes ( id int identity, pin_code varchar(36) ) go set nocount on declare @i int set @i = 0 while (@i != 10000) begin insert #pin_codes values(newid()) set @i = @i + 1 end |
Каждое значение должно располагаться на новой строке.
ubb -g -ik"known_uuid.txt" -ab8 -of"pins.txt" "sqlservr.dmp" ---------------------------------- Analysis started. Press <Ctrl>+<Break> to abort. Analyzing...| Known UUID #1 index: 1594 Known UUID #1: 9EF15183-EFB9-4FCC-B9A5-5321E7A15385 Gen output #1: 9EF15183-EFB9-4FCC-8D50-0742E7A15385 Initial val #1: 00000000-0000-0000-34F5-546300000000 Known UUID #2 index: 73 Known UUID #2: 7D7EE0E8-3BE3-409D-B56B-BF6118F48C33 Gen output #2: 7D7EE0E8-3BE3-409D-819E-EB0218F48C33 Initial val #2: 00000000-0000-0000-34F5-546300000000 Known UUID #3 index: 6101 Known UUID #3: 7F7DF678-FD5A-418F-BBC2-289031D79174 Gen output #3: 7F7DF678-FD5A-418F-8F37-7CF331D79174 Initial val #3: 00000000-0000-0000-34F5-546300000000 Common initial: 00000000-0000-0000-34F5-546300000000 Uuids was successfully saved to file pins.txt. |
Known UUID #n: строка n в файле known_uuid.txt.
Known UUID #n index: порядковый номер i вызова UuidCreate с того момента, как был снят «слепок» состояния генератора.
Gen output #n: Oi, соответствующее данному Pi.
Initial val #n: Ii, соответствующее данному Pi.
Проверьте, насколько точны предсказания. Выберите из таблицы первое понравившееся значение и поищите его в файле pins.txt.
Консольная утилита Uuid Big Brother должна использоваться исключительно в учебно-познавательных и лечебно-оздоровительных целях. Автор не несёт никакой ответственности за возможный вред, прямо или косвенно причинённый с её помощью.
Описание параметров и советы по применению вы можете найти в Приложении Б.
SQL Internals Profiler, версия 1.0.3, ©Yan Liberman
Чтобы утилита смогла получить беспрепятственный доступ к памяти внешнего процесса её необходимо запускать от имени учётной записи, обладающей полномочиями Debug Programs (например, от имени локального системного администратора).
ubb [<switches>...] <dump_file> |
Ключ | Описание | Пример |
---|---|---|
-h | Выводит в консоль описание параметров утилиты. | ubb –h |
-e | Выводит в консоль примеры использования утилиты. | ubb –e |
Комбинирование информационных ключей с остальными параметрами не допускается.
Ключ | Описание | Пример |
---|---|---|
-d{Process id} <dump_file> | Снимает с указанного процесса дамп состояния Uuid-генератора и сохраняет его в файл dump_file. Значение {Process Id} целевого процесса соответствует значению в колонке PID Диспетчера задач (Task Manager). | ubb –d1234 uuid_state.dmp |
-b | Отображает отладочную информацию. Может использоваться только с ключом –d. | ubb –d1234 –b uuid_state.dmp |
Если не все экземпляры RC4 инициализированы процессом (было произведено менее 8 обращений к UuidCreate за время его работы), вы получите сообщение:
Warning: not all of the RC4 instances were initialized. Please, initiate 8 UUIDs generation. |
Это предупреждение означает, что в файл дампа сохранено состояние только тех экземпляров, которые к моменту записи уже инициализированы и, следовательно, их выход является предопределённым. Последовательность UUID-значений, предсказанных утилитой на основании неполного дамп-файла, будет содержать «пропуски». Зачастую, такая ситуация возникает в самом начале работы целевого процесса или в ситуациях, когда процесс очень редко вызывает UuidCreate.
В случае успешного выполнения операции выводится сообщение:
State was successfully dumped to file <dump_file>. |
Ключ | Описание | Пример |
---|---|---|
-g{f<cnt>|b<cnt>} <dump_file> | Загружает состояние генератора из dump_file и вычисляет относительно него первые cnt последующих (f) или предыдущих (b) UUID-значений. Если параметр cnt не задан или равен 0, UBB рассчитает всю возможную последовательность значений. Вычисления будут продолжаться до тех пор, пока счётчик байт выхода у каждого экземпляра RC4 (Accumulator) не станет равен 0 (флаг b) или 500 000 (для флага f).Если ключ –g указан без дополнительных параметров, то будут действовать значения по умолчанию, а именно: -gf0. | ubb –gf35 uuid_state.dmp |
-o{c<display_count>|f<uuids_file>} | Определяет, куда выводить рассчитанную последовательность UUID-значений.c<display_count> — выводит найденные UUID в консоль порциями по display_count значений, ожидая после каждой порции нажатия любой клавиши. По умолчанию параметр display_count равен 8.f<uuids_file> — сохраняет рассчитанную последовательность в текстовый файл uuid_file.Если параметры ключа –o не заданы, по умолчанию будут действовать значения: -oc8. | Вывод в консоль:ubb –gf100 –oc25 uuid_state.dmpВывод в файл:ubb –gf100 –of”uuids.txt” uuid_state.dmp |
-i{0x<value>|k<known_uuid_file>} | Позволяет задать или рассчитать значение Ii, которым инициализируется параметр *Uuid перед вызовом функции UuidCreate(*Uuid).0x<value> — присваивает всем 16 байтам Ii значение value (Ii[1]=Ii[2]=...=Ii[16]= value). value не должно превышать 0xFF.k<known_uuid_file> — вычисляет Ii по трём известным Uuid-значениям, указанным в файле known_uuid_file. Каждое значение нужно располагать на новой строке (разделитель 0x0D0A).Если параметр не указан, по умолчанию действует опция –i0x0. | Все байты Ii равны 0xFF:ubb -g –i0xFF "sqlservr.dmp"Ii необходимо вычислить по трём известным Px, Py, Pz: ubb -g -ik"known_uuid.txt" "sqlservr.dmp" |
-au{<uuid_count>} | Ограничивает число Uuid-значений, которые используются для вычисления Ii. Этот ключ имеет смысл указывать только в комбинации с -ik<known_uuid_file>.По умолчанию uuid_count = 250 000. | ubb -g -ik"known_uuid.txt" –au7500 "sqlservr.dmp" |
-ab{<collisions>} | Определяет точность оператора сравнения при подборе Ii. Ix = Iy, если у них совпадают любые collisions байт. Число collisions не должно превышать 16. По умолчанию оно равно 8. | Считать Ix = Iy, если любые 10 байт Ix совпадут с любыми 10 байтами Iy:ubb -g -ik"known_uuid.txt" –ab10 "sqlservr.dmp" |
Операционная система | Версия библиотеки rpcrt4.dll |
---|---|
Windows Server 20035.2.3790 Service Pack 2 Build 3790 | 5.2.3790.3959 |
Windows XP5.1.2600 Service Pack 2 Build 2600 | 5.1.2600.2180 |
Windows Vista6.0.6000 Build 6000 | 6.0.6000.16386 |
Windows Server 20086.0.6001 Beta 3, v.126 Build 6001 | 6.0.6001.16510 |
Windows Server 20086.0.6001 Build 6001 | 6.0.6001.18000 |