Pull to refresh

Расшифровка базы данных KeePass: пошаговое руководство

Reading time9 min
Views92K

image


На днях мне нужно было реализовать расшифровку базы данных KeePass. Меня поразило то, что нет ни одного документа и ни одной статьи с исчерпывающей информацией об алгоритме расшифровки файлов .kdb и .kdbx с учетом всех нюансов. Это и побудило меня написать данную статью.


На данный момент существует 2 версии KeePass:


  • KeePass 1.x (генерирует файлы .kdb);
  • KeePass 2.x (генерирует файлы .kdbx).

Структура файла с базой данных KeePass (.kdb, .kdbx) состоит из 3 частей:


  • Подпись (не зашифрована);
  • Заголовок (не зашифрован);
  • Данные (зашифрованы).

Далее я подробно расскажу о том, как дешифровать базу данных KeePass 1.x и KeePass 2.x.


Расшифровка базы данных KeePass


Последовательность действий:

  1. Читаем подпись базы данных.
  2. Читаем заголовок базы данных.
  3. Генерируем мастер-ключ.
  4. Расшифровываем базу данных.
  5. Проверяем целостность данных.
  6. Если файл был сжат, распаковываем его.
  7. Расшифровываем пароли.

Пункты 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 файла состоит из следующий полей:


  1. Flags (4 байта): данное поле говорит о том, какие виды шифрования использовались при создании файла:
    • 0x01 — SHA256;
    • 0x02 — AES256;
    • 0x04 — ARC4;
    • 0x08 — Twofish.
  2. Version (4 байта): версия файла.
  3. Master Seed (16 байт): используется для создания мастер-ключа.
  4. Encryption IV (16 байт): используется для расшифровки данных.
  5. Number of Groups (4 байта): общее количество групп в базе данных.
  6. Number of Entries (4 байта): общее количество записей в базе данных.
  7. Content Hash (32 байта): hash расшифрованных данных.
  8. Transform Seed (32 байта): используется для создания мастер-ключа.
  9. Transform Rounds (4 байта): используется для создания мастер-ключа.

Заголовок KeePass 2.x

В .kdbx файлах каждое поле заголовка состоит из 3 частей:


  1. ID поля (1 байт): возможные значения от 0 до 10.
  2. Длина данных (2 байта).
  3. Данные ([длина данных] байт)

Заголовок .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. Генерация составного ключа;
  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. Генерация мастер-ключа на основе составного ключа

  1. Нужно зашифровать составной ключ, полученный выше, с помощью алгоритма AES-256-ECB.
    • В качестве ключа нужно использовать Transform Seed из заголовка.
    • Данное шифрование нужно произвести Transform Rounds (из заголовка) раз.
  2. С помощью SHA256 получаем хэш от зашифрованного составного ключа.
  3. Соединяем Master Seed из заголовка с полученным хэшем.
  4. С помощью 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

Сразу после заголовка начинается сама зашифрованная база данных. Алгоритм расшифровки следующий:


  1. Весь оставшийся кусок файла расшифровываем с помощью алгоритма AES-256-CBC.
    • В качестве ключа используем сгенерированный выше мастер-ключ.
    • В качестве вектора инициализации используем Encryption IV из заголовка.
  2. Последние несколько байт расшифрованной базы данных являются лишними — это несколько одинаковых байт в конце файла (padding). Чтобы устранить их влияние, нужно прочитать последний байт расшифрованной БД — это то количество «лишних» байт, которое в дальнейшем учитывать не надо.
  3. С помощью SHA256 получаем хэш от расшифрованных данных (байты из предыдущего пункта не учитываем).
  4. Проверяем, что полученный хэш совпадает с полем 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 заголовка начинается сама зашифрованная база данных. Алгоритм расшифровки следующий:


  1. Весь оставшийся кусок файла расшифровываем с помощью алгоритма AES-256-CBC.
    • В качестве ключа используем сгенерированный выше мастер-ключ.
    • В качестве вектора инициализации используем Encryption IV из заголовка.
  2. Последние несколько байт расшифрованной базы данных являются лишними — это несколько одинаковых байт в конце файла (padding). Чтобы устранить их влияние, нужно прочитать последний байт расшифрованной БД — это то количество «лишних» байт, которое в дальнейшем учитывать не надо.
  3. Проверяем, что первые 32 байта расшифрованной базы данных совпадают с полем Stream Start Bytes заголовка:
    • eсли данные совпадают, значит мы сгенерировали правильный мастер-ключ,
    • eсли данные не совпадают, это значит, что либо был предоставлен неверный пароль, файл-ключ или WUA, либо данные были повреждены.
  4. Если предыдущий пункт выполнен успешно, отбрасываем первые 32 байта. Проверяем поле Compression Flags заголовка. Если было использовано GZip сжатие файла, то распаковываем данные.
  5. Приступаем к проверке целостности данных. Данные разбиты на блоки, максимальный размер блока равен 1024*1024. Каждый блок данных начинается с заголовка. Структура заголовка следующая:
    • ID блока (4 байта): номер блока начиная с 0;
    • Хэш данных блока (32 байта);
    • Размер блока (4 байта).
  6. Следовательно, порядок действий следующий:
    • Считываем заголовок блока.
    • Считываем данные блока.
    • С помощью SHA256 получаем хэш от данных блока.
    • Проверяем, что хэш совпадает с хэшем из заголовка.
  7. Осуществляем последовательность действий из предыдущего пункта для каждого блока данных. Если данные во всех блоках сохранны, то вырезаем все заголовки блоков, и полученная последовательность и есть расшифрованная база данных.
  8. ВНИМАНИЕ: даже в расшифрованном .kdbx файле пароли могут находиться в зашифрованном виде.
  9. Сохраняем расшифрованные и обезглавленные данные как .xml файл.
  10. Находим в нем все ноды с именем «Value», атрибутом «Protected», значением этого атрибута «True» и берем значения этих нод. Это и есть все еще зашифрованные пароли.
  11. Декодируем все зашифрованные пароли с помощью алгоритма base64decode.
  12. В поле Inner Random Stream ID заголовка смотрим, какой алгоритм использовался при шифровании паролей. В моем случае это был Salsa20.
  13. Генерируем псевдослучайную 64 байтную последовательность с помощью алгоритма Salsa20:
    • В качестве ключа используем хэш поля Protected Stream Key заголовка, полученный с помощью SHA256.
    • В качестве вектора инициализации используем константную 8-ми байтную последовательность 0xE830094B97205D2A.
  14. ВАЖНО: С помощью этой 64 байтной последовательности можно расшифровать ровно 64 символа по порядку соединенных вместе декодированных паролей. Если этого недостаточно для расшифровки всех паролей, нужно сгенерировать следующую псевдослучайную последовательность и продолжить расшифровку паролей и т.д. до конца.
  15. Для получения финального пароля, необходимо сделать XOR декодированного с помощью base64decode пароля с псевдослучайной последовательностью, полученной в предыдущем пункте (более понятно последовательность действий представлена в псевдокоде ниже).
  16. ОЧЕНЬ ВАЖНО: пароли должны расшифровываться по порядку! Именно в той последовательности, в которой они представлены в xml файле.
  17. Находим в xml файле все ноды с именем «Value», атрибутом «Protected», значением этого атрибута «True»:
    • Заменяем значение атрибута на «False».
    • Значение ноды заменяем расшифрованным паролем.
  18. И вот только теперь мы получили полностью расшифрованную базу данных 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;

}


Вот в общем-то и все, что я хотела рассказать. Надеюсь данное руководство избавит кого-нибудь от лишней головной боли и будет познавательным и информативным=)

Tags:
Hubs:
Total votes 67: ↑67 and ↓0+67
Comments39

Articles