أصدرت موزيلا
Quantum CSS لـ Firefox العام الماضي ، وبلغت ذروتها في ثماني سنوات من تطوير Rust ، وهي لغة برمجة نظام صديقة للذاكرة. استغرق الأمر أكثر من عام لإعادة كتابة مكون المتصفح الرئيسي في Rust.
حتى الآن ، تتم كتابة جميع محركات المستعرضات الرئيسية بلغة C ++ ، وذلك لأسباب الكفاءة. ولكن مع الأداء الرائع ، تأتي مسؤولية كبيرة: يجب على مبرمجي C ++ إدارة الذاكرة يدويًا ، والذي يفتح مربع الضعف في Pandora. لا يعمل Rust على إصلاح مثل هذه الأخطاء فحسب ، ولكن أساليبه تمنع أيضًا
سباقات البيانات ، مما يتيح للمبرمجين تنفيذ التعليمات البرمجية الموازية بشكل أكثر كفاءة.
ما هو أمن الذاكرة؟
عندما نتحدث عن إنشاء تطبيقات آمنة ، نذكر غالبًا أمان الذاكرة. بشكل غير رسمي ، نعني أنه لا يمكن للبرنامج في أي حال الوصول إلى ذاكرة غير صالحة. أسباب انتهاكات الأمن:
- حفظ المؤشر بعد تحرير الذاكرة (الاستخدام بعد الاستخدام) ؛
- dereferencing مؤشر فارغ؛
- استخدام ذاكرة غير مهيأة ؛
- محاولة البرنامج لتحرير الخلية نفسها مرتين (خالية من مزدوجة) ؛
- تجاوز سعة المخزن المؤقت.
للحصول على تعريف أكثر رسمية ، راجع Michael Hicks
'What is Memory Security ، وكذلك
مقالة علمية حول هذا الموضوع.
مثل هذه الانتهاكات يمكن أن تؤدي إلى تعطل غير متوقع أو تغيير في السلوك المتوقع للبرنامج. العواقب المحتملة: تسرب المعلومات ، تنفيذ التعليمات البرمجية التعسفي وتنفيذ التعليمات البرمجية عن بُعد.
إدارة الذاكرة
إدارة الذاكرة أمر بالغ الأهمية لأداء التطبيق والأمن. في هذا القسم ، سننظر في نموذج الذاكرة الأساسي. واحدة من المفاهيم الأساسية هي
المؤشرات . هذه هي المتغيرات التي يتم تخزين عناوين الذاكرة. إذا ذهبنا إلى هذا العنوان ، فسنرى بعض البيانات هناك. لذلك ، نقول أن المؤشر هو إشارة إلى هذه البيانات (أو يشير إليها). تمامًا كما يخبر عنوان المنزل الأشخاص بمكان العثور عليك ، فإن عنوان الذاكرة يعرض البرنامج مكان العثور على البيانات.
يوجد كل شيء في البرنامج في عناوين ذاكرة محددة ، بما في ذلك التعليمات البرمجية. الاستخدام غير الصحيح للمؤشرات يمكن أن يؤدي إلى نقاط ضعف خطيرة ، بما في ذلك تسرب المعلومات وتنفيذ التعليمات البرمجية التعسفية.
تخصيص / الإصدار
عندما ننشئ متغيرًا ، يجب أن يخصص البرنامج مساحة كافية في الذاكرة لتخزين بيانات هذا المتغير. نظرًا لأن كل عملية بها كمية محدودة من الذاكرة ، بالطبع ، فأنت بحاجة إلى وسيلة
لتحرير الموارد. عندما يتم تحرير الذاكرة ، تصبح متوفرة لتخزين البيانات الجديدة ، ولكن البيانات القديمة تعيش هناك حتى يتم الكتابة فوق الخلية.
مخازن
المخزن المؤقت هو منطقة ذاكرة متجاورة حيث يتم تخزين عدة مثيلات من نفس نوع البيانات. على سبيل المثال ، سيتم تخزين العبارة "قطتي باتمان" في مخزن مؤقت مكون من 16 بايت. يتم تحديد المخازن المؤقتة حسب عنوان البداية والطول. حتى لا تتلف البيانات الموجودة في الذاكرة المجاورة ، من المهم التأكد من أننا لا نقرأ أو نكتب خارج المخزن المؤقت.
تدفق التحكم
تتكون البرامج من إجراءات تعمل بترتيب معين. في نهاية الروتين الفرعي ، ينتقل الكمبيوتر إلى المؤشر المخزن إلى الجزء التالي من الكود (يسمى
عنوان المرسل ). عندما تذهب إلى عنوان المرسل ، يحدث واحد من ثلاثة أشياء:
- تستمر العملية بشكل طبيعي (لم يتم تغيير عنوان المرسل).
- تعطل العملية (تم تغيير العنوان ويشير إلى ذاكرة غير قابلة للتنفيذ).
- تستمر العملية ، ولكن ليس كما هو متوقع (تم تغيير عنوان المرسل وتغير تدفق التحكم).
كيف توفر اللغات أمان الذاكرة
تنتمي جميع لغات البرمجة إلى أجزاء مختلفة من
الطيف . على جانب واحد من الطيف توجد لغات مثل C / C ++. أنها فعالة ، ولكنها تتطلب إدارة الذاكرة اليدوية. من ناحية أخرى ، يتم ترجمة اللغات ذات الإدارة التلقائية للذاكرة (على سبيل المثال ، حساب المرجع وجمع البيانات المهملة (GC)) ، لكنها تؤتي ثمارها مع الأداء. حتى اللغات ذات مجموعة البيانات المهملة المحسنة جيدًا لا يمكن مقارنتها في
الأداء باللغات التي لم تستخدم GC.
إدارة الذاكرة اليدوية
تتطلب بعض اللغات (على سبيل المثال ، C) من المبرمجين إدارة الذاكرة يدويًا: متى وكيف يتم تخصيص الذاكرة ، ومتى يتم تحريرها. هذا يعطي المبرمج سيطرة كاملة على كيفية استخدام البرنامج للموارد ، وتوفير رمز سريع وفعال. ولكن هذا النهج هو عرضة للخطأ ، وخاصة في قواعد التعليمات البرمجية المعقدة.
الأخطاء التي يسهل ارتكابها:
- ننسى أن الموارد مجانية ومحاولة استخدامها ؛
- لا تخصص مساحة كافية لتخزين البيانات ؛
- قراءة الذاكرة خارج المخزن المؤقت.
تعليمات السلامة المناسبة لأولئك الذين يديرون الذاكرة يدويامؤشرات ذكية
توفر
المؤشرات الذكية معلومات إضافية لمنع الإدارة غير الصحيحة للذاكرة. يتم استخدامها لإدارة الذاكرة التلقائي وفحص الحدود. بخلاف المؤشر العادي ، فإن المؤشر الذكي قادر على التدمير الذاتي ولن ينتظر المبرمج لحذفه يدويًا.
هناك العديد من الخيارات لمثل هذا البناء ، الذي يلتف المؤشر الأصلي في العديد من التجريدات المفيدة. تقوم بعض المؤشرات الذكية
بحساب الإشارات إلى كل كائن ، بينما يقوم البعض الآخر بتطبيق سياسة تحديد النطاق لتقييد عمر المؤشر بشروط معينة.
عند حساب الارتباطات ، يتم تحرير الموارد عند حذف المرجع الأخير إلى الكائن. تعاني عمليات حساب المرجع المرجعية الأساسية من ضعف الأداء وزيادة استهلاك الذاكرة ومن الصعب استخدامها في بيئات متعددة الخيوط. إذا كانت الكائنات تشير إلى بعضها البعض (روابط دائرية) ، فإن عدد المرجع لكل كائن لن يصل أبدًا إلى الصفر ، لذلك هناك حاجة إلى طرق أكثر تعقيدًا.
جمع القمامة
تنفذ بعض اللغات (مثل Java و Go و Python)
مجموعة البيانات المهملة . جزء من بيئة وقت التشغيل ، يسمى أداة تجميع مجمعي البيانات المهملة (GC) ، يتعقب المتغيرات ويحدد الموارد التي يتعذر الوصول إليها في الرسم البياني للارتباط بين الكائنات. بمجرد أن يصبح الكائن غير متوفر ، يحرر GC الذاكرة الأساسية لإعادة استخدامها في المستقبل. أي تخصيص وتحرير للذاكرة يحدث بدون أمر مبرمج واضح.
على الرغم من أن GC يضمن أن الذاكرة تُستخدم دائمًا بشكل صحيح ، إلا أنها لا تحرر الذاكرة بالطريقة الأكثر فعالية - في بعض الأحيان يحدث الاستخدام الأخير للكائن في وقت أبكر بكثير من قيام أداة تجميع مجمعي البيانات المهملة بتحرير الذاكرة. تكاليف الأداء باهظة بالنسبة للتطبيقات المهمة: في بعض الأحيان تحتاج إلى استخدام ذاكرة أكثر 5 مرات لتجنب تدهور الأداء.
حيازة
يستخدم Rust الملكية لضمان الأداء العالي وأمان الذاكرة. بشكل أكثر رسمية ، هذا مثال على
كتابة التقارب . تتبع كافة التعليمات البرمجية Rust قواعد معينة تسمح لبرنامج التحويل البرمجي بإدارة الذاكرة دون فقد وقت التنفيذ:
- كل قيمة لها متغير يسمى المالك.
- مالك واحد فقط يمكن أن يكون في وقت واحد.
- عندما يتحرك المالك خارج النطاق ، يتم حذف القيمة.
يمكن
نقل القيم أو
اقتراضها من متغير إلى آخر. تنطبق هذه القواعد على جزء من برنامج التحويل البرمجي يسمى "مدقق الاقتراض".
عندما يخرج المتغير عن نطاقه ، يحرر Rust هذه الذاكرة. في المثال التالي ، تتجاوز المتغيرات
s1
و
s2
النطاق ، كلاهما يحاول تحرير نفس الذاكرة ، مما يؤدي إلى حدوث خطأ مزدوج. لمنع هذا ، عند نقل قيمة من متغير ، يصبح المالك السابق غير صالح. إذا حاول المبرمج استخدام متغير غير صالح ، فسوف يرفض المترجم الشفرة. يمكن تجنب ذلك عن طريق إنشاء نسخة عميقة من البيانات أو استخدام الروابط.
مثال 1 : نقل الملكية
let s1 = String::from("hello"); let s2 = s1;
تتعلق مجموعة أخرى من قواعد مدقق الاقتراض بعمر المتغيرات. يحظر الصدأ استخدام المتغيرات غير المهيأة ومؤشرات التعلق بالأشياء غير الموجودة. إذا قمت بترجمة الكود من المثال أدناه ،
r
إلى الذاكرة التي يتم تحريرها عندما يخرج
x
عن النطاق: يحدث مؤشر التعلق. يقوم المحول البرمجي بمراقبة جميع المناطق والتحقق من صلاحية جميع عمليات النقل ، مما يتطلب في بعض الأحيان من المبرمج الإشارة صراحة إلى عمر المتغير.
مثال 2 : مؤشر معلق
let r; { let x = 5; r = &x; } println!("r: {}", r);
يوفر نموذج الملكية أساسًا قويًا للوصول الصحيح إلى الذاكرة ، مما يمنع السلوك غير المحدد.
نقاط ضعف الذاكرة
النتائج الرئيسية للذاكرة الضعيفة:
- الأعطال : الوصول إلى ذاكرة غير صالحة قد يؤدي إلى إنهاء تطبيق غير متوقع.
- تسرب المعلومات : توفير غير مقصود للبيانات الخاصة ، بما في ذلك المعلومات السرية ، مثل كلمات المرور.
- تنفيذ التعليمات البرمجية التعسفي (ACE) : يسمح للمهاجم بتنفيذ الأوامر التعسفية على الجهاز الهدف. إذا حدث هذا عبر الشبكة ، فإننا نسميها تنفيذ التعليمات البرمجية عن بُعد (RCE).
مشكلة أخرى هي
تسرب للذاكرة عندما لا يتم تحرير الذاكرة المخصصة بعد انتهاء البرنامج. حتى تتمكن من استخدام كل الذاكرة المتاحة: ثم يتم حظر طلبات الموارد ، مما سيؤدي إلى رفض الخدمة. هذه مشكلة في الذاكرة لا يمكن حلها على مستوى PL.
في أفضل الأحوال ، مع وجود خطأ في الذاكرة ، سوف يتعطل التطبيق. في أسوأ الحالات ، يتحكم المهاجم في البرنامج من خلال ثغرة أمنية (مما قد يؤدي إلى مزيد من الهجمات).
إساءة استخدام الذاكرة المحررة (الاستخدام بعد انتهاء الاستخدام ، وضعف الاستخدام مجانًا)
تحدث هذه الفئة الفرعية من الثغرات الأمنية عند تحرير أحد الموارد ، ولكن لا يزال يتم الاحتفاظ بالرابط إلى عنوانه. هذه
طريقة قراصنة قوية يمكن أن تؤدي إلى الوصول خارج النطاق ، وتسرب المعلومات ، وتنفيذ التعليمات البرمجية ، وأكثر من ذلك بكثير.
تمنع اللغات التي تحتوي على تجميع البيانات المهملة وعدد المراجع من استخدام مؤشرات غير صالحة ، وتدمير الكائنات التي يتعذر الوصول إليها فقط (والتي يمكن أن تؤدي إلى تدهور الأداء) ، وتكون اللغات التي يتم التحكم فيها يدويًا عرضة لهذه الثغرة الأمنية (خاصة في قواعد الأكواد المعقدة). لا تسمح أداة مدقق الاقتراض في Rust بتدمير الكائنات أثناء الرجوع إليها ، لذلك تتم إزالة هذه الأخطاء في مرحلة الترجمة.
متغيرات غير مهيأة
إذا تم استخدام المتغير قبل التهيئة ، فيمكن أن تحتوي هذه البيانات على أي بيانات ، بما في ذلك البيانات المهملة العشوائية أو البيانات التي تم التخلص منها مسبقًا ، مما يؤدي إلى تسرب المعلومات (تسمى أحيانًا
مؤشرات غير صالحة ). لمنع هذه المشكلات ، تستخدم لغات إدارة الذاكرة غالبًا إجراء التهيئة التلقائي بعد تخصيص الذاكرة.
كما هو الحال في C ، لم تتم تهيئة معظم المتغيرات في Rust في البداية. ولكن على عكس C ، لا يمكنك قراءتها قبل التهيئة. لا يتم ترجمة التعليمات البرمجية التالية:
مثال 3 : استخدام متغير غير مهيأ
fn main() { let x: i32; println!("{}", x); }
مؤشرات فارغة
عندما يقوم تطبيق ما بإلغاء تحديد مؤشر يظهر أنه لاغٍ ، فإنه عادةً ما يصل إلى البيانات المهملة ويسبب تعطلًا. في بعض الحالات ، يمكن أن تؤدي نقاط الضعف هذه إلى تنفيذ تعليمات برمجية عشوائية (
1 ،
2 ،
3 ). الصدأ له نوعان من المؤشرات:
الروابط والمؤشرات الأولية. الروابط آمنة ، لكن المؤشرات الأولية يمكن أن تكون مشكلة.
يمنع الصدأ إلغاء تسجيل مؤشر فارغ بطريقتين:
- تجنب مؤشرات لاغية.
- تجنب إلغاء تسجيل المؤشرات الأولية.
الصدأ يتجنب المؤشرات الفارغة عن طريق استبدالها
Option
الخاص. لتغيير القيمة المحتملة الخالية في نوع
Option
، تتطلب اللغة من المبرمج معالجة الحالة بوضوح بقيمة فارغة ، وإلا فلن يتم تجميع البرنامج.
ماذا تفعل إذا كانت المؤشرات التي تسمح بقيمة فارغة لا يمكن تجنبها (على سبيل المثال ، عند التفاعل مع الكود بلغة أخرى)؟ محاولة عزل الضرر. يجب أن تحدث إزالة مؤشرات المؤشرات الأولية في كتلة غير آمنة معزولة. إنه
يخفف من قواعد الصدأ ويحل بعض العمليات التي قد تسبب سلوكًا غير محدد (على سبيل المثال ، إلغاء مرجع مؤشر خام).
"كل شيء عن اقتراض chekcer ... ماذا عن هذا المكان المظلم؟"
- هذه كتلة غير آمنة. لا تذهب إلى هناك يا سيمباتجاوز سعة المخزن المؤقت
ناقشنا نقاط الضعف التي يمكن تجنبها عن طريق تقييد الوصول إلى ذاكرة غير محددة. ولكن المشكلة تكمن في أن تجاوز سعة المخزن المؤقت لا يصل بشكل غير صحيح إلى ذاكرة ، ولكنه مخصص قانونًا للذاكرة. مثل هذا الخطأ بعد الاستخدام ، يمكن أن يكون هذا الوصول مشكلة لأنه يصل إلى الذاكرة المحررة ، والتي لا تزال تحتوي على معلومات سرية لم تعد موجودة.
تدفقات المخزن المؤقت تعني ببساطة الوصول خارج الحدود. نظرًا لطريقة تخزين المخازن المؤقتة في الذاكرة ، فإنها غالبًا ما تسرب المعلومات التي قد تحتوي على بيانات حساسة ، بما في ذلك كلمات المرور. في الحالات الأكثر خطورة ، تكون ثغرات ACE / RCE ممكنة عن طريق الكتابة فوق مؤشر التعليمات.
مثال 4: تجاوز سعة المخزن المؤقت (رمز C)
int main() { int buf[] = {0, 1, 2, 3, 4};
إن أبسط حماية ضد التدفقات الزائدة للمخزن المؤقت هي أن تطلب دائمًا فحص الحدود عند الوصول إلى العناصر ، لكن هذا يؤدي إلى
ضعف الأداء .
ماذا يفعل الصدأ؟ تتطلب أنواع المخزن المؤقت المدمج في المكتبة القياسية فحص الحدود لأي وصول عشوائي ، ولكن توفر أيضًا واجهات برمجة التطبيقات التفاعلية لتسريع المكالمات المتسلسلة. هذا يضمن أن القراءة والكتابة خارج الحدود غير ممكن لهذه الأنواع. يعزز Rust الأنماط التي تتطلب عمليات فحص الحدود فقط في الأماكن التي من شبه المؤكد أنه يتعين عليك وضعها يدويًا في C / C ++.
أمن الذاكرة ليست سوى نصف المعركة
تؤدي خروقات الأمان إلى حدوث ثغرات أمنية مثل تسرب البيانات وتنفيذ التعليمات البرمجية عن بُعد. هناك عدة طرق لحماية الذاكرة ، بما في ذلك المؤشرات الذكية وجمع البيانات المهملة. يمكنك حتى
إثبات أمن الذاكرة رسميا . على الرغم من أن بعض اللغات قد تعاملت مع تدهور الأداء من أجل أمان الذاكرة ، فإن مفهوم ملكية Rust يوفر الأمان ويقلل من النفقات العامة.
لسوء الحظ ، فإن أخطاء الذاكرة ليست سوى جزء من القصة عندما نتحدث عن كتابة رمز آمن. في المقالة التالية ، سننظر في أمان الخيط والهجمات على الكود الموازي.
استغلال نقاط ضعف الذاكرة: موارد إضافية