سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240843)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621376)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). كيفيّة فصل «المعالجات التي تحتاج امتيازات المسؤول فقط» في تطبيقات Windows. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621376 https://comcomponent.com/ar/blog/2026/03/16/001-windows-admin-broker-deep-dive/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621376
- DOI (هذه النسخة)
- 10.5281/zenodo.22279794
في مقال سابق، «قائمة تحقّق للحدّ الأدنى من الأمان في تطوير تطبيقات Windows»، كتبتُ الخطّ: اجعل asInvoker أساساً، وافصل المعالجات التي تحتاج امتيازات المسؤول فقط.
هذه المرّة أدخل ذلك الجزء حتّى كيفيّة كتابته فعلاً.
في تطبيقات Windows لا يمكن تنفيذ جزء فقط من المعالجات داخل نفس العمليّة «بصفة المسؤول» كما يحلو لك. الرفع شأن حدود العمليّة، فالمطلوب تصميم «يقطع تلك المعالجة وحدها إلى وحدة تنفيذ أخرى».
flowchart TB
accTitle: الرفع شأن حدود العمليّة
accDescr: مخطّط يبيّن أنّه لا يمكن تنفيذ جزء فقط من المعالجات داخل نفس العمليّة بصفة المسؤول، وأنّ الرفع شأن حدود العمليّة لذا يلزم تصميم يقطع تلك المعالجة وحدها إلى وحدة تنفيذ أخرى.
wish1["نريد جزءاً فقط من المعالجات بصفة المسؤول"] -.->|"مستحيل داخل نفس العمليّة"| in1["رفع داخل نفس العمليّة"]
wish1 -->|"ما يمكن هو هذا"| cut1["قطع المعالجة إلى وحدة تنفيذ أخرى"]
الشكل 1: الرفع بوحدة العمليّة لا بوحدة الدالّة، لذا لا يقوم إلا تصميم القطع.
يسير هذا المقال بالترتيب التالي.
- الفرضيّات أوّلاً
- أيّ نموذج فصل نختار
- الشكل الأسهل استخداماً في العمل:
asInvoker+ helper EXE للمسؤول - مطبات لا تريد تجاوزها عند التنفيذ
- أمثلة شيفرة ملموسة
أمثلة الشيفرة بفرضيّة .NET 8 / تطبيق سطح مكتب Windows. إطار الواجهة أيّ من WPF / WinForms / WinUI، والفرق يظهر تقريباً في معالج الأحداث في جهة الواجهة فقط.
الشيفرة الواردة هنا منشورة على GitHub كعيّنة كاملة قابلة للبناء والتشغيل (مكتبة عقد مشتركة، وعرض توضيحيّ للواجهة / helper المسؤول، واختبارات وحدة تعمل على Linux أيضاً).
windows-admin-broker-deep-dive - komurasoft-blog-samples (GitHub)
كيف تقرأ هذا المقال
المقال طويل، لذا نضع مساراً أوّلاً.
| ما تريد معرفته | أين تقرأ |
|---|---|
| اختيار نموذج الفصل فقط | الفصول 1〜4 (الخلاصة، مقارنة النماذج الأربعة، الشكل الموصى به) |
| أحكام التصميم وأسبابها | الفصل 5 (allowlist، تثبيت المسار، runas، ACL للـ pipe، تحقّق PID) |
| شيفرة التنفيذ | الفصول 7〜14 (البنية، البيان، العقد المشترك، جهة الواجهة، جهة الـ helper) |
| كيفيّة التحقّق من أنّ الفصل تمّ فعلاً | 15.6 |
| أشكال لا ينبغي فعلها | الفصل 16 |
نصّ الشيفرة الكامل نفسه موجود أيضاً في عيّنة GitHub. يمكن القراءة هكذا: إن أردت حديث التصميم فقط فالفصول 1〜5 و 15〜16، وإن أردت التنفيذ فبالتسلسل.
ما تجهّزه قبل التجريب
لتشغيل هذه العيّنة فعلاً تحتاج ما يلي.
- جهاز Windows (مطالبة رفع UAC، و ACL صريحة عبر
PipeSecurity، وGetNamedPipeClientProcessId، والكتابة إلى HKLM كلّها حصريّة لـ Windows) - .NET 8 SDK فما فوق
- حساب يستطيع الموافقة على الرفع. إن كان حساب مسؤول فمطالبة موافقة (consent prompt)، وإن كان مستخدماً قياسيّاً فمطالبة بيانات اعتماد (credential prompt). إن أردت تأكيد المسارين فجهّز الحسابين
- جهاز يجوز فيه تعديل HKLM. العيّنة تنشئ
HKLM\SOFTWARE\Classes\*\shell\MyApp.Openعلى نطاق الجهاز. أأمن التجريب على جهاز افتراضيّ للتقييم لا على جهاز تطوير يوميّ
أوامر البناء والتشغيل الملموسة ملخّصة في README العيّنة. ضع في بالك مسبقاً نقطة واحدة: انشر الواجهة والـ helper في نفس المجلّد ثمّ نفّذ (لأنّ الـ helper يحلّ MyApp.exe في نفس مجلّده حلاً ثابتاً).
1. الخلاصة أوّلاً
نرتّب أوّلاً موضع الهبوط في العمل.
- تطبيق الواجهة العاديّ يعمل كما هو بـ
asInvoker - المعالجات التي تحتاج امتيازات المسؤول تُقطَع إلى EXE منفصل
- ذلك الـ helper EXE يُجعَل
requireAdministrator - الإقلاع بـ
runas - التواصل مع الـ helper ليس بالإدخال/الإخراج القياسيّ سيّئ التوافق مع
runas، بل بـ IPC مثل named pipe - ما يُمرَّر إلى الـ helper ليس «سلسلة أوامر خامّة» بل طلبات ذات نوع فقط
- في جهة الـ helper يُتحقَّق من محتوى الطلب مرّة أخرى
- جهة اتّصال IPC تُضيَّق بـ SID المستخدم المستدعي و PID المتوقَّع
«أسهل إن عمل بصفة المسؤول» صحيح في المرّة الأولى فقط. لاحقاً، حول UAC والسحب والإفلات وتصميم السجلّ والدخل الخارجيّ وتشغيل الدعم وتحميل DLL وموضع حفظ الإعداد، تُقابَل عادةً بوجه كريه.
flowchart TB
accTitle: هيكل موضع الهبوط في العمل
accDescr: مخطّط يبيّن الهيكل: الواجهة تعمل كما هي بـ asInvoker، والمعالجات التي تحتاج امتيازات المسؤول تُقطَع إلى EXE منفصل بـ requireAdministrator وتُقلَع بـ runas، وتُتبادل طلبات ذات نوع فقط عبر named pipe.
uix1["الواجهة تبقى asInvoker"] -->|"إقلاع بـ runas"| hx1["helper EXE (requireAdministrator)"]
uix1 -->|"طلب ذو نوع عبر named pipe"| hx1
hx1 --> vfy1["إعادة التحقّق من الطلب في جهة الـ helper"]
الشكل 2: الهيكل يُحسَم بثلاث: واجهة غير مرفوعة + helper مرفوع + IPC لطلبات ذات نوع.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 21، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. ترتيب الفرضيّة: لا يمكن جعل جزء فقط من نفس العمليّة بصفة المسؤول
UAC في Windows ليس «رفعاً بوحدة الدالّة» بل يُتحكَّم فيه بـ «بأيّ رمز / مستوى سلامة تعمل العمليّة». التطبيق الذي يحتاج رمز وصول المسؤول يصير هدفاً لمطالبة الرفع، وترث عمليّات الأب والابن الرمز بنفس مستوى السلامة. أي أنّ تصميماً ينفّذ دالّة معيّنة فجأة بامتيازات المسؤول داخل عمليّة واجهة غير مرفوعة غير ممكن. إن لزم فاستخدم وحدة تنفيذ أخرى: عمليّة منفصلة أو خدمة أو مهمّة أو COM مرفوع.
إن فُكّر بإسقاط هذه الفرضيّة، صار الاستشارة التصميميّة نوعاً مؤسفاً قليلاً: «نريد أن نصير مسؤولين لحظة الضغط على هذا الزرّ فقط». Windows لا يملأ ذلك بسحر.
flowchart TB
accTitle: UAC يُتحكَّم فيه بالرمز ومستوى السلامة
accDescr: مخطّط يبيّن أنّ UAC ليس رفعاً بوحدة الدالّة بل يُتحكَّم فيه بالرمز ومستوى السلامة اللذين تعمل بهما العمليّة، وأنّ عمليّات الأب والابن ترث الرمز بنفس المستوى فلا يمكن رفع بوحدة الدالّة ويلزم وحدة تنفيذ أخرى.
uac1["وحدة تحكّم UAC"] --> tk1["رمز العمليّة ومستوى السلامة"]
tk1 --> inh1["الأب والابن يرثان نفس المستوى"]
inh1 --> no1["لا يمكن الرفع بوحدة الدالّة"]
no1 --> alt2["عمليّة أخرى أو خدمة أو مهمّة أو COM مرفوع"]
الشكل 3: ما دامت وحدة التحكّم هي العمليّة، فلا موضع للمعالجة التي تريد رفعها إلا وحدة تنفيذ أخرى.
2.1 ثبّت أوّلاً مقابلة مستوى السلامة (integrity level)
في ما يلي من المقال يظهر لفظا medium integrity / high integrity. نقرّر المقابلة مسبقاً.
| مستوى السلامة | معناه في هذا المقال | مثال |
|---|---|---|
| medium | عمليّة تعمل كمستخدم قياسيّ | تطبيق واجهة asInvoker |
| high | عمليّة مرفوعة | helper EXE بـ requireAdministrator |
في Mandatory Integrity Control في Windows تُعرَّف أربع درجات: low / medium / high / system، والمستخدم القياسيّ يتلقّى medium، والمستخدم المرفوع high. أي أنّ تصميم هذا المقال حديث عن جعل «عمليّة واجهة medium» و «عمليّة helper high» تتحدّثان بعد رسم خطّ صريح بينهما.
إن استقرّت هذه المقابلة في الذهن، صار حديث CurrentUserOnly في 5.6 وحديث 16.4 أيضاً قابلاً للقراءة مباشرة.
flowchart TB
accTitle: خطّ الفصل بين medium و high
accDescr: مخطّط يبيّن أنّ واجهة asInvoker العاملة كمستخدم قياسيّ تتلقّى medium، وأنّ helper requireAdministrator المرفوع يتلقّى high، وأنّ تصميم المقال حديث عن جعل الاثنين يتحدّثان بعد رسم خطّ صريح.
med1["medium: عمليّة واجهة asInvoker"] -->|"نتحدّث بعد رسم خطّ"| hg1["high: عمليّة helper مرفوعة"]
med1 -.-> sd2["رمز المستخدم القياسيّ"]
hg1 -.-> ad2["رمز المستخدم المرفوع"]
الشكل 4: هذا التصميم حديث عن رسم حدود صريحة بين واجهة medium و helper high.
3. أيّ نموذج فصل نختار
في Microsoft Learn تُذكَر أساساً أربعة أساليب لفصل التطبيقات التي تحتاج امتيازات المسؤول.
| النموذج | الشكل التقريبيّ | المشهد المناسب |
|---|---|---|
| Administrator Broker Model | تطبيق واجهة لمستخدم قياسيّ + helper EXE للمسؤول | عمليّات المسؤول متفرّقة ويكفي إظهار UAC في اللحظة اللازمة |
| Operating System Service Model | واجهة مستخدم قياسيّ + خدمة مقيمة | وظائف إدارة دائمة التشغيل، ومراقبة في الخلفيّة، ومعالجة بلا مراقبة |
| Elevated Task Model | واجهة مستخدم قياسيّ + مهمّة مجدولة بامتيازات المسؤول | معالجة نمطيّة تنتهي قصيرة في كلّ مرّة |
| Administrator COM Object Model | واجهة مستخدم قياسيّ + COM مرفوع | حين يوجد تصميم COM قائم والوظائف محدودة جدّاً |
مؤشّر الاختيار كالتالي.
3.1 الأسهل النظر فيه أوّلاً هو broker EXE
broker EXE يناسب عمليّات كهذه.
- تسجيل / إلغاء تسجيل تكامل Explorer
- تغيير إعداد على نطاق الجهاز تحت HKLM
- تسجيل / إلغاء تسجيل خدمة التطبيق نفسه
- إضافة / حذف قاعدة جدار ناريّ
- عمليّة مسؤول تحت Program Files
هذه غير لازمة عادةً، وتصير لازمة فقط عند الضغط على زرّ معيّن في شاشة الإعداد. عندئذ أوضح من إخراج خدمة مقيمة شكل إقلاع helper EXE للمسؤول مرّة واحدة ثمّ الانتهاء.
flowchart TB
accTitle: الشكل الذي يناسب broker EXE
accDescr: مخطّط يبيّن أنّه إن كانت عمليّة المسؤول غير لازمة عادةً وتصير لازمة فقط عند الضغط على زرّ معيّن في شاشة الإعداد، فأوضح من إخراج خدمة مقيمة شكل إقلاع helper EXE للمسؤول مرّة واحدة ثمّ الانتهاء.
btn1["الضغط على زرّ معيّن في شاشة الإعداد"] --> once2["إقلاع helper EXE مرّة واحدة"]
once2 --> done1["تنتهي العمليّة ويختفي الـ helper"]
btn1 -.->|"بهذا التواتر لا لزوم"| svc2["إخراج خدمة مقيمة"]
الشكل 5: لعمليّات المسؤول المتفرّقة، أوضح helper EXE يعيش في اللحظة اللازمة فقط.
3.2 اختيار الخدمة حين تكون «دائمة» و«بلا مراقبة» و«متكرّرة»
الخدمة نموذج يتواصل فيه تطبيق المستخدم القياسيّ عبر RPC ونحوه. الميزة تلقّي معالجة جهة الإدارة بلا مطالبة رفع، لكن مقابل ذلك تزيد مسؤوليّة تشغيل عمليّة مقيمة.
flowchart TB
accTitle: مقايضة نموذج الخدمة
accDescr: مخطّط يبيّن مقايضة نموذج الخدمة: ميزة تلقّي معالجة جهة الإدارة بلا مطالبة رفع مقابل زيادة مسؤوليّة تشغيل عمليّة مقيمة.
svm1["Operating System Service Model"] -->|"الميزة"| npr1["يمكن التلقّي بلا مطالبة رفع"]
svm1 -->|"الثمن"| res2["مسؤوليّة تشغيل عمليّة مقيمة"]
svm1 -.-> fitx["يناسب استخداماً دائماً وبلا مراقبة ومتكرّراً"]
الشكل 6: الخدمة اختيار تحمل تشغيل مقيم مقابل الاستغناء عن المطالبة.
ما يناسب الخدمة استخدامات كهذه.
- مراقبة دائمة
- جمع السجلّات
- تحديث في الخلفيّة
- ارتباط دائم بجهاز أو عفريت
- وظيفة إدارة تُشارَك من عدّة جلسات واجهة
3.3 المهمّة تناسب «معالجة نمطيّة قصيرة»
Elevated Task Model شكل يقلع فيه تطبيق المستخدم القياسيّ مهمّة مجدولة تعمل بامتيازات المسؤول. أخفّ من الخدمة وتُغلق عند الانتهاء، فيناسب وظائف نمطيّة لكلّ مرّة.
3.4 COM المرفوع محدود جدّاً
يبدو COM elevation moniker مريحاً، لكن موضع الاستخدام يضيق. حتّى Microsoft Learn يقول إنّ واجهة التحكّم في COM المرفوع يلزم تقديمها في جهة COM، ولا يناسب اتّجاه «ترك COM المرفوع يفعل ما يشاء من واجهة غير مرفوعة».
flowchart TB
accTitle: ضيق موضع استخدام COM المرفوع
accDescr: مخطّط يبيّن أنّ COM المرفوع يبدو مريحاً لكن يلزم تقديم واجهة التحكّم فيه في جهة COM، ولا يناسب اتّجاه ترك COM المرفوع يفعل ما يشاء من واجهة غير مرفوعة.
look1["COM المرفوع يبدو مريحاً"] -.-> free1["تشغيل حرّ من واجهة غير مرفوعة"]
free1 -.->|"لا يناسب هذا الاتّجاه"| ngc1["موضع الاستخدام يضيق"]
ui2["واجهة التحكّم تُقدَّم في جهة COM"] --> ngc1
الشكل 7: COM المرفوع بفرضيّة أنّ واجهة التحكّم في جهة COM، فلا يصير مهرباً عامّاً.
4. الموصى به هذه المرّة: واجهة asInvoker + helper EXE بـ requireAdministrator
من هنا نجعل الشكل الأسهل استخداماً في العمل ملموساً.
flowchart TB
subgraph MED["medium integrity ── لا يُرفَع حتّى النهاية"]
UI["MyApp.exe (asInvoker)<br/>يتلقّى تشغيل المستخدم ويجمع الطلب فقط"]
end
subgraph HIGH["high integrity ── عمليّة رفع قصيرة العمر"]
BR["MyApp.AdminBroker.exe<br/>(requireAdministrator)"]
PIPE["منفذ named pipe<br/>من يُسمَح له بالاتّصال SID مستخدم الواجهة فقط<br/>ويُطابَق أيضاً PID جهة الاتّصال"]
DISP["توزيع بـ allowlist للعمليّات<br/>وإعادة التحقّق من الوسائط في جهة الـ helper"]
end
TGT["هدف ثابت يحتاج امتيازات المسؤول<br/>مفتاح تحت HKLM / تسجيل خدمة / قاعدة جدار ناريّ"]
UI -->|"إقلاع بمسار مطلق + Verb=runas<br/>(هنا تظهر مطالبة UAC)"| BR
BR --> PIPE
UI -->|"طلب ذو نوع<br/>(لا تُمرَّر سلسلة أوامر خامّة)"| PIPE
PIPE --> DISP
DISP --> TGT
الشكل 8: نطابق حدود الرفع بحدود العمليّة. جهة الواجهة تبقى medium، والـ helper وحده يعمل high لوقت قصير
النقاط ثلاث.
- عمليّة الواجهة تبقى غير مرفوعة حتّى النهاية
- helper المسؤول قصير العمر
- العمليّات التي يقبلها الـ helper allowlist ثابتة فقط
بالالتزام بهذه الثلاث وحدها يترتب التصميم كثيراً.
flowchart TB
accTitle: ثلاث نقاط ينبغي الالتزام بها
accDescr: مخطّط يبيّن أنّ الالتزام بثلاث: عمليّة الواجهة تبقى غير مرفوعة حتّى النهاية، و helper المسؤول قصير العمر، والعمليّات التي يقبلها الـ helper allowlist ثابتة فقط، يرتّب التصميم كثيراً.
p1["الواجهة غير مرفوعة حتّى النهاية"] --> tidy1["يترتب التصميم كثيراً"]
p2["الـ helper قصير العمر"] --> tidy1
p3["ما يُقبَل allowlist فقط"] --> tidy1
الشكل 9: بالالتزام بغير المرفوع وقصير العمر و allowlist وحدها، يتحدّد شكل حدود الامتياز.
5. قواعد لا تريد تجاوزها في التنفيذ
هذا موضع أولى تقريره قبل كتابة الشيفرة.
5.1 لا تجعل الـ helper «يفعل كلّ شيء»
مثال سيّئ هو هذا.
- تمرير
reg add ...كاملاً كسلسلة من الواجهة إلى الـ helper - تمرير
sc.exe ...كاملاً كسلسلة من الواجهة إلى الـ helper - تمرير مسار سجلّ اعتباطيّ أو مسار EXE اعتباطيّ من الواجهة إلى الـ helper
إن فُعل هذا، إن انكسرت الواجهة انكسر الـ helper معها. helper المسؤول داخل حدود الرفع. إن صُنع هنا «منفذ ينفّذ أيّ شيء» فالأمر خطر جدّاً.
الشكل الحسن كالتالي.
set-explorer-context-menuinstall-serviceadd-firewall-rule
ثبّت العمليّة نفسها هكذا، وقرّب الوسائط اللازمة أيضاً إلى bool / enum / عدد / سلسلة محدودة.
flowchart TB
accTitle: الشكل الذي يمنع «يفعل كلّ شيء»
accDescr: مخطّط يبيّن أنّ تمرير سلسلة أوامر خامّة أو مسار اعتباطيّ إلى الـ helper يصنع منفذ تنفيذ لأيّ شيء فتنكسر الواجهة والـ helper معاً، لذا تُثبَّت العمليّة نفسها وتُقرَّب الوسائط إلى أنواع محدودة.
raw1["تمرير سلسلة أوامر خامّة"] --> hole1["يُصنَع منفذ ينفّذ أيّ شيء"]
hole1 --> both1["إن انكسرت الواجهة انكسر الـ helper"]
fix2["تثبيت العمليّة وتقريب الوسائط إلى أنواع محدودة"] --> narrow1["يضيق معنى الـ helper"]
الشكل 10: ما يُمرَّر إلى داخل حدود الرفع عمليّة ثابتة ووسائط محدودة فقط.
5.2 مسار الممرَّر إلى الـ helper مطلق، ولا تفرط الواجهة في تقريره
EXE الـ helper نفسه الذي يُقلَع بـ runas يُحدَّد بمسار مطلق.
تجنّب البحث في PATH أو الاتكال على مسار نسبيّ.
ثمّ هدف ما ينفّذه الـ helper أيضاً يُحلّ ثابتاً قدر الإمكان في جهة الـ helper.
في عيّنة هذه المرّة، EXE هدف التسجيل في قائمة سياق Explorer مثبَّت على MyApp.exe في نفس مجلّد الـ helper.
5.3 إن استخدمتَ Verb="runas" فصرّح بـ UseShellExecute=true
في .NET لا ينفع ProcessStartInfo.Verb إلا حين UseShellExecute=true.
ثمّ القيمة الافتراضيّة لـ UseShellExecute تختلف بين .NET Framework و .NET Core / .NET.
إن تُرك للافتراضيّ حدث لاحقاً حادث مزعج بهدوء: «بيئة تعمل وبيئة لا تعمل».
لذلك صرّح حتماً هنا.
flowchart TB
accTitle: إعداد ينبغي التصريح به عند إقلاع runas
accDescr: مخطّط يبيّن أنّ Verb في ProcessStartInfo لا ينفع إلا حين UseShellExecute صحيح، وأنّ القيمة الافتراضيّة تختلف بين .NET Framework و .NET، فإن تُرك للافتراضيّ عمل في بيئة ولم يعمل في أخرى، لذا ينبغي التصريح حتماً.
vb1["نريد استخدام Verb=runas"] --> req2["يلزم UseShellExecute=true"]
req2 -.-> defd1["الافتراضيّ يختلف بين Framework و .NET"]
defd1 -->|"إن تُرك"| envd1["تظهر بيئة تعمل وبيئة لا تعمل"]
req2 -->|"لذلك"| exp2["صرّح حتماً"]
الشكل 11: لشرط عمل Verb واختلاف الافتراضيّ، ثبّت UseShellExecute بالتصريح.
5.4 runas وإعادة توجيه الإدخال/الإخراج القياسيّ سيّئا التوافق
مع UseShellExecute=true يصعب التواصل المفترض على إعادة توجيه الإدخال/الإخراج القياسيّ.
لذلك أوضح أن يكون التبادل مع الـ helper IPC آخر مثل named pipe.
5.5 لا تعتمد على ACL الافتراضيّة لـ named pipe
named pipe، بواصف الأمن الافتراضيّ، يدخل حقّ قراءة لـ Everyone وللمجهول في الافتراضيّ. استخدام ذلك كما هو في IPC لـ helper المسؤول مستخفّ جدّاً.
أولى ضبط PipeSecurity صريح حتماً.
5.6 لا تستخدم PipeOptions.CurrentUserOnly لهذا الغرض
هذا يبدو مريحاً من النظرة الأولى.
لكن على Windows، CurrentUserOnly يتحقّق لا من حساب المستخدم وحده بل من مستوى الرفع أيضاً.
أي أنّه لا يناسب التواصل بين واجهة غير مرفوعة و helper مرفوع.
ثمّ من يتّصل بالـ pipe وبأيّ رمز يتغيّر حسب نوع مطالبة UAC. إن ثُبِّت هذا في جدول سهل تتبّع سبب الحاجة إلى ACL صريحة.
| حساب تنفيذ الواجهة | مطالبة UAC الظاهرة | الحساب الذي يعمل به الـ helper | WindowsIdentity.GetCurrent() للـ helper منشئ الـ pipe |
SID جهة الواجهة المتّصلة |
|---|---|---|---|---|
| حساب مسؤول (غير مرفوع) | مطالبة موافقة (الضغط على «نعم» فقط) | رمز رفع لنفس المستخدم | نفس مستخدم الواجهة | مستخدم الواجهة |
| مستخدم قياسيّ | مطالبة بيانات اعتماد (إدخال بيانات حساب مسؤول آخر) | حساب مسؤول آخر أُدخل | مستخدم آخر غير الواجهة | مستخدم الواجهة |
القراءة كالتالي.
- في الصفّ الأعلى «المستخدم الحالي للـ helper = مستخدم الواجهة»، فإن صنعتَ ACL بالنظر إلى SID الخاصّ بالـ helper فقط اتّصل الأمر مصادفة
- في الصفّ الأسفل المستخدم الحالي للـ helper ومستخدم الواجهة شخصان مختلفان. إن صنعتَ ACL بالنظر إلى
WindowsIdentity.GetCurrent()وحده هنا، تعذّر على مستخدم الواجهة الأصليّ إرسال طلبه - في الصفّين كليهما، يُرفَض
CurrentUserOnlyبفرق مستوى الرفع «واجهة medium» و «helper high»
أي أنّ الشكل الوحيد الذي يقوم في الصفّين هو «استلام SID من جهة الواجهة وإعطاء حقّ الاتّصال لذلك SID».
لذلك هذه المرّة يكون الشكل:
- في جهة الواجهة الحصول على SID الخاصّ بها وتمريره إلى الـ helper
- في جهة الـ helper إعطاء حقّ اتّصال الـ pipe لـ SID مستخدم الواجهة فقط
- ثمّ تأكيد PID جهة الاتّصال أيضاً بـ
GetNamedPipeClientProcessId
flowchart TB
accTitle: تمرير SID يجعل المسارين يقومان
accDescr: مخطّط يبيّن الشكل الوحيد الذي يقوم في مطالبة الموافقة ومطالبة بيانات الاعتماد كليهما: جهة الواجهة تحصل على SID الخاصّ بها وتمرّره إلى الـ helper، والـ helper يعطي حقّ اتّصال الـ pipe لذلك SID فقط، ثمّ يؤكّد أيضاً PID جهة الاتّصال.
sid1["الواجهة تحصل على SID الخاصّ بها وتمرّره"] --> aclx["الـ helper يعطي حقّ الاتّصال لذلك SID فقط"]
aclx --> pidx["تأكيد PID جهة الاتّصال أيضاً"]
cuo1["استخدام CurrentUserOnly"] -.->|"يُرفَض بفرق مستوى الرفع"| ngz1["لا يقوم لهذا الغرض"]
الشكل 12: ما يقوم في مساري المطالبة كليهما شكل بناء ACL بـ SID الممرَّر من الواجهة فقط.
5.7 تحقّق PID دفاع إضافيّ لتقليل «الدخول الجانبيّ المستخفّ»
اسم pipe عشوائيّ وحده أفضل بكثير، لكن مجال اتّصال عمليّة أخرى تعمل بنفس المستخدم أوّلاً ليس صفراً.
لذلك يستخدم الـ helper GetNamedPipeClientProcessId ويؤكّد هل يطابق PID عمليّة الواجهة المتوقَّع.
طبعاً، ليس أنّ تطابق PID يعني الوثوق بأيّ شيء. إن اختُرقت الواجهة وصلت طلبات خطرة إلى الـ helper أيضاً. لذلك يلزم allowlist العمليّات والتحقّق من الوسائط في جهة الـ helper.
flowchart TB
accTitle: دفاع إضافيّ برصف طبقات
accDescr: مخطّط يبيّن فكرة تقليل الدخول الجانبيّ المستخفّ والطلبات الخطرة كليهما برصف طبقات: اسم pipe عشوائيّ، و ACL مقصورة على SID، ومطابقة PID جهة الاتّصال، و allowlist العمليّات والتحقّق من الوسائط.
l1["اسم pipe عشوائيّ"] --> l2["ACL مقصورة على SID"]
l2 --> l3["مطابقة PID جهة الاتّصال"]
l3 --> l4["allowlist وإعادة التحقّق من الوسائط"]
l4 -.-> why3["حتّى مع تطابق PID لا نثق بالطلب"]
الشكل 13: لا طبقة واحدة كاملة، فنضيّق برصف طبقات على الاتّصال والطلب كليهما.
إن رتّبنا القواعد حتّى هنا من الإقلاع إلى الانتهاء، صارت كالتالي. يكفي أن تُقرأ على أنّ كلّ خطوة في جهة الواجهة يقابلها تأكيد في جهة الـ helper.
sequenceDiagram
participant UI as MyApp.exe (medium)
participant OS as Windows / UAC
participant BR as AdminBroker.exe (high)
participant TGT as هدف يحتاج امتيازات المسؤول
UI->>UI: نقرّر اسم الـ pipe ونجهّز SID و PID الخاصّين بنا
UI->>OS: إقلاع بمسار مطلق + Verb=runas
OS->>BR: إن وُوفق يُقلَع برمز مرفوع
BR->>BR: إنشاء pipe بـ ACL تسمح لذلك SID فقط
UI->>BR: الاتّصال بالـ pipe
BR->>BR: مطابقة PID جهة الاتّصال
UI->>BR: طلب ذو نوع (اسم العمليّة والوسائط)
BR->>BR: رفض ما خارج allowlist والوسائط غير المتوقَّعة
BR->>TGT: التشغيل على الهدف الثابت فقط
BR-->>UI: إعادة النتيجة
BR->>BR: الإنهاء دون إبقاء حالة الرفع
الشكل 14: لكلّ من الإقلاع والاتّصال والطلب تأكيد مقابل في جهة الـ helper. إن أُسقط واحد صار ذلك السلم مفتوحاً
6. موضوع العيّنة
هذه المرّة مثال تسجيل / إلغاء تسجيل قائمة النقر باليمين في Explorer على نطاق الجهاز.
السبب بسيط:
- تحتاج امتيازات المسؤول
- حدود العمليّة واضحة
- لا حاجة لتمرير سلسلة أوامر اعتباطيّة إلى الـ helper
- وارد كالعادة في العمل
موضع التسجيل مفتاح ثابت كهذا.
HKLM\SOFTWARE\Classes\*\shell\MyApp.OpenHKLM\SOFTWARE\Classes\*\shell\MyApp.Open\command
الواجهة تحمل مربّع اختيار «التسجيل في قائمة النقر باليمين في Explorer» فقط، وعمليّة السجلّ الفعليّة تتمّ في جهة الـ helper.
7. بنية الحلّ
MyApp/
MyApp/ UI アプリ (asInvoker)
app.manifest
ElevationBrokerClient.cs
SettingsPage.xaml.cs
MyApp.AdminBroker/ 管理者 helper (requireAdministrator)
app.manifest
Program.cs
BrokerLaunchOptions.cs
ExplorerContextMenuRegistration.cs
MyApp.BrokerProtocol/ 共通契約
BrokerProtocol.cs
إن جُعل العقد المشترك مشروعاً منفصلاً، سهل توحيد
- أسماء العمليّات
- أنواع request / response
- شكل رسالة الـ pipe
بين الواجهة والـ helper.
8. البيان (manifest)
8.1 جهة الواجهة (MyApp/app.manifest)
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApp.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
8.2 جهة الـ helper (MyApp.AdminBroker/app.manifest)
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApp.AdminBroker.app" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
الواجهة asInvoker دائماً.
الـ helper وحده requireAdministrator.
إن عُكس هذا اختفى معنى الفصل الذي كُلِّف.
9. شيفرة العقد المشترك
9.1 MyApp.BrokerProtocol/BrokerProtocol.cs
using System.Buffers.Binary;
using System.Text.Json;
namespace MyApp.BrokerProtocol;
public static class BrokerJson
{
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public static class BrokerOperations
{
public const string SetExplorerContextMenu = "set-explorer-context-menu";
}
public sealed record BrokerRequest(string Operation, JsonElement Payload);
public sealed record BrokerResponse(bool Success, string? ErrorCode, string? Message)
{
public static BrokerResponse Ok(string? message = null) => new(true, null, message);
public static BrokerResponse Fail(string errorCode, string message) =>
new(false, errorCode, message);
}
public sealed record SetExplorerContextMenuRequest(bool Enabled);
public static class PipeMessageSerializer
{
private const int MaxPayloadBytes = 256 * 1024;
public static async Task WriteAsync<T>(Stream stream, T value, CancellationToken cancellationToken)
{
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(value, BrokerJson.Options);
if (payload.Length > MaxPayloadBytes)
{
throw new InvalidDataException($"Payload is too large: {payload.Length} bytes.");
}
byte[] header = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(header, payload.Length);
await stream.WriteAsync(header.AsMemory(0, header.Length), cancellationToken);
await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancellationToken);
await stream.FlushAsync(cancellationToken);
}
public static async Task<T> ReadAsync<T>(Stream stream, CancellationToken cancellationToken)
{
byte[] header = await ReadExactAsync(stream, sizeof(int), cancellationToken);
int payloadLength = BinaryPrimitives.ReadInt32LittleEndian(header);
if (payloadLength <= 0 || payloadLength > MaxPayloadBytes)
{
throw new InvalidDataException($"Invalid payload length: {payloadLength}");
}
byte[] payload = await ReadExactAsync(stream, payloadLength, cancellationToken);
return JsonSerializer.Deserialize<T>(payload, BrokerJson.Options)
?? throw new InvalidDataException($"Failed to deserialize {typeof(T).FullName}.");
}
private static async Task<byte[]> ReadExactAsync(Stream stream, int length, CancellationToken cancellationToken)
{
byte[] buffer = new byte[length];
int offset = 0;
while (offset < length)
{
int read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset), cancellationToken);
if (read == 0)
{
throw new EndOfStreamException("Pipe was closed before the expected number of bytes was read.");
}
offset += read;
}
return buffer;
}
}
النقطة: لا تُسال JSON كما هي في الـ pipe، بل تُرسَل بطول مرفق. بروتوكول بسيط: طلب واحد، استجابة واحدة، يقلّل الحوادث.
flowchart TB
accTitle: بروتوكول بسيط بطول مرفق
accDescr: مخطّط يبيّن أنّ عدم إسالة JSON كما هي في الـ pipe وإرسالها بترويسة طول، وجعل البروتوكول بسيطاً طلب واحد واستجابة واحدة، يقلّل الحوادث.
m1["كتابة ترويسة الطول"] --> m2["كتابة JSON المتن"]
m2 --> m3["الطرف يقرأ بقدر الطول حتماً"]
m3 -.-> simple1["تنفع بساطة طلب واحد واستجابة واحدة"]
الشكل 15: الرسالة تُرسَل بطول مرفق وتُضيَّق إلى طلب واحد واستجابة واحدة فيقلّ الحادث.
10. جهة الواجهة: إقلاع الـ helper والتواصل
10.1 MyApp/ElevationBrokerClient.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;
namespace MyApp;
public sealed class ElevationBrokerClient
{
private readonly string _helperExePath;
public ElevationBrokerClient(string helperExePath)
{
_helperExePath = Path.GetFullPath(helperExePath);
if (!Path.IsPathRooted(_helperExePath))
{
throw new ArgumentException("Helper executable path must be absolute.", nameof(helperExePath));
}
if (!File.Exists(_helperExePath))
{
throw new FileNotFoundException("Helper executable was not found.", _helperExePath);
}
}
public async Task SetExplorerContextMenuEnabledAsync(bool enabled, CancellationToken cancellationToken = default)
{
string pipeName = $"myapp-broker-{Guid.NewGuid():N}";
int clientPid = Environment.ProcessId;
string clientSid = GetCurrentUserSid();
StartHelper(pipeName, clientPid, clientSid);
using var pipe = new NamedPipeClientStream(
serverName: ".",
pipeName: pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.Asynchronous);
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
connectCts.CancelAfter(TimeSpan.FromSeconds(30));
await pipe.ConnectAsync(connectCts.Token);
BrokerRequest request = new(
BrokerOperations.SetExplorerContextMenu,
JsonSerializer.SerializeToElement(
new SetExplorerContextMenuRequest(enabled),
BrokerJson.Options));
await PipeMessageSerializer.WriteAsync(pipe, request, cancellationToken);
BrokerResponse response = await PipeMessageSerializer.ReadAsync<BrokerResponse>(pipe, cancellationToken);
if (!response.Success)
{
throw new InvalidOperationException(
$"Admin broker returned an error. Code={response.ErrorCode}, Message={response.Message}");
}
}
private void StartHelper(string pipeName, int clientPid, string clientSid)
{
string workingDirectory = Path.GetDirectoryName(_helperExePath)
?? throw new InvalidOperationException("Helper executable directory could not be resolved.");
var startInfo = new ProcessStartInfo
{
FileName = _helperExePath,
Arguments = BuildArguments(pipeName, clientPid, clientSid),
WorkingDirectory = workingDirectory,
UseShellExecute = true,
Verb = "runas"
};
try
{
Process.Start(startInfo)
?? throw new InvalidOperationException("The helper process could not be started.");
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new OperationCanceledException("أُلغي اعتماد صلاحيات المسؤول.", ex);
}
}
private static string GetCurrentUserSid()
{
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
return identity.User?.Value
?? throw new InvalidOperationException("Current user SID could not be resolved.");
}
private static string BuildArguments(string pipeName, int clientPid, string clientSid)
{
return string.Join(
" ",
"--pipe",
QuoteArgument(pipeName),
"--client-pid",
clientPid.ToString(CultureInfo.InvariantCulture),
"--client-sid",
QuoteArgument(clientSid));
}
private static string QuoteArgument(string value)
{
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
}
ما يُمرَّر هنا إلى الـ helper الحدّ الأدنى اللازم لاسم الـ pipe وتأكيد جهة الاتّصال فقط. عمليّة المسؤول نفسها تُحبَس في طلب ذي نوع يُرسَل داخل الـ pipe.
flowchart TB
accTitle: تقسيم الأدوار بين وسائط الإقلاع والـ pipe
accDescr: مخطّط يبيّن تقسيم الأدوار: وسائط إقلاع الـ helper تحمل الحدّ الأدنى اللازم لاسم الـ pipe وتأكيد جهة الاتّصال فقط، وعمليّة المسؤول نفسها تُحبَس في طلب ذي نوع يُرسَل داخل الـ pipe.
argx["وسائط الإقلاع"] -->|"ما يُمرَّر"| minx["الحدّ الأدنى: اسم الـ pipe و PID و SID"]
pipx["داخل الـ pipe"] -->|"ما يُحبَس"| reqx["عمليّة المسؤول في طلب ذي نوع"]
minx -.-> nolx["محتوى العمليّة لا يُحمَل على الوسائط"]
الشكل 16: الوسائط لترتيب الاتّصال فقط، ومحتوى العمليّة يُغلَق في الـ pipe كطلب ذي نوع.
QuoteArgument هذا تنفيذ أدنى يفترض قيماً بسيطة مثل اسم الـ pipe و PID و SID الممرَّرة في هذه العيّنة. إن مرّرتَ مسار Windows اعتباطيّاً أو سلسلة إدخال حرّ كوسيط سطر أوامر، فاستبدله بمعالجة تهريب مخصّصة وفق قواعد تحليل argv في Windows.
11. جهة الـ helper: تحليل وسائط الإقلاع
11.1 MyApp.AdminBroker/BrokerLaunchOptions.cs
namespace MyApp.AdminBroker;
internal sealed class BrokerLaunchOptions
{
public required string PipeName { get; init; }
public required int ExpectedClientProcessId { get; init; }
public required string ClientUserSid { get; init; }
public static BrokerLaunchOptions Parse(string[] args)
{
string? pipeName = null;
int? clientPid = null;
string? clientSid = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--pipe":
pipeName = ReadNextValue(args, ref i, "--pipe");
break;
case "--client-pid":
string pidText = ReadNextValue(args, ref i, "--client-pid");
if (!int.TryParse(pidText, out int pid) || pid <= 0)
{
throw new ArgumentException($"Invalid client PID: {pidText}");
}
clientPid = pid;
break;
case "--client-sid":
clientSid = ReadNextValue(args, ref i, "--client-sid");
break;
default:
throw new ArgumentException($"Unknown argument: {args[i]}");
}
}
if (string.IsNullOrWhiteSpace(pipeName))
{
throw new ArgumentException("--pipe is required.");
}
if (clientPid is null)
{
throw new ArgumentException("--client-pid is required.");
}
if (string.IsNullOrWhiteSpace(clientSid))
{
throw new ArgumentException("--client-sid is required.");
}
return new BrokerLaunchOptions
{
PipeName = pipeName,
ExpectedClientProcessId = clientPid.Value,
ClientUserSid = clientSid
};
}
private static string ReadNextValue(string[] args, ref int index, string optionName)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException($"A value is required after {optionName}.");
}
index++;
return args[index];
}
}
جهة الـ helper تُخطئ لحظة نقص الوسائط / وجود وسائط زائدة. داخل حدود الرفع، «التفسير بجهد مؤقّتاً» أولى ألّا يُفعَل.
12. جهة الـ helper: إنشاء الـ pipe وتحقّق PID جهة الاتّصال و dispatch
12.1 MyApp.AdminBroker/Program.cs
using System.ComponentModel;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;
namespace MyApp.AdminBroker;
internal static class Program
{
public static async Task<int> Main(string[] args)
{
BrokerLaunchOptions options = BrokerLaunchOptions.Parse(args);
using var brokerCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using NamedPipeServerStream pipe = CreatePipeServer(options);
await pipe.WaitForConnectionAsync(brokerCts.Token);
VerifyClientProcessId(pipe, options.ExpectedClientProcessId);
BrokerRequest request = await PipeMessageSerializer.ReadAsync<BrokerRequest>(pipe, brokerCts.Token);
BrokerResponse response = await DispatchAsync(request);
await PipeMessageSerializer.WriteAsync(pipe, response, brokerCts.Token);
return response.Success ? 0 : 2;
}
private static Task<BrokerResponse> DispatchAsync(BrokerRequest request)
{
try
{
return request.Operation switch
{
BrokerOperations.SetExplorerContextMenu => HandleSetExplorerContextMenuAsync(request.Payload),
_ => Task.FromResult(
BrokerResponse.Fail(
"unsupported_operation",
$"Unsupported operation: {request.Operation}"))
};
}
catch (JsonException ex)
{
return Task.FromResult(BrokerResponse.Fail("invalid_payload", ex.Message));
}
catch (Exception ex)
{
return Task.FromResult(BrokerResponse.Fail("broker_failure", ex.Message));
}
}
private static NamedPipeServerStream CreatePipeServer(BrokerLaunchOptions options)
{
var pipeSecurity = new PipeSecurity();
var clientSid = new SecurityIdentifier(options.ClientUserSid);
SecurityIdentifier helperSid = WindowsIdentity.GetCurrent().User
?? throw new InvalidOperationException("Helper user SID could not be resolved.");
pipeSecurity.AddAccessRule(new PipeAccessRule(
clientSid,
PipeAccessRights.ReadWrite,
AccessControlType.Allow));
pipeSecurity.AddAccessRule(new PipeAccessRule(
helperSid,
PipeAccessRights.FullControl,
AccessControlType.Allow));
pipeSecurity.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
PipeAccessRights.FullControl,
AccessControlType.Allow));
return NamedPipeServerStreamAcl.Create(
options.PipeName,
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough,
inBufferSize: 0,
outBufferSize: 0,
pipeSecurity: pipeSecurity);
}
private static void VerifyClientProcessId(NamedPipeServerStream pipe, int expectedClientProcessId)
{
if (!GetNamedPipeClientProcessId(
pipe.SafePipeHandle.DangerousGetHandle(),
out uint actualClientProcessId))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (actualClientProcessId != (uint)expectedClientProcessId)
{
throw new InvalidOperationException(
$"Unexpected pipe client PID. Expected={expectedClientProcessId}, Actual={actualClientProcessId}");
}
}
private static Task<BrokerResponse> HandleSetExplorerContextMenuAsync(JsonElement payload)
{
SetExplorerContextMenuRequest request = payload.Deserialize<SetExplorerContextMenuRequest>(BrokerJson.Options)
?? throw new JsonException("Payload could not be parsed.");
ExplorerContextMenuRegistration.Apply(request.Enabled);
return Task.FromResult(BrokerResponse.Ok("Explorer context menu setting was updated."));
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeClientProcessId(
IntPtr pipe,
out uint clientProcessId);
}
ما ينفع هنا النقاط التالية.
- بناء ACL الـ pipe صراحةً
- ACL تُمنَح لا لـ SID المستخدم الحالي للـ helper وحده، بل أيضاً لـ SID مستخدم الواجهة المستدعي
- بعد الاتّصال التحقّق من client PID
- بعد تلقّي الطلب dispatch باسم العمليّة أيضاً
إن جُعل الشكل لا يمرّر إلا عمليّات ثابتة بـ switch (request.Operation)، صعب أن يصير الـ helper «صندوق أيّ شيء مرفوع».
flowchart TB
accTitle: ترتيب التحقّق النافع في جهة الـ helper
accDescr: مخطّط يبيّن ترتيب التحقّق النافع في جهة الـ helper: إنشاء الـ pipe بـ ACL صريحة، والتحقّق من PID العميل بعد الاتّصال، و dispatch الطلب باسم العمليّة فلا يمرّ إلا تشغيل ثابت.
hs1["إنشاء الـ pipe بـ ACL صريحة"] --> hs2["التحقّق من PID جهة الاتّصال"]
hs2 --> hs3["dispatch باسم العمليّة"]
hs3 -->|"داخل allowlist"| hs4["تنفيذ العمليّة الثابتة فقط"]
hs3 -->|"خارج allowlist"| hs5["الرفض والردّ"]
الشكل 17: الطلب الذي يعبر ثلاث ACL و PID و dispatch فقط يصل إلى التشغيل الثابت.
13. متن عمليّة المسؤول: تسجيل قائمة النقر باليمين في Explorer
13.1 MyApp.AdminBroker/ExplorerContextMenuRegistration.cs
using System;
using System.IO;
using Microsoft.Win32;
namespace MyApp.AdminBroker;
internal static class ExplorerContextMenuRegistration
{
private const string MenuKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open";
private const string CommandKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open\command";
private const string MenuText = "Open with MyApp";
private const string ClientExecutableName = "MyApp.exe";
public static void Apply(bool enabled)
{
string clientExePath = ResolveClientExecutablePath();
using RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, GetRegistryView());
if (enabled)
{
using RegistryKey menuKey = hklm.CreateSubKey(MenuKeyPath)
?? throw new InvalidOperationException($"Failed to create registry key: {MenuKeyPath}");
menuKey.SetValue(null, MenuText, RegistryValueKind.String);
menuKey.SetValue("Icon", $"\"{clientExePath}\",0", RegistryValueKind.String);
using RegistryKey commandKey = hklm.CreateSubKey(CommandKeyPath)
?? throw new InvalidOperationException($"Failed to create registry key: {CommandKeyPath}");
commandKey.SetValue(null, $"\"{clientExePath}\" \"%1\"", RegistryValueKind.String);
}
else
{
hklm.DeleteSubKeyTree(@"SOFTWARE\Classes\*\shell\MyApp.Open", throwOnMissingSubKey: false);
}
}
private static string ResolveClientExecutablePath()
{
string clientExePath = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, ClientExecutableName));
if (!File.Exists(clientExePath))
{
throw new FileNotFoundException("Client executable was not found.", clientExePath);
}
return clientExePath;
}
private static RegistryView GetRegistryView()
{
return Environment.Is64BitOperatingSystem
? RegistryView.Registry64
: RegistryView.Registry32;
}
}
لبّ هذه الشيفرة فيما لم يُستلَم من الواجهة.
- لم يُستلَم مسار سجلّ اعتباطيّ من الواجهة
- لم تُستلَم سلسلة أوامر اعتباطيّة من الواجهة
- EXE هدف التسجيل يُحلّ ثابتاً في جهة الـ helper
- محتوى الطلب
Enabledفقط
أي أنّ الـ helper جُعل لا يحمل إلا معنى واحداً: «تبديل حالة تسجيل قائمة النقر باليمين في Explorer».
14. مثال الاستدعاء من الواجهة
14.1 MyApp/SettingsPage.xaml.cs
using System.Windows;
namespace MyApp;
public partial class SettingsPage
{
private readonly ElevationBrokerClient _broker = new(
Path.Combine(AppContext.BaseDirectory, "MyApp.AdminBroker.exe"));
private async void ExplorerMenuCheckBox_Click(object sender, RoutedEventArgs e)
{
bool enabled = ExplorerMenuCheckBox.IsChecked == true;
try
{
await _broker.SetExplorerContextMenuEnabledAsync(enabled);
MessageBox.Show("Setting has been updated.", "MyApp");
}
catch (OperationCanceledException)
{
MessageBox.Show("The administrator approval prompt was canceled.", "MyApp");
ExplorerMenuCheckBox.IsChecked = !enabled;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Failed to update the setting.");
ExplorerMenuCheckBox.IsChecked = !enabled;
}
}
}
جهة الواجهة عاديّة.
- قراءة حالة مربّع الاختيار
- استدعاء عميل الـ broker
- إرجاع الواجهة عند الفشل
فقط. لا تلمس السجلّ مباشرة. هذا هو الفصل.
15. ما يُمسَك في هذا التنفيذ
الخطّ الذي تحميه هذه العيّنة فعلاً كالتالي.
15.1 فصل مسؤوليّات الواجهة والـ helper
- الواجهة تتلقّى تشغيل المستخدم فقط
- الـ helper ينفّذ عمليّة مسؤول ثابتة فقط
15.2 لم يُصنَع «منفذ تنفيذ اعتباطيّ» في الـ helper
- لا يستقبل مسار سجلّ اعتباطيّ
- لا يستقبل سطر أوامر اعتباطيّ
- لا يستقبل مسار EXE اعتباطيّ
15.3 مسار الإقلاع ثابت
- helper EXE بمسار مطلق
runasمصرَّحUseShellExecute = trueمصرَّح
15.4 جهة اتّصال IPC مضيَّقة
- ACL الـ pipe مقصورة على SID مستخدم الواجهة
- بعد الاتّصال تأكيد client PID
15.5 هدف عمليّة المسؤول ثابت أيضاً
- hive / path السجلّ ثابت
- EXE هدف التسجيل يُحلّ ثابتاً أيضاً
إلى هذا الحدّ تبتعد كثيراً عن حالة «إن انكسرت الواجهة استطاع الـ helper أيّ شيء».
15.6 نتأكّد أنّ الفصل تمّ فعلاً
حتّى هنا حديث تصميم. هل ما كُتب مفصول فعلاً لا يُعرَف دون تشغيله والتأكّد. «ارتفعت الواجهة كلّها دون أن ندري» حادث يصعب ملاحظته بقراءة الشيفرة.
flowchart TB
accTitle: تأكيد الفصل على أربع درجات
accDescr: مخطّط يبيّن تدفّق التأكيد بالترتيب: هل عمليّة الواجهة غير مرفوعة كما هي، وهل تظهر مطالبة UAC عند إقلاع الـ helper فقط، وهل نفعت عمليّة المسؤول فعلاً، وهل يفشل حين ينبغي أن يفشل.
v1["هل الواجهة غير مرفوعة كما هي"] --> v2["هل تظهر المطالبة عند إقلاع الـ helper فقط"]
v2 --> v3["هل نفعت العمليّة فعلاً"]
v3 --> v4["هل يفشل حين ينبغي أن يفشل"]
v4 -.-> whyv["دون الرابعة لا يصير تأكيد الفصل"]
الشكل 18: إن نُظرت الدرجات الأربع بالترتيب عند التشغيل، التُقط تسرّب رفع لا يُلاحَظ بالشيفرة وحدها.
التأكيد ينظر إلى الأربع التالية بالترتيب.
1. هل عمليّة الواجهة غير مرفوعة كما هي
أهمّ نقطة. أقلع الواجهة وتأكّد بعد تنفيذ عمليّة المسؤول مرّة.
- مدير المهامّ: في علامة التبويب «تفاصيل» انقر يميناً على الأعمدة واعرض عمود «مرفوع». إن كان
MyApp.exe«لا» وMyApp.AdminBroker.exeوحده «نعم» فهذا المتوقَّع - Process Explorer: اعرض عمود Integrity. إن كانت الواجهة
Mediumوالـ helperHighفهذا الصحيح (مستوى السلامة في Windows كما في 2.1: مستخدم قياسيّ = medium، مرفوع = high)
إن أردت النظر من الشيفرة، يكفي تأكيد مرّة واحدة فور إقلاع الواجهة.
using System.Security.Principal;
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
// UI プロセスでは false であるべき
bool isElevatedAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
2. هل تظهر مطالبة UAC عند إقلاع الـ helper فقط
- تظهر المطالبة عند إقلاع الواجهة -> بيان جهة الواجهة ليس
asInvoker - تظهر لحظة الضغط على مربّع اختيار الإعداد -> كما يُتوقَّع
- لا تظهر المطالبة مرّة ومع ذلك يتغيّر الإعداد -> يحتمل أن يكون الـ helper مرفوعاً دائماً بمسار آخر
إن كان حساب مسؤول فمطالبة موافقة، وإن كان مستخدماً قياسيّاً فمطالبة بيانات اعتماد (جدول 5.6). جرّب الاثنين فتؤكّد أيضاً أنّ تمرير SID صحيح.
3. هل نفعت عمليّة المسؤول فعلاً
لتسجيل قائمة Explorer، أسرع النظر مباشرة إلى السجلّ.
reg query "HKLM\SOFTWARE\Classes\*\shell\MyApp.Open" /s
جهة الإلغاء تُؤكَّد بالمثل. إن جُرِّب التسجيل وحده دون الإلغاء بقي عيب في جهة DeleteSubKeyTree.
4. هل «يفشل حين ينبغي أن يفشل»
دون تأكيد هذا لا يُعرَف هل تمّ الفصل.
- إلغاء مطالبة الرفع -> الإعداد لا يتغيّر ويعود مربّع اختيار الواجهة أيضاً (معاملة
ERROR_CANCELLED= 1223. الفصل 10) - إقلاع الـ helper مباشرة -> حتّى إن طُرق يدوياً مثل
MyApp.AdminBroker.exe --pipe x --client-pid 1 --client-sid S-1-5-18لا تتقدّم المعالجة بتحقّق PID جهة الاتّصال وانتهاء المهلة - إرسال عمليّة ليست في allowlist -> تُرفَض بـ
unsupported_operation(DispatchAsyncفي الفصل 12)
أوامر الإجراءات الملموسة ملخّصة بنفس التدفّق في «إجراءات التأكيد على Windows» في README العيّنة.
16. أخطاء شائعة
16.1 جعل الواجهة كلّها requireAdministrator
زرّ واحد في شاشة الإعداد يحتاج امتيازات المسؤول، ومع ذلك يُقلَع الكلّ مرفوعاً. هذا اتّجاه يسحق حدود الامتياز باستخفاف.
16.2 تمرير سلسلة أوامر خامّة إلى الـ helper
مثلاً تصميم كهذا.
UI -> helper に "reg add HKLM\\.... /v ... /d ..."
هذا يجعل الـ helper منفّذ أوامر. الأولى التوقّف.
16.3 استخدام ACL الافتراضيّة لـ named pipe كما هي
«IPC محلّيّ إذن لا بأس» خطر قليلاً. الـ pipe هدف أمن Windows، فأولى صنع ACL كما ينبغي.
16.4 القفز إلى CurrentUserOnly
يبدو مريحاً، لكنّه لا يناسب واجهة medium integrity ↔ helper high integrity هذه المرّة. هنا ACL صريحة أسهل معاملة.
16.5 الـ helper يستقبل مساراً اعتباطيّاً ويشغّل
مثلاً مثل:
- نسخ ملفّ اعتباطيّ إلى Program Files
- كتابة مفتاح اعتباطيّ إلى HKLM
- حذف اسم خدمة اعتباطيّ
- إضافة قاعدة جدار ناريّ بأمر اعتباطيّ
إن قبله الـ helper صار الـ helper نفسه منفذ تنفيذ عامّاً بامتيازات المسؤول. العمليّة ينبغي تثبيتها حتماً.
17. الخلاصة
«جزء فقط من المعالجات يحتاج امتيازات المسؤول» في تطبيقات Windows ليس حديثاً نادراً.
لكن حلّه ليس «جعل الكلّ requireAdministrator» بل قطع حدود التنفيذ.
ما يسهل تناوله أوّلاً هذا الشكل.
- الواجهة
asInvoker - معالجة المسؤول تُفصَل إلى helper EXE
- الـ helper
requireAdministrator - الإقلاع
runas - التواصل named pipe
- الـ helper لا يقبل إلا عمليّة ثابتة
- جهة الاتّصال تُضيَّق بـ ACL الـ pipe و client PID
- الوسائط يُعاد التحقّق منها في جهة الـ helper
إن جُعل بهذا الشكل، سهل الانتقال لاحقاً حين تريد التحويل إلى خدمة. إن فُصل عقد العمليّة كما ينبغي، صارت حدود الواجهة ومعالجة المسؤول كما هي أصلاً تصميميّاً.
flowchart TB
accTitle: الحدود تصير أصلاً تصميميّاً
accDescr: مخطّط يبيّن أنّ فصل عقد العمليّة كما ينبغي يجعل حدود الواجهة ومعالجة المسؤول واضحة فتصير الحدود كما هي أصلاً تصميميّاً ويسهل الانتقال لاحقاً إلى خدمة.
ctr1["فصل عقد العمليّة مسبقاً"] --> bd1["حدود الواجهة ومعالجة المسؤول واضحة"]
bd1 --> asset1["تصير الحدود كما هي أصلاً تصميميّاً"]
asset1 -.-> future1["يسهل أيضاً الانتقال إلى خدمة"]
الشكل 19: الحدود المقطوعة بنوع broker تصير أصلاً يُستخدم كما هو أيضاً عند التحويل إلى خدمة مستقبلاً.
حديث الأمن أنجع فيه عدم إبقاء حدود مستخفّة من إضافة وظائف لامعة. امتيازات المسؤول كذلك. لا تُحمَل كلّها جملة، بل تُمرَّر حيث تلزم، بأضيق قدر ممكن. هذا القدر من الهدوء يفعل فعله لاحقاً.
18. روابط مرجعية
بعض الروابط التالية تحمل في عنوان URL تعييناً للإصدار مثل view=net-10.0. هذا تعيين أيّ إصدار .NET تعرضه وثائق Microsoft Learn، ولا يعني تعارضاً مع فرضيّة هذا المقال .NET 8. PipeOptions / NamedPipeServerStreamAcl / RegistryView المستخدمة هنا متاحة كلّها في .NET 8. إن أردت موافقة العرض مع .NET 8 فبدّل من منتقي الإصدار أعلى الصفحة.
- شيفرة عيّنة هذا المقال كاملة (مكتبة عقد مشتركة، عرض توضيحيّ، اختبارات وحدة) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-admin-broker-deep-dive
- المقال السابق: قائمة تحقّق للحدّ الأدنى من الأمان في تطوير تطبيقات Windows
- Microsoft Learn, Administrator Broker Model - Win32 apps
- Microsoft Learn, Developing Applications that Require Administrator Privilege
- Microsoft Learn, Operating System Service Model - Win32 apps
- Microsoft Learn, Elevated Task Model - Win32 apps
- Microsoft Learn, Administrator COM Object Model - Win32 apps
- Microsoft Learn, The COM Elevation Moniker
- Microsoft Learn, How User Account Control works
- Microsoft Learn, Mandatory Integrity Control - Win32 apps
- Microsoft Learn, Process Explorer - Sysinternals
- Microsoft Learn, WindowsPrincipal.IsInRole Method
- Microsoft Learn, ProcessStartInfo.UseShellExecute
- Microsoft Learn, Named Pipe Security and Access Rights
- Microsoft Learn, PipeOptions Enum
- Microsoft Learn, NamedPipeServerStreamAcl.Create
- Microsoft Learn, GetNamedPipeClientProcessId
- Microsoft Learn, RegistryView Enum
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
حفظ أسرار تطبيقات Windows - تجنّب إعداد النصّ الصريح بـ DPAPI
نرتّب هنا فكرة DPAPI و ProtectedData، والفرق بين CurrentUser و LocalMachine، ونقاط التنفيذ، كي لا تُحفَظ معلومات الاتّصال ورموز API بالنص...
قائمة التحقّق الدنيا لأمان تطبيقات Windows
ننظّم في صورة قائمة تحقّق أساسيات الصلاحيات والتوقيع والتحديث والأسرار و HTTPS والتحقّق من الإدخال وتحميل DLL والسجلات لتطبيقات أعمال WPF...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows ── حدّ 260 حرفاً، الأسماء المحجوزة، النقطة الأخيرة، حساسيّة الأحرف
ننظّم مشكلات حدود المسارات وأسماء الملفّات، وهي سبب شائع لظهور «الملفّ غير موجود». نشرح تفاصيل MAX_PATH=260 حرفاً، وتفعيل المسارات الطويل...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأنه يمسّ تصميم الصلاحيات في تطبيق Windows بأكمله، بما يشمل UAC وhelper EXE وتقدير الحاجة إلى التحويل إلى خدمة وتغيير الإعدادات على مستوى الجهاز (machine-wide)، فهو موضوع يتناسب مع تطوير تطبيقات Windows.
الاستشارات التقنية ومراجعة التصميم
إذا أردتم مراجعة الاعتماد الدائم على requireAdministrator في تطبيق قائم، وإعادة ترتيب تصميم الـ broker وحدود IPC، فهو موضوع يسهل التقدّم فيه ضمن الاستشارة التقنيّة ومراجعة التصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل يمكن تنفيذ جزء فقط من المعالجات داخل نفس العمليّة بامتيازات المسؤول؟
- لا. UAC في Windows ليس رفعاً بوحدة الدالّة، بل يُتحكَّم فيه بالرمز ومستوى السلامة اللذين تعمل بهما العمليّة. ترث عمليّات الأب والابن الرمز بنفس مستوى السلامة، لذا يستحيل تصميم ينفّذ دالّة معيّنة فقط بامتيازات المسؤول داخل عمليّة واجهة غير مرفوعة. المعالجات اللازمة تُقطَع إلى وحدة تنفيذ أخرى: عمليّة منفصلة أو خدمة أو مهمّة مجدولة أو COM مرفوع.
- ما خيارات فصل المعالجات التي تحتاج امتيازات المسؤول؟
- Microsoft Learn يذكر أساساً أربعة نماذج. Administrator Broker Model الذي يجمع واجهة مستخدم قياسيّة مع helper EXE للمسؤول، و Operating System Service Model الذي يستخدم خدمة مقيمة، و Elevated Task Model الذي يستخدم مهمّة مجدولة بامتيازات المسؤول، و Administrator COM Object Model الذي يستخدم COM مرفوعاً. إن كانت عمليّات المسؤول متفرّقة ويكفي إظهار UAC في اللحظة اللازمة فـ broker EXE، وإن كانت دائمة وبلا مراقبة ومتكرّرة فالخدمة، وإن كانت معالجة نمطيّة قصيرة تنتهي في كلّ مرّة فالمهمّة.
- هل يمكن استخدام الإدخال/الإخراج القياسيّ للتواصل مع helper EXE المقلَع بـ runas؟
- صعب الاستخدام فالأولى تجنّبه. في .NET لا ينفع ProcessStartInfo.Verb إلا حين UseShellExecute=true، ومع UseShellExecute=true يتعذّر التواصل المفترض على إعادة توجيه الإدخال/الإخراج القياسيّ. لذلك أوضح استخدام IPC مثل named pipe مع الـ helper. لا تعتمد على ACL الافتراضيّة للـ pipe؛ اضبط PipeSecurity صريحاً، قصّر حقّ الاتّصال على SID المستخدم المستدعي، ثمّ تحقّق أيضاً من PID جهة الاتّصال بـ GetNamedPipeClientProcessId.
- أليس استخدام PipeOptions.CurrentUserOnly على named pipe كافياً للأمان؟
- لا يناسب التواصل بين واجهة غير مرفوعة و helper مرفوع. CurrentUserOnly على Windows يتحقّق لا من حساب المستخدم وحده بل من مستوى الرفع أيضاً، فلا يمكن الاتّصال بين عمليّتين بمستويي سلامة مختلفين. ثمّ في بيئة مستخدم قياسيّ قد يصير UAC مطالبة ببيانات اعتماد، فيعمل الـ helper بحساب مسؤول آخر. أوضح أن تحصل جهة الواجهة على SID الخاصّ بها وتمرّره إلى الـ helper، وأن يعطي الـ helper حقّ اتّصال الـ pipe لذلك SID وحده عبر ACL صريحة.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.