暗号パズル:WebMoneyキーを暗号サービスプロバイダーにインポートする

Windowsシステムの秘密鍵は通常、特別なキーストアに保存されます。 これらのキーの使用は、暗号化プロバイダー(以降CSP)の機能を呼び出すことで行われます。 標準のCSP(Microsoft Base Cryptographic Provider)を使用する場合、ユーザーキーはフォルダーC:\ Users \ [Vasia] \ AppData \ Roaming \ Microsoft \ Cryptoに保存されます。 特殊なデバイスを使用する場合、キーはデバイス自体のメモリに保存されます。

セキュリティを向上させるために、WebMoneyキー(インターフェイスリクエストの署名に使用される.kwm)をCSPにインポートすることが決定されました。 通常、キーを使用してWMインターフェイスへの要求に署名する人は、ファイルシステムの.kwmファイルまたはxml表現として保存します。両方のオプションはあまり安全ではありません。

それほど単純ではないことが判明しました。

決済サービスのセキュリティを強化する際に発生する問題については、以下をご覧ください。



だから。 kwmファイル形式から始めましょう。 ファイルには複雑な形式があります(小さなものが欠けています):

1.整合性をチェックするためのハッシュ。
2.暗号化されたプライベートRSA出展者(D)。
H.暗号化されたRSAモジュール(モジュラス)。

問題番号1:鍵ファイルを復号化した後、必要な8つのパラメーターのうち2つだけを取得します。

Dおよびモジュラス、必要:指数、モジュラス、P、Q、DP、DQ、InverseQ、D(CSPへのインポート用のキーをもたらす必要がある構造の詳細については、MSDN msdn.microsoft.com/en-を参照してください) us /ライブラリ/ Aa387401 )。

E(開指数)がわかっていれば、これらのパラメーターをすべて見つけることは難しくありません。 通常、EはFermat番号のいずれかを取ります:17、257、65537または4294967297。チェックは非常に簡単です:

1.任意のメッセージメッセージをパラメーターDとモジュラスで暗号化します。

encrypted = message^D % Modulus


2. Exponent(想定)およびModulusを使用して復号化を試行します。

message = encrypted^Exponent % Modulus


復号化後に受信したメッセージが元のメッセージと一致した場合、指数は正しく選択されていました。

注:以下では、演算子「^」はべき乗であり、演算子「%」は除算の剰余です

問題番号2:私たちは公開出展者を知りません、そしてそれは非常に大きいです。

残念ながら、これらのフェルマー数のいずれも、公開指数として登場しませんでした。 これは少し動揺です。 さらに、この数がまだ大きすぎないことを期待して-4バイト未満のすべての数が試行されました(ブルートフォース)。 それらのどれも現れなかった。 それは行き止まりのようで、この考えを忘れることができます...

これは終了した可能性があります。 オープンな出展者を獲得することはできませんでした。数十年(または数百年)強引にそれを強要しました。

しかし、話は続きます。 dの値が小さい場合に、RSAシステムをハックする方法を考案したMichael J. Wienerのような人が住んでいました。 少し逆のことがあります:開いた指数の小さな値ですが、これはわかりません。

実際、RSAに対するWienerの攻撃を使用すると、キーのオープン指数をほとんど瞬時に見つけることができます(それほど長くない場合)。

起こったことは次のとおりです。

元のDおよびモジュラス値(kwmファイルから取得):

D: 19715AB67A97257C5C80C8CD8F97448199F6F3FF8A3724DEA911C32CB5E64395D3175D6112A51DC14911FBA4E8FD107C1C65BE062A3491B1131168DF423408E2593

Modulus: 789BE5F2D0C90430EAEFC640B752FE707D75EB12C9C76F776C981014C1825C48989F15F5F53AFBF9B9C11D5C9AF184CC4F3938A48045414F814636C1275321F3AB9


RSAに対するWienerの攻撃を使用して即座に復元された公開出展者:

Exponent: 21EA463DEB0B


それは小さな問題のように思えました。受け取ったパラメーターを秘密鍵BLOBの構造に持ち込み、任意の暗号プロバイダーにインポートします。 しかし、それほど単純ではありません...

問題3:公開指数は4バイトより長く、秘密鍵BLOB形式では、最大4バイト(4バイトを含む)までの指数のみが許可されます。

なんてこった! 問題には解決策がないように思われ、それを忘れることができます(2回目)。

とはいえ、結局のところ、暗号システムのパラメーター(特に、素数pとq)があります。 したがって、これを行うことができます。

1.元のパラメーターPおよびQを使用します。

2.適切な公開指数Exponent2(4バイト以内)を選択します。 フェルマー番号65537を取得します。

3.閉指数D2を計算します(eモジュロ(P-1)に乗算的に逆*)
(Q-1))。 D2は、kwmファイルの元のDとは異なります。

4.パラメーターを計算します:DP2、DQ2、InverseQ2。 これらは、 中国の剰余定理を使用して署名をすばやく取得するために必要です。

5.最も重要なこと! 元のD(元のキーkwmにある)と変更された暗号システムのD2(つまりD2-D)の差を計算します。

これで、変更されたキーをCSPストレージ(eToken PROなど)にプライベートRSAキーとして保存し、偏差をPKCS#11オブジェクトとして保存できます。 Microsoft CSPを使用する場合、偏差をユーザーのリポジトリに保存できます。

原則として、偏差の値は秘密ではありませんが、変更されたキー(CSPに格納されています)は秘密です。

変更されたキーをCSPにインポートするC#の例を次に示します。

public void ImportPrivateKey( byte [] modulusBytes, byte [] privateExponentBytes)
{
//

var modulus = new BigInteger(modulusBytes);
var d = new BigInteger(privateExponentBytes);

var p = Wiener.Calculate(d, modulus); // RSA P ( )
var q = modulus/p;
var f = (p - 1)*(q - 1); //
var e = new BigInteger( new byte [] {1, 0, 1}); // 65537

var d2 = e.modInverse(f); // d2 --

var dp2 = d2%(p - 1);
var dq2 = d2%(q - 1);
var iq = q.modInverse(p);

var rsaParameters = new RSAParameters
{
D = toByteArray(d2),
DP = toByteArray(dp2),
DQ = toByteArray(dq2),
Exponent = toByteArray(e),
InverseQ = toByteArray(iq),
Modulus = toByteArray(modulus),
P = toByteArray(p),
Q = toByteArray(q)
};

BigInteger deviation;
byte flag; // d > d2,

if (d > d2)
{
deviation = d - d2;
flag = 0;
}
else
{
deviation = d2 - d;
flag = 1;
}

// CSP ( eToken, CSP "eToken Base Cryptographic Provider")
var cspParameters = new CspParameters
{
Flags =
CspProviderFlags.UseNonExportableKey | CspProviderFlags.UseUserProtectedKey,
KeyNumber = ( int )KeyNumber.Exchange,
KeyContainerName = _containerName
};

//
RSACryptoServiceProvider.UseMachineKeyStore = false ;

using ( var cryptoServiceProvider = new RSACryptoServiceProvider(cspParameters))
{
//
cryptoServiceProvider.ImportParameters(rsaParameters);
}

// deviation ( )
Storage.SaveDataInStorage(_containerName, toByteArray(deviation));
Storage.SaveDataInStorage(_containerName + "_flag" , new [] {flag});
}


* This source code was highlighted with Source Code Highlighter .


メッセージメッセージの署名を受信する方法

元の署名は次のように取得されます。

signature = hash ^ D % Modulus


現在Dはありませんが、D2と偏差があり、D2 +偏差= Dです。D2への直接アクセスはありません。 CSPによって管理されます。標準関数を呼び出すことによってのみ、このD2の署名を取得できます。 代数を思い出してください:

a^(b+c) = a^b * a^c


この法則はモジュラー代数にも適用されます。 したがって、元の署名(Dの知識なし)は次のようになります。

part1 = hash ^ D2 % Modulus

part2 = hash ^ deviation % Modulus

signature = part1 * part2 % Modulus


利益! 現在、キーはCSPによって管理されています(ハードウェアデバイスの場合、これはかなり信頼できます)。 少し迷惑-全体が「適合しませんでした」、まだ偏差の形で「部分」がありました。 しかし、この逸脱は秘密ではありません。D2の知識がなければ、実際には元のDについての手がかりは得られません。

しかし、急いでリラックスしないでください。

問題4:署名されるデータ構造が、一般に受け入れられているものと異なります。

Win CAPIによって取得された署名を復号化すると(公開指数とモジュールを使用して復号化できます)、次のような構造が表示されます。

1FFFFFFFFFFFFFFFFFFFFFFFF003031300D0609608648016503040201050004209F64A747E1B97F131FABB6B447296C9B6F0201E79FB3C5356E6C77E89B6A806


最初は標準ヘッダーで、最後は署名付きメッセージのハッシュの32バイトです(SHA-256で引用)。

WebMoney Signerの署名は異なります。最初のバイトは乱数で埋められ、次に16バイトのMD4ハッシュ、次に2バイトのヘッダーが埋め込まれます。

問題は、CSPがモジュラー秘密鍵べき乗の直接機能をサポートしていないことです(可能な場合は解決されていました)。 許可のみ:

1.メッセージに署名します。 明示的に私たちに適していない WebMoneyは署名を認識しません-構造が異なります。

2.メッセージを暗号化します。 繰り返しますが、暗号化の一種(元のメッセージは変更され、乱数が追加されます)-私たちの形式と一致しません。

再びデッドロック。 とはいえ、もしCSPを実際のハッシュではなく、必要な形式でデータを挿入する擬似ハッシュをスリップしたらどうなるでしょうか? ハッシュ関数は可逆ではないため、ハッシュが本物かどうかを確認する方法はありません。

これを行うには、かなり長いハッシュを持つアルゴリズムを選択する必要があります:16バイトのMD4と2バイトのヘッダーの両方が収まるようにし、セキュリティのためにデータを乱数で補完するのも良いでしょう。

SHA-512は大きすぎることが判明しましたが、SHA-256はちょうどよかったです。

署名を生成するための関数は次のとおりです。

public string Sign( string value )
{
if ( string .IsNullOrEmpty( value ))
throw new ArgumentNullException( "value" );

var toSign = new byte [32]; // - SHA256

var random = new byte [14]; //
var hash = getHash( value ); // MD4
var prefix = new byte [] {0, 56}; // WebMoney

var rngCryptoServiceProvider = new RNGCryptoServiceProvider();
rngCryptoServiceProvider.GetBytes(random);

Buffer.BlockCopy(random, 0, toSign, 0, random.Length);
Buffer.BlockCopy(hash, 0, toSign, random.Length, hash.Length);
Buffer.BlockCopy(prefix, 0, toSign, random.Length + hash.Length, prefix.Length);

var cspParameters = new CspParameters
{
Flags =
CspProviderFlags.UseNonExportableKey | CspProviderFlags.UseUserProtectedKey,
KeyNumber = ( int ) KeyNumber.Exchange,
KeyContainerName = _containerName
};

byte [] signature1;
BigInteger modulus;

using ( var rsaCryptoServiceProvider = new RSACryptoServiceProvider(528, cspParameters))
{
signature1 = rsaCryptoServiceProvider.SignHash(toSign, CryptoConfig.MapNameToOID( "SHA256" ));
modulus = new BigInteger(rsaCryptoServiceProvider.ExportParameters( false ).Modulus);
}

var deviation = new BigInteger(Storage.LoadDataFromStorage(_containerName));

// SHA-256
var header = new byte []
{
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
0x30,
0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05,
0x00, 0x04, 0x20
};

var toEncrypt = new byte [65];

Buffer.BlockCopy(header, 0, toEncrypt, 0, header.Length);
Buffer.BlockCopy(random, 0, toEncrypt, header.Length, random.Length);
Buffer.BlockCopy(hash, 0, toEncrypt, header.Length + random.Length, hash.Length);
Buffer.BlockCopy(prefix, 0, toEncrypt, header.Length + random.Length + hash.Length, prefix.Length);

byte flag = Storage.LoadDataFromStorage(_containerName + "_flag" )[0];

var part1 = new BigInteger(signature1);
BigInteger part2;

if (1 == flag)
{
// -- --
part2 = new BigInteger(toEncrypt).modInverse(modulus).modPow(deviation, modulus);
}
else
part2 = new BigInteger(toEncrypt).modPow(deviation, modulus);

var signature = toByteArray((part1*part2)%modulus);

// little-endian
Array.Reverse(signature);

//
var uResult = new ushort [KeyBytesLength/2];

Buffer.BlockCopy(signature, 0, uResult, 0, signature.Length);

var stringBuilder = new StringBuilder ();

for ( int pos = 0; pos < uResult.Length; pos++)
stringBuilder.Append( string .Format(CultureInfo.InvariantCulture, "{0:x4}" , uResult[pos]));

return stringBuilder.ToString();
}


* This source code was highlighted with Source Code Highlighter .


これですべてが時計のように機能します。キーはWindowsシステムで実行されている暗号化デバイス(eToken、ruTokenなど)と互換性があり、署名は有効です。

PS
著者から。

誰かが「簡単な方法」を選択しなかった理由を理解するのはおそらく難しいでしょう。 私は説明します:私は暗号パズルが大好きです。 パズルを解く人がいますが、この形式のパズルが好きです。

Source: https://habr.com/ru/post/J111928/


All Articles