"C تجعل من السهل إطلاق النار على قدمك. من الصعب القيام بذلك في C ++ ، لكن الأمر سيستغرق وقتًا طويلاً "- Björn Stroustrup ، مصمم 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();
}
ربما وجدت هذا الرمز بسيطًا وواضحًا وآمنًا بدرجة كافية؟ أو ربما وجدت بعض المشاكل فيه؟ ربما حتى دزينة أو اثنتين؟
حسنًا ، يوجد بالفعل أكثر من 43 تهديدًا محتملاً بدرجات متفاوتة من الأهمية في هذا المقتطف !
ما يجب الانتباه إليه
1) sizeof (d) (حيث d هو مزدوج طويل) ليس بالضرورة مضاعف sizeof (int)
int i[sizeof(d) / sizeof(int)];
لم يتم اختبار هذا الموقف أو التعامل معه هنا. على سبيل المثال ، قد يكون المضاعف الطويل 10 بايت على بعض الأنظمة الأساسية (وهذا ليس صحيحًا بالنسبة لمترجم MS VS ، ولكنه ينطبق على RAD Studio ، المعروف سابقًا باسم C ++ Builder ).
يمكن أن يكون 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;
يمكن أن يؤدي الوصول إلى متغير من هذا النوع من وحدات نمطية قابلة للتنفيذ مختلفة (على سبيل المثال ، الملف القابل للتنفيذ وملف DLL الذي يتم تحميله) إلى القراءة / الكتابة خارج حدود الكائن ، إذا تم تجميع الثنائيين بتمثيل مادي مختلف من هذا النوع. وهذا بدوره سيؤدي إلى تلف الذاكرة أو قراءات غير صحيحة.
الحل: تأكد من استخدام نفس الأنواع ذات الحجم المحدد بدقة لتبادل البيانات بين جميع الوحدات:
int64_t time;
3) لم يتم تحميل B.dll (المقبض الذي تم تخزينه بواسطة متغير OTHER_LIB ) في الوقت الذي نصل فيه إلى المتغير أعلاه ، لذلك لا يمكننا الحصول على عناوين وظائف هذه المكتبة 4) مشكلة ترتيب تهيئة الكائنات الثابتة ( SIOF ): (كائن OTHER_LIB المستخدمة في الكود قبل أن تتم تهيئته)
func = GetProcAddress(OTHER_LIB, "func");
FINALIZER هو كائن ثابت تم إنشاؤه قبل استدعاء دالة DllMain . في المنشئ ، نحاول استخدام مكتبة لم يتم تحميلها بعد. تتفاقم المشكلة بسبب حقيقة أن OTHER_LIB الثابت المستخدم بواسطة FINALIZER الثابت يتم وضعه في وحدة الترجمة في اتجاه مجرى النهر. هذا يعني أنه سيتم أيضًا تهيئته (صفرية) لاحقًا. أي أنه في الوقت الحالي الذي سيتم فيه الوصول إليه ، سيحتوي على بعض القمامة العشوائية الزائفة . WinAPIبشكل عام ، يجب أن يتفاعل بشكل طبيعي مع هذا ، لأنه مع وجود درجة عالية من الاحتمالية لن يكون هناك ببساطة وحدة محملة بمثل هذا الوصف على الإطلاق. وحتى لو حدثت مصادفة لا تصدق على الإطلاق ولا تزال كذلك ، فمن غير المرجح أن تحتوي على وظيفة تسمى "Func" .
الحل: النصيحة العامة هي تجنب استخدام الكائنات العامة ، خاصة تلك المعقدة ، خاصةً إذا كانت تعتمد على بعضها البعض ، خاصة في مكتبات DLL . ومع ذلك ، إذا كنت لا تزال بحاجة إليها لأي سبب ، فكن شديد الحذر والحذر بشأن الترتيب الذي تمت تهيئتها به. إلى السيطرة على هذا النظام ، ووضع جميع الحالات (التعريفات) من كائنات عمومية إلى واحد وحدة الترجمةبالترتيب الصحيح لضمان تهيئتها بشكل صحيح.
5) لم يتم فحص النتيجة التي تم إرجاعها مسبقًا قبل الاستخدام
auto data = func();
func هو مؤشر دالة . ويجب أن تشير إلى دالة من B.dll . ومع ذلك ، نظرًا لأننا فشلنا تمامًا في كل شيء في الخطوة السابقة ، فسيكون هذا nullptr . وبالتالي ، في محاولة لإلغاء الإشارة إليها ، بدلاً من استدعاء الوظيفة المتوقع ، نحصل على انتهاك وصول أو خطأ حماية عام أو شيء من هذا القبيل.
الحل: عند العمل برمز خارجي (في حالتنا مع WinAPI ) ، تحقق دائمًا من نتيجة إرجاع الوظائف المسماة. بالنسبة للأنظمة الموثوقة والمتسامحة مع الأخطاء ، تنطبق هذه القاعدة حتى على الوظائف التي يوجد بشأنها عقد صارم [حول ما يجب إعادته ومتى].
6) قراءة / كتابة القمامة عند تبادل البيانات بين الوحدات المترجمة بإعدادات محاذاة / حشو مختلفة
auto str = data->c;
إذا كانت بنية البيانات (التي تُستخدم لتبادل المعلومات بين وحدات الاتصال) تحتوي على هذه الوحدات النمطية نفسها في عرض مادي مختلف ، فسيؤدي ذلك إلى جميع انتهاكات الوصول المذكورة سابقًا ، وحماية ذاكرة الخطأ ، وتجزئة الأعطال ، وفساد الكومة ، إلخ. أو سنقرأ القمامة فقط. تعتمد النتيجة الدقيقة على سيناريو الاستخدام الفعلي لهذه الذاكرة. يمكن أن يحدث كل هذا بسبب عدم وجود إعدادات محاذاة / حشو واضحة للهيكل نفسه . لذلك ، إذا كانت هذه الإعدادات العامة في وقت التجميع مختلفة بالنسبة للوحدات النمطية المتفاعلة ، فسنواجه مشاكل.
القرار:تأكد من أن جميع هياكل البيانات المشتركة لها تمثيل مادي قوي ومحدد بوضوح (باستخدام أنواع ذات حجم ثابت ، ومحاذاة محددة بوضوح ، وما إلى ذلك) و / أو تم تجميع الثنائيات القابلة للتشغيل المتبادل باستخدام نفس إعدادات المحاذاة العامة / حشوة.
7) استخدام حجم مؤشر إلى مصفوفة بدلاً من حجم المصفوفة نفسها
memset(str, 0, sizeof(str));
عادة ما يكون هذا نتيجة لخطأ إملائي بسيط. ولكن يمكن أن تنشأ هذه المشكلة أيضًا عند التعامل مع تعدد الأشكال الثابت أو عند استخدام الكلمة الأساسية التلقائية دون تفكير ( خاصةً عندما يتم استخدامها بشكل مفرط ). ومع ذلك ، يود المرء أن يأمل في أن يكون المترجمون المعاصرون أذكياء بما يكفي لاكتشاف مثل هذه المشكلات في وقت الترجمة ، وذلك باستخدام قدرات المحلل الثابت الداخلي .
القرار:
- لا تخلط أبدًا بين sizeof ( <full object type> ) و sizeof ( <object pointer type> ) ؛
- لا تتجاهل تحذيرات المترجم ؛

- يمكنك أيضًا استخدام القليل من السحر المعياري لـ C ++ من خلال الجمع بين typeid و constexpr و static_assert للتأكد من أن الأنواع صحيحة في وقت الترجمة (يمكن أيضًا أن تكون سمات الكتابة مفيدة هنا ، ولا سيما std :: is_pointer ).
8) سلوك غير محدد عند محاولة قراءة حقل اتحاد مختلف عما تم استخدامه سابقًا لتعيين القيمة
9) من الممكن محاولة القراءة من منطقة الذاكرة الصالحة إذا كان حجم مزدوج طويل يختلف بين الوحدات الثنائية
const int i0 = data->u.i[sizeof(long double) - 1U];
تم ذكر هذا سابقًا ، لذلك لدينا هنا نقطة أخرى لوجود المشكلة المذكورة سابقًا.
الحل: لا تشير إلى حقل آخر غير الحقل الذي قمت بتعيينه مسبقًا إلا إذا كنت متأكدًا من أن المترجم الخاص بك يتعامل معه بشكل صحيح. تأكد من أن أحجام أنواع الكائنات المشتركة هي نفسها عبر جميع الوحدات النمطية المتفاعلة.
10) حتى إذا تم تحميل B.dll بشكل صحيح وتم تصدير واستيراد وظيفة "func" بشكل صحيح ، فسيظل B.dll غير محمّل من الذاكرة بحلول هذا الوقت (لأنه تم استدعاء وظيفة نظام FreeLibrary مسبقًا في قسم DLL_PROCESS_DETACH لوظيفة رد الاتصال DllMain )
auto data = func();
من المحتمل أن يؤدي استدعاء وظيفة افتراضية على كائن تم تدميره سابقًا من نوع متعدد الأشكال ، بالإضافة إلى استدعاء وظيفة في مكتبة ديناميكية غير محملة بالفعل ، إلى خطأ استدعاء افتراضي خالص .
الحل: قم بتنفيذ إجراء الإنهاء الصحيح في التطبيق للتأكد من أن جميع مكتبات DLL قد اكتملت / تم تفريغها بالترتيب الصحيح. تجنب استخدام كائنات ثابتة ذات منطق معقد في DL L. تجنب إجراء أي عمليات داخل المكتبة بعد استدعاء DllMain / DLL_PROCESS_DETACH (عندما تدخل المكتبة المرحلة الأخيرة من دورة حياتها - مرحلة تدمير كائناتها الثابتة).
يجب أن تفهم ما هي دورة حياة ملف 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) سلوك غير محدد إذا تم تخصيص الذاكرة باستخدام جديد وحذفها باستخدام حذف []
delete[] data2;
بشكل عام ، يجب أن تكون حذرًا دائمًا عند تحرير الكائنات المستلمة من الوحدات الخارجية.
من الممارسات الجيدة أيضًا عدم وجود مؤشرات للأشياء المدمرة.
القرار:
- عند حذف كائن ، يجب معرفة نوعه بالكامل
- يجب أن يكون لجميع المدمرات جسم
- لا ينبغي إلغاء تحميل المكتبة التي تم تصدير الشفرة منها مبكرًا
- دائما استخدم الأشكال المختلفة الجديدة وحذفها بشكل صحيح ، لا تخلط بينها
- يجب أن يكون المؤشر إلى الكائن البعيد صفريًا.
نلاحظ أيضا ما يلي:
- لدعوة حذف على مؤشر إلى الفراغ سيؤدي في سلوك غير معرف
بحتة وظائف افتراضية لا ينبغي أن يسمى من منشئ
- استدعاء دالة الظاهري في منشئ لن يكون الظاهري
- في محاولة لتجنب إدارة الذاكرة اليدوية - استخدام حاويات ، دلالات هذه الخطوة، و مؤشرات ذكية
15) ExitThread هي الطريقة المفضلة للخروج من مؤشر ترابط في C. في C ++ ، سيؤدي استدعاء هذه الوظيفة إلى إنهاء الخيط قبل استدعاء مدمرات الكائنات المحلية (وأي تنظيف تلقائي آخر) ، لذلك يجب إنهاء مؤشر ترابط في C ++ ببساطة عن طريق العودة من وظيفة مؤشر الترابط
ExitThread(0U);
الحل: لا تستخدم هذه الوظيفة يدويًا في كود C ++.
16) في نص DllMain ، يمكن أن يؤدي استدعاء أي وظائف قياسية تتطلب ملفات DLL للنظام بخلاف Kernel32.dll إلى العديد من المشكلات التي يصعب تشخيصها
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
الحل في DllMain:
- تجنب أي تهيئة معقدة (de)
- تجنب استدعاء دوال من مكتبات أخرى (أو على الأقل كن حذرًا جدًا مع هذا)
17) تهيئة غير صحيحة لمولد الأرقام العشوائية الزائفة في بيئة متعددة مؤشرات الترابط
18) نظرًا لأن الوقت الذي يتم إرجاعه بواسطة وظيفة الوقت لديه دقة 1 ثانية. ، أي مؤشر ترابط في البرنامج يستدعي هذه الوظيفة خلال هذه الفترة الزمنية سوف يتلقى نفس القيمة عند الإخراج. يمكن أن يؤدي استخدام هذا الرقم لتهيئة PRNG إلى حدوث تصادمات (على سبيل المثال ، إنشاء نفس الأسماء العشوائية الزائفة للملفات المؤقتة ، ونفس أرقام المنافذ ، وما إلى ذلك). أحد الحلول الممكنة هو خلط ( xor ) النتيجة بنوع من القيمة العشوائية الزائفة ، مثل عنوان أي مكدس أو كائن في الكومة ، ووقت أكثر دقة ، وما إلى ذلك.
srand(time(nullptr));
الحل: تتطلب MS VS تهيئة PRNG لكل مؤشر ترابط . بالإضافة إلى ذلك ، يوفر استخدام وقت Unix كأداة تهيئة إنتروبيا غير كافية ، ويفضل إنشاء قيمة تهيئة أكثر تقدمًا .
أنظر أيضا
19) قد يتعطل أو يتعطل (أو ينشئ حلقات تبعية في ترتيب تحميل DLL )
OTHER_LIB = LoadLibrary("B.dll");
الحل: لا تستخدم LoadLibrary في نقطة دخول DllMain . يجب تنفيذ أي تهيئة معقدة (de) في بعض وظائف مطور DLL المُصدرة ، مثل "Init" و "Deint" . توفر المكتبة هذه الوظائف للمستخدم ، ويجب على المستخدم الاتصال بها بشكل صحيح في الوقت المناسب. يجب على كلا الطرفين الالتزام الصارم بهذا العقد.
20) خطأ مطبعي (الشرط خاطئ دائمًا) ومنطق برنامج خاطئ وتسرب محتمل للموارد (لأنه لا يتم إلغاء تحميل OTHER_LIB مطلقًا عند تنزيل ناجح)
if (OTHER_LIB = nullptr)
return FALSE;
يقوم عامل الإسناد عن طريق النسخ بإرجاع ارتباط من النوع الأيسر ، أي إذا كان سيتحقق من قيمة OTHER_LIB (والتي ستكون nullptr) وسيتم تفسير nullptr على أنه خطأ.
الحل: استخدم دائمًا النموذج العكسي لتجنب الأخطاء الإملائية مثل هذا:
if/while (<constant> == <variable/expression>)
21) يوصى باستخدام وظيفة نظام _beginthread لإنشاء سلسلة رسائل جديدة في التطبيق (خاصة إذا كان التطبيق مرتبطًا بإصدار ثابت من مكتبة وقت تشغيل C) وإلا فقد يحدث تسرب للذاكرة عند استدعاء ExitThread و DisableThreadLibraryCalls
22) يتم إجراء تسلسل لجميع المكالمات الخارجية لـ DllMain ، لذلك في الجسم يجب ألا تحاول هذه الوظيفة إنشاء مؤشرات ترابط / عمليات أو التفاعل معها ، وإلا فقد تحدث حالات توقف تام
CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
23) استدعاء وظائف COM أثناء إنهاء DLL يمكن أن يؤدي إلى وصول غير صحيح للذاكرة ، حيث قد يكون قد تم إلغاء تحميل المكون المقابل بالفعل
CoUninitialize();
24) لا توجد طريقة للتحكم في ترتيب التحميل والتفريغ لخدمات COM / OLE قيد المعالجة ، لذلك لا تستدعي OleInitialize أو OleUninitialize من دالة DllMain
OleUninitialize();
25) استدعاء مجاني لكتلة من الذاكرة المخصصة مع
26 جديدًا ) إذا كانت عملية التطبيق في طور إنهاء عملها (كما هو مشار إليه بقيمة غير صفرية للمعامل lpvReserved) ، فإن جميع مؤشرات الترابط في العملية ، باستثناء الحالية الحالية ، إما أنهيت بالفعل أو تم إيقافها قسريًا عند استدعاء الدالة ExitProcess ، والتي يمكن أن تترك بعض موارد العملية ، مثل الكومة ، في حالة غير متناسقة. نتيجة لذلك ، ليس DLL- آمن لتنظيف الموارد . بدلاً من ذلك ، يجب أن تسمح مكتبة الارتباط الحيوي (DLL) لنظام التشغيل باستعادة الذاكرة.
free(INTEGERS);
الحل: تأكد من عدم خلط نمط C القديم الخاص بالتخصيص اليدوي للذاكرة بنمط C ++ "الجديد". كن حذرًا للغاية عند إدارة الموارد في وظيفة DllMain .
27) يمكن أن يتسبب في استخدام DLL حتى بعد أن يقوم النظام بتنفيذ كود الخروج الخاص به
const BOOL result = FreeLibrary(OTHER_LIB);
الحل: لا تتصل بـ FreeLibrary عند نقطة دخول DllMain.
28) سيتعطل الخيط الحالي (ربما الرئيسي)
throw new std::runtime_error(" ");
الحل: تجنب طرح الاستثناءات في وظيفة DllMain. إذا تعذر تحميل DLL بشكل صحيح لأي سبب من الأسباب ، يجب أن ترجع الدالة FALSE ببساطة. يجب أيضًا عدم طرح استثناءات من قسم DLL_PROCESS_DETACH.
كن حذرًا دائمًا عند طرح استثناءات خارج DLL. يمكن لأي كائنات معقدة (على سبيل المثال ، فئات المكتبة القياسية ) أن يكون لها تمثيل مادي مختلف (وحتى منطق العمل) في وحدات نمطية مختلفة قابلة للتنفيذ إذا تم تجميعها باستخدام إصدارات مختلفة (غير متوافقة) من مكتبات وقت التشغيل .
حاول تبادل أنواع البيانات البسيطة فقط بين الوحدات(بحجم ثابت وتمثيل ثنائي واضح المعالم).
تذكر أن إنهاء مؤشر الترابط الرئيسي سيؤدي تلقائيًا إلى إنهاء جميع مؤشرات الترابط الأخرى (التي لا تنتهي بشكل صحيح وبالتالي يمكن أن تلحق الضرر بالذاكرة ، تاركًا العناصر الأولية للتزامن والكائنات الأخرى في حالة غير متوقعة وغير صحيحة. علاوة على ذلك ، ستتوقف هذه الخيوط عن الوجود بالفعل في الوقت الذي ستبدأ الكائنات الثابتة في التفكيك الخاص بها ، لذلك لا تحاول الانتظار حتى تنتهي أي خيوط في مدمرات الكائنات الثابتة).
29) يمكنك طرح استثناء (على سبيل المثال ، std :: bad_alloc) ، والذي لم يتم اكتشافه هنا
THREADS.push_back(std::this_thread::get_id());
نظرًا لاستدعاء قسم DLL_THREAD_ATTACH من بعض التعليمات البرمجية الخارجية غير المعروفة ، فلا تتوقع رؤية السلوك الصحيح هنا.
الحل: استخدم try / catch لتضمين العبارات التي قد تطرح استثناءات على الأرجح لا يمكن معالجتها بشكل صحيح (خاصةً إذا خرجت من DLL ).
30) UB إذا تم تقديم التدفقات قبل تحميل DLL هذا
THREADS.pop_back();
لا تستدعي الخيوط الموجودة بالفعل في الوقت الذي يتم فيه تحميل DLL (بما في ذلك تلك التي تقوم بتحميل DLL مباشرة ) وظيفة نقطة إدخال DLL المحملة (وهذا هو سبب عدم تسجيلها في متجه THREADS أثناء حدث DLL_THREAD_ATTACH) ، بينما لا تزال تستدعيها مع حدث DLL_THREAD_DETACH عند الانتهاء.
هذا يعني أن عدد المكالمات إلى أقسام DLL_THREAD_ATTACH و DLL_THREAD_DETACH للدالة DllMain سيكون مختلفًا.
31) من الأفضل استخدام أنواع الأعداد الصحيحة ذات الحجم الثابت
.
33) يمكن أن يتسبب الوصول إلى الكائن c من خلال عنوانه الظاهري (الذي تشاركه الوحدات النمطية) في حدوث مشكلات إذا تم التعامل مع المؤشرات بشكل مختلف في هذه الوحدات (على سبيل المثال ، إذا كانت الوحدات النمطية مرتبطة بمعلمات 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]
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]
و ...
بالكاد تكتمل القائمة أعلاه ، لذا يمكنك على الأرجح إضافة شيء مهم في التعليقات.
يعد العمل باستخدام المؤشرات في الواقع أكثر تعقيدًا مما يعتقده الناس عادةً. بدون شك ، سيكون المطورون المتمرسون قادرين على تذكر الفروق الدقيقة والدقيقة الأخرى الموجودة (على سبيل المثال ، شيء عن الاختلاف بين المؤشرات إلى كائن والمؤشرات إلى وظيفة ، والتي ربما لا يمكن استخدام كل أجزاء المؤشر بسببها ، إلخ. .).

34) يمكن طرح استثناء داخل دالة :
INTEGERS = new std::vector<int>(integers);
() مواصفات هذه الوظيفة فارغة:
__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
يتم استدعاء std :: غير متوقع بواسطة وقت تشغيل C ++ عند انتهاك أحد مواصفات الاستثناء: يتم طرح استثناء من وظيفة لا تسمح مواصفات الاستثناء الخاصة بها باستثناءات من هذا النوع.
الحل: استخدم try / catch (خاصة عند تخصيص الموارد ، خاصة في مكتبات DLL ) أو شكل nothrow للمشغل الجديد. على أي حال ، لا تفترض أبدًا أن كل محاولات تخصيص أنواع مختلفة من الموارد ستنتهي دائمًا بنجاح .

المشكلة الأولى: تكوين مثل هذه القيمة "الأكثر عشوائية" غير صحيح. وفقًا لنظرية الحد المركزي ، يميل مجموع المتغيرات العشوائية المستقلة إلى التوزيع الطبيعي ، وليس إلى التوزيع المنتظم (حتى لو تم توزيع القيم الأصلية نفسها بشكل موحد).
المشكلة 2: احتمال تجاوز نوع العدد الصحيح (وهو سلوك غير محدد لأنواع الأعداد الصحيحة الموقعة )
return rand() + rand();
عند العمل باستخدام مولدات الأرقام شبه العشوائية والتشفير وما شابه ، احذر دائمًا من استخدام "حلول" محلية الصنع. ما لم يكن لديك تعليم وخبرة متخصص في هذه المجالات المحددة للغاية ، فإن الاحتمالات كبيرة جدًا أنك ستفوق نفسك ببساطة وتجعل الموقف أسوأ.
35) سيتم تزيين (تغيير) اسم الوظيفة التي تم تصديرها لمنع هذا الاستخدام الخارجي
للأسماء "C" 36 التي تبدأ بـ "_" ممنوعة ضمنيًا في C ++ ، حيث إن نمط التسمية هذا محجوز للمحكمة الخاصة بلبنان
__declspec(dllexport) long long int __cdecl _GetInt(int a)
عدة مشاكل (وحلولها الممكنة):
37) راند ليست خيطًا آمنًا ، استخدم rand_r / rand_s بدلاً من ذلك
38) rand مهملة ، أفضل استخدام حديث
C++11 <random>
39) ليس حقيقة أن وظيفة rand قد تمت تهيئتها خصيصًا للخيط الحالي (تتطلب MS VS تهيئة هذه الوظيفة لكل مؤشر ترابط حيث سيتم تسميتها)
40) هناك مولدات خاصة لأرقام شبه عشوائية ، ومن الأفضل استخدامها في حلول مقاومة الاختراق (فهي مناسبة الحلول المحمولة مثل Libsodium / randombytes_buf ، بينسل / RAND_bytes ، وما إلى ذلك)
41) تقسيم محتمل على صفر: قد يسبب الترابط الحالي إلى تعطل
42) للمشغلين الأسبقية مختلفة و تستخدم في نفس الصف ، الأمر الذي يضفي حالة من الفوضى في ترتيب حساب - قوسين استخدام و / أو نقاط التسلسللتحديد التسلسل الواضح للحساب
43) تجاوز عدد صحيح محتمل
return 100 / a <= 0 ? a : a + 1 + Random();
وهذا ليس كل شيء!
تخيل أن لديك بعض المحتويات المهمة في الذاكرة (على سبيل المثال ، كلمة مرور المستخدم). بالطبع ، لا تريد الاحتفاظ بها في الذاكرة لفترة أطول مما هو ضروري حقًا ، وبالتالي زيادة احتمال أن يتمكن شخص ما من قراءتها من هناك .
قد يبدو النهج الساذج لحل هذه المشكلة كما يلي:
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 وظيفة ، إذا لا تزال تستخدم، من المرجح أن يكون المدمج في ).
أنظر أيضا
"الحل" غير الصحيح رقم 2: حاول "تحسين" "الحل" السابق من خلال التلاعب بالكلمة الأساسية المتغيرة
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 ).
ومع ذلك ، لن تعمل هذه التقنية بالشكل المتوقع مع كافة المجمعين .
أنظر أيضا
"الحل" الخاطئ رقم 3: استخدم وظيفة واجهة برمجة تطبيقات نظام التشغيل غير المناسبة (مثل RtlZeroMemory ) أو STL (مثل: std :: fill و std :: for_each)
RtlZeroMemory(memBuf, memBufSize);
المزيد من الأمثلة على محاولات حل هذه المشكلة هنا .
وكيف هو صحيح؟
- استخدام وظيفة OS API الصحيحة ، على سبيل المثال ، RtlSecureZeroMemory لنظام التشغيل Windows
- استخدام الدالة C11 memset_s :
بالإضافة إلى ذلك ، يمكننا منع المترجم من تحسين الكود عن طريق طباعة (إلى ملف أو وحدة تحكم أو دفق آخر) قيمة المتغير ، ولكن من الواضح أن هذا ليس مفيدًا للغاية.
أنظر أيضا
تلخيص لما سبق
هذه ، بالطبع ، ليست قائمة كاملة بجميع المشاكل والفروق الدقيقة والتفاصيل الدقيقة التي قد تواجهها عند كتابة التطبيقات في C / C ++ .
هناك أيضًا أشياء رائعة مثل:
- أقفال الحية .
- (, , ABA, , );
- ;
- (- , , );
- GDI ;
- , volatile atomic ;
- (, 0603 603);
- وقت التحقق من مشكلة وقت الاستخدام ؛
- تعبيرات لامدا التي تعيش لفترة أطول من الأشياء المشار إليها ؛
- استخدام مواصفات تنسيق غير صحيحة في وظائف عائلة printf ؛
- اتصال غير صحيح بين جهازين بترتيب بايت مختلف (على سبيل المثال ، عبر شبكة) ، وما إلى ذلك ، إلخ.
وأكثر بكثير.
أي شيء تضيفه؟ شارك تجاربك الشيقة في التعليقات!
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
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