На днях мне нужно было реализовать расшифровку базы данных KeePass. Меня поразило то, что нет ни одного документа и ни одной статьи с исчерпывающей информацией об алгоритме расшифровки файлов .kdb и .kdbx с учетом всех нюансов. Это и побудило меня написать данную статью.
На данный момент существует 2 версии KeePass:
- KeePass 1.x (генерирует файлы .kdb);
- KeePass 2.x (генерирует файлы .kdbx).
Структура файла с базой данных KeePass (.kdb, .kdbx) состоит из 3 частей:
- Подпись (не зашифрована);
- Заголовок (не зашифрован);
- Данные (зашифрованы).
Далее я подробно расскажу о том, как дешифровать базу данных KeePass 1.x и KeePass 2.x.
Расшифровка базы данных KeePass
Последовательность действий:
- Читаем подпись базы данных.
- Читаем заголовок базы данных.
- Генерируем мастер-ключ.
- Расшифровываем базу данных.
- Проверяем целостность данных.
- Если файл был сжат, распаковываем его.
- Расшифровываем пароли.
Пункты 5, 6 и 7 относятся только к .kdbx файлам!
Подпись
BaseSignature (4 байта)
Первая подпись одинакова для .kdb и .kdbx файлов. Она говорит о том, что данный файл является базой данных KeePass:
- 0x9AA2D903
VersionSignature (4 байта)
Bторая подпись указывает на версию KeePass и, следовательно, отличается для .kdb и .kdbx файлов:
- 0xB54BFB65 — KeePass 1.x (файл .kdb).
- 0xB54BFB66 — KeePass 2.x pre-release (файл .kdbx).
- 0xB54BFB67 — KeePass 2.x post-release (файл .kdbx).
FileVersion (4 байта)
Третья подпись есть только у файлов .kdbx и содержит в себе версию файла. Для файлов .kdb данная информация содержится в заголовке базы данных.
Таким образом, в KeePass 1.x длина подписи составляет 8 байт, а в KeePass 2.x — 12 байт.
Заголовок
После подписи базы данных начинается заголовок.
Заголовок KeePass 1.x
Заголовок .kdb файла состоит из следующий полей:
- Flags (4 байта): данное поле говорит о том, какие виды шифрования использовались при создании файла:
- 0x01 — SHA256;
- 0x02 — AES256;
- 0x04 — ARC4;
- 0x08 — Twofish.
- Version (4 байта): версия файла.
- Master Seed (16 байт): используется для создания мастер-ключа.
- Encryption IV (16 байт): используется для расшифровки данных.
- Number of Groups (4 байта): общее количество групп в базе данных.
- Number of Entries (4 байта): общее количество записей в базе данных.
- Content Hash (32 байта): hash расшифрованных данных.
- Transform Seed (32 байта): используется для создания мастер-ключа.
- Transform Rounds (4 байта): используется для создания мастер-ключа.
Заголовок KeePass 2.x
В .kdbx файлах каждое поле заголовка состоит из 3 частей:
- ID поля (1 байт): возможные значения от 0 до 10.
- Длина данных (2 байта).
- Данные ([длина данных] байт)
Заголовок .kdbx файла состоит из следующий полей:
- ID=0x01 Comment: данное поле может быть представлено в заголовке, но в моей базе данных его не было.
- ID=0x02 Cipher ID: UUID, указывающий на используемый метод шифрования (например, для AES 256 UUID = [0x31, 0xC1, 0xF2, 0xE6, 0xBF, 0x71, 0x43, 0x50, 0xBE, 0x58, 0x05, 0x21, 0x6A, 0xFC, 0x5A, 0xFF]).
- ID=0x03 Compression Flags: ID алгоритма, использующегося для сжатия базы данных:
- 0x00: None;
- 0x01: GZip.
- ID=0x04 Master Seed: используется для создания мастер-ключа.
- ID=0x05 Transform Seed: используется для создания мастер-ключа.
- ID=0x06 Transform Rounds: используется для создания мастер-ключа.
- ID=0x07 Encryption IV: используется для расшифровки данных.
- ID=0x08 Protected Stream Key: используется для расшифровки паролей.
- ID=0x09 Stream Start Bytes: первые 32 байта расшифрованной базы данных. Они используются для проверки целостности расшифрованных данных и корректности мастер-ключа. Эти 32 байта рандомно генерируются каждый раз, когда в файле сохраняются изменения.
- ID=0x0A Inner Random Stream ID: ID алгоритма, использующегося для расшифровки паролей:
- 0x00: None;
- 0x01: ARC4;
- 0x02: Salsa20.
- ID=0x00 End of Header: последнее поле заголовка базы данных, после него начинается сама база данных.
Генерация мастер-ключа
Генерация мастер-ключа происходит в 2 этапа:
- Генерация составного ключа;
- Генерация мастер-ключа на основе составного ключа.
1. Генерация составного ключа
Для генерации составного ключа используется хэш-алгоритм SHA256. В таблицах ниже представлен псевдокод для генерации составного ключа, исходя из того, какая версия KeePass используется, и какие входные данные необходимы для расшифровки базы данных (только пароль, только файл-ключ или все вместе):
KeePass 1.x
Пароль | sha256(password) |
Файл-ключ | sha256(keyfile) |
Пароль + Файл-ключ | sha256(concat(sha256(password), sha256(keyfile))) |
KeePass 2.x
Пароль | sha256(sha256(password)) |
Файл-ключ | sha256(sha256(keyfile)) |
Пароль + Файл-ключ | sha256(concat(sha256(password), sha256(keyfile))) |
Windows User Account (WUA) | sha256(sha256(WUA)) |
Пароль + Файл-ключ + (WUA) | sha256(concat(sha256(password), sha256(keyfile), sha256(WUA))) |
Обращаю внимание на то, что если для расшифровки базы данных необходимо несколько сущностей (например, пароль и файл-ключ), то сначала нужно получить хэш от каждой сущности, а потом соединить их вместе (concat) и взять хэш от объединенной последовательности.
2. Генерация мастер-ключа на основе составного ключа
- Нужно зашифровать составной ключ, полученный выше, с помощью алгоритма AES-256-ECB.
- В качестве ключа нужно использовать Transform Seed из заголовка.
- Данное шифрование нужно произвести Transform Rounds (из заголовка) раз.
- С помощью SHA256 получаем хэш от зашифрованного составного ключа.
- Соединяем Master Seed из заголовка с полученным хэшем.
- С помощью SHA256 получаем хэш от объединенной последовательности — это и есть наш мастер-ключ!
<p>void GenerateMasterKey()
{
//шифруем составной ключ TransformRounds раз
for(int i = 0; i < TransformRounds; i++) {
result = encrypt_AES_ECB(TransformSeed, composite_key);
composite_key = result;
}</p>
<source>//получаем хэш от зашифрованного составного ключа
hash = sha256(composite_key);
//объединяем полученный хэш с полем MasterSeed из заголовка
key = concat(MasterSeed, hash);
//получаем хэш от объединенной выше последовательности
master_key = sha256(key);
}
Расшифровка данных KeePass 1.x
Сразу после заголовка начинается сама зашифрованная база данных. Алгоритм расшифровки следующий:
- Весь оставшийся кусок файла расшифровываем с помощью алгоритма AES-256-CBC.
- В качестве ключа используем сгенерированный выше мастер-ключ.
- В качестве вектора инициализации используем Encryption IV из заголовка.
- Последние несколько байт расшифрованной базы данных являются лишними — это несколько одинаковых байт в конце файла (padding). Чтобы устранить их влияние, нужно прочитать последний байт расшифрованной БД — это то количество «лишних» байт, которое в дальнейшем учитывать не надо.
- С помощью SHA256 получаем хэш от расшифрованных данных (байты из предыдущего пункта не учитываем).
- Проверяем, что полученный хэш совпадает с полем Content Hash из заголовка:
- eсли хэш совпадает, то мы успешно расшифровали нашу базу данных! Можно сохранить расшифрованные данные как .xml файл и убедиться, что все логины с паролями расшифрованы верно,
- eсли хэш не совпадает, это значит, что либо был предоставлен не верный пароль или файл-ключ, либо данные были повреждены.
<p>bool DecryptKeePass1x()
{
//определяем длину зашифрованной БД
//(размер файла - размер подписи - размер заголовка)
db_len = file_size - signature_size - header_size;</p>
<source>//расшифровываем данные
decrypted_data = decrypt_AES_256_CBC(master_key, EncryptionIV, encrypted_data);
//узнаем количество "лишних" байт
extra = decrypted_data[db_len - 1];
//получаем хэш от данных (без учета extra байт!)
content_hash = sha256(decrypted_data[:(db_len - extra)]);
//проверяем, что полученный хэш совпадает с полем СontentHash из заголовка
if (СontentHash == content_hash)
return true;
else
return false;
}
Расшифровка данных KeePass 2.x
Сразу после поля End of Header заголовка начинается сама зашифрованная база данных. Алгоритм расшифровки следующий:
- Весь оставшийся кусок файла расшифровываем с помощью алгоритма AES-256-CBC.
- В качестве ключа используем сгенерированный выше мастер-ключ.
- В качестве вектора инициализации используем Encryption IV из заголовка.
- Последние несколько байт расшифрованной базы данных являются лишними — это несколько одинаковых байт в конце файла (padding). Чтобы устранить их влияние, нужно прочитать последний байт расшифрованной БД — это то количество «лишних» байт, которое в дальнейшем учитывать не надо.
- Проверяем, что первые 32 байта расшифрованной базы данных совпадают с полем Stream Start Bytes заголовка:
- eсли данные совпадают, значит мы сгенерировали правильный мастер-ключ,
- eсли данные не совпадают, это значит, что либо был предоставлен неверный пароль, файл-ключ или WUA, либо данные были повреждены.
- Если предыдущий пункт выполнен успешно, отбрасываем первые 32 байта. Проверяем поле Compression Flags заголовка. Если было использовано GZip сжатие файла, то распаковываем данные.
- Приступаем к проверке целостности данных. Данные разбиты на блоки, максимальный размер блока равен 1024*1024. Каждый блок данных начинается с заголовка. Структура заголовка следующая:
- ID блока (4 байта): номер блока начиная с 0;
- Хэш данных блока (32 байта);
- Размер блока (4 байта).
- Следовательно, порядок действий следующий:
- Считываем заголовок блока.
- Считываем данные блока.
- С помощью SHA256 получаем хэш от данных блока.
- Проверяем, что хэш совпадает с хэшем из заголовка.
- Осуществляем последовательность действий из предыдущего пункта для каждого блока данных. Если данные во всех блоках сохранны, то вырезаем все заголовки блоков, и полученная последовательность и есть расшифрованная база данных.
- ВНИМАНИЕ: даже в расшифрованном .kdbx файле пароли могут находиться в зашифрованном виде.
- Сохраняем расшифрованные и обезглавленные данные как .xml файл.
- Находим в нем все ноды с именем «Value», атрибутом «Protected», значением этого атрибута «True» и берем значения этих нод. Это и есть все еще зашифрованные пароли.
- Декодируем все зашифрованные пароли с помощью алгоритма base64decode.
- В поле Inner Random Stream ID заголовка смотрим, какой алгоритм использовался при шифровании паролей. В моем случае это был Salsa20.
- Генерируем псевдослучайную 64 байтную последовательность с помощью алгоритма Salsa20:
- В качестве ключа используем хэш поля Protected Stream Key заголовка, полученный с помощью SHA256.
- В качестве вектора инициализации используем константную 8-ми байтную последовательность 0xE830094B97205D2A.
- ВАЖНО: С помощью этой 64 байтной последовательности можно расшифровать ровно 64 символа по порядку соединенных вместе декодированных паролей. Если этого недостаточно для расшифровки всех паролей, нужно сгенерировать следующую псевдослучайную последовательность и продолжить расшифровку паролей и т.д. до конца.
- Для получения финального пароля, необходимо сделать XOR декодированного с помощью base64decode пароля с псевдослучайной последовательностью, полученной в предыдущем пункте (более понятно последовательность действий представлена в псевдокоде ниже).
- ОЧЕНЬ ВАЖНО: пароли должны расшифровываться по порядку! Именно в той последовательности, в которой они представлены в xml файле.
- Находим в xml файле все ноды с именем «Value», атрибутом «Protected», значением этого атрибута «True»:
- Заменяем значение атрибута на «False».
- Значение ноды заменяем расшифрованным паролем.
- И вот только теперь мы получили полностью расшифрованную базу данных KeePass 2.x! Ура!=)
<p>bool DecryptKeePass2x()
{
//определяем длину зашифрованной БД
//(размер файла - размер подписи - размер заголовка)
db_len = file_size - signature_size - header_size;</p>
<source>//расшифровываем данные
decrypted_data = decrypt_AES_256_CBC(master_key, EncryptionIV, encrypted_data);
//узнаем количество "лишних" байт
extra = decrypted_data[db_len - 1];
db_len -= extra;
//проверяем, что первые 32 байта расшифрованной БД
//совпадают с полем StreamStartBytes заголовка
if (StreamStartBytes != decrypted_data[0:32])
return false;
//отбрасываем эти 32 байта
db_len -= 32;
decrypted_data += 32;
//проверяем поле CompressionFlag заголовка
//если файл был сжат, распаковываем его
if (CompressionFlag == 1)
unzip(decrypted_data);
//проверяем целостность данных
while (db_len > (BlockHeaderSize))
{
//считываем заголовок базы данных
block_data = decrypted_data[0:BlockHeaderSize];
decrypted_data += BlockHeaderSize;
db_len -= BlockHeaderSize;
if (block_data.blockDataSize == 0) {
break;
}
//получаем хэш данных блока
hash = sha256(decrypted_data[0:block_data.blockDataSize]);
//проверяем, что полученный хэш совпадает с хэшем из заголовка
if(block_data.blockDataHash == hash) {
pure_data += decrypted_data[0:block_data.blockDataSize];
decrypted_data += block_data.blockDataSize;
db_len -= block_data.blockDataSize;
}
else {
return false;
}
}
//сохраняем расшифрованные и обезглавленные данные как xml файл
xml = pure_data.ToXml();
//получаем хэш от поля ProtectedStreamKey заголовка
key = sha256(ProtectedStreamKey);
//инициализируем алгоритм Salsa20
IV_SALSA = 0xE830094B97205D2A;
salsa.setKey(key);
salsa.setIv(IV_SALSA);
stream_pointer = 0;
key_stream[64] = salsa.generateKeyStream();
//расшифровываем пароли
while(true)
{
//находим следующую попорядку ноду с именем "Value",
//атрибутом "Protected", значением атрибута "True"
node = xml.FindNextElement("Value", "Protected", "True");
if (node == NULL) {
break;
}
//берем значение ноды и декодируем с помощью алгоритма base64decode
decoded_pass = base64decode(node.value);
//расшифровываем пароль с помощью псевдослучайной последовательности key_stream
for (int i = 0; i < len(decoded_pass); i++) {
decoded_pass[i] = decoded_pass[i] ^ key_stream[stream_pointer];
stream_pointer++;
//если 64 байтной псевдослучайной последовательности не хватило,
//генерируем еще одну последовательность
if (stream_pointer >= 64) {
key_stream[64] = salsa.generateKeyStream();
stream_pointer = 0;
}
}
//заменяем значение атрибута "Protected" на "False"
node.attribute.value = "False";
//заменяем зашифрованный пароль дешифрованным
node.value = decoded_pass;
}
return true;
}
Вот в общем-то и все, что я хотела рассказать. Надеюсь данное руководство избавит кого-нибудь от лишней головной боли и будет познавательным и информативным=)
Комментарии (31)
dmx102
19.08.2016 12:40В тексте у вас использованы расширения .kdb и .kbd. Предполагается что это одно и тоже или это разные форматы?
Beholder
19.08.2016 12:54-5А в чём секретность знания-то? Все исходники же доступны, можно просто там прочитать.
enabokov
19.08.2016 13:28+18Автор проанализировал исходники и превратил компьютерный язык в человеческий.
devpony
19.08.2016 16:41+7АгаМладший брат сегодня спросил.
Брат: А правда, что у тебя в университете все вопросы или билеты выдают перед экзаменом?
Я: Да
Брат: Так их же можно все выучить.three1415
19.08.2016 16:13+1Меня поразило то, что нет ни одного документа и ни одной статьи с исчерпывающей информацией об алгоритме расшифровки файлов .kdb и .kdbx с учетом всех нюансов.
Не знаю как автор искал, но неофициальная спецификация уже пару лет висит на гитхабе, буквально на первой странице гугла.
boblenin
19.08.2016 17:34Это вы для вашей виртуальной клавиатуры прикручиваете keepass базы?
ana_lazareva
19.08.2016 20:12Да, данная задача решалась в рамках разработки аппаратного менеджера паролей.
boblenin
19.08.2016 22:26Думалось самому что-то подобное сделать. Теперь подожду пока вы доделаете, т.к. купить точно выйдет проще.
anmipo
20.08.2016 19:59+2если для расшифровки базы данных необходимо несколько сущностей (например, пароль и файл-ключ), то сначала нужно получить хэш от каждой сущности
Здесь есть нюансы:
1) если файл-ключ размером 32 байта, то его содержимое используется «как есть» (хеш брать не надо);
2) если файл-ключ размером 64 байта и там только hex-цифры — они тоже напрямую преобразуются в 32-байтный ключ; если не получилось преобразовать — тогда берём хеш.
3) Cамое интересное: файл-ключ может содержать примерно такой XML:
<?xml version="1.0" encoding="utf-8"?> <KeyFile> <Meta> <Version>1.00</Version> </Meta> <Key> <Data>ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI=</Data> </Key> </KeyFile>
KeePass 2.x парсит XML и вытаскивает значение ключа, а KeePass 1.x не разбираясь берёт хеш от содержимого файла. В результате разные версии KeePass из одного и того же файла получают разные значения ключа.Antelle
21.08.2016 10:52+3И это ещё не всё.
Проверка на hex-string сделана таким образом(ch >= 'A') && (ch <= 'Z')
, а чтение — таким(ch >= 'A') && (ch <= 'F')
. При этом в "если не получилось преобразовать" мы попадаем из catch-all, если хотя бы что-то где-то упало. Прекрасно, это просто шедевр.Antelle
24.08.2016 10:01Зарепортил багу в keepass, пофиксят в след. версии. А ещё там появится Argon2 в качестве KDF вместо AES-256-ECB, который сейчас.
kamtec1
21.08.2016 22:23-3Oтличная статья! вот что тогда использовать если даже KeePass ломают…
ana_lazareva
21.08.2016 22:52+2Мы сейчас разрабатываем аппаратный менеджер паролей, который сведет шансы взлома KeePass к минимуму, и будет самостоятельно вводить пароль. Об устройстве мы уже писали здесь. Уязвимость KeePass состоит в том, что расшифрованная база данных находится в оперативке => если очень захотеть, то эти данные можно украсть. В нашем же случае, расшифрованная БД находится в оперативной памяти устройства и только на момент ввода пароля. Если используются файлы-ключи, то они находятся в защищенной от чтения и записи области флеш памяти устройства. Таким образом, даже если к компьютеру подключен наш аппаратный менеджер паролей, сторонний человек не сможет им воспользоваться, т.к. при очередной попытке ввода логина и пароля, устройство запрашивает мастер-пароль и лишь при его корректности расшифровывает базу данных. Если устройство украли, то это тоже мало чем поможет, т.к. на внешней флеш памяти лежит только зашифрованная БД, а все файл-ключи и мастер-пароли защищены от чтения.
dewil
23.08.2016 11:49Интересная идея.
А как можно использовать ваше устройство с PC и Смартофона?ana_lazareva
23.08.2016 15:17Да, мы планируем сделать 2 версии: стационарную, которая будет как переходник между клавиатурой и системным блоком, и портативную — для планшетов, ноутбуков и сматрфонов. Однако на данный момент мы закончили только прототип устройства (основной функционал реализован), чтобы можно уже было демонстрировать потенциальным клиентам.
Стационарная версия: для PC наше устройство является клавиатурой и дисковым накопителем. Все сообщения от подключенной клавиатуры транслируются в PC через наше устройство, т.о. конфиденциальная информация (например, мастер-пароль) никогда не попадет в оперативку PC => кейлоггеры беспомощны тут.
Портативная версия: все то же самое, однако управление устройством, ввод мастер-пароля и выбор логина и пароля будет осуществляться через кнопочный интерфейс на самом устройстве. Детально эта версия еще не прорабатывалась.
Я ответила на ваш вопрос, или вы имели ввиду что-то другое?
Более подробно об устройстве написано здесь. В блоге мы регулярно будем писать о нашем прогрессе)dewil
23.08.2016 17:24Почти ответили :)
Больше интересно для ноутбука и смартофона.
Еще не понятно, а есть какая то синхронизация самого контейнера с паролями с облаком?
Буду рад стать бета тестером :) (спб)
ana_lazareva
21.08.2016 23:05А еще можно украсть с помощью кейлоггера мастер-пароль и получить полный доступ к базе, а в нашем случае придется красть еще и само устройство, что усложняет задачку, т.к. это невозможно сделать дистанционно и владелец уж точно поймет, что произошла утечка )
Sliver
23.08.2016 15:02Есть же куча opensource-реализаций KeePass — под каждую из существующих платформ, есть модуль на cpan.
Для чего всё это?ana_lazareva
23.08.2016 15:22+2Мне нужно расшифровывать базу, которая лежит на внешнем флеше микроконтроллера, а там своя атмосфера: мало оперативки, нельзя использовать динамическое выделение памяти… Поэтому надо писать свой порт.
Akuma
Я то думал тут будет взлом базы KeePass, а тут просто расшифровка при известных ключах. Эх :)
zolti
об этом здесь
bukovki
Интересно было бы посмотреть на аналогичную статью с KeePass на Linux.
Chupakabra303
Только вот не понял, на счет подбора пароля к базе, это же не гарантированный взлом? Какова оценка?
MrJeos
В статье в конце приводится ссылка на утилиту KeeThief, которая просто из памяти достаёт введённый пароль. Так что оценка O(1) судя по всему.
В репозитории так же есть вроде как пропатченные исходники версии 2.34, в которых эта проблема исправлена.
Chupakabra303
Из какой памяти, на машине, где совершён вход по паролю в базу? Я имел ввиду в случае утечки зашифрованной базы с парольной защитой.