صلاحيات الكتابة غير الآمنة في OPC-UA: عندما يكون زر الطوارئ مجرد Boolean على الشبكة

🇺🇸EN🇸🇦AR

تخيّل إن زر الإيقاف الطارئ في المصنع مربوط بخدمة شبكة تقبل أي شهادة تولّدها على لابتوبك. ما تحتاج باسورد. ما تحتاج تكون داخل شبكة المصنع فعلياً. بس تحتاج تعرف رقم الـ node وتعيّن كم قيمة لـ false.

هذي خلاصة مشكلة الكتابة غير الآمنة في OPC-UA. البروتوكول نفسه قوي. طريقة إعداد كثير من السيرفرات هي المشكلة. المقال هذا يشرح الفكرة من الصفر، يعرض سيناريو واقعي (لكن عام)، وينتهي بطرق دفاع عملية.

وش هو OPC-UA أصلاً؟

OPC Unified Architecture هو المعيار الحديث لتبادل البيانات في الأنظمة الصناعية. أنظمة SCADA والـ PLC والـ HMI تستخدمه لقراءة قيم العملية، وأحياناً لكتابة الأوامر والـ setpoints.

بخلاف البروتوكولات القديمة (مثل Modbus)، OPC-UA مصمم أصلاً مع أمن: شهادات، تشفير، توقيع، وصلاحيات حسب الدور. من الناحية النظرية تحصل على مصادقة متبادلة وسرية وسلامة بيانات. عملياً كثير من التركيبات لسه تشتغل بهالشكل:

  • قبول شهادات self-signed أو غير موثوقة بشكل افتراضي
  • صلاحيات كتابة واسعة على nodes المفروض تكون للقراءة فقط
  • ما في تفويض على مستوى التطبيق بعد ما الشهادة تُقبل
  • معاملات السلامة الحرجة ظاهرة كمتغيرات عادية قابلة للكتابة

البروتوكول يدعم سياسات أمن متعددة. سياسة None واضحة إنها سيئة. سياسة Basic256Sha256 مع SignAndEncrypt تبدو قوية، لكن لو السيرفر ما يفرض قائمة ثقة للشهادات وما يربط الشهادة بأدوار محدودة، أي عميل عنده شهادة شكلها صحيح يقدر يكتب على كل الـ nodes المتاحة.

ليش تهتم؟

  • Boolean واحد قابل للكتابة لنظام تبريد طارئ أو قفل رحلة يقدر يعطّل آخر خط دفاع.
  • الشبكات الصناعية صارت أكثر اتصالاً بمحطات الهندسة وحتى الشبكة الإدارية. الوصول أسهل مما يتوقع كثير من مدراء المصانع.
  • التأثير فيزيائي: ارتفاع حرارة، ضغط زائد، توقف إنتاج، أو أسوأ.
  • كثير من تركيبات OPC-UA "الآمنة" لسه تعتبر handshake الشهادة تفويض كافي. هذي هي الثغرة.

مثال عملي: سيرفر التحكم في التبريد

تخيّل نظام تبريد لمفاعل كيميائي (سيناريو عام). المصنع يستخدم OPC-UA للـ nodes التالية (namespace 2 للتوضيح):

Nodeالمعنىالصلاحية المقصودة
ns=2;i=11موضع صمام التحكم (%)قراءة/كتابة (مشغّل)
ns=2;i=26مضخة الدورة الرئيسيةقراءة/كتابة (مشغّل)
ns=2;i=27مضخة الدورة الثانويةقراءة/كتابة (مشغّل)
ns=2;i=38تفعيل التبريد الطارئقراءة فقط (سلامة)
ns=2;i=41قفل الرحلة مسلّحقراءة فقط (سلامة)

في نظام مضبوط صح، الـ nodes الأخيرة يكون AccessLevel فيها CurrentRead فقط، والكتابة تُرفض حتى من عملاء مصادقين. في المثال المكسور، السيرفر يخليها قابلة للكتابة ويقبل أي شهادة عميل تطابق سياسة الأمن المتوقعة.

الاكتشاف

مهندس (أو مهاجم) يتصل بمكتبة مثل opcua في Python، يقدّم شهادة self-signed، ويتصفح مساحة العناوين. خاصية AccessLevel على كل node تكشف فوراً أي المتغيرات تقبل الكتابة. nodes السلامة اللي المفروض مقفلة تظهر بنفس صلاحية الـ setpoints العادية.

منطق الاستغلال

  1. تعطيل قفل الرحلة ( ns=2;i=41 false ). الإيقاف التلقائي صار معطل.
  2. تعطيل التبريد الطارئ ( ns=2;i=38 false ). مسار التبريد الاحتياطي اختفى.
  3. إيقاف مضختي الدورة ( ns=2;i=26 و ns=2;i=27 false ). إزالة الحرارة توقفت.
  4. دفع صمام التحكم للوضع اللي يزيد معدل التفاعل أو توليد الحرارة ( ns=2;i=11 0 أو 100 حسب العملية).

لأن منطق السلامة يعيش على نفس السيرفر ويثق بالكتابات القادمة، العملية تنحرف لحالة غير آمنة. كتابات متكررة كل كم ثانية تتغلب على أي إعادة ضبط ناعمة يحاولها الـ HMI.

نفس النمط يظهر كل ما كان قفل سلامة أو شرط سماح أو وظيفة حماية ممثلة كـ Boolean أو رقم عادي مع صلاحية كتابة واسعة زيادة عن اللزوم.

كود ضعيف مقابل كود محصّن

النسخة الضعيفة (مقتطف سيرفر بأسلوب freeopcua)

# خطير: nodes السلامة قابلة للكتابة، وما في فحص دور
from opcua import ua, Server

server = Server()
server.set_security_policy([ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt])

# يقبل أي شهادة عميل تقدم السياسة الصحيحة
# (ما في قائمة ثقة، وما في ربط بمستخدمين)

ns = server.register_namespace("urn:example:cooling")
safety = objects.add_object(ns, "Safety")
trip = safety.add_variable(ns, "TripArmed", True)
trip.set_writable()          # <-- المفروض ما تكون writable من الشبكة أبداً
eccs = safety.add_variable(ns, "EmergencyCooling", True)
eccs.set_writable()          # <-- نفس المشكلة

عميل يقدر يكمل handshake الشهادة يقدر الآن يستدعي set_value(False) على الـ nodes الاثنين.

النسخة المحصّنة

# أفضل: nodes السلامة للقراءة فقط، الشهادة مربوطة بدور، والكتابة تُرفض
trip = safety.add_variable(ns, "TripArmed", True)
trip.set_read_only()         # AccessLevel = CurrentRead فقط

eccs = safety.add_variable(ns, "EmergencyCooling", True)
eccs.set_read_only()

# بالإضافة: فرض قائمة ثقة وربط الشهادات بأدوار
# ما تعطي صلاحية "SafetyWriter" للعملاء الخارجيين أبداً.

الأفضل من كذا: خَلّي وظائف الحماية أصلاً برا مساحة العناوين القابلة للكتابة عبر الشبكة. الـ safety PLC هو اللي يملك الأقفال، والطبقة الإشرافية تشوف الحالة فقط (قراءة).

الدفاع / كيف تصلح

  1. لا تعرض أقفال السلامة كـ nodes قابلة للكتابة عبر الشبكة. خليها حالة للقراءة فقط. منطق الرحلة الفعلي يعيش في متحكم مصنّف للسلامة وما يقبل كتابات بعيدة.
  2. فرض قائمة ثقة للشهادات. ارفض الشهادات self-signed أو المجهولة. دوّرها وألغِها زي أي credential ثاني.
  3. اربط الشهادات (أو أسماء المستخدمين) بأدوار بأقل صلاحية. محطة هندسة ممكن تحتاج كتابة على setpoints، لكن ما لازم تاخذ كتابة على وظائف الحماية.
  4. راجع AccessLevel و UserAccessLevel على كل node وقت التشغيل وبعد أي تحديث firmware أو إعدادات.
  5. قسّم الشبكة. سيرفرات OPC-UA اللي تتكلم مع أنظمة سلامة ما لازم تكون قابلة للوصول من شبكات الهندسة العامة أو الشبكة الإدارية بدون jump host ومصادقة إضافية.
  6. راقب الكتابات غير المتوقعة. سجّل كل كتابة على nodes حرجة ونبّه على تغييرات في متغيرات السلامة برا نوافذ الصيانة.
  7. فضّل SignAndEncrypt، لكن اعتبره ضروري مو كافي. التشفير ما يعادل التفويض.

خلاصة

OPC-UA أعطى الصناعة بروتوكول يقدر يتأمن بشكل صحيح. كثير من التركيبات لسه تعامل "العميل جاب شهادة" كدليل إنه مسموح له يطفي زر الطوارئ. القفل مجرد Boolean على node أحد نسي يقفله. صلّح نموذج الصلاحيات، وخَلّي وظائف الحماية برا مساحة الكتابة، وزر القتل عن بُعد يختفي.

نظام السلامة الأكثر أماناً هو اللي ما يسمع لكتابات الشبكة من الأساس.