メモリの上書き-なぜですか?

Win32 APIの腸には、非常に簡潔な説明を持つSecureZeroMemory関数があります。この関数から、この関数はメモリ領域をゼロで上書きし、コードを最適化するときにコンパイラがこの関数の呼び出しを決して削除しないように設計されています。 また、この関数を使用して、パスワードと暗号化キーを保存するために以前に使用したメモリを上書きする必要があることも示しています。



1つの質問が残っています-これはなぜですか? プログラムメモリをスワップファイル、休止状態ファイル、またはクラッシュダンプに書き込むリスクについて、攻撃者が見つけることができる長い引数を見つけることができます。 これは妄想に似ています-すべての攻撃者がこれらのファイルに手を出す機会を持っているわけではありません。



実際、プログラムが上書きするのを忘れたデータにアクセスする機会がはるかに多くあります-マシンにアクセスする必要さえない場合もあります。 さらに例を検討し、パラノイアの正当性を誰もが判断します。



すべての例は、疑わしいことにC ++に似た擬似コードになります。 多くの文字があり、あまりきれいなコードではないので、きれいなコードでは状況はそれほど良くないことが明らかになります。



だから。 遠距離機能では、暗号化キー、パスワード、またはクレジットカード番号(以降-単なる秘密)を取得し、それを使用して上書きしません。

 { const int secretLength = 1024; WCHAR secret[secretLength] = {}; obtainSecret( secret, secretLength ); processWithSecret( what, secret, secretLength ); }
      
      







前の関数とはまったく関係のない別の関数では、プログラムインスタンスが別のインスタンスから何らかの名前のファイルを要求します。 これには、RPCが使用されます。これは、恐竜のような古代の技術で、多くのプラットフォームに存在し、Windowsがプロセス間およびマシンの対話を実装するために広く使用されています。



通常、RPCを使用するには、IDLインターフェイスの説明を記述する必要があります。 次のような方法を説明します。

 //MAX_FILE_PATH == 1024 error_status_t rpcRetrieveFile( [in] const WCHAR fileName[MAX_FILE_PATH], [out] BYTE_PIPE filePipe );
      
      



ここで、2番目のパラメーターは特別なタイプであり、任意の長さのデータストリームを送信できます。 最初のパラメーターは、ファイル名の下の文字の配列です。



この記述はMIDLコンパイラーによってコンパイルされ、関数を含むヘッダーファイル(.h)になります

 error_status_t rpcRetrieveFile ( handle_t IDL_handle, const WCHAR fileName[1024], BYTE_PIPE filePipe);
      
      





ここで、MIDLはユーティリティパラメーターを追加しました。2番目と3番目のパラメーターは前の説明と同じです。



この関数を呼び出します:

 void retrieveFile( handle_t binding ) { WCHAR remoteFileName[MAX_FILE_PATH]; retrieveFileName( remoteFileName, MAX_FILE_PATH ); CBytePipeImplementation pipe; rpcRetrieveFile( binding, remoteFileName, pipe ); }
      
      



すべて正常です-retrieveFileName()はMAX_FILE_PATH − 1以下の長さの行を受け取り、ヌル文字で終了します(ヌル文字を忘れていません)、着呼側はその行を受け取って処理します-ファイルへのフルパスを受け取り、それを開いてデータを転送します。



誰もが楽観的で、このコードで製品のいくつかのリリースが行われていますが、象にまだ気づいていません。 ここに象。 C ++の観点から、関数パラメーター

 const WCHAR fileName[1024]
      
      



これは配列ではなく、配列の最初の要素へのポインターです。 rpcRetrieveFile()関数は、同じMIDLによって生成される単なるレイヤーです。 すべてのパラメーターをパックし、常に同じWinAPI NdrClientCall2()関数を呼び出します。これは、「Windows、これらのパラメーターを使用してRPC呼び出しを行ってください」という意味で、パラメーターをNdrClientCall2()関数のリストに渡します。 最初のパラメーターの1つは、IDLに記述されているMIDLによって生成されるフォーマット文字列です。 古き良きprintf()と非常によく似ています。



NdrClientCall2()は、受信したフォーマット文字列を注意深く調べ、反対側への送信用のパラメーターをパックします(これはマーシャリングと呼ばれます)。 各パラメーターの横には、そのタイプが示されています-各パラメーターは、タイプに応じてパッケージ化されています。 この場合、fileNameパラメーターには、配列の最初の要素のアドレスが指定され、型は「WCHAR型の1024要素の配列」です。



これで、コード内で2つの呼び出しが連続して行われます。

 processWithSecret( whatever ); retrieveFile( binding );
      
      



processWithSecret()関数は、スタックに秘密を保存するために2キロバイトを消費し、終了時にそれらを忘れます。 次に、retrieveFile()関数が呼び出され、18文字(18文字+最後のゼロ-わずか19、つまり38バイト)の長さのファイル名を取得します。 ファイル名は再びスタックに保存され、おそらく、最初の関数で密かに使用されたのとまったく同じメモリ領域になります。



次に、リモートコールが行われ、パッケージング機能がアレイ全体(38バイトではなく2048)をパケットに忠実にパックし、このパケットがネットワークを介して送信されます。



非常に予期しない



秘密はネットワークを介して送信されます。 プログラムは、ネットワーク上で秘密を送信することすら計画していませんでしたが、送信されます。 このような欠陥は、ページファイルを表示するよりも「使用」する方がはるかに便利です。 今誰が妄想ですか?



上記の例はかなり複雑に見えます。 codepad.orgで試すことができる同様のコードを次に示します

 const int bufferSize = 32; void first() { char buffer[bufferSize]; memset( buffer, 'A', sizeof( buffer ) ); } void second() { char buffer[bufferSize]; memset( buffer, 'B', bufferSize / 2 ); printf( "%s", buffer ); } int main() { first(); second(); }
      
      



あいまいな動作をします。 投稿を書いている時点では、作業の結果は16文字の「B」と16文字の「A」の文字列です。



今は、誰もが普通の配列を頭の中で使っていない、熊手や松明、怒りの叫び声を振る時です。std:: vector、std :: string、Universal Universalクラスを使用する必要があります。 9000以上のコメント。



実際、これはここでは役に立ちません。RPC腸内のパッケージング関数は、呼び出しコードがそこに書き込んだよりも多くのデータを読み取ります。 その結果、最も近いアドレスのデータが読み取られるか、メモリが正しくアクセスされなかった場合に(場合によっては)エラーが発生します。 これらの最も近いアドレスには、ネットワークを介して送信できないデータが含まれている可能性があります。



ここで誰のせいですか? いつものように、開発者は責任を負います-rpcRetrieveFile()関数が受け取ったパラメーターをどのように扱うかを誤解していました。 その結果、未定義の動作が発生し、この場合、ネットワークを介した制御されないデータ伝送が発生します。 これは、RPCインターフェイスを変更して両側でコードを編集するか、十分に大きなサイズの配列を使用してパラメーターをコピーする前に完全に書き換えることで修正できます。



SecureZeroMemory()はこの状況で役立ちます-最初の関数が完了前にシークレットを書き換えた場合、2番目の関数のエラーは少なくとも書き換えられた配列の転送につながります。 ダーウィン賞を獲得するのは難しいです。



ドミトリー・メッシェリャコフ、

開発者製品部門



All Articles