Розшифровка бази даних KeePass: покрокове керівництво

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 байта)
Перша підпис однакова .kbd і .kdbx файлів. Вона говорить про те, що даний файл є базою даних KeePass:
  • 0x9AA2D903
VersionSignature (4 байта)
Друга підпис вказує на версію KeePass і, отже, відрізняється .kbd і .kdbx файлів:
  • 0xB54BFB65 — KeePass 1.x (файл .kbd).
  • 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-EBC.
    • В якості ключа потрібно використовувати 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_EBC(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 з заголовка:
    • якщо хеш збігається, то ми успішно розшифрували нашу базу даних! Можна зберегти розшифровані дані .xml файл і переконатися, що всі логіни з паролями розшифровані вірно,
    • якщо хеш не збігається, це означає, що або було надано не вірний пароль або файл-ключ, або дані були пошкоджені.

Псевдокод
<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 заголовка:
    • якщо дані збігаються, значить ми згенерували правильний майстер-ключ,
    • якщо дані не збігаються, це означає, що або було надано невірний пароль, файл-ключ або 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;

}

Ось загалом і все, що я хотіла розповісти. Сподіваюся дане керівництво позбавить кого-небудь від зайвого головного болю і буде пізнавальним і інформативним=)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.