Перевести Windows RC4 CryptDeriveKey на PHP для openssl - PullRequest
0 голосов
/ 28 декабря 2018

Это второй компонент перевода устаревшей системы, который мы пытались сделать.Нам удалось точно сопоставить исходный двоичный пароль / ключ, который генерирует Windows :: CryptHashData.

Этот пароль / ключ передается в :: CryptDeriveKey, где он выполняет ряд шагов для создания окончательного ключа, который будет использоватьсяby :: CryptEncrypt.Мои исследования привели меня к документации CryptDeriveKey, где четко описываются шаги, необходимые для получения ключа для :: CryptEncrypt, но до сих пор я не смог заставить его расшифровать файл на стороне PHP.https://docs.microsoft.com/en-us/windows/desktop/api/wincrypt/nf-wincrypt-cryptderivekey

На основании документации :: CryptDeriveKey могут быть некоторые дополнительные недокументированные шаги для нашего конкретного размера ключа, которые могут быть не совсем понятны.Текущий Windows :: CryptDeriveKey по умолчанию установлен для ZERO SALT, что, очевидно, несколько отличается от NO_SALT.См. Функциональную ценность соли здесь: https://docs.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality

Параметры в CryptAPI для нашей устаревшей системы следующие:

Тип провайдера: PROV_RSA_FULL

Имя провайдера: MS_DEF_PROV

Алгоритм ID CALG_RC4

Описание Алгоритм шифрования потока RC4

Длина ключа: 40 бит.

Длина соли: 88 бит.ZERO_SALT

Специальное примечание: 40-битный симметричный ключ с нулевой солью, однако, не эквивалентен 40-битному симметричному ключу без соли.Для совместимости ключи должны быть созданы без соли.Эта проблема возникает из-за условия по умолчанию, которое возникает только с ключами, длина которых точно равна 40 битам.

Я не собираюсь экспортировать ключ, но воспроизвести процесс, который создает окончательный ключ шифрования, который передается в :: CryptEncryptдля алгоритма шифрования RC4 и заставить его работать с openssl_decrypt.

Вот текущий код Windows, который отлично работает для шифрования.

try {
    BOOL bSuccess;
    bSuccess = ::CryptAcquireContextA(&hCryptProv, 
                                      CE_CRYPTCONTEXT, 
                                      MS_DEF_PROV_A, 
                                      PROV_RSA_FULL, 
                                      CRYPT_MACHINE_KEYSET);

    ::CryptCreateHash(hCryptProv, 
                      CALG_MD5, 
                      0, 
                      0, 
                      &hSaveHash);

    ::CryptHashData(hSaveHash, 
                    baKeyRandom, 
                    (DWORD)sizeof(baKeyRandom), 
                    0);

    ::CryptHashData(hSaveHash, 
                    (LPBYTE)T2CW(pszSecret), 
                    (DWORD)_tcslen(pszSecret) * sizeof(WCHAR), 
                     0);

    ::CryptDeriveKey(hCryptProv, 
                     CALG_RC4, 
                     hSaveHash, 
                     0, 
                     &hCryptKey);

    // Now Encrypt the value
    BYTE * pData = NULL;
    DWORD dwSize = (DWORD)_tcslen(pszToEncrypt) * sizeof(WCHAR); 
    // will be a wide str
    DWORD dwReqdSize = dwSize;

    ::CryptEncrypt(hCryptKey, 
                   NULL, 
                   TRUE, 
                   0, 
                   (LPBYTE)NULL, 
                   &dwReqdSize, 0);

    dwReqdSize = max(dwReqdSize, dwSize);

    pData = new BYTE[dwReqdSize];

    memcpy(pData, T2CW(pszToEncrypt), dwSize);

    if (!::CryptEncrypt(hCryptKey, 
                        NULL, 
                        TRUE, 
                        0, 
                        pData, 
                        &dwSize, 
                        dwReqdSize)) {

            printf("%l\n", hCryptKey);
            printf("error during CryptEncrypt\n");
            }

    if (*pbstrEncrypted)
    ::SysFreeString(*pbstrEncrypted);
    *pbstrEncrypted = ::SysAllocStringByteLen((LPCSTR)pData, dwSize);
    delete[] pData;
    hr = S_OK;
}

Вот код PHP, который пытается реплицировать:Функция CryptDeriveKey, как описано в документации.

Пусть n - необходимая длина производного ключа в байтах.Полученный ключ - это первые n байтов значения хеша после того, как CryptDeriveKey завершил вычисление хеша.Если хеш не является членом семейства SHA-2 и требуемый ключ предназначен для 3DES или AES, ключ получается следующим образом:

  1. Формирует 64-байтовый буфер с помощьюповторяя константу 0x36 64 раза.Пусть k будет длиной значения хеша, которое представлено входным параметром hBaseData.Установите первые k байтов буфера в качестве результата операции XOR первых k байтов буфера со значением хеш-функции, которое представлено входным параметром hBaseData.

  2. Форма a64-байтовый буфер, повторяя константу 0x5C 64 раза.Установите первые k байтов буфера в качестве результата операции XOR первых k байтов буфера со значением хеш-функции, которое представлено входным параметром hBaseData.

  3. Хешируйте результатшага 1 с использованием того же алгоритма хеширования, который использовался для вычисления значения хеш-функции, представленного параметром hBaseData.

  4. Хешируйте результат шага 2, используя тот же алгоритм хеширования, что икоторый используется для вычисления значения хеш-функции, представленного параметром hBaseData.

  5. Объединить результат шага 3 с результатом шага 4.

  6. Использоватьпервые n байтов результата шага 5 в качестве производного ключа.

PHP-версия :: CryptDeriveKey.

function cryptoDeriveKey($key){

    //Put the hash key into an array
    $hashKey1 = str_split($key,2);
    $count = count($hashKey1);
    $hashKeyInt = array();

    for ($i=0; $i<$count; $i++){
        $hashKeyInt[$i] = hexdec($hashKey1[$i]);
    }
    $hashKey = $hashKeyInt;

    //Let n be the required derived key length, in bytes.  CALG_RC4 = 40 bits key or 88 salt bytes
    $n = 40/8;

    //Let k be the length of the hash value that is represented by the input parameter hBaseData
    $k = 16;

    //Step 1 Form a 64-byte buffer by repeating the constant 0x36 64 times   
    $arraya = array_fill(0, 64, 0x36);

    //Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value 
    for ($i=0; $i<$k; $i++){
        $arraya[$i] = $arraya[$i] ^ $hashKey[$i];
    }

    //Hash the result of step 1 by using the same hash algorithm as hBaseData
    $arrayPacka = pack('c*', ...$arraya);
    $hashArraya = md5($arrayPacka);

    //Put the hash string back into the array
    $hashKeyArraya = str_split($hashArraya,2);
    $count = count($hashKeyArraya);
    $hashKeyInta = array();
    for ($i=0; $i<$count; $i++){
        $hashKeyInta[$i] = hexdec($hashKeyArraya[$i]);
    }

    //Step 2 Form a 64-byte buffer by repeating the constant 0x5C 64 times. 
    $arrayb = array_fill(0, 64, 0x5C);

    //Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value
    for ($i=0; $i<$k; $i++){
        $arrayb[$i] =  $arrayb[$i] ^ $hashKey[$i];
    }

    //Hash the result of step 2 by using the same hash algorithm as hBaseData    
    $arrayPackb = pack('c*', ...$arrayb);
    $hashArrayb = md5($arrayPackb);

    //Put the hash string back into the array
    $hashKeyArrayb = str_split($hashArrayb,2);
    $count = count($hashKeyArrayb);
    $hashKeyIntb = array();
    for ($i=0; $i<$count; $i++){
        $hashKeyIntb[$i] = hexdec($hashKeyArrayb[$i]);
    }

    //Concatenate the result of step 3 with the result of step 4.
    $combined = array_merge($hashKeyInta, $hashKeyIntb);

    //Use the first n bytes of the result of step 5 as the derived key.
    $finalKey = array();
    for ($i=0; $i <$n; $i++){
        $finalKey[$i] =  $combined[$i];
    }
    $key = $finalKey;

    return $key;
}

Функция расшифровки PHP

function decryptRC4($encrypted, $key){
    $opts = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
    $cypher = ‘rc4-40’;
    $decrypted = openssl_decrypt($encrypted, $cypher, $key, $opts);
    return $decrypted; 
}

Итак, вот большие вопросы:

Кто-нибудь смог успешно реплицировать :: CryptDeriveKey с RC4 в другой системе?

Кто-нибудь знает, чего не хватает в созданном нами PHP-скрипте, который мешает ему создать тот же ключ и расшифровывает зашифрованный файл Windows CryptoAPI с помощью openssl_decrypt?

WhКак и как мы можем создать 88-битную нулевую соль, которая требуется для 40-битного ключа?

Каковы правильные параметры openssl_decrypt, которые бы приняли этот ключ и расшифровали то, что было сгенерировано :: CryptDeriveKey?

Да, мы знаем, что это небезопасно и не используется для паролей или PII.Мы хотели бы отойти от этого старого и небезопасного метода, но нам нужно сделать этот промежуточный шаг - сначала перевести оригинальное шифрование на PHP для взаимодействия с существующими развернутыми системами.Буду признателен за любую помощь или руководство.

1 Ответ

0 голосов
/ 30 декабря 2018

На всякий случай, если кто-нибудь еще пойдет по этому пути, вот ответы на все вышеупомянутые вопросы.

Вы можете реплицировать :: CryptDeriveKey на PHP, используя openssl, но есть некоторые предварительные условия, которые должны быть выполнены насторона окон первая.

CryptDeriveKey ДОЛЖЕН быть установлен в CRYPT_NO_SALT следующим образом:

::CrypeDeriveKey(hCryptProv, CALG_RC4, hSaveHash, CRYPT_NO_SALT, &hCryptKey)

Это позволит вам создать ключ из вашего хэша и сгенерировать соответствующий ключ в PHP, который будет работать на openssl.Если вы не установите никаких параметров соли, вы получите ключ, созданный с помощью неизвестного запатентованного алгоритма соли, который не может быть сопоставлен с другой системой.

Причина, по которой вы должны установить CRYPT_NO_SALT, заключается в том, что и CryptAPI, и openssl имеют собственные алгоритмы соли, и нет способа заставить их соответствовать.Так что солить надо отдельно.Подробнее об этой функциональной соли можно узнать здесь: https://docs.microsoft.com/en-us/windows/desktop/SecCrypto/salt-value-functionality

Вот как должен выглядеть PHP-скрипт для создания эквивалентного ключа доступа для использования openssl.

<?php
$random = pack('c*', 87,194,...........);
$origSecret = 'ASCII STRING OF CHARACTERS AS PASSWORD'; 

//Need conversion to match format of Windows CString or wchar_t*
//Windows will probably be UTF-16LE and LAMP will be UTF-8
$secret = iconv('UTF-8','UTF-16LE', $origSecret);

//Create hash key from Random and Secret
//This is basically a hash and salt process.
$hash = hash_init("md5");
hash_update($hash, $random);
hash_update($hash, $secret);
$key = hash_final($hash);

$key = cryptoDeriveKey($key);
//Convert the key hex array to a hex string for openssl_decrypt
$count = count($key);
$maxchars = 2;
for ($i=0; $i<$count; $i++){
    $key .= str_pad(dechex($key[$i]), $maxchars, "0", STR_PAD_LEFT);
}

ВАЖНО: OpenSSL ожидает, что ключом будут необработанные шестнадцатеричные значения, полученные из хеша, к сожалению, openssl_decrypt () хочет получить то же значение, что и строка или пароль.Для этого вы должны сделать преобразование из шестнадцатеричной строки в этот момент.Здесь есть отличная запись о том, почему вы должны это делать.http://php.net/manual/en/function.openssl-encrypt.php

$opts = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
//Convert key hex string to a string for openssl_decrypt
//Leave it as it is for openssl command line.
$key = hexToStr($key);
$cipher = 'rc4-40';
$encrypted = “the data you want to encrypt or decrypt”;
$decrypted = openssl_decrypt($encrypted, $cipher, $key, $opts);  

echo $decrypted;  //This is the final information you’re looking for


function cryptoDeriveKey($key){
//convert the key into hex byte array as int
    $hashKey1 = str_split($key,2);
    $count = count($hashKey1);
    $hashKeyInt = array();
    for ($i=0; $i<$count; $i++){
        $hashKeyInt[$i] = hexdec($hashKey1[$i]);
    }
    $hashKey = $hashKeyInt;
    //Let n be the required derived key length, in bytes.  CALG_RC4 = 40 bits key with 88 salt bits
    $n = 40/8;
    //Chop the key down to the first 40 bits or 5 bytes.
    $finalKey = array();
    for ($i=0; $i <$n; $i++){
        $finalKey[$i] =  $hashKey[$i];
    }
    return $finalKey;
}


function hexToStr($hex){
    $string='';
    for ($i=0; $i < strlen($hex)-1; $i+=2){
        $string .= chr(hexdec($hex[$i].$hex[$i+1]));
    }
return $string;
}
?>

Если у вас возникают проблемы с получением правильных значений после использования приведенного выше кода, вы можете попробовать экспортировать значение ключа из CryptoAPI и проверить его с помощью командной строки openssl.

Сначала вы должны установить CryptDeriveKey, чтобы разрешить экспорт ключа с помощью CRYPT_EXPORTABLE и CRYPT_NO_SALT

::CrypeDeriveKey(hCryptProv, CALG_RC4, hSaveHash, CRYPT_EXPORTABLE | CRYPT_NO_SALT, &hCryptKey)

Если вы хотите узнать, как отобразить PLAINTEXTKEYBLOB из экспортированного ключа, перейдите по этой ссылке.https://docs.microsoft.com/en-us/windows/desktop/seccrypto/example-c-program--importing-a-plaintext-key

Вот пример экспортированного ключевого двоичного объекта 0x08 0x02 0x00 0x00 0x01 0x68 0x00 0x00 0x00 0x05 0x00 0x00 0x00 0xAA 0xBB 0xCC 0xDD 0xEE

0x08 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00заголовок соответствует почти точно 0x05 0x00 0x00 0x00 // Длина ключа в байтах правильная 5 байтов 0xAA 0xBB 0xCC 0xDD 0xEE // Первые 5 байтов созданного нами ключа хеша !!

Используйте значение экспортированного ключа из BLOB какЗначение шестнадцатеричного ключа в приведенной ниже команде openssl enc.

openssl enc -d -rc4-40 -in testFile-NO_SALT-enc.txt -out testFile-NO_SALT-dec.txt -K "Hex Key Value" -nosalt -nopad

Это расшифрует файл, который был зашифрован на компьютере с Windows, используя CryptEncrypt.

Как вы можете видеть, когда вы устанавливаетеCryptDeriveKey to CRYPT_NO_SALT все, что вам нужно для пароля или ключа openssl, - это первые биты «длины ключа» вашего пароля CryptHashData.Достаточно просто сказать, но настоящая боль, чтобы добраться до.Удачи и надеюсь, что это поможет кому-то еще с устаревшими проблемами перевода Windows.

...