איך כותבים בפועל הפרדה של "רק תהליכים שדורשים הרשאות מנהל" ביישום Windows
· עודכן בתאריך: · Go Komura · פיתוח Windows, אבטחה, UAC, C# / .NET, Win32
במאמר הקודם, “רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows”, כתבנו את הקו: להתבסס על asInvoker, ולהפריד רק תהליכים שדורשים הרשאות מנהל.
הפעם ניכנס עד לאיך בפועל כותבים את החלק הזה.
ביישום Windows, לא ניתן להריץ בנוחות רק חלק מהתהליכים באותו תהליך “כמנהל”. ההעלאה היא עניין של גבול תהליך, ולכן מה שנדרש הוא “תכנון שמוציא רק את אותו תהליך ליחידת הרצה נפרדת”.
flowchart TB
accTitle: ההעלאה היא עניין של גבול תהליך
accDescr: תרשים המראה שלא ניתן להריץ כמנהל רק חלק מהתהליכים באותו תהליך, וכיוון שההעלאה היא עניין של גבול תהליך, נדרש תכנון שמוציא את אותו תהליך ליחידת הרצה נפרדת.
wish1["רוצים להעלות רק חלק מהתהליכים"] -.->|"בלתי אפשרי באותו תהליך"| in1["העלאה בתוך אותו תהליך"]
wish1 -->|"מה שכן אפשרי"| cut1["הוצאת התהליך ליחידת הרצה נפרדת"]
איור 1: ההעלאה נקבעת ברמת התהליך ולא ברמת הפונקציה, ולכן רק תכנון שמוציא לחוץ אפשרי.
במאמר הזה נתקדם בסדר הבא:
- קודם הנחות היסוד
- איזה מודל הפרדה בוחרים
- הצורה הכי נוחה לעבודה מעשית:
asInvoker+ helper EXE מנהל - מלכודות שלא כדאי לפספס במימוש
- דוגמאות קוד קונקרטיות
דוגמאות הקוד מניחות .NET 8 / יישום שולחני ל-Windows. תשתית ה-UI יכולה להיות כל אחת מ-WPF / WinForms / WinUI, וההבדל היחיד שנוצר הוא בערך ב-handler-ים של אירועים בצד ה-UI.
בנוסף, הקוד שמופיע במאמר הזה זמין כסט קוד לדוגמה שלם שאפשר לבנות ולהריץ (ספריית חוזה משותפת, הדגמה של UI / helper מנהל, ובדיקות יחידה שרצות גם ב-Linux) ב-GitHub.
windows-admin-broker-deep-dive - komurasoft-blog-samples (GitHub)
איך קוראים את המאמר הזה
מאמר ארוך, ולכן נציג קודם מפת ניווט.
| מה רוצים לדעת | מה לקרוא |
|---|---|
| רוצים לדעת רק איך בוחרים מודל הפרדה | פרקים 1〜4 (מסקנה, השוואת ארבעת המודלים, הצורה המומלצת) |
| החלטות תכנון והנימוקים שלהן | פרק 5 (allowlist, קיבוע נתיב, runas, ACL של pipe, אימות PID) |
| קוד המימוש | פרקים 7〜14 (הרכב, מניפסט, חוזה משותף, צד UI, צד 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. בטוח יותר לנסות במחשב וירטואלי להערכה, ולא במחשב הפיתוח הרגיל
הפקודות הקונקרטיות ל-build והרצה מרוכזות ב-README של הדוגמה. רק נקודה אחת כדאי להכניס לראש מראש: מפרסמים (publish) את ה-UI וה-helper לאותה תיקייה ורק אז מריצים (כי ה-helper פותר בקיבוע את MyApp.exe שבאותה תיקייה כמוהו).
1. קודם המסקנה
נסדר קודם את הפתרון המעשי.
- יישום UI רגיל ממשיך לרוץ ב-
asInvoker - תהליך שדורש הרשאות מנהל מוצא ל-EXE נפרד
- אותו helper EXE הופך ל-
requireAdministrator - ההפעלה מתבצעת עם
runas - התקשורת עם ה-helper משתמשת ב-pipe בעל שם או IPC דומה, ולא בקלט/פלט סטנדרטי שלא מתאים ל-
runas - מה שמעבירים ל-helper הוא לא “מחרוזת פקודה גולמית”, אלא רק בקשה מוגדרת-טיפוס
- בצד ה-helper, מאמתים שוב את תוכן הבקשה
- מקור ה-IPC מוגבל לפי ה-SID של המשתמש הקורא וה-PID הצפוי
“נוח כשהכול רץ כמנהל” נכון רק בפעם הראשונה. בהמשך, זה נוטה לגרום לפרצוף עצוב סביב UAC, גרירה ושחרור, תכנון לוגים, קלט חיצוני, תפעול תמיכה, טעינת DLL, ומיקום שמירת תצורה.
flowchart TB
accTitle: השלד של הפתרון המעשי
accDescr: תרשים המראה שה-UI נשאר asInvoker, תהליך שדורש הרשאות מנהל מוצא ל-EXE נפרד עם requireAdministrator שמופעל עם runas, ומעבירים דרך pipe בעל שם רק בקשה מוגדרת-טיפוס.
uix1["ה-UI נשאר asInvoker"] -->|"הפעלה עם runas"| hx1["helper EXE (requireAdministrator)"]
uix1 -->|"בקשה מוגדרת-טיפוס דרך pipe בעל שם"| hx1
hx1 --> vfy1["ה-helper מאמת שוב את הבקשה"]
איור 2: השלד נקבע בשלוש נקודות — UI לא-מועלה + helper מועלה + IPC של בקשה מוגדרת-טיפוס.
מפת הידע של המאמר
המאמר הזה עוסק במימוש קונקרטי של הפרדת רק הפעולות הדורשות הרשאות מנהל ביישום Windows. UAC נשלט ברמת ה-integrity של התהליך, ותהליך צאצא יורש token באותה רמה כמו תהליך ההורה, ולכן אי אפשר להעלות בהרשאה רק חלק מאותו תהליך — ומכאן שהצורה הבסיסית היא Administrator Broker Model, שמשלב UI של משתמש רגיל עם helper EXE בעל הרשאות מנהל. הפעלת ה-helper נעשית עם runas, והתקשורת בין ה-UI ל-helper מתבצעת דרך pipe בעל שם עם PipeSecurity מפורש במקום הסתמכות על ACL ברירת המחדל; אימות ה-PID של הצד המתחבר, יחד עם allowlist בצד ה-helper שמקבל רק פעולות קבועות מראש, סוגרים את הפתח להרצת פקודה שרירותית. PipeOptions.CurrentUserOnly בודק גם הבדל ברמת ה-integrity, ולכן אינו מתאים לשימוש הזה.
flowchart LR
accTitle: מפת הידע של הפרדת עיבוד הרשאות מנהל ביישום
accDescr: תרשים המראה, במגבלה שמטילה רמת ה-integrity של UAC, מדוע Administrator Broker Model מחבר UI של משתמש רגיל ל-helper EXE של מנהל דרך pipe בעל שם, ושומר על הגבול באמצעות ACL, אימות PID ו-allowlist של פעולות — יחד עם ההשוואה למודלי הפרדה אחרים.
administrator_broker_model["Administrator Broker Model"]
uac["UAC (בקרת חשבון משתמש)"]
integrity_level["רמת שלמות (integrity level)"]
admin_rights["הרשאות מנהל"]
requested_execution_level["requestedExecutionLevel (הצהרת רמת ההרצה)"]
runas_verb["הפעלה עם runas (ShellExecute Verb)"]
named_pipe["named pipe (צינור בעל שם)"]
pipe_security_acl["ACL מפורש באמצעות PipeSecurity"]
unauthorized_pipe_access["התחברות בלתי מורשית לצינור"]
client_pid_verification["אימות ה-PID של הצד המתחבר"]
helper_operation_allowlist["רשימת הפעולות המותרות ל-helper"]
arbitrary_command_execution["פרצה להרצת פקודות שרירותיות"]
sporadic_admin_operation["פעולת מנהל מזדמנת"]
os_service_model["Operating System Service Model"]
continuous_unattended_admin_operation["פעולת מנהל רציפה, תכופה וללא נוכחות אדם"]
elevated_task_model["Elevated Task Model"]
short_scheduled_admin_task["משימת מנהל קצרה ושגרתית"]
admin_com_object_model["Administrator COM Object Model"]
existing_com_integration["שילוב המבוסס על COM קיים"]
currentuseronly_pipeoption["PipeOptions.CurrentUserOnly"]
administrator_broker_model -->|"משתמש ב"| uac
uac -->|"משתמש ב"| integrity_level
uac -.->|"מחייב"| admin_rights
administrator_broker_model -->|"מחייב"| requested_execution_level
runas_verb -->|"משתמש ב"| uac
administrator_broker_model -.->|"משתמש ב"| named_pipe
named_pipe -->|"מוגדר באמצעות"| pipe_security_acl
named_pipe -.->|"עלול לגרום ל"| unauthorized_pipe_access
pipe_security_acl -->|"מצמצם"| unauthorized_pipe_access
client_pid_verification -->|"מצמצם"| unauthorized_pipe_access
administrator_broker_model -.->|"משתמש ב"| client_pid_verification
administrator_broker_model -->|"משתמש ב"| helper_operation_allowlist
helper_operation_allowlist -->|"מונע"| arbitrary_command_execution
administrator_broker_model -->|"מענה מומלץ ל"| sporadic_admin_operation
os_service_model -->|"מענה מומלץ ל"| continuous_unattended_admin_operation
elevated_task_model -->|"מענה מומלץ ל"| short_scheduled_admin_task
admin_com_object_model -->|"מענה מומלץ ל"| existing_com_integration
currentuseronly_pipeoption -->|"אינו מתיישב עם"| administrator_broker_model
os_service_model -->|"מחייב"| admin_rights
elevated_task_model -->|"מחייב"| admin_rights
admin_com_object_model -->|"מחייב"| admin_rights
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 21, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
2. סידור הנחות היסוד: לא ניתן להפוך רק חלק מאותו תהליך למנהל
ה-UAC של Windows נשלט לא לפי “העלאה ברמת פונקציה”, אלא לפי “באיזה טוקן / רמת שלמות רץ התהליך”. יישום שדורש טוקן גישה של מנהל הופך למועמד להנחיית העלאה, ותהליך אב ותהליך בן יורשים את הטוקן באותה רמת שלמות. כלומר, לא ניתן לתכנן להריץ פתאום רק שיטה מסוימת בהרשאות מנהל בתוך תהליך UI לא-מועלה. אם צריך, משתמשים ביחידת הרצה נפרדת — תהליך נפרד, שירות, משימה, 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 | תהליך שרץ כמשתמש רגיל | יישום UI עם asInvoker |
| high | תהליך שהועלה | helper EXE עם requireAdministrator |
ב-Mandatory Integrity Control של Windows מוגדרות ארבע רמות: low / medium / high / system, ו-משתמש רגיל מקבל medium, ומשתמש שהועלה מקבל high. כלומר, התכנון של המאמר הזה הוא לתת ל-“תהליך UI ברמת medium” ול-“תהליך helper ברמת high” לשוחח דרך גבול מפורש.
אם ההתאמה הזו נמצאת בראש, גם הדיון ב-CurrentUserOnly בפרק 5.6 וגם 16.4 נקראים בטבעיות.
flowchart TB
accTitle: קו הגבול בין medium ל-high
accDescr: תרשים המראה שה-UI ברמת asInvoker שרץ כמשתמש רגיל מקבל medium, וה-helper המועלה ברמת requireAdministrator מקבל high, ושהתכנון של המאמר הזה נותן לשניים לשוחח דרך גבול מפורש.
med1["medium: תהליך UI ב-asInvoker"] -->|"שיחה דרך גבול מפורש"| hg1["high: תהליך helper מועלה"]
med1 -.-> sd2["טוקן של משתמש רגיל"]
hg1 -.-> ad2["טוקן של משתמש מועלה"]
איור 4: התכנון הזה מותח גבול מפורש בין ה-UI ברמת medium ל-helper ברמת high.
3. איזה מודל הפרדה בוחרים
ב-Microsoft Learn מפורטים בעיקר ארבעה אופנים להפרדת יישום שדורש הרשאות מנהל.
| מודל | הצורה הגסה | מתי מתאים |
|---|---|---|
| Administrator Broker Model | יישום UI של משתמש רגיל + helper EXE מנהל | הפעולה המנהלית מזדמנת, ומספיק להוציא UAC רק ברגע הנדרש |
| Operating System Service Model | UI של משתמש רגיל + שירות קבוע | תכונת ניהול תמידית, ניטור ברקע, עיבוד ללא נוכחות אדם |
| Elevated Task Model | UI של משתמש רגיל + משימה מתוזמנת בהרשאות מנהל | תהליך מוגדר-מראש שמסתיים מהר בכל פעם |
| Administrator COM Object Model | UI של משתמש רגיל + 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: השירות מקבל בתמורה להיעדר הנחיה את האחריות לתפעל הרצה קבועה.
שירות מתאים לשימושים כמו:
- ניטור תמידי
- איסוף לוגים
- עדכון ברקע
- שילוב תמידי עם מכשיר או daemon
- תכונת ניהול שמשותפת בין כמה פגישות UI
3.3 משימה מתאימה ל”תהליך קצר מוגדר-מראש”
Elevated Task Model הוא צורה שבה יישום משתמש רגיל מפעיל משימה מתוזמנת שרצה בהרשאות מנהל. היא קלה יותר משירות, ונסגרת כשהיא מסתיימת, ולכן היא מתאימה לעבודה מוגדרת-מראש שקורית פעם בכל פעם.
3.4 COM מועלה מוגבל מאוד
COM elevation moniker נראה נוח, אבל מקום השימוש בו מצומצם. גם ב-Microsoft Learn כתוב שה-UI שיכול לשלוט ב-COM המועלה צריך להיות מוצג בצד ה-COM, וזה לא מתאים לכיוון “לתת ל-UI לא-מועלה לעשות מה שרוצים ב-COM מועלה”.
flowchart TB
accTitle: הצמצום במקום השימוש ב-COM מועלה
accDescr: תרשים המראה ש-COM מועלה נראה נוח אך ה-UI שיכול לשלוט בו חייב להיות מוצג בצד ה-COM עצמו, ולכן זה לא מתאים לכיוון של הפעלה חופשית מ-UI לא-מועלה, ומקום השימוש מצומצם.
look1["COM מועלה נראה נוח"] -.-> free1["הפעלה חופשית מ-UI לא-מועלה"]
free1 -.->|"לא מתאים לכיוון הזה"| ngc1["מקום השימוש מצומצם"]
ui2["ה-UI השולט מוצג בצד ה-COM"] --> ngc1
איור 7: COM מועלה מניח ש-UI השליטה קיים בצד ה-COM עצמו, ולכן הוא לא מוצא נגישות כללי.
4. ההמלצה של המאמר הזה: UI מסוג asInvoker + helper EXE מסוג requireAdministrator
מכאן נבנה בפירוט את הצורה הכי נוחה לעבודה מעשית.
flowchart TB
accTitle: מבנה ה-UI ברמת medium מול ה-helper ברמת high
accDescr: תרשים המראה ש-UI ברמת medium integrity מפעיל עם runas את ה-helper ברמת high integrity, שולח לו בקשה מוגדרת-טיפוס דרך נקודת קצה של pipe בעל שם המאמתת SID ו-PID, וה-helper מבצע חלוקה לפי allowlist לפני שהוא פועל על היעד הקבוע שדורש הרשאות מנהל.
subgraph MED["medium integrity — לא מתעלה עד הסוף"]
UI["MyApp.exe (asInvoker) — מקבל את פעולת המשתמש ורק מרכיב את הבקשה"]
end
subgraph HIGH["high integrity — תהליך מועלה קצר-חיים"]
BR["MyApp.AdminBroker.exe (requireAdministrator)"]
PIPE["נקודת קצה של pipe בעל שם — מאפשרת חיבור רק ל-SID של משתמש ה-UI, ומאמתת גם PID של הצד המתחבר"]
DISP["חלוקה לפי allowlist של operation — הארגומנטים מאומתים שוב בצד ה-helper"]
end
TGT["יעד קבוע שדורש הרשאות מנהל — מפתח תחת HKLM / רישום שירות / כלל חומת אש"]
UI -->|"הפעלה עם נתיב מוחלט ו-Verb=runas — כאן מופיעה הנחיית UAC"| BR
BR --> PIPE
UI -->|"בקשה מוגדרת-טיפוס — לא מעבירים מחרוזת פקודה גולמית"| PIPE
PIPE --> DISP
DISP --> TGT
איור 8: גבול ההעלאה מתאים בדיוק לגבול התהליך. ה-UI נשאר ברמת medium, ורק ה-helper רץ ברמת high לזמן קצר.
שלוש נקודות מרכזיות:
- תהליך ה-UI נשאר לא-מועלה עד הסוף
- ה-helper המנהל קצר-חיים
- הפעולה שה-helper מקבל רק allowlist קבוע
רק על ידי שמירה על שלוש אלה, התכנון מתארגן משמעותית.
flowchart TB
accTitle: שלוש הנקודות שכדאי לשמור עליהן
accDescr: תרשים המראה שאם שומרים על שלוש הנקודות - ה-UI נשאר לא-מועלה עד הסוף, ה-helper קצר-חיים, והפעולה שה-helper מקבל היא רק allowlist קבוע - התכנון מתארגן משמעותית.
p1["ה-UI נשאר לא-מועלה עד הסוף"] --> tidy1["התכנון מתארגן משמעותית"]
p2["ה-helper קצר-חיים"] --> tidy1
p3["הפעולה המתקבלת רק allowlist"] --> tidy1
איור 9: שמירה על שלושת אלה — לא-מועלה, קצר-חיים, allowlist — קובעת את צורת גבול ההרשאות.
5. כללים שלא כדאי לפספס במימוש
כאן כדאי להחליט לפני כתיבת הקוד.
5.1 לא הופכים את ה-helper ל”כל-יכול”
הדוגמה השגויה נראית כך:
- ה-UI מעביר ל-helper
reg add ...כמחרוזת שלמה - ה-UI מעביר ל-helper
sc.exe ...כמחרוזת שלמה - ה-UI מעביר ל-helper נתיב רישום מערכת שרירותי או נתיב EXE שרירותי
אם עושים את זה, אם ה-UI נשבר, גם ה-helper נשבר יחד איתו. ה-helper המנהל נמצא בתוך גבול ההעלאה. יצירת “פתח שאפשר להריץ בו הכול” כאן מסוכנת מאוד.
הצורה הטובה נראית כך:
set-explorer-context-menuinstall-serviceadd-firewall-rule
מקבעים את הפעולה עצמה כמו אלה, ומעבירים גם את הארגומנטים הנדרשים לטיפוסים מוגבלים: bool / enum / מספר / מחרוזת מוגבלת.
flowchart TB
accTitle: הצורה שנמנעת מ"כל-יכול"
accDescr: תרשים המראה שהעברת מחרוזת פקודה גולמית או נתיב שרירותי ל-helper יוצרת פתח שאפשר להריץ בו הכול, וש-UI שנשבר גורר גם את ה-helper, בעוד שקיבוע הפעולה והגבלת הארגומנטים לטיפוס מוגבל מצמצם את משמעות ה-helper.
raw1["העברת מחרוזת פקודה גולמית"] --> hole1["נוצר פתח שאפשר להריץ בו הכול"]
hole1 --> both1["ה-UI שנשבר גורר גם את ה-helper"]
fix2["קיבוע הפעולה והגבלת הארגומנטים לטיפוס"] --> narrow1["המשמעות של ה-helper מצטמצמת"]
איור 10: מה שמעבירים אל תוך גבול ההעלאה הוא רק פעולה קבועה וארגומנטים מוגבלים.
5.2 הנתיב שמעבירים ל-helper — absolute, ולא מוחלט מדי מצד ה-UI
את ה-helper EXE עצמו, שמופעל עם 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 הוא true, וברירת המחדל שלו שונה בין .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, קשה יותר להשתמש בתקשורת שמניחה הפניית קלט/פלט סטנדרטי.
לכן טבעי יותר להשתמש ב-IPC אחר, כמו named pipe, לתקשורת עם ה-helper.
5.5 pipe בעל שם לא נשען על ACL ברירת מחדל
ב-pipe בעל שם, תיאור האבטחה בברירת המחדל מכיל בברירת מחדל זכות קריאה ל-Everyone ולאלמוני. שימוש ישיר בזה לצורך ה-IPC של helper מנהל הוא די רשלני.
בטוח יותר להגדיר תמיד PipeSecurity מפורש.
5.6 לא משתמשים ב-PipeOptions.CurrentUserOnly לצורך הזה
זה נראה נוח במבט ראשון.
אבל ב-Windows, CurrentUserOnly בודק לא רק את חשבון המשתמש, אלא גם את רמת ההעלאה.
כלומר, הוא לא מתאים לתקשורת בין UI לא-מועלה ל-helper מועלה.
בנוסף, מי מתחבר ל-pipe עם איזה טוקן משתנה לפי סוג הנחיית ה-UAC. אם מקבעים את זה בטבלה, קל יותר לעקוב אחר הסיבה שנדרש ACL מפורש (explicit).
| חשבון ההרצה של ה-UI | הנחיית UAC שמופיעה | חשבון שה-helper רץ תחתיו | WindowsIdentity.GetCurrent() של ה-helper שיוצר את ה-pipe |
ה-SID של צד ה-UI שמתחבר |
|---|---|---|---|---|
| חשבון מנהל (לא-מועלה) | consent prompt (רק לחיצה על “כן”) | טוקן מועלה של אותו משתמש | אותו משתמש כמו ה-UI | משתמש ה-UI |
| משתמש רגיל | credential prompt (הזנת אישורים של חשבון אחר) | חשבון מנהל אחר שהוזן | משתמש שונה מה-UI | משתמש ה-UI |
איך קוראים את זה:
- בשורה העליונה, “המשתמש הנוכחי של ה-helper = משתמש ה-UI”, ולכן גם אם ה-helper יוצר ACL רק לפי ה-SID של עצמו, זה מתחבר במקרה
- בשורה התחתונה, המשתמש הנוכחי של ה-helper ומשתמש ה-UI הם אנשים שונים. אם יוצרים כאן ACL רק לפי
WindowsIdentity.GetCurrent(), משתמש ה-UI המקורי לא יוכל לשלוח את בקשתו - בשתי השורות,
CurrentUserOnlyנדחה בגלל ההבדל ברמת ההעלאה בין “UI ברמת medium” ל-“helper ברמת high”
כלומר, הצורה היחידה שמתקיימת בשתי השורות היא “מקבלים את ה-SID מצד ה-UI, ונותנים זכות התחברות לאותו SID”.
לכן, הפעם:
- ה-UI מקבל את ה-SID של עצמו ומעביר אותו ל-helper
- ה-helper נותן זכות התחברות ל-pipe רק ל-SID של משתמש ה-UI
- ובנוסף, מאמת גם את PID מקור ההתחברות עם
GetNamedPipeClientProcessId
בצורה כזו.
flowchart TB
accTitle: העברת SID מספקת גם עבור שני המסלולים
accDescr: תרשים המראה שהצורה היחידה שמתקיימת גם ב-consent prompt וגם ב-credential prompt היא שה-UI מקבל את ה-SID של עצמו ומעביר אותו ל-helper, וה-helper נותן זכות התחברות רק לאותו SID, ומאמת גם את PID מקור ההתחברות.
sid1["ה-UI מקבל את ה-SID שלו ומעביר"] --> aclx["ה-helper נותן זכות חיבור רק לאותו SID"]
aclx --> pidx["מאמת גם את PID מקור ההתחברות"]
cuo1["שימוש ב-CurrentUserOnly"] -.->|"נדחה בגלל הבדל ברמת ההעלאה"| ngz1["לא מתקיים בשימוש הזה"]
איור 12: הצורה היחידה שמתקיימת בשני מסלולי ההנחיה היא ACL שנבנה מה-SID שהועבר מה-UI.
5.7 אימות PID הוא הגנה נוספת לצמצום “חדירה רשלנית”
גם שם pipe אקראי כבר עוזר מאוד, אבל האפשרות שתהליך אחר שרץ תחת אותו משתמש יתחבר קודם היא לא אפס.
לכן, בצד ה-helper, משתמשים ב-GetNamedPipeClientProcessId כדי לבדוק האם ה-PID תואם את תהליך ה-UI הצפוי.
כמובן, זה לא אומר שאם ה-PID מתאים אפשר לסמוך על הכול. אם ה-UI נפרץ, גם בקשות מסוכנות מגיעות ל-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: אף שכבה בודדת אינה מושלמת, ולכן ההתחברות והבקשה מוגבלות בשכבות.
אם מסדרים את הכללים עד כאן לפי הסדר מההפעלה ועד הסיום, זה נראה כך. מספיק לקלוט את הצורה שבה כל צעד בצד ה-UI מקביל לבדיקה בצד ה-helper.
sequenceDiagram
accTitle: הרצף המלא מהפעלת ה-helper ועד סיומו בלי מצב מועלה
accDescr: תרשים רצף המראה שה-UI מכין SID ו-PID ומפעיל את ה-helper עם runas, ה-helper יוצר pipe מוגן ב-ACL ומאמת PID בהתחברות, מקבל בקשה מוגדרת-טיפוס, דוחה מה שמחוץ ל-allowlist, פועל על היעד הקבוע, מחזיר תוצאה ל-UI ומסתיים בלי להשאיר מצב מועלה.
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: בקשה מוגדרת-טיפוס (שם operation וארגומנטים)
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
ה-UI מחזיק רק תיבת סימון של “רישום בתפריט לחיצה ימנית של 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
אם מוציאים את החוזה המשותף לפרויקט נפרד, קל יותר להתאים בין ה-UI וה-helper את:
- שם ה-operation
- סוגי ה-request / response
- פורמט ההודעה ב-pipe
8. המניפסטים
8.1 צד ה-UI (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>
ה-UI תמיד 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, כותבים כותרת אורך ואז את גוף ה-JSON, כך שהצד השני קורא בדיוק את אורך המידע, ופרוטוקול של בקשה אחת ותגובה אחת פחות נוטה לתקלה.
m1["כתיבת כותרת אורך"] --> m2["כתיבת גוף ה-JSON"]
m2 --> m3["הצד השני קורא בדיוק את האורך"]
m3 -.-> simple1["פשטות של בקשה אחת ותגובה אחת עוזרת"]
איור 15: ההודעה נשלחת עם אורך מצורף, ופרוטוקול של בקשה אחת ותגובה אחת מצמצם תקלות.
10. צד ה-UI: הפעלת ה-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 והמידע המינימלי שנדרש לאימות מקור ההתחברות. פעולת הניהול עצמה נכלאת ב-request מוגדר-טיפוס שנשלח בתוך ה-pipe.
flowchart TB
accTitle: חלוקת התפקידים בין ארגומנטי ההפעלה ל-pipe
accDescr: תרשים המראה שארגומנטי ההפעלה של ה-helper מעבירים רק את המידע המינימלי - שם pipe, PID ו-SID - ואילו פעולת הניהול עצמה נכלאת בבקשה מוגדרת-טיפוס שנשלחת בתוך ה-pipe.
argx["ארגומנטי ההפעלה"] -->|"מה שמועבר"| minx["מידע מינימלי: שם pipe, PID, SID"]
pipx["תוך ה-pipe"] -->|"מה שנכלא"| reqx["פעולת הניהול כ-request מוגדר-טיפוס"]
minx -.-> nolx["תוכן הפעולה לא נטען על הארגומנטים"]
איור 16: הארגומנטים משמשים רק לסידור החיבור, ותוכן הפעולה נכלא כ-request מוגדר-טיפוס ב-pipe.
ה-QuoteArgument הזה הוא מימוש מינימלי שמניח ערכים פשוטים כמו שם ה-pipe, PID ו-SID שמועברים בדוגמה הזו. אם מעבירים נתיב שרירותי של Windows או מחרוזת קלט חופשי לארגומנט שורת פקודה, יש להחליף בטיפול escape ייעודי שתואם את כללי ניתוח ה-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 של משתמש ה-UI הקורא
- אחרי ההתחברות, מאמתים את ה-PID של הצד המתחבר
- גם אחרי קבלת ה-request, מבצעים dispatch לפי שם ה-operation
אם מקבעים בצורה של switch (request.Operation) שרק פעולות קבועות עוברות, קשה יותר ל-helper להפוך ל”תיבה מועלית שעושה הכול”.
flowchart TB
accTitle: סדר האימותים שפועלים בצד ה-helper
accDescr: תרשים המראה שה-helper בונה pipe עם ACL מפורש, מאמת אחרי ההתחברות את PID הצד המתחבר, ומבצע dispatch של ה-request לפי שם operation שמעביר רק פעולות קבועות.
hs1["בניית pipe עם ACL מפורש"] --> hs2["אימות PID מקור ההתחברות"]
hs2 --> hs3["dispatch לפי שם operation"]
hs3 -->|"בתוך ה-allowlist"| hs4["ביצוע רק הפעולה הקבועה"]
hs3 -->|"מחוץ ל-allowlist"| hs5["דחייה ותשובה בהתאם"]
איור 17: רק request שעבר שלושה שלבים — 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;
}
}
המהות של הקוד הזה נמצאת במה שהוא לא מקבל מה-UI.
- לא מקבל מה-UI נתיב רישום מערכת שרירותי
- לא מקבל מה-UI מחרוזת פקודה שרירותית
- ה-EXE שנרשם נפתר בקיבוע בצד ה-helper
- תוכן ה-request הוא רק
Enabled
כלומר, ה-helper מוגבל למשמעות אחת בלבד: “החלפת מצב הרישום של תפריט לחיצה ימנית ב-Explorer”.
14. דוגמת קריאה מה-UI
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;
}
}
}
צד ה-UI רגיל.
- קורא את מצב תיבת הסימון
- קורא ל-broker client
- אם נכשל, מחזיר את ה-UI
זהו בלבד. לא נוגע ישירות ברישום המערכת. זו ההפרדה.
15. מה שהמימוש הזה שומר עליו
הקווים שהדוגמה הזו באמת שומרת עליהם:
15.1 הפרדת אחריות בין UI ל-helper
- ה-UI רק מקבל את פעולת המשתמש
- ה-helper רק מבצע פעולת ניהול קבועה
15.2 לא נוצר ל-helper “פתח הרצה חופשי”
- לא מקבל נתיב רישום מערכת שרירותי
- לא מקבל שורת פקודה שרירותית
- לא מקבל נתיב EXE שרירותי
15.3 מסלול ההפעלה קבוע
- ה-helper EXE בנתיב מוחלט
runasמוגדר במפורשUseShellExecute = trueמוגדר במפורש
15.4 מקור התחברות ה-IPC מוגבל
- ACL של ה-pipe מוגבל ל-SID של משתמש ה-UI
- PID הצד המתחבר נבדק אחרי ההתחברות
15.5 גם היעד של פעולת הניהול קבוע
- ה-hive / הנתיב של הרישום קבועים
- גם ה-EXE שנרשם נפתר בקיבוע
עם רמת ההקפדה הזו, מתרחקים מאוד ממצב שבו “אם ה-UI נשבר, אפשר לעשות עם ה-helper הכול”.
15.6 בודקים אם ההפרדה תקינה בפועל
עד כאן זה סיפור תכנון. אם מה שכתבנו באמת מופרד, אפשר לדעת רק על ידי הרצה ובדיקה. “כל ה-UI התעלה בלי שמים לב” היא תקרית שקשה לשים לב אליה גם בקריאת הקוד.
flowchart TB
accTitle: ארבעת השלבים לבדיקת ההפרדה
accDescr: תרשים המראה שבודקים בסדר את ארבעת הדברים - האם תהליך ה-UI נשאר לא-מועלה, האם הנחיית UAC מופיעה רק בהפעלת ה-helper, האם פעולת הניהול באמת פעלה, והאם היא נכשלת כשצריכה להיכשל.
v1["האם ה-UI נשאר לא-מועלה"] --> v2["האם ההנחיה מופיעה רק בהפעלת ה-helper"]
v2 --> v3["האם הפעולה באמת פעלה"]
v3 --> v4["האם היא נכשלת כשצריך"]
v4 -.-> whyv["בלי הבדיקה הרביעית, ההפרדה לא מאומתת"]
איור 18: הרצה ובדיקה של ארבעת השלבים בסדר תופסות פרצות העלאה שקוד בלבד לא חושף.
הבדיקה מתבצעת לפי ארבעת השלבים הבאים, בסדר.
1. האם תהליך ה-UI נשאר לא-מועלה
הנקודה החשובה ביותר. מפעילים את ה-UI, ובודקים אחרי שביצעו פעם אחת פעולת ניהול.
- מנהל המשימות: בלשונית “פרטים”, לוחצים ימנית על העמודות ומציגים את עמודת “העלאה”. אם
MyApp.exeמציג “לא” ורקMyApp.AdminBroker.exeמציג “כן”, זה כמצופה - Process Explorer: מציגים את עמודת ה-Integrity. נכון אם ה-UI מציג
Mediumוה-helper מציגHigh(רמות השלמות של Windows, כפי שבפרק 2.1, הן: משתמש רגיל = medium, מועלה = high)
אם רוצים לראות מהקוד, מספיקה בדיקה חד-פעמית מיד אחרי הפעלת ה-UI.
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
- אם ההנחיה מופיעה בהפעלת ה-UI ← המניפסט בצד ה-UI לא
asInvoker - אם היא מופיעה ברגע שלוחצים על תיבת הסימון בהגדרות ← כמצופה
- אם ההגדרה משתנה בלי שההנחיה מופיעה אף פעם ← ייתכן שה-helper מועלה תמידית במסלול אחר
חשבון מנהל נותן consent prompt, ומשתמש רגיל נותן credential prompt (הטבלה בפרק 5.6). אם בודקים את שני המסלולים, אפשר לוודא גם אם העברת ה-SID תקינה.
3. האם פעולת הניהול באמת פעלה
לרישום התפריט ב-Explorer, הכי מהיר לבדוק ישירות ברישום המערכת.
reg query "HKLM\SOFTWARE\Classes\*\shell\MyApp.Open" /s
בודקים באותה צורה גם את צד הביטול. אם בודקים רק את הרישום ולא את הביטול, באג בצד DeleteSubKeyTree עלול להישאר.
4. האם זה “נכשל כשצריך להיכשל”
בלי לבדוק את זה, לא ניתן לדעת אם ההפרדה תקינה.
- ביטול הנחיית ההעלאה ← ההגדרה לא משתנה, וגם תיבת הסימון ב-UI חוזרת למצבה (הטיפול ב-
ERROR_CANCELLED= 1223, פרק 10) - הפעלה ישירה של ה-helper ← גם אם מריצים ידנית משהו כמו
MyApp.AdminBroker.exe --pipe x --client-pid 1 --client-sid S-1-5-18, אימות PID מקור ההתחברות וה-timeout מונעים מהעיבוד להתקדם - שליחת operation שלא ב-allowlist ← נדחה עם
unsupported_operation(DispatchAsyncמפרק 12)
הפקודות הקונקרטיות לשלבים מרוכזות באותה זרימה ב-README, “נהלי הבדיקה ב-Windows” של הדוגמה.
16. תקלות נפוצות
16.1 להפוך את כל ה-UI ל-requireAdministrator
רק כפתור אחד במסך ההגדרות דורש הרשאות מנהל, אבל מפעילים הכול מועלה. זה כיוון שמוחק ברשלנות את גבול ההרשאות.
16.2 להעביר ל-helper פקודה כמחרוזת גולמית
לדוגמה, תכנון כזה:
UI -> helper מקבל "reg add HKLM\\.... /v ... /d ..."
זה הופך את ה-helper ל-command executor. כדאי להימנע מזה.
16.3 להשתמש ב-ACL ברירת המחדל של pipe בעל שם כמו שהוא
“זה IPC מקומי, אז בטח בסדר” קצת מסוכן. ה-pipe נמצא תחת מנגנון האבטחה של Windows, ולכן בטוח יותר ליצור ACL כראוי.
16.4 לקפוץ ישר ל-CurrentUserOnly
זה נראה נוח, אבל הוא לא מתאים ל-UI ברמת medium integrity מול helper ברמת high integrity הפעם. כאן, ACL מפורש (explicit) נוח יותר לטיפול.
16.5 ה-helper מקבל נתיב שרירותי ומבצע עליו פעולה
לדוגמה, כמו אלה:
- העתקת קובץ שרירותי ל-Program Files
- כתיבת מפתח שרירותי ל-HKLM
- מחיקת שם שירות שרירותי
- הוספת כלל חומת אש עם פקודה שרירותית
אם ה-helper מקבל את זה, הוא עצמו הופך לפתח הרצה כללי בהרשאות מנהל. בטוח יותר לקבע תמיד את הפעולה.
17. סיכום
ביישום Windows, “רק חלק מהתהליכים דורש הרשאות מנהל” זה לא סיפור נדיר.
עם זאת, הפתרון הוא לא “להפוך הכול ל-requireAdministrator”, אלא לחתוך את גבול ההרצה.
הצורה הראשונה שקל לאמץ היא זו:
- ה-UI ב-
asInvoker - תהליך הניהול מופרד ל-helper EXE
- ה-helper ב-
requireAdministrator - ההפעלה עם
runas - התקשורת ב-named pipe
- ה-helper מקבל רק operation קבוע
- מקור ההתחברות מוגבל ב-ACL של ה-pipe וב-PID הלקוח
- הארגומנטים מאומתים שוב בצד ה-helper
עם הצורה הזו, קל יותר גם לעבור אחר כך להפוך לשירות, אם רוצים. אם מפרידים היטב את חוזה ה-operation, גבול ה-UI ותהליך הניהול הופך בעצמו לנכס תכנוני.
flowchart TB
accTitle: הגבול הופך לנכס תכנוני
accDescr: תרשים המראה שאם מפרידים היטב את חוזה ה-operation, הגבול בין ה-UI לתהליך הניהול נהיה ברור, והוא הופך בעצמו לנכס תכנוני, וקל יותר גם לעבור בהמשך להפוך לשירות.
ctr1["הפרדת חוזה ה-operation"] --> bd1["הגבול בין UI לתהליך הניהול ברור"]
bd1 --> asset1["הגבול עצמו הופך לנכס תכנוני"]
asset1 -.-> future1["קל יותר גם לעבור בהמשך להפוך לשירות"]
איור 19: הגבול שנחתך בסגנון broker נשאר נכס שמשמש גם כשעוברים בעתיד לשירות.
בנושא האבטחה, לא הוספת תכונות בולטות, אלא לא להשאיר גבול רשלני — זה מה שמשפיע יותר. כך גם עם הרשאות מנהל. לא להחזיק את כולן יחד, אלא למסור רק את מה שנדרש, בצמצום ככל האפשר. פשטות מהסוג הזה משפיעה בהמשך.
18. מקורות
שימו לב, בחלק מהקישורים למטה מופיעה בכתובת ה-URL הגדרת גרסה כמו view=net-10.0. זו הגדרה בצד Microsoft Learn עבור איזו גרסת .NET מוצג התיעוד, וזה לא אומר שיש חוסר התאמה להנחת היסוד של המאמר הזה — .NET 8. PipeOptions / NamedPipeServerStreamAcl / RegistryView שמופיעים כאן זמינים כולם גם ב-.NET 8. אם רוצים להתאים את התצוגה ל-.NET 8, אפשר להחליף בבורר הגרסה שבראש הדף.
- סט קוד הדוגמה השלם של המאמר הזה (ספריית חוזה משותפת, הדגמה, בדיקות יחידה) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-admin-broker-deep-dive
- המאמר המקורי: רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows https://comcomponent.com/he/blog/windows-app-security-minimum-checklist/
- Administrator Broker Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/administrator-broker-model
- Developing Applications that Require Administrator Privilege https://learn.microsoft.com/en-us/windows/win32/secauthz/developing-applications-that-require-administrator-privilege
- Operating System Service Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/operating-system-service-model
- Elevated Task Model - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/elevated-task-model
- Administrator COM Object Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/administrator-com-object-model
- The COM Elevation Moniker https://learn.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker
- How User Account Control works https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works
- Mandatory Integrity Control - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control
- Process Explorer - Sysinternals https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer
- WindowsPrincipal.IsInRole Method https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsprincipal.isinrole
- ProcessStartInfo.UseShellExecute https://learn.microsoft.com/ja-jp/dotnet/fundamentals/runtime-libraries/system-diagnostics-processstartinfo-useshellexecute
- Named Pipe Security and Access Rights https://learn.microsoft.com/ja-jp/windows/win32/ipc/named-pipe-security-and-access-rights
- PipeOptions Enum https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.pipeoptions?view=net-10.0
- NamedPipeServerStreamAcl.Create https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstreamacl.create?view=net-10.0
- GetNamedPipeClientProcessId https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-getnamedpipeclientprocessid
- RegistryView Enum https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.win32.registryview?view=net-8.0
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
שמירת מידע רגיש ביישומי Windows - נמנעים מתצורה גלויה עם DPAPI
מסדרים כדי לא לשמור מחרוזות חיבור וטוקני API בטקסט גלוי בקובצי תצורה של יישום Windows, את התפיסה של DPAPI / ProtectedData, ההבדל בין Cur...
רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows
מסדרים בצורת רשימת בדיקה את היסודות של הרשאות, חתימה, עדכונים, סודות, HTTPS, אימות קלט, טעינת DLL ולוגים ביישומים עסקיים ב-WPF / WinForm...
המלכודות של יישום תקשורת טורית - עד לתכנון חיבור מחדש ויומן
המאמר מסדר, מנקודת מבט מעשית, את המלכודות שכדאי להימנע מהן ביישום תקשורת טורית לחיבור ציוד ובקרת מכשירי מדידה - מבניית מסגרות, timeout, ...
תכנון שמירת יומנים ו-dump בקריסת יישום Windows
המאמר מסדר איך לשלב יומן רגיל, סמן קריסה סופי, WER LocalDumps ותהליך ניטור, כדי שגם כשיישום Windows קורס מחריגה בלתי צפויה או מבאג בתוכני...
עיצוב UX ליישומי Windows - סדרי עדיפות לפי סביבת שימוש
המאמר מסדר מה צריך להעדיף בעיצוב UX ליישומי Windows, בטבלת החלטה לפי ToC/ToB, אמצעי קלט, סביבת שימוש ועלות טעות.
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
זה נוגע בתכנון ההרשאות של כל יישום Windows, כולל UAC, helper EXE, ההחלטה על הפיכה לשירות, ושינויי תצורה ברמת המכונה, ולכן זה מתאים היטב לפיתוח יישומי Windows.
ייעוץ טכני וסקירת תכנון
אם רוצים לבחון מחדש שימוש קבוע ב-`requireAdministrator` ביישום קיים, או לסדר מחדש תכנון broker וגבולות IPC, זה נושא שקל להתקדם בו כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- אפשר להריץ רק חלק מהתהליכים באותו תהליך בהרשאות מנהל?
- לא. ה-UAC של Windows לא נשלט ברמת פונקציה, אלא נקבע לפי הטוקן ורמת השלמות שבהם רץ התהליך. תהליך אב ותהליך בן יורשים טוקן באותה רמת שלמות, ולכן אי אפשר לתכנן שרק שיטה מסוימת בתוך תהליך UI לא מועלה תרוץ בהרשאות מנהל. תהליכים שדרושים מוציאים ליחידת הרצה נפרדת — תהליך נפרד, שירות, משימה מתוזמנת, או COM מועלה.
- אילו אפשרויות יש להפרדת תהליכים שדורשים הרשאות מנהל?
- ב-Microsoft Learn מפורטים בעיקר ארבעה מודלים: Administrator Broker Model שמשלב UI של משתמש רגיל עם 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 כמו pipe בעל שם לתקשורת עם ה-helper. ה-pipe לא נשען על ACL ברירת מחדל, אלא מגדירים PipeSecurity מפורש, מגבילים את זכות ההתחברות ל-SID של המשתמש הקורא, ומאמתים גם את PID מקור ההתחברות עם GetNamedPipeClientProcessId.
- האם השימוש ב-PipeOptions.CurrentUserOnly בפייפ בעל שם לא בטוח?
- הוא לא מתאים לתקשורת בין UI לא-מועלה ל-helper מועלה. ה-CurrentUserOnly של Windows בודק לא רק את חשבון המשתמש, אלא גם את רמת ההעלאה, ולכן לא ניתן להתחבר בין תהליכים ברמות שלמות שונות. בנוסף, בסביבת משתמש רגיל, UAC הופך ל-credential prompt, וה-helper עלול לרוץ תחת חשבון מנהל אחר. ACL מפורש שבו ה-UI מקבל את ה-SID של עצמו, מעביר אותו ל-helper, וה-helper מעניק זכות התחברות ל-pipe רק לאותו SID — נוח יותר לטיפול.