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;

}


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

Поделиться с друзьями
-->

Комментарии (31)


  1. Akuma
    19.08.2016 12:29
    +2

    Я то думал тут будет взлом базы KeePass, а тут просто расшифровка при известных ключах. Эх :)


    1. zolti
      19.08.2016 13:14
      +7

      об этом здесь


      1. bukovki
        19.08.2016 16:14

        Интересно было бы посмотреть на аналогичную статью с KeePass на Linux.


      1. Chupakabra303
        19.08.2016 16:41

        Только вот не понял, на счет подбора пароля к базе, это же не гарантированный взлом? Какова оценка?


        1. MrJeos
          19.08.2016 22:49

          В статье в конце приводится ссылка на утилиту KeeThief, которая просто из памяти достаёт введённый пароль. Так что оценка O(1) судя по всему.
          В репозитории так же есть вроде как пропатченные исходники версии 2.34, в которых эта проблема исправлена.


          1. Chupakabra303
            20.08.2016 12:12

            Из какой памяти, на машине, где совершён вход по паролю в базу? Я имел ввиду в случае утечки зашифрованной базы с парольной защитой.


  1. dmx102
    19.08.2016 12:40

    В тексте у вас использованы расширения .kdb и .kbd. Предполагается что это одно и тоже или это разные форматы?


    1. ana_lazareva
      19.08.2016 12:53

      это опечатка) сейчас исправлю.


  1. Beholder
    19.08.2016 12:54
    -5

    А в чём секретность знания-то? Все исходники же доступны, можно просто там прочитать.


    1. enabokov
      19.08.2016 13:28
      +18

      Автор проанализировал исходники и превратил компьютерный язык в человеческий.


    1. devpony
      19.08.2016 16:41
      +7

      Ага
      Младший брат сегодня спросил.
      Брат: А правда, что у тебя в университете все вопросы или билеты выдают перед экзаменом?
      Я: Да
      Брат: Так их же можно все выучить.


  1. gotch
    19.08.2016 13:02
    +5

    Просто и познавательно, спасибо.


  1. three1415
    19.08.2016 16:13
    +1

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

    Не знаю как автор искал, но неофициальная спецификация уже пару лет висит на гитхабе, буквально на первой странице гугла.


  1. Whity314
    19.08.2016 16:14

    Исправте плиз AES-256-EBC на AES-256-ECB


  1. boblenin
    19.08.2016 17:34

    Это вы для вашей виртуальной клавиатуры прикручиваете keepass базы?


    1. ana_lazareva
      19.08.2016 20:12

      Да, данная задача решалась в рамках разработки аппаратного менеджера паролей.


      1. boblenin
        19.08.2016 22:26

        Думалось самому что-то подобное сделать. Теперь подожду пока вы доделаете, т.к. купить точно выйдет проще.


  1. MrJeos
    19.08.2016 22:12

    Для расшифровывания базы может использоваться не только AES-256, но и Twofish, Serpent или любой другой алгоритм, добавленный плагином.


    1. MrJeos
      19.08.2016 22:21

      Но на самом деле статья отличная! Очень интересно было узнать, как оно там внутри. Спасибо!


  1. 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 из одного и того же файла получают разные значения ключа.


    1. Antelle
      21.08.2016 10:52
      +3

      И это ещё не всё.
      Проверка на hex-string сделана таким образом (ch >= 'A') && (ch <= 'Z'), а чтение — таким (ch >= 'A') && (ch <= 'F'). При этом в "если не получилось преобразовать" мы попадаем из catch-all, если хотя бы что-то где-то упало. Прекрасно, это просто шедевр.


      1. Antelle
        24.08.2016 10:01

        Зарепортил багу в keepass, пофиксят в след. версии. А ещё там появится Argon2 в качестве KDF вместо AES-256-ECB, который сейчас.


    1. ana_lazareva
      21.08.2016 22:53
      +1

      Огромное спасибо! Это очень ценная информация.


  1. kamtec1
    21.08.2016 22:23
    -3

    Oтличная статья! вот что тогда использовать если даже KeePass ломают…


    1. ana_lazareva
      21.08.2016 22:52
      +2

      Мы сейчас разрабатываем аппаратный менеджер паролей, который сведет шансы взлома KeePass к минимуму, и будет самостоятельно вводить пароль. Об устройстве мы уже писали здесь. Уязвимость KeePass состоит в том, что расшифрованная база данных находится в оперативке => если очень захотеть, то эти данные можно украсть. В нашем же случае, расшифрованная БД находится в оперативной памяти устройства и только на момент ввода пароля. Если используются файлы-ключи, то они находятся в защищенной от чтения и записи области флеш памяти устройства. Таким образом, даже если к компьютеру подключен наш аппаратный менеджер паролей, сторонний человек не сможет им воспользоваться, т.к. при очередной попытке ввода логина и пароля, устройство запрашивает мастер-пароль и лишь при его корректности расшифровывает базу данных. Если устройство украли, то это тоже мало чем поможет, т.к. на внешней флеш памяти лежит только зашифрованная БД, а все файл-ключи и мастер-пароли защищены от чтения.


      1. dewil
        23.08.2016 11:49

        Интересная идея.
        А как можно использовать ваше устройство с PC и Смартофона?


        1. ana_lazareva
          23.08.2016 15:17

          Да, мы планируем сделать 2 версии: стационарную, которая будет как переходник между клавиатурой и системным блоком, и портативную — для планшетов, ноутбуков и сматрфонов. Однако на данный момент мы закончили только прототип устройства (основной функционал реализован), чтобы можно уже было демонстрировать потенциальным клиентам.
          Стационарная версия: для PC наше устройство является клавиатурой и дисковым накопителем. Все сообщения от подключенной клавиатуры транслируются в PC через наше устройство, т.о. конфиденциальная информация (например, мастер-пароль) никогда не попадет в оперативку PC => кейлоггеры беспомощны тут.
          Портативная версия: все то же самое, однако управление устройством, ввод мастер-пароля и выбор логина и пароля будет осуществляться через кнопочный интерфейс на самом устройстве. Детально эта версия еще не прорабатывалась.
          Я ответила на ваш вопрос, или вы имели ввиду что-то другое?
          Более подробно об устройстве написано здесь. В блоге мы регулярно будем писать о нашем прогрессе)


          1. dewil
            23.08.2016 17:24

            Почти ответили :)
            Больше интересно для ноутбука и смартофона.
            Еще не понятно, а есть какая то синхронизация самого контейнера с паролями с облаком?
            Буду рад стать бета тестером :) (спб)


    1. ana_lazareva
      21.08.2016 23:05

      А еще можно украсть с помощью кейлоггера мастер-пароль и получить полный доступ к базе, а в нашем случае придется красть еще и само устройство, что усложняет задачку, т.к. это невозможно сделать дистанционно и владелец уж точно поймет, что произошла утечка )


  1. Sliver
    23.08.2016 15:02

    Есть же куча opensource-реализаций KeePass — под каждую из существующих платформ, есть модуль на cpan.
    Для чего всё это?


    1. ana_lazareva
      23.08.2016 15:22
      +2

      Мне нужно расшифровывать базу, которая лежит на внешнем флеше микроконтроллера, а там своя атмосфера: мало оперативки, нельзя использовать динамическое выделение памяти… Поэтому надо писать свой порт.