engineering

قابلية الملاحظة في العمليات الهجومية

وصل البريد الإلكتروني في الساعة 09:14 من صباح يوم ثلاثاء. كان CISO العميل مقتضباً: "ماذا فعل وكيلكم بالضبط في الساعة 03:42 من ليلة أمس؟ رأى SOC اتصالاً صادراً لم يكن في الـ runbook فحجبه. نحتاج أن نعرف ما إذا كان من فعلكم."

أجبناه في إحدى عشرة دقيقة. ليس لأن ذاكرتنا جيدة، بل لأننا بحثنا عن الـ trace_id الخاص بالأمر الذي أرسله إلينا SOC وأعدنا بناء السلسلة بأكملها: كان الوكيل قد عثر على endpoint /api/internal/health يستجيب بـ banner إصدار، وقرر التحقق مما إذا كان قابلاً للاستغلال، وقبل إرسال أي شيء قطع Sentinel العملية لأن الوجهة كانت خارج الـ CIDR المُصرَّح به. "الاتصال الصادر" كان عبارة عن SYN لم يُكمل حتى الـ handshake. أرسلنا للعميل الـ span الكامل، مع ATT&CK ID وقرار policy engine والـ 47 بايتاً التي خرجت عبر السلك.

في ذلك اليوم قررنا أن قابلية الملاحظة ستكون منتجاً، وليست شيئاً من قبيل nice-to-have.

قابلية الملاحظة الهجومية لا تشبه الدفاعية

أمضيت سنوات في بناء خطوط أنابيب القياس عن بُعد لمراكز SOC. Loki وTempo وJaeger، أي شيء يناسب. وفي البداية ظننت أن مشكلة توثيق وكيل هجومي هي نفس المشكلة، فقط بـ schema آخر. كنت مخطئاً.

يفترض SIEM دفاعي أن ضوضاء النظام إشارة: كل auth.fail مهم، وكل DNS غريب مهم. تنفجر الـ cardinality لكن النموذج الذهني واضح: احفظ كل شيء، قرر لاحقاً. في الهجوم ينعكس النموذج. الوكيل هو الضوضاء بحكم التعريف: يفحص ويختبر ويفشل ويعيد المحاولة. إذا حفظت كل شيء بحبيبية EDR، فإن عملية مدتها أربع ساعات تنتج لك 18 GB من القياس عن بُعد لن يقرأها أحد.

لكن هناك شيء يجب أن تكون قادراً على إعادة بنائه بدقة جراحية: السلسلة السببية لأي إجراء مسّ العميل. أي إجراء. إذا أرسل الوكيل payload إلى الإنتاج، عليك أن تعرف لماذا قرر استخدام ذلك الـ payload، وما المعلومات السابقة التي بررته، وما الذي تحقق منه policy engine، وكيف استجاب الهدف. ليس هذا جنون شك: هذا ما يفصل red team محترفاً عن script kiddie لديه ميزانية.

تقول أدلة ربط MITRE ATT&CK دون مواربة: يجب أن يكون الدليل مرتكزاً على قياس عن بُعد حقيقي، لا على افتراضات المشغّل (CISA, 2023)[1]. إذا قال تقريرك "تم تنفيذ T1190 ضد المضيف X"، فأنا أريد أن أرى الـ span مع http.request.body، وقرار الوكيل، ورمز الاستجابة. بدون ذلك، فهو مجرد رأي.

تصميم الأحداث: spans وtraces وattributes

نستخدم OpenTelemetry كعمود فقري. ليس لأنه رائج، بل لأن الـ semantic conventions تحلّ مشكلة حقيقية واجهتنا: كل مشغّل كان يكتب الـ logs على هواه. يوماً target_host، ويوماً dst، ويوماً victim_ip. مستحيل الربط. يفرض OpenTelemetry schema مشتركاً، والأهم، ينشر الـ trace context عبر العمليات والـ workers (OpenTelemetry, 2024)[2].

يُصدر Gandalf CLI كل أمر كـ span. يحمل كل span الـ trace_id الخاص به، وspan_id، وparent_span_id يشير إلى التفكير الذي ولّده، وattributes تتبع اصطلاحنا berialabs.* فوق الدلالات القياسية. القاعدة الداخلية: إذا كان attribute موجوداً في المواصفات الرسمية، نستخدمه كما هو. إذا كان خاصاً بالهجوم (تقنية ATT&CK، قرار الوكيل، هاش الـ payload)، نضيف بادئة لتجنب تلويث الـ namespace.

مثال ملموس. هذا span حقيقي (مع إخفاء الهوية) من وكيل يختبر حقن SQL على معامل بحث:

{
  "name": "gandalf.exploit.sqli_attempt",
  "trace_id": "4a1f9b2c8e3d7f6a5b4c3d2e1f0a9b8c",
  "span_id": "7c8b9a0d1e2f3a4b",
  "parent_span_id": "6b7a8c9d0e1f2a3b",
  "start_time_unix_nano": 1709823742891000000,
  "end_time_unix_nano": 1709823743104000000,
  "kind": "SPAN_KIND_CLIENT",
  "status": { "code": "STATUS_CODE_OK" },
  "attributes": {
    "http.request.method": "GET",
    "http.response.status_code": 500,
    "url.full": "https://target.example.com/api/search?q=*REDACTED*",
    "server.address": "10.42.7.18",
    "server.port": 443,
    "berialabs.attack.tactic": "TA0001",
    "berialabs.attack.technique": "T1190",
    "berialabs.agent.decision_id": "dec_8f3a",
    "berialabs.agent.reasoning_ref": "trace://4a1f9b2c.../6b7a8c9d",
    "berialabs.payload.sha256": "9e3f...c7a1",
    "berialabs.payload.family": "boolean_blind_sqli",
    "berialabs.sentinel.scope_check": "passed",
    "berialabs.sentinel.cidr_match": "10.42.0.0/16",
    "berialabs.evidence.response_signature": "mysql_error_xpath"
  },
  "events": [
    {
      "name": "sentinel.validation",
      "attributes": {
        "policy.id": "scope_v3",
        "policy.result": "allow"
      }
    },
    {
      "name": "response.received",
      "attributes": {
        "response.size_bytes": 1247,
        "response.contains_error_signature": true
      }
    }
  ]
}

ثلاثة أمور تهمّني في هذا التنسيق. أولاً، يشير parent_span_id إلى تفكير الوكيل، لا إلى الأمر السابق؛ هذا يسمح لي بإعادة بناء لماذا فعل ما فعله، لا فقط ماذا فعل. ثانياً، يُشار إلى sha256 الـ payload، ولا يُضمَّن داخل الـ span: يعيش الجسم الكامل في مخزن منفصل، ويحتفظ الـ span بالهاش فقط. ثالثاً، تلتقط أحداث الـ span التحولات الرئيسية (التحقق من Sentinel، الاستجابة) التي لا يستطيع attribute مسطّح تمثيلها جيداً.

ATT&CK كـ attribute، لا كـ tag مفلوت

نربط كل إجراء بتقنية ATT&CK الخاصة به في لحظة إصدار الـ span، لا بأثر رجعي. يحمل الوكيل جدولاً داخلياً يربط عائلات الـ payload بمعرّفات التقنية، ويسافر الـ attribute مع الـ span حتى Tempo. عندما يطلب منا العميل تقريراً، فالأمر ليس تمريناً أركيولوجياً: إنه query في Grafana يصفّي حسب berialabs.attack.technique.

eBPF لالتقاط ما لا يخبرك به الوكيل

إليك الحيلة التي استغرقني وقتاً لقبولها. مهما وثّقت وكيلك جيداً، هناك أمور لا يعرف الوكيل أنه يفعلها. مكتبة طرف ثالث تفتح socket لم تكن تتوقعه. استدعاء DNS يمرّ عبر getaddrinfo دون المرور عبر عميل HTTP الخاص بك. عملية فرعية تكتب ملفاً مؤقتاً. إذا كنت تثق فقط بتوثيق userspace، فإن قابلية التتبع لديك بها ثغرات.

لهذا وضعنا hooks لـ eBPF في kernel المضيف الذي يعمل عليه الوكيل. يسمح eBPF بتنفيذ برامج معزولة داخل الـ kernel والتقاط الأحداث دون تعديله (Gregg, 2019; ebpf.io)[3]. نربط أربعة أشياء: tcp_connect وexecve وopenat وحلّ DNS عبر udp_sendmsg. يُثرَى كل حدث بـ cgroup_id الخاص بالعملية، الذي نربطه بـ trace_id النشط للوكيل عبر جدول صغير في الذاكرة المشتركة.

النتيجة: إذا فتح الوكيل اتصالاً بـ IP يقول الـ span إنه فتحه، فممتاز، كل شيء يتطابق. إذا فتح اتصالاً بـ IP لا يظهر في أي span، يُطلَق إنذار. استخدمنا هذا مرتين لاكتشاف أن مكتبة scraping كانت تقوم بـ prefetch لأيقونات المفضلة دون تنبيه. لم يكن خبيثاً، لكنه كان يمكن أن يكون كذلك، ومن حق العميل أن يعرف.

ملاحظة محرجة: eBPF قوي، لكنه ليس منيعاً. هناك عمل علني يُظهر كيف يمكن لـ rootkits مصممة خصيصاً أن تعمي الأدوات القائمة على eBPF إذا كان المهاجم يسيطر بالفعل على الـ kernel (Matheuz, 2024)[4]. في حالتنا، نموذج التهديد مختلف (نريد تدقيق وكيلنا، لا الدفاع ضد خصم لديه root)، لكن من الجدير وضعه في الاعتبار.

خط الأنابيب

من الوكيل إلى Grafana، القفزات هي هذه. يُصدر Gandalf CLI OTLP عبر gRPC إلى OpenTelemetry Collector الذي يعمل كـ sidecar. يقوم الـ Collector بثلاثة أشياء: يصفّي الـ attributes الحساسة بـ processor يكتب إلى /dev/null أي شيء يطابق أنماط PII (البريد الإلكتروني، الأرقام التي تشبه البطاقات، headers الـ auth)، ويقوم بالـ batch ويعيد التصدير. تذهب الـ traces إلى Tempo. تذهب الـ logs المُهيكلة إلى Loki عبر otlphttp exporter، الذي يقبله Loki محلياً منذ الإصدار 3.0 (Grafana Labs, 2024)[5]. تذهب الـ metrics (الكمون لكل تقنية، نسبة الـ payloads المحجوبة بواسطة Sentinel، throughput الطلبات) إلى Prometheus.

فوق كل شيء، Grafana. والـ killer feature ليست أي dashboard جميل: إنها trace-to-logs. انقر على أي span، اقفز إلى الـ logs المرتبطة بـ trace_id. انقر على أي log، اقفز إلى الـ span. هذا الربط هو ما سمح لنا بالرد على CISO في إحدى عشرة دقيقة.

المقايضات التي تؤلم

ليس كل شيء جميلاً. ثلاثة توترات حقيقية لا نزال نتفاوض عليها.

الضوضاء مقابل الإشارة. إذا وثّقت كل قرار داخلي للوكيل، تولّد ملايين الـ spans لكل عملية. إذا وثّقت الأوامر الخارجية فقط، تفقد السلسلة السببية. وجدنا توازناً معقولاً بتوثيق عُقد قرار الوكيل (لا كل token) وside-effects الخارجية دون استثناء. ومع ذلك، في العمليات الطويلة شاهدنا قمماً تصل إلى 200k spans/ساعة.

الـ Retention. Tempo على S3 رخيص، لكن الـ logs في Loki ذات الـ cardinality العالية أغلى. سياستنا الحالية: traces كاملة 90 يوماً، logs 30 يوماً، تجميعات (metrics وملخصات) ثلاث سنوات. الضغط القانوني من SOC 2 وعقود red team يدفعنا للاحتفاظ بأكثر، لا أقل.

PII في الـ logs. هذا ما يقلقني أكثر. إذا استخرج الوكيل dump قاعدة بيانات لإظهار التأثير، فهذا الـ dump يجب ألا ينتهي في الـ logs. مصفاة الـ Collector تساعد، لكنها لا تكفي. نحتفظ بطبقة ثانية: تُشفَّر النتائج الحساسة بالمفتاح العام للعميل قبل ملامسة الـ pipeline، وتُخزَّن الإشارات فقط.

كيف غيّر هذا طريقة عملنا

قبل امتلاك هذا الـ pipeline، كنا نعدّ تقارير red team يدوياً. لقطات شاشة، نسخ ولصق outputs، سرد مُعاد بناؤه من الذاكرة. كان يستغرقنا أياماً. الآن يُولَّد معظم التقرير بالاستعلام عن Tempo وLoki، ويتفرّغ المشغّل البشري للتفسير، لا النسخ.

الأهم: نناقش مع العميل من أرضية مشتركة. ليس "نظن أن الوكيل فعل X" بل "هذا الـ span في الساعة 03:42:18 مع القرار والـ payload والاستجابة". أخبرنا عميل الشهر الماضي أنها أول مرة يسلّم له فيها red team قياساً عن بُعد يستطيع SOC الخاص به استيعابه كما هو لتدريب قواعد الكشف لديه. هذا، بالنسبة لي، هو المقياس المهم: أن تكون قابلية الملاحظة الهجومية مفيدة أيضاً للمدافع.

وعندما يصل بريد إلكتروني في الساعة 09:14 من صباح يوم ثلاثاء، نردّ في إحدى عشرة دقيقة.

المراجع

  1. CISA (2023). Best Practices for MITRE ATT&CK Mapping.
  2. OpenTelemetry Authors (2024). Semantic Conventions Specification 1.41.
  3. eBPF Foundation (2024). What is eBPF? An Introduction and Deep Dive into the eBPF Technology.
  4. Matheuz (2024). Breaking eBPF Security: How Kernel Rootkits Blind Observability Tools.
  5. Grafana Labs (2024). Ingesting logs to Loki using OpenTelemetry Collector.