C ++:狡猾さと愛、それとも何がうまくいかないのか?





「Cを使用すると、足を簡単に撃つことができます。C ++でこれを行うのは難しいですが、足全体が外れます。」-BjörnStroustrup、C ++クリエーター。


この記事では、安定した、安全で信頼性の高いコードを作成する方法と、実際に意図せずに完全に破壊することがいかに簡単であるかを示します。このために、私たちは最も有用で魅力的な資料を収集しようとしました。







SimbirSoftでは、Secure Code Warriorプロジェクトと緊密に連携して、安全なソリューションを作成するために他の開発者をトレーニングしています。特にHabrについては、CodeProject.comポータル用に著者が書いた記事を翻訳しまし



だからコードに!



これは抽象的なC ++コードの小さな部分です。このコードは、非常に実際のプロジェクトで見つかる可能性のあるあらゆる種類の問題と脆弱性を示すために特別に作成されました。ご覧のとおり、これはWindows DLLコードです(これは重要なポイントです)。誰かがこのコードを何らかの(もちろん安全な)ソリューションで使用するとします。



コードを詳しく見てみましょう。あなたの意見では、何がうまくいかない可能性がありますか?



コード
class Finalizer
{
    struct Data
    {
        int i = 0;
        char* c = nullptr;
        
        union U
        {
            long double d;
            
            int i[sizeof(d) / sizeof(int)];
            
            char c [sizeof(i)];
        } u = {};
        
        time_t time;
    };
    
    struct DataNew;
    DataNew* data2 = nullptr;
    
    typedef DataNew* (*SpawnDataNewFunc)();
    SpawnDataNewFunc spawnDataNewFunc = nullptr;
    
    typedef Data* (*Func)();
    Func func = nullptr;
    
    Finalizer()
    {
        func = GetProcAddress(OTHER_LIB, "func")
        
        auto data = func();
        
        auto str = data->c;
        
        memset(str, 0, sizeof(str));
        
        data->u.d = 123456.789;
        
        const int i0 = data->u.i[sizeof(long double) - 1U];
        
        spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")
        data2 = spawnDataNewFunc();
    }
    
    ~Finalizer()
    {
        auto data = func();
        
        delete[] data2;
    }
};

Finalizer FINALIZER;

HMODULE OTHER_LIB;
std::vector<int>* INTEGERS;

DWORD WINAPI Init(LPVOID lpParam)
{
    OleInitialize(nullptr);
    
    ExitThread(0U);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    static std::vector<std::thread::id> THREADS;
    
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            CoInitializeEx(nullptr, COINIT_MULTITHREADED);
            
            srand(time(nullptr));
            
            OTHER_LIB = LoadLibrary("B.dll");
            
            if (OTHER_LIB = nullptr)
                return FALSE;
            
            CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
        break;
        
        case DLL_PROCESS_DETACH:
            CoUninitialize();
            
            OleUninitialize();
            {
                free(INTEGERS);
                
                const BOOL result = FreeLibrary(OTHER_LIB);
                
                if (!result)
                    throw new std::runtime_error("Required module was not loaded");
                
                return result;
            }
        break;
        
        case DLL_THREAD_ATTACH:
            THREADS.push_back(std::this_thread::get_id());
        break;
        
        case DLL_THREAD_DETACH:
            THREADS.pop_back();
        break;
    }
    return TRUE;
}

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
{
    for (int i : integers)
        i *= c;
    
    INTEGERS = new std::vector<int>(integers);
}

int Random()
{
    return rand() + rand();
}

__declspec(dllexport) long long int __cdecl _GetInt(int a)
{
    return 100 / a <= 0 ? a : a + 1 + Random();
}




おそらく、このコードは単純で、明白で、十分に安全だと思いましたか?または多分あなたはそれにいくつかの問題を見つけましたか?それとも、1、2、2つですか?



このスニペットには、実際にさまざまな程度の重要性を持つ43を超える潜在的な脅威があります







注意すべき点



1)sizeof(d)(dはlong double)は、必ずしもsizeof(int)の倍数である必要はありません。



int i[sizeof(d) / sizeof(int)];


この状況は、ここではテストも処理もされていません。たとえば、一部のプラットフォームでは、long doubleが10バイトになる場合があります(これは、MS VSコンパイラは当てはまりません、以前はC ++ Builderと呼ばれていたRADStudioには当てはまります)。intは、プラットフォームに応じてサイズが異なる場合もあります(上記のコードは、Windows用であるため、この特定の状況に関連して、問題は多少不自然ですが、ポータブルコードの場合、この問題は非常に関連性があります)。 いわゆるタイピングパンを使用したい場合、これらすべてが問題になる可能性があります。ちなみに、それは未定義の動作を引き起こします







C ++言語標準に準拠。ただし、最近のコンパイラは通常、特定のケースに対して正しい予想される動作を定義するため、タイピングパン使用するのが一般的な方法です(たとえば、GCC定義するように)。出典:Medium.com ちなみに、C ++とは異なり、現代のCでは、タイピングパンは完全に受け入れられます(C ++とCは異なる言語であり、C ++を知っている場合は、Cを知っていると期待すべきではありません。逆ですよね?)解決策:static_assertを使用します















コンパイル時にそのようなすべての仮定を制御します。タイプサイズに問題がある場合は警告が表示されます。



static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);


2)time_tはマクロであり、Visual Studioでは、32ビット(古い)または64ビット(新しい)の整数型を参照できます。



time_t time;


2つのバイナリがこのタイプの異なる物理表現でコンパイルされている場合、異なる実行可能モジュール(たとえば、実行可能ファイルとそれがロードするDLL)からこのタイプの変数にアクセスすると、オブジェクトの境界外で読み取り/書き込みが発生する可能性があります。これにより、メモリの破損やガベージの読み取りが発生します。







解決策:すべてのモジュール間のデータ交換に、厳密に定義された同じタイプのサイズが使用されていることを確認してください。



int64_t time;


3)B.DLL(で格納されているのハンドルOTHER_LIB変数)していない、まだされて私たちは、このライブラリの関数のアドレスを取得することはできませんので、我々は上記の変数にアクセスするときにロードされた



4)静的オブジェクト(の初期化順序に問題SIOF):(OTHER_LIBオブジェクト初期化される前にコードで使用されていました)



func = GetProcAddress(OTHER_LIB, "func");


FINALIZERDllMain関数を呼び出す前に作成される静的オブジェクトです。そのコンストラクターでは、まだロードされていないライブラリーを使用しようとしています。この問題は、静的FINALIZERによって使用される静的OTHER_LIBダウンストリームの変換ユニットに配置されるという事実によって悪化ます。これは、後で初期化(ゼロ化)されることも意味します。つまり、アクセス時に、疑似ランダムガベージが含まれますWinAPI一般に、これに正常に反応するはずです。なぜなら、高い確率で、そのような記述子を持つロードされたモジュールがまったく存在しないからです。そして、絶対に信じられないほどの偶然が起こったとしても、それが「Func」という名前の関数を含む可能性は低いです



解決策:一般的なアドバイスは、特にDLLで相互に依存している場合は特に、グローバルオブジェクト、特に複雑なオブジェクトの使用を避けることです。ただし、何らかの理由でそれでも必要な場合は、初期化の順序に細心の注意を払ってください。この順序制御するには、グローバルオブジェクトのすべてのインスタンス(定義)を1つの 変換単位に配置しますそれらが正しく初期化されることを保証するために正しい順序で。



5)以前に返された結果は使用前にチェックされません



auto data = func();


funcは関数ポインタです。また、B.dllの関数を指している必要があります。ただし、前の手順ですべてを完全に失敗したため、これはnullptrになります。したがって、予期された関数呼び出しの代わりに、それを逆参照しようとすると、アクセス違反または一般的な保護障害などが発生します。



解決策:外部コード(この場合はWinAPI)を操作するときは、呼び出された関数の戻り結果を常に確認してください。信頼性が高く、障害に強いシステムの場合、このルールは、[何をいつ返すかについて]厳密な契約が結ばれている機能にも適用されます。



6)異なる配置/パディング設定でコンパイルされたモジュール間でデータを交換するときのガベージの読み取り/書き込み



auto str = data->c;


データ構造(通信モジュール間で情報を交換するために使用される)にこれらの同じモジュールが異なる物理的表示である場合、前述のすべてのアクセス違反エラーメモリ保護障害セグメンテーションヒープ破損などが発生します。または、ゴミを読むだけです。正確な結果は、このメモリの実際の使用シナリオによって異なります。構造自体に明示的な配置/パディング設定がないため、これはすべて発生する可能性があります。したがって、コンパイル時のこれらのグローバル設定が相互作用するモジュールで異なる場合、問題が発生します。







決定:すべての共有データ構造が強力で明示的に定義された明白な物理的表現(固定サイズタイプ、明示的に指定された配置などを使用)を持っていること、および/または相互運用可能なバイナリが同じグローバル配置設定でコンパイルされていることを確認してください/ 充填。



も参照してください
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7)配列自体のサイズではなく、配列へのポインタのサイズを使用する



memset(str, 0, sizeof(str));


これは通常、些細なタイプミスの結果です。ただし、この問題は、静的多態性を処理する場合、またはautoキーワードを無意識に使用する場合特に明らかに使いすぎている場合)にも発生する可能性があります。ただし、最新のコンパイラは、内部静的アナライザの機能を使用して、コンパイル時にこのような問題を検出するのに十分なほどスマートであることを期待したいと思います



決定:



  • sizeof(<完全なオブジェクトタイプ>)とsizeof(<オブジェクトポインタタイプ>)を混同しないでください
  • コンパイラの警告を無視しないでください;

  • typeid、constexpr、およびstatic_assert組み合わせて、C ++ボイラープレートの魔法を少し使用して、コンパイル時に型が正しいことを確認することもできます(ここでは、型の特性、特にstd :: is_pointerも役立ちます)。


8)以前に値を設定するために使用されたものとは異なるユニオンフィールドを読み取ろうとしたときの未定義の動作



9)long doubleの長さがバイナリモジュール間で異なる場合、範囲外で読み取ろうとする可能性があります



const int i0 = data->u.i[sizeof(long double) - 1U];


これはすでに前述したので、ここで前述の問題の別の存在点を取得しました。



解決策:コンパイラーが正しく処理していることが確実でない限り、前に設定したフィールド以外のフィールドを参照しないでください。共有オブジェクトタイプのサイズが、相互作用するすべてのモジュールで同じであることを確認してください。



も参照してください
Type-punning and strict-aliasing

What is the Strict Aliasing Rule and Why do we care?



10)B.dllが正しくロードされ、「func」関数が正しくエクスポートおよびインポートされた場合でも、この時点でB.dllはメモリからアンロードされます(FreeLibraryシステム関数は以前にDllMainコールバック関数のDLL_PROCESS_DETACHセクションで呼び出されていたため) )



auto data = func();


以前に破棄されたポリモーフィックタイプのオブジェクトで仮想関数を呼び出すだけでなく、すでにアンロードされている動的ライブラリで関数を呼び出すと、純粋な仮想呼び出しエラーが発生する可能性があります



解決策:アプリケーションに正しいファイナライズ手順を実装して、すべてのDLLが正しい順序で終了/アンロードされるようにします。避けに複雑なロジックで静的オブジェクトを使用してDL(ライブラリがそのライフサイクルの最後の段階に入る-その静的オブジェクトを破壊する相)のDllMain / DLL_PROCESS_DETACHを呼び出した後、ライブラリ内の任意の操作を実行L.は避けてください。



DLLのライフサイクルを理解する必要があります。



) LoadLibrary



  • ( , )
  • DllMain -> DLL_PROCESS_ATTACH ( , )
  • [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
  • , , (, ),
  • ( / , , )
  • , ()
  • ( / , , )
  • - : ,


) FreeLibrary



  • DllMain -> DLL_PROCESS_DETACH ( , )
  • ( , )






11)不透明なポインタの削除コンパイラはデストラクタを呼び出すために完全な型を知る必要があるため、不透明なポインタを使用してオブジェクトを削除すると、メモリリークやその他の問題が発生する可能性があります)



12)DataNewデストラクタが仮想の場合、クラスが正しくエクスポートおよびインポートされ、完全である場合でもそれに関する情報は、とにかくこの段階でそのデストラクタを呼び出すことは問題です-これはおそらく純粋に仮想の関数呼び出しにつながるでしょう(DataNewタイプはすでにアンロードされたB.dllファイルからインポートされるため)。この問題は、デストラクタが仮想でない場合でも発生する可能性があります。



13)DataNewクラスが抽象多態性タイプの場合、およびその基本クラスには、本体のない純粋な仮想デストラクタがありますいずれの場合も純粋な仮想関数呼び出しが発生します



14)メモリがnewを使用して割り当てられ、delete []を使用して削除された場合の未定義の動作



delete[] data2;


一般に、外部モジュールから取得したオブジェクトを解放するときは、常に注意する必要があります。破壊されたオブジェクトへのポインタゼロにすること



も良い習慣です。決定:







  • オブジェクトを削除するときは、その完全なタイプを知っている必要があります
  • すべての破壊者は体を持っている必要があります
  • コードのエクスポート元のライブラリを早めにアンロードしないでください
  • 常に新しいフォームを使用して正しく削除し、混同しないでください
  • リモートオブジェクトへのポインタはゼロにする必要があります。






また、次の点に注意してください

- voidへのポインタに削除呼び出すとなり、未定義の動作で

、純粋仮想関数すべきではないコンストラクタから呼び出される

コンストラクタで仮想関数を呼び出す-ではありません仮想

-回避しようと手動メモリ管理を-使用するコンテナ移動セマンティクスを、そしてスマートポインター



も参照してください
Heap corruption: What could the cause be?



15)ExitThreadは、Cでスレッドを終了するための推奨される方法です。C++では、この関数を呼び出すと、ローカルオブジェクトのデストラクタ(およびその他の自動クリーンアップ)を呼び出す前にスレッドが終了するため、C ++でスレッドを終了するには、スレッド関数から戻るだけです。



ExitThread(0U);


解決策: C ++コードでこの関数を手動で使用しないでください。



16)DllMainの本体でKernel32.dll以外のシステムDLLを必要とする標準関数を呼び出すと、さまざまな診断が難しい問題が発生する可能性があります。



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


DllMainのソリューション:



  • 複雑な(de)初期化を回避する
  • 他のライブラリから関数を呼び出さないでください(または少なくともこれには非常に注意してください)






17)マルチスレッド環境での疑似乱数ジェネレーターの誤った初期化



18)time関数によって返される時間の解像度は1秒であるため、この期間中にこの関数を呼び出すプログラム内のスレッドは、出力で同じ値を受け取ります。この番号を使用してPRNGを初期化すると、衝突が発生する可能性があります(たとえば、一時ファイルに同じ疑似ランダム名を生成したり、同じポート番号を生成したりするなど)。考えられる解決策の1つは、結果の結果を、ヒープ内のスタックまたはオブジェクトのアドレス、より正確な時間などの疑似ランダム値と混合(xor)することです



srand(time(nullptr));


解決策: MS VSでは、スレッドごとにPRNGの初期化が必要です。さらに、初期化子としてUnix時間使用するとエントロピーが不十分になるためより高度な初期化値の生成が推奨されます。



も参照してください
Is there an alternative to using time to seed a random number generation?

C++ seeding surprises

Getting random numbers in a thread-safe way [C#]


19)デッドロックまたはクラッシュする可能性があります(またはDLLのロード順序で依存関係ループが作成されます



OTHER_LIB = LoadLibrary("B.dll");


解決策: DllMainエントリポイントでLoadLibraryを使用しないでください。複雑な(de)初期化は、「Init」「Deint」などの特定のDLL開発者がエクスポートした関数で実行する必要があります。ライブラリはこれらの関数をユーザーに提供し、ユーザーは適切なタイミングでそれらを正しく呼び出す必要があります。両当事者は、この契約を厳守する必要があります。







20)タイプミス(条件は常にfalse)、間違ったプログラムロジック、およびリソースリークの可能性(ダウンロードが成功したときにOTHER_LIBがアンロードされないため)



if (OTHER_LIB = nullptr)
    return FALSE;


コピーによる割り当て演算子は、左側のタイプのリンクを返します。ifはOTHER_LIB値(nullptrになります)をチェックし、nullptrはfalseとして解釈されます。



解決策:次のようなタイプミスを避けるために、常に逆の形式を使用してください。



if/while (<constant> == <variable/expression>)


21)_beginthreadシステム関数を使用してアプリケーションに新しいスレッドを作成することをお勧めします(特に、アプリケーションが静的バージョンのCランタイムライブラリにリンクされている場合)。そうしないと、ExitThread、DisableThreadLibraryCallsを呼び出すときにメモリリークが発生する可能性があります。22



)DllMainへのすべての外部呼び出しはシリアル化されるため、本文ではこの関数は、スレッド/プロセスを作成したり、それらと対話したりしないでください。そうしないと、デッドロックが発生する可能性があります



CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);


23)対応するコンポーネントがすでにアンロードされている可能性があるため、DLLの終了中にCOM関数を呼​​び出すと、誤ったメモリアクセスが発生する可能性があります



CoUninitialize();


24)インプロセスCOM / OLEサービスのロードとアンロードの順序を制御する方法がないため、DllMain関数からOleInitializeまたはOleUninitializeを呼び出さないでください。



OleUninitialize();


も参照してください
COM Clients and Servers

In-process, Out-of-process, and Remote Servers



25)新しい割り当てられたメモリのブロックを解放する



26)アプリケーションプロセスがその作業を終了するプロセスにある場合(lpvReservedパラメータのゼロ以外の値で示される)、現在のスレッドを除くプロセス内のすべてのスレッドは、すでに終了しているか、次の場合に強制的に停止されています。 ExitProcess関数を呼び出すと、ヒープなどの一部のプロセスリソースが不整合な状態のままになる可能性があります。その結果、リソースをクリーンアップすることはDLLセーフではありません。代わりに、DLLはオペレーティングシステムがメモリを再利用できるようにする必要があります。



free(INTEGERS);


解決策:古いCスタイルの手動メモリ割り当てが「新しい」C ++スタイルと混在していないことを確認してください。DllMain関数でリソースを管理するときは、細心の注意を払ってください



27)システムが終了コードを実行した後でも、DLLが使用される可能性があります



const BOOL result = FreeLibrary(OTHER_LIB);


解決策: DllMainエントリポイントでFreeLibrary呼び出さないでください 28)現在の(おそらくメインの)スレッドがクラッシュします







throw new std::runtime_error("    ");


解決策: DllMain関数で例外をスローしないようにします。何らかの理由でDLLを正しくロードできない場合、関数は単にFALSEを返す必要があります。また、DLL_PROCESS_DETACHセクションから例外をスローしないでください。



DLLの外部で例外をスローするときは、常に注意してください。複雑なオブジェクト(たとえば、標準ライブラリのクラス)は、ランタイムライブラリの異なる(互換性のない)バージョンでコンパイルされている場合、異なる実行可能モジュールで異なる物理的表現(および作業のロジック)を持つことができます







モジュール間で単純なデータタイプのみを交換してみてください(固定サイズと明確に定義されたバイナリ表現を使用)。



メインスレッドを終了すると、他のすべてのスレッドが自動的に終了することに注意してください(正しく終了しないため、メモリが損傷し、同期プリミティブやその他のオブジェクトが予測できない誤った状態になります。さらに、これらのスレッドは、次の時点ですでに存在しなくなります。静的オブジェクトは独自の分解を開始するため、静的オブジェクトのデストラクタでスレッドが終了するのを待たないでください)。



も参照してください
Top 20 C++ multithreading mistakes and how to avoid them



29)ここではキャッチされない例外(たとえば、std :: bad_alloc)をスローできます。



THREADS.push_back(std::this_thread::get_id());


DLL_THREAD_ATTACHセクションは不明な外部コードから呼び出されるため、ここで正しい動作が見られるとは思わないでください。



解決策: try / catchコマンドを使用して、正しく処理できない可能性が最も高い例外をスローする可能性のあるステートメントを囲みます(特に、DLLから終了する場合)。



も参照してください
How can I handle a destructor that fails?



30)このDLLをロードする前にストリームが提示された場合UB



THREADS.pop_back();


既に存在しているスレッド時にDLLが(直接ロードすることも含め読み込まれるDLLは)ロードされて呼び出すことはありませんDLLのエントリポイント関数彼らはまだDLL_THREAD_DETACHのイベントでそれを呼び出している間、(それらはDLL_THREAD_ATTACHイベント中THREADSベクターで登録されていない理由です)完了時に。

これは、DllMain関数のDLL_THREAD_ATTACHセクションとDLL_THREAD_DETACHセクションへの呼び出しの数が異なることを意味します。



31)固定サイズの整数型を使用することをお勧めします



32)異なるリンクおよびコンパイル設定とフラグ(異なるバージョンのランタイムライブラリなど)でコンパイルすると、モジュール間で複雑なオブジェクトを渡すとクラッシュする可能性があります



ポインタが(これらのモジュールに異なる方法で処理されている場合、モジュールによって共有され、その仮想アドレス()によってオブジェクトCへのアクセス33)が問題を引き起こすことができ、例えば、モジュールは異なる関連付けられている場合LARGEADDRESSAWAREのパラメータ



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


も参照してください
Is it possible to use more than 2 Gbytes of memory in a 32-bit program launched in the 64-bit Windows?

Application with LARGEADDRESSAWARE flag set getting less virtual memory

Drawbacks of using /LARGEADDRESSAWARE for 32 bit Windows executables?

how to check if exe is set as LARGEADDRESSAWARE [C#]

/LARGEADDRESSAWARE [Ru]

ASLR (Address Space Layout Randomization) [Ru]



そして...
Virtual memory

Physical Address Extension

Tagged pointer

std::ptrdiff_t

What is uintptr_t data type

Pointer arithmetic

Pointer aliasing

What is the strict aliasing rule?

reinterpret_cast conversion

restrict type qualifier



上記のリストはほとんど完全ではないので、おそらくコメントに重要な何かを追加することができます。



ポインタの操作は、実際には、人々が通常考えるよりもはるかに複雑です。間違いなく、経験豊富な開発者は、他の既存のニュアンスや微妙な点を覚えることができます(たとえば、オブジェクトへのポインタと関数へのポインタの違いについての何か。そのため、おそらく、ポインタのすべてのビットを使用できるわけではありませんなど。 。)。







34)関数内で例外をスローできます



INTEGERS = new std::vector<int>(integers);


この関数のthrow()指定は空です:



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


std ::予期しないものは、例外仕様に違反したときにC ++ランタイムによって呼び出されます。例外仕様がこのタイプの例外を許可しない関数から、例外がスローされます。



解決策: try / catch(特に、特にDLLでリソースを割り当てる場合)またはnothrow形式のnew演算子を使用します。いずれにせよ、さまざまな種類のリソースを割り当てるすべての試みが常に正常に終了するという素朴な仮定から始めないでください。



も参照してください
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









問題1:そのような「よりランダムな」値の形成は正しくありません。中央限界定理によれば、独立したランダム変数の合計は、均一な分布ではなく正規の分布になる傾向があります(初期値自体が均一に分布している場合でも)。



問題2:整数型のオーバーフローの可能性(符号付き整数型の未定義の動作



return rand() + rand();


疑似乱数ジェネレータや暗号化などを使用する場合は、常に自家製の「ソリューション」の使用に注意してください。これらの非常に特殊な分野で専門的な教育と経験を持っていない限り、単に自分を裏切って状況を悪化させる可能性は非常に高いです。



35)エクスポートされた関数の名前はextern "C"の使用を防ぐために装飾(変更)されます



36)この命名スタイルはSTL用に予約されているため、 '_'で始まる名前はC ++では暗黙的に禁止されています



__declspec(dllexport) long long int __cdecl _GetInt(int a)


いくつかの問題(とその可能な解決策):



37)ランドがされて いないスレッドセーフでは、使用rand_rは、 /の代わりにrand_s



38)ランドが、廃止されてより有効に活用現代
C++11 <random>


39)rand関数が現在のスレッド専用に初期化されたという事実ではありません(MS VSでは、呼び出されるスレッドごとにこの関数の初期化が必要です)



40)疑似ランダム番号の特別なジェネレーターがあり、ハッキングに強いソリューションで使用することをお勧めします(これらは適切です)Libsodium / randombytes_bufOpenSSL / RAND_bytesなどのポータブルソリューション



41)ゼロによる潜在的な除算:現在のスレッドを終了させる可能性があります



42)優先順位の異なる演算子が同じ行使用されているため、計算の順序に混乱が生じます-括弧を使用し、 /またはシーケンスポイント明らかな計算順序を指定する



43)潜在的な整数オーバーフロー



return 100 / a <= 0 ? a : a + 1 + Random();




も参照してください
Do not use std::rand() for generating pseudorandom numbers





そして...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





そして、それだけではありません!



メモリに重要なコンテンツ(ユーザーのパスワードなど)があると想像してください。もちろん、実際に必要な時間より長くメモリに保持したくないので、誰かがそこから読み取る可能性が高くなります。



この問題を解決するための素朴なアプローチは、次のようになります。



bool login(char* const userNameBuf, const size_t userNameBufSize,
           char* const pwdBuf, const size_t pwdBufSize) throw()
{
    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)
        return false;
    
    // Here some actual implementation, which does not checks params
    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,
    //   while both of them obviously contains private information 
    const bool result = doLoginInternall(userNameBuf, pwdBuf);
    
    // We want to minimize the time this private information is stored within the memory
    memset(userNameBuf, 0, userNameBufSize);
    memset(pwdBuf, 0, pwdBufSize);
}


そして、それは確か私たちが望むように機能しませんそれでは何をすべきでしょうか?:(



間違った「解決策」#1:memsetが機能しない場合は、手動で実行しましょう!



void clearMemory(char* const memBuf, const size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
}


なぜこれも私たちに合わないのですか?事実、このコードには、最新のコンパイラが最適化できないような制限はありません(ちなみに、memset関数がまだ使用されている場合は、おそらく組み込みになります)。



も参照してください
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



間違った「解決策」#2:volatileキーワードをいじって、前の「解決策」を「改善」しようとする



void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (volatile size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
    
    *(volatile char*)memBuf = *(volatile char*)memBuf;
    // There is also possibility for someone to remove this "useless" code in the future
}


これは機能しますか?多分。たとえば、このアプローチはRtlSecureZeroMemoryで使用されますWindows SDKソースでこの関数の実際の実装を確認することで自分で確認できます)。



ただし、この手法はすべてのコンパイラで期待どおりに機能するとは限りません



も参照してください
volatile member functions



間違った「解決策」#3:不適切なOS API関数(例:RtlZeroMemory)またはSTL(例:std :: fill、std :: for_each)を使用する



RtlZeroMemory(memBuf, memBufSize);


この問題を解決する試みの他の例はここにあります



そして、それはどのように正しいのですか?



  • 正しい使用OSのAPIの機能を例えば、RtlSecureZeroMemoryのために、Windowsの
  • 利用機能C11の memset_s


さらに、変数の値を(ファイル、コンソール、またはその他のストリームに)出力することで、コンパイラーがコードを最適化するのを防ぐことができますが、これは明らかにあまり役に立ちません。



も参照してください
Safe clearing of private Data



まとめ



もちろん、これは、C / C ++でアプリケーションを作成するときに発生する可能性のあるすべての問題、ニュアンス、および微妙な点の完全なリストではありません



次のような素晴らしいものもあります。





そして、はるかに。







追加するものはありますか?コメントであなたの興味深い経験を共有してください!



PSもっと知りたいですか?
Software security errors

Common weakness enumeration

Common types of software vulnerabilities



Vulnerability database

Vulnerability notes database

National vulnerability database



Coding standards

Application security verification standard

Guidelines for the use of the C++ language in critical systems



Secure programming HOWTO

32 OpenMP Traps For C++ Developers

A Collection of Examples of 64-bit Errors in Real Programs




All Articles