ai-ml

RAG التكتيكي: من الـ Writeups إلى الفعل

في العام الماضي، خلال مهمة ضد بنية تحتية داخلية، أضعتُ أربعين دقيقة بحثًا عن تفصيل دقيق جدًا: كيف نفّذ أحدهم تصعيد امتيازات ضد إصدار معيّن من خدمة Java مُعدّة بشكل خاطئ تكشف JMX. كنت أعلم أنني قرأت ذلك. في writeup ما، في خيط على Twitter، في Discord ما. لم أتذكّر أين. كان Google يقذفني بنتائج SEO، ومفضّلات المتصفح مقبرة، ومجلد ~/notes لديّ يحوي 1,300 ملف markdown غير مفهرس.

أربعون دقيقة. في pentest محدّد بزمن، هذا يعني Domain Admin لن تحصل عليه.

من هناك خرجت فكرة Beorn: RAG لا يبحث عن وثائق، بل يسلّم سياقًا قابلًا للتنفيذ لعميل قيد العمل. اليوم يحمل 9115 متجهًا مفهرسًا، و28 مجموعة موضوعية، ويُجيب في أقل من 50 مللي ثانية. لكن الوصول إلى ذلك كان أقل نظافة بكثير مما يبدو.

الـ RAG العام لا ينفع للهجوم

كان النموذج الأوّل من الكتاب: langchain، تضمينات OpenAI، ChromaDB محلية، تقطيع تكراري كل 512 رمزًا (tokens). كان يعمل لأسئلة من نوع "ما هو Kerberoasting؟". وفشل بشكل مُدوٍّ مع "لديّ SeImpersonatePrivilege على Windows Server 2019 يعمل عليه IIS 10 ضمن application pool، أعطني الطريق".

المشكلة لم تكن في النموذج. كانت في أن معظم أدبيات RAG تفترض أن مجموعتك النصّية هي FAQ مؤسسية أو توثيق منتج أو سياسات في PDF. الـ writeups الهجومية شيء آخر: تمزج السرد ("لاحظت أن الـ banner أعاد Apache 2.4.49")، وحمولات (payloads) حرفية، ومخرجات أدوات، واستنتاجات ضمنية لا تفهمها إلا إذا كنت تعرف TTP أصلًا. إن قطعتَ ذلك بطول ثابت، فإنك تقطع payloads، وتفصل CVE عن شرحه، ويصبح ترتيب النتائج رمية نرد.

ثمّة نمط موثّق من فريق PentestAgent في ورقتهم في NeurIPS 2024 (Shen et al., 2024)[1]: المعرفة الهجومية إجرائية، لا تقريرية. أنت لا تبحث عن حقيقة. أنت تبحث عن وصفة مشروطة بسياقك. وهذا يغيّر تصميم المسترجِع برمّته.

ما تعلّمناه من فهرسة 9115 writeup

التقطيع بالوحدة الدلالية، لا بالرموز

انتقلنا من التقطيع التكراري إلى تقطيع يَعي البنية. كل writeup من HTB له مراحل قابلة للتمييز نسبيًا: استطلاع، موطئ قدم أولي، تحرك جانبي، تصعيد امتيازات، استمرارية. نُحلّلها بمعالج أوّلي يَنظر إلى رؤوس markdown، وكتل الكود، والفواصل الصريحة. كل chunk يحمل بيانات وصفية:

{
  "box": "Sauna",
  "os": "Windows",
  "phase": "privesc",
  "ttp": "asreproast",
  "tools": ["impacket", "hashcat"],
  "cve": [],
  "difficulty": "easy",
  "lang": "en"
}

استقرّ متوسط الـ chunk عند 380 رمزًا، لكن بتباين عالٍ: بعضها 90 (أمر مع مخرجه)، وأخرى 700 (شرح كامل لـ Kerberos delegation). نشر فريق NVIDIA هذا العام تحليلًا تجريبيًا (Wang et al., 2024)[2] أظهر أن الحجم الأمثل يتعلق بالنطاق، وأن فرض التوحيد يُدهور الدقة بين 8% و15%. توافق مع ما كنا نراه.

تضمينات متعددة اللغات لأن الـ writeups ليست بالإنجليزية فقط

قرابة ثلث مجموعتنا النصية بالإسبانية أو الفرنسية أو الروسية. جرّبنا text-embedding-3-small من OpenAI، وe5-multilingual-large، وانتهينا إلى bge-m3 (Chen et al., 2024)[3]، الذي يدعم استرجاعًا كثيفًا (dense) ومتفرّقًا (sparse) ومتعدّد المتجهات في آن واحد، في أكثر من 100 لغة، بسياق طوله 8192 رمزًا. هو الأفضل لدينا لمزج اللغات في المجموعة نفسها دون فقدان recall.

تفصيلة غير بديهية: استعلامات العميل تأتي عادة بإنجليزية تقنية ("CVE-2021-26855 SSRF chain") لكنها تشير إلى شروح مكتوبة بالإسبانية. بلا تضمينات متعددة اللغات عالية الجودة، ينهار ذلك الـ cross-lingual recall. مع bge-m3 قِسنا nDCG@10 بقيمة 0.81 على مجموعة من 200 استعلام مُقيَّمة يدويًا؛ مع نموذج OpenAI كنا عالقين عند 0.69.

Reranking، لأن الـ top-k الكثيف يكذب

إعادة أول 10 متجهات بحسب تشابه الجيب تمام (cosine) هو ما تعلّمك إياه الدروس. وهو أيضًا ما ينكسر في الإنتاج. التمريرة الأولى تأتي بمرشحين مقبولين لكن الترتيب صاخب. أضفنا reranker من نوع bge-reranker-v2-m3 على أفضل 50 مرشحًا واكتفينا بأفضل 5. الكلفة: +18 مللي ثانية. تحسين Precision@5: من 0.64 إلى 0.87.

# Simplified pipeline
candidates = vector_store.search(query_emb, k=50)
reranked = reranker.score(query, [c.text for c in candidates])
top_k = sorted(zip(candidates, reranked), key=lambda x: -x[1])[:5]

تسليم السياق إلى عميل يقوم بالاستغلال

Beorn ليس روبوت دردشة. يعيش كخدمة خلف Gandalf Gateway، ويُجري الاستعلامات العميلُ دون تدخل بشري. هذا يُغيّر العقد: يجب أن تكون الاستجابة قابلة للهضم من نموذج لغوي آخر في ميلي ثوانٍ، لا أن يقرأها إنسان.

استعلام حقيقي من آخر مهمة:

beorn query \
  --filter "os=linux,phase=privesc" \
  --context "kernel 5.4.0, sudo 1.8.31, capabilities cap_dac_read_search+ep" \
  "binary with capabilities exploitation"

الاستجابة:

{
  "latency_ms": 41,
  "results": [
    {
      "chunk_id": "htb-academy-linux-privesc-cap-dac",
      "score": 0.94,
      "ttp": "T1548.001",
      "summary": "cap_dac_read_search permite leer cualquier fichero...",
      "command": "getcap -r / 2>/dev/null | grep dac_read",
      "next_steps": ["read /etc/shadow", "extract hashes",
                     "john --wordlist"]
    }
  ]
}

العميل يستلم هذا، لا فقرة نثر. الفرق بين أن تُعطي Gandalf "إليك نصًّا، استَنبط ماذا تفعل" وأن تُعطيه "إليك TTP وفق MITRE، والأمر الأوّل، والخطوات الثلاث التالية" هو الفرق بين عميل يرتجل وعميل يُنفّذ.

مقاييس حقيقية

نُشغّل Beorn في الإنتاج الداخلي منذ أربعة أشهر. الأرقام التي نراقبها:

  • زمن الاستجابة p50: 41 مللي ثانية. P99: 78 مللي ثانية. بلا تخزين مؤقت.
  • Precision@5 على مجموعة موسومة من 312 استعلامًا حقيقيًا: 0.87.
  • Recall@20: 0.93.
  • تغطية المجموعة النصية: 9115 chunk، 28 مجموعة.
  • اللغات: EN 64%، ES 22%، FR 8%، RU 4%، أخرى 2%.

حالة ملموسة تساوي أكثر من الجدول: في تقييم حديث لـ Active Directory، حدّد العميل مسار هجوم ESC8 (ADCS HTTP enrollment) في 6 دقائق من أول موطئ قدم. بدون Beorn، في تمارين مماثلة سابقة، كانت المرحلة المعادلة تستغرق منا بين 40 و90 دقيقة من البحث اليدوي عن تفاصيل الاستغلال. ليس سحرًا. هو ببساطة عدم إضاعة الوقت في تذكّر أين كانت التفصيلة.

ما لن نُكرّره

أوّلًا: بدأنا بحشر كل شيء في مجموعة عملاقة واحدة، مع استخدام البيانات الوصفية فلترًا. فكرة سيئة. حين تصل إلى آلاف عدّة من المتجهات، تصبح فلاتر ما بعد الاسترجاع مكلفة، ويتدهور الـ recall لأن تضمينات نطاقات مختلفة تتنافس في الفضاء نفسه. الـ 28 مجموعة الحالية مُقسّمة بحسب نوع المصدر ونظام التشغيل. والـ gateway يُنسّق بينها.

ثانيًا: استهنا بإزالة التكرار. ثلاثة writeups مختلفة تشرح التقنية نفسها بكلمات تكاد تكون متطابقة. بلا dedup دلالي، يُعيد لك top-5 خمس نسخ من الفقرة ذاتها. أضفنا خطوة MMR (Maximal Marginal Relevance)، فارتفعت الفائدة الذاتية لـ top-k بشكل ملحوظ، وإن كان من الصعب قياس ذلك بمقاييس كلاسيكية.

ثالثًا، وقد آلَمَنا: لم يكن لدى أول إصدار تقييم مستمر. كنّا نرفع تضمينات جديدة، ونغيّر الـ reranker، ونمضي بحدسنا. حتى أنزل تغيير في النموذج precision@5 لدينا ثماني نقاط ولم يلاحظ أحد طوال أسبوع. الآن لدينا golden set من 300+ استعلام بإجابات مُتحقَّق منها، وأي تغيير يُقيَّم قبل ترقيته. الدرس موثّق أيضًا في مسح Gao et al. (2024)[4]: بدون حلقة تقييم، تَشيخ منظومة RAG بشكل سيئ.

RAG ليس سحرًا، ولا هو محرّك بحث. هو الذاكرة التشغيلية التي كان ينبغي لعميلك أن يمتلكها منذ البداية. التعامل معها بهذه الصفة يُغيّر التصميم من الـ chunk حتى عقد الإخراج.

إن كان فريقك يبني شيئًا شبيهًا، فأنفع نصيحة يمكنني تقديمها: ابدأ من العقد. لا من النموذج. ما الذي يحتاج المستهلك — إنسانًا كان أم عميلًا — إلى استلامه ليتصرّف؟ ذلك يُحدّد التقطيع، والبيانات الوصفية، وصيغة الاستجابة. كل ما تبقى هندسة.

المراجع

  1. Shen, X., et al. (2024). PentestAgent: Incorporating LLM Agents to Automated Penetration Testing. arXiv:2411.05185.
  2. Wang, Y., et al. (2024). Finding the Best Chunking Strategy for Accurate AI Responses. NVIDIA Technical Blog.
  3. Chen, J., et al. (2024). BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings. BAAI / Hugging Face.
  4. Gao, A., et al. (2024). Retrieval Augmented Generation for Robust Cyber Defense. PNNL-36792.