Расшифровка файла mcrypt в .Net (C #) - PullRequest
0 голосов
/ 24 сентября 2018

Я уже некоторое время увлекаюсь C # и хотел бы иметь промежуточные навыки разработки, но практически не обладать знаниями в области шифрования.В рамках стороннего проекта мне нужно расшифровать файлы, которые были зашифрованы с помощью MCrypt.Не похоже, что в команду были переданы какие-то особые аргументы.Например, это довольно часто (ключ и имя файла изменены), а ключи имеют разную длину, где-то от 14-18 символов.
mcrypt -a rijndael-256 fileToEncrypt.tar.gz -k 0123456789abcdef1

До сих пор я использовал два подхода к этой задаче.Во-первых, используйте mcrypt.exe и запустите процесс, используя Process.Тем не менее, я чувствую, что это делает код (и поток программы) очень неуклюжим.Второе - попытаться напрямую расшифровать файл из моей программы и иметь нулевые зависимости от внешних программ;Я бы хотел пойти по этому пути.

Я немного запутался с форматом MCrypt.Я рассмотрел документ FORMAT в исходном коде ( здесь для просмотра в Интернете), и я полагаю, что о начальной части заголовка позаботились правильно.Однако я не могу расшифровать зашифрованные данные в файле.

1) Насколько велика IV и как передать ее в мой расшифровщик?
2) Насколько велика контрольная сумма наконец файла и нужен ли он мне?
3) Вышеупомянутая статическая длина?
4) Что такое keymode (mcrypt-sha1) и как он используется?
5) Я заметил, что когдаправильно расшифровывать (используя mcrypt.exe), что существует разница в 140 байтов между зашифрованным и дешифрованным файлом.Что составляет эти 140 байтов?

Код и начало зашифрованного файла ниже;Без сомнения, мой код неверен, начиная с комментария «Получить данные». Любые указатели в правильном направлении будут с благодарностью.

Sample mcrypt Rijndael-256 file

/// <summary>
/// Decrypt an mcrypt file using rijndael-256
/// </summary>
/// <param name="inputFile">File to decrypt</param>
/// <param name="encryptionKey">Password</param>
/// <param name="purge"></param>
public static bool Decrypt (string inputFile, string encryptionKey)
{
    var rv = false;
    if (File.Exists(inputFile) == true)
    {
        using (FileStream stream = new FileStream(inputFile, FileMode.Open))
        {
            var buffer = new byte[1024];

            // MCrypt header
            stream.Read(buffer, 0, 3);

            if (buffer[0] == 0x00 && buffer[1] == 0x6D && buffer[2] == 0x03)
            {
                // Flag
                // Bit 7 - Salt Used
                // Bit 8 - IV not used
                var flag = (byte)stream.ReadByte();

                byte[] saltVal = null;
                var saltUsed = Utils.GetBit(flag, 6);
                byte[] ivVal = new byte[16];
                var ivUsed = (Utils.GetBit(flag, 7) == false);

                var algorithmName = Utils.GetNullTerminatedString(stream);

                stream.Read(buffer, 0, 2);
                var keyLen = (buffer[1] << 8) + buffer[0];

                var algorithModeName = Utils.GetNullTerminatedString(stream);

                var keygenName = Utils.GetNullTerminatedString(stream);

                if (saltUsed)
                {
                    var saltFlag = (byte)stream.ReadByte();
                    if (Utils.GetBit(saltFlag, 0))
                    {
                        // After clearing the first bit the salt flag is now the length
                        Utils.ClearBit (ref saltFlag, 0);
                        saltVal = new byte[saltFlag];
                        stream.Read(saltVal, 0, saltFlag);
                    }
                }

                var algorithmModeName = Utils.GetNullTerminatedString(stream);

                if (ivUsed)
                {
                    stream.Read(ivVal, 0, ivVal.Length);
                }

                // Get the data - how much to get???
                buffer = new byte[stream.Length - stream.Position + 1];
                var bytesRead = stream.Read(buffer, 0, buffer.Length);

                using (MemoryStream ms = new MemoryStream())
                {
                    using (RijndaelManaged rijndael = new RijndaelManaged())
                    {
                        rijndael.KeySize = 256;
                        rijndael.BlockSize = 128;

                        var key = new Rfc2898DeriveBytes(System.Text.Encoding.ASCII.GetBytes(encryptionKey), saltVal, 1000);
                        rijndael.Key = key.GetBytes(rijndael.KeySize / 8);
                        //AES.Key = System.Text.Encoding.ASCII.GetBytes(encryptionKey);
                        //AES.IV = key.GetBytes(AES.BlockSize / 8);
                        rijndael.IV = ivVal;

                        rijndael.Mode = CipherMode.CBC;
                        rijndael.Padding = PaddingMode.None;

                        using (var cs = new CryptoStream(ms, rijndael.CreateDecryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(buffer, 0, buffer.Length);
                            cs.Close();

                            using (FileStream fs = new FileStream(inputFile + Consts.FILE_EXT, FileMode.Create))
                            {
                                byte[] decryptedBytes = ms.ToArray();
                                fs.Write(decryptedBytes, 0, decryptedBytes.Length);
                                fs.Close();
                                rv = true;
                            }
                        }
                    }
                }
            }
        }
    }

    return rv;
}

Редактировать
Я получаю следующее при включении его подробного режима и без указания rijndael-256.Когда я указываю алгоритм, он отражает это в подробном выводе;оба расшифровывают файл должным образом.Сюжет утолщается ...

Алгоритм: rijndael-128
Размер ключа: 32
Режим: cbc
Режим ключевого слова: mcrypt-sha1
Формат файла: mcrypt

Также "пароли"используется для шифрования в различных частях программного обеспечения длиной от 12 до 28 символов.

1 Ответ

0 голосов
/ 26 сентября 2018

Формат файла MCrypt

Наблюдения были сделаны с использованием mcrypt-2.6.7-win32 , шифрование следующего файла с помощью команды mcrpyt.exe --no-openpgp -V test_in.txt

test_in.txt незашифрованоимеет длину 25 байт, и приведенная выше команда шифрует следующим образом, в результате получается файл test_out.txt.nc, длина которого составляет 125 байт.

+-------------+----------------------+----------------+---------------------------------------------+
| File Offset | Field Length (bytes) | Field Content  | Description                                 |
+-------------+----------------------+----------------+---------------------------------------------+
| 0           | 1                    | 0x0            | Zero byte                                   |
+-------------+----------------------+----------------+---------------------------------------------+
| 1           | 1                    | 0x6d           | m                                           |
+-------------+----------------------+----------------+---------------------------------------------+
| 2           | 1                    | 0x3            | Version                                     |
+-------------+----------------------+----------------+---------------------------------------------+
| 3           | 1                    | 0x40           | Flags - bit 7 set = salt, bit 8 set = no IV |
+-------------+----------------------+----------------+---------------------------------------------+
| 4           | 13                   | rijndael-128   | Algorithm name                              |
+-------------+----------------------+----------------+---------------------------------------------+
| 17          | 2                    | 32             | Key Size                                    |
+-------------+----------------------+----------------+---------------------------------------------+
| 19          | 4                    | cbc            | Algorithm mode                              |
+-------------+----------------------+----------------+---------------------------------------------+
| 23          | 12                   | mcrypt-sha1    | Key generator algorithm                     |
+-------------+----------------------+----------------+---------------------------------------------+
| 35          | 1                    | 21             | Salt length + 1                             |
+-------------+----------------------+----------------+---------------------------------------------+
| 36          | 20                   | Salt data      | Salt                                        |
+-------------+----------------------+----------------+---------------------------------------------+
| 56          | 5                    | sha1           | Check sum algorithm                         |
+-------------+----------------------+----------------+---------------------------------------------+
| 61          | 16                   | IV data        | Initialisation vector                       |
+-------------+----------------------+----------------+---------------------------------------------+
| 77          | 48                   | Encrypted data | 25 original data + 20 check sum + 3 padding |
+-------------+----------------------+----------------+---------------------------------------------+
| TOTAL       | 125                  |                |                                             |
+-------------+----------------------+----------------+---------------------------------------------+

Наблюдая вывод в различных сценариях, следующий блок / ключ /Используются размеры IV:

+--------------+--------------------+------------+------------------+
| Algorithm    | Block Size (bytes) | IV (bytes) | Key Size (bytes) |
+--------------+--------------------+------------+------------------+
| rijndael-128 | 16                 | 16         | 32               |
+--------------+--------------------+------------+------------------+
| rijndael-256 | 32                 | 32         | 32               |
+--------------+--------------------+------------+------------------+

Контрольная сумма производится для исходных данных перед шифрованием и добавляется в конец исходных данных.По умолчанию используется алгоритм контрольной суммы SHA-1, в результате чего получается 20-байтовый хэш.Итак, исходные данные размером 25 байт становятся 45 байтами.С размером блока 128 бит (16 байтов) это приводит к заполнению 3 байтами для достижения размера блока 48 байтов.При размере блока в 256 бит (32 байта) было бы 19 байтов заполнения, чтобы получить 64 байта.Нулевые байты используются для заполнения, что важно во время расшифровки, так как они не удаляются автоматически, поскольку размер исходных данных неизвестен.

Чтение заголовка

Вот пример кодачтение заголовка и зашифрованных данных в конце файла.Для краткости включены не все вспомогательные функции.

public void ReadHeader(Stream stream)
{
    byte[] buffer = new byte[512];
    stream.Read(buffer, 0, 3);
    if (buffer[0] != 0x0) throw new FormatException($"First byte is not 0x0, invalid MCrypt file");
    if ((char)buffer[1] != 'm') throw new FormatException($"Second byte is not null, invalid MCrypt file");
    if (buffer[2] != 0x3) throw new FormatException($"Third byte is not 0x3, invalid MCrypt file");

    byte flags = (byte)stream.ReadByte();
    KeyGeneratorUsesSalt = (flags & (1 << 6)) != 0;
    HasInitialisationVector = (flags & (1 << 7)) != 1;
    AlgorithmName = ReadNullTerminatedString(stream);
    stream.Read(buffer, 0, 2);
    KeySize = BitConverter.ToUInt16(buffer, 0);
    BlockSize = GetBlockSize(AlgorithmName);

    var cipherModeAsString = ReadNullTerminatedString(stream);
    CipherMode cipherMode;
    if (Enum.TryParse<CipherMode>(cipherModeAsString, out cipherMode))
        CipherMode = cipherMode;

    KeyGeneratorName = ReadNullTerminatedString(stream);

    if (KeyGeneratorUsesSalt)
    {
        var saltSize = ((byte)stream.ReadByte()) - 1;
        Salt = new byte[saltSize];
        stream.Read(Salt, 0, saltSize);
    }

    CheckSumAlgorithmName = ReadNullTerminatedString(stream);

    if (HasInitialisationVector)
    {
        InitialisationVector = new byte[BlockSize / 8];
        stream.Read(InitialisationVector, 0, BlockSize / 8);
    }

    int read = 0;
    byte[] remainingData = null;
    using (MemoryStream mem = new MemoryStream())
    {
        while ((read = stream.Read(buffer, 0, buffer.Length)) != 0)
        {
            mem.Write(buffer, 0, read);
            remainingData = mem.ToArray();
        }
    }

    EncryptedData = remainingData;
}

Генерация ключей

Алгоритм генерации ключей указан в заголовке и по умолчанию в формате MCrypt - mcrypt-sha1.Если посмотреть на источник mcrypt, этот ключ генерируется с использованием библиотеки mhash.Он комбинирует парольную фразу с солью, чтобы получить ключ с необходимым количеством байтов для алгоритма (32 байта в обоих случаях, которые я рассматривал).Я перевел функцию _mhash_gen_key_mcrypt из библиотеки mhash на C #, как показано ниже - возможно, она уже где-то в .NET Framework, но если так, я не смог ее найти.

public byte[] GenerateKeyMcryptSha1(string passPhrase, byte[] salt, int keySize)
{
    byte[] key = new byte[KeySize], digest = null;
    int hashSize = 20;
    byte[] password = Encoding.ASCII.GetBytes(passPhrase);
    int keyBytes = 0;

    while (true)
    {
        byte[] inputData = null;
        using (MemoryStream stream = new MemoryStream())
        {
            if (Salt != null)
                stream.Write(salt, 0, salt.Length);
            stream.Write(password, 0, password.Length);
            if (keyBytes > 0)
                stream.Write(key, 0, keyBytes);
            inputData = stream.ToArray();
        }

        using (var sha1 = new SHA1Managed())
            digest = sha1.ComputeHash(inputData);

        if (keySize > hashSize)
        {
            Buffer.BlockCopy(digest, 0, key, keyBytes, hashSize);
            keySize -= hashSize;
            keyBytes += hashSize;
        }
        else
        {
            Buffer.BlockCopy(digest, 0, key, keyBytes, keySize);
            break;
        }                
    }

    return key;
}

Расшифровка

Мы можем использовать стандартные криптографические классы .NET для выполнения большей части расшифровки, передавая 32-байтовый ключ, который мы сгенерировали путем хэширования парольной фразы и соли, и где мы используем 128-битный или 256-битный вариант на основе алгоритманазвание из шапки.Мы присваиваем вектор инициализации (IV), который мы читаем из заголовка, с помощью rijndael.IV = InitialisationVector;.

/// <summary>
/// Decrypt using Rijndael
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Unencrypted data</returns>
private byte[] DecryptRijndael(byte[] key, int keySize)
{
    using (RijndaelManaged rijndael = GetRijndael(key, keySize))
    {
        rijndael.IV = InitialisationVector;
        using (MemoryStream unencryptedStream = new MemoryStream())
        using (MemoryStream encryptedStream = new MemoryStream(EncryptedData))
        {
            using (var cs = new CryptoStream(encryptedStream, rijndael.CreateDecryptor(), CryptoStreamMode.Read))
                cs.CopyTo(unencryptedStream);

            byte[] unencryptedData = RemovePaddingAndCheckSum(unencryptedStream.ToArray(), GetCheckSumLen());                    
            return unencryptedData;
        }
    }
}

/// <summary>
/// Set algorithm mode/settings
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Instance ready to decrypt</returns>
private RijndaelManaged GetRijndael(byte[] key, int keySize)
{
    var rijndael = new RijndaelManaged()
    {
        Mode = CipherMode, // e.g. CBC
        KeySize = keySize, // e.g. 256 bits
        Key = key, // e.g. 32-byte sha-1 hash of passphrase + salt
        BlockSize = BlockSize, // e.g. 256 bits
        Padding = PaddingMode.Zeros
    };

    return rijndael;
}

Поскольку стиль заполнения равен 0 байтам, они не удаляются при расшифровке, поскольку мы не знаем размерисходных данных в этой точке, поэтому дешифрованные данные всегда будут кратны размеру блока независимо от размера исходных данных.Также будет добавлена ​​контрольная сумма до конца.Мы могли бы просто удалить все нулевые байты из хвоста дешифрованного блока, но мы рискнули бы испортить контрольную сумму и исходные данные, если бы это действительно заканчивалось нулевым байтом.

Так что вместо этого мы могли бы работать в обратном направлении на один байтвремя от конца и используйте контрольную сумму для проверки правильности исходных данных.

/// <summary>
/// Remove zero padding by starting at the end of the data block assuming
/// no padding, and using the check sum appended to the end of the data to
/// verify the original data, incrementing padding until we match the 
/// check sum or conclude data is corrupt
/// </summary>
/// <param name="data">Decrypted data block, including zero padding and checksum at end</param>
/// <param name="checkSumLen">Length of the checksum appended to the end of the data</param>
/// <returns>Unencrypted original data without padding and without check sum</returns>
private byte[] RemovePaddingAndCheckSum(byte[] data, int checkSumLen)
{
    byte[] checkSum = new byte[checkSumLen];
    int padding = 0;

    while ((data.Length - checkSumLen - padding) > 0)
    {
        int checkSumStart = data.Length - checkSumLen - padding;
        Buffer.BlockCopy(data, checkSumStart, checkSum, 0, checkSumLen);
        int dataLength = data.Length - checkSumLen - padding;
        byte[] dataClean = new byte[dataLength];
        Buffer.BlockCopy(data, 0 , dataClean, 0, dataLength);

        if (VerifyCheckSum(dataClean, checkSum))
            return dataClean;

        padding++;
    }

    throw new InvalidDataException("Unable to decrypt, check sum does not match");
}

Контрольная сумма в 20 байтов SHA1 может быть проверена по данным просто следующим образом:

private bool VerifySha1Hash(byte[] data, byte[] checkSum)
{
    using (SHA1Managed sha1 = new SHA1Managed())
    {
        var checkSumRedone = sha1.ComputeHash(data);
        return checkSumRedone.SequenceEqual(checkSum);
    }
}

И все, с 128-битным после 3 попыток мы должны получить правильную контрольную сумму и соответствующие исходные данные, которые мы затем возвращаем вызывающей стороне как незашифрованные исходные данные.

...