הורידו את רשימת הבדיקה בגרסת Excel
תוכן הקובץ הזה זהה לרשימת הבדיקה שלפני שחרור בפרק 4 (8 קטגוריות, 32 סעיפים). ההבדל היחיד הוא שיש בו שדות מילוי Status ו-Notes, ושני גליונות Checklist-ja / Checklist-en שמכילים גם יפנית וגם אנגלית. אם קוראים תוך כדי בדיקה — פרק 4 מספיק, ואם מחלקים כרשומת סקירה — גרסת ה-Excel.
כשמדברים על אבטחת יישומי Windows, הנושא נוטה להתנפח מהר. Zero Trust, EDR, SBOM (Software Bill of Materials, כתב כמויות תוכנה), ניהול אישורים, ניהול פגיעויות. כל אלה חשובים, אבל בעבודה מעשית יש לפני כן כמה יסודות שלא כדאי לפספס.
בפרט, ביישומים מהסוג הבא, איטום הפרצות הבסיסיות עוזר יותר מאשר “הגנה מתקדמת”.
- יישומי שולחן עבודה ב-WPF / WinForms / WinUI
- יישומי Win32 ב-C++ / C#
- שילוב עם מכשירים, שילוב קבצים, חיבור למסד נתונים, כלי הפצה פנים-ארגוניים
- יישומים עסקיים עם מנגנון עדכון אוטומטי
- הרכב שכולל שירותי Windows או קובצי EXE נלווים
בפיתוח יישומי Windows, מעשי יותר להתחיל מאיטום פרצות שברור שהן מסוכנות, ולא לנסות להשלים הכול בבת אחת.
כאן נסדר לפי סדר תכנון, מימוש, הפצה ותפעול, את הנקודות שברור שלא כדאי לפספס.
flowchart TB
accTitle: אופן ההתקדמות של המאמר
accDescr: תרשים המראה שקודם אוטמים פרצות בסיסיות ורק אז עוברים להגנה מתקדמת, ושהמאמר מסדר את הנקודות המינימליות לפי תכנון, מימוש, הפצה ותפעול.
adv1["הגנה מתקדמת (Zero Trust וכדומה)"] -.->|"קודם לכן"| base1["איטום הפרצות הבסיסיות"]
base1 --> o1["תכנון"]
o1 --> o2["מימוש"]
o2 --> o3["הפצה"]
o3 --> o4["תפעול"]
איור 1: לפני הגנה מתקדמת אוטמים את הפרצות הבסיסיות, ולאחר מכן עוברים לפי הסדר על תכנון, מימוש, הפצה ותפעול.
1. קודם המסקנה
- הדבר הראשון שלא כדאי לפספס: לא לדרוש הרשאות מנהל שלא לצורך, לחתום, לא להחזיק סודות בטקסט גלוי, ולא לבטל אימות אישורים.
- ביישומי Windows, קובצי ההפצה עצמם מהווים משטח תקיפה. בטוח יותר להתייחס גם ל-EXE / DLL / MSI / MSIX / מודול עדכון אוטומטי.
-
ServerCertificateValidationCallback => true, מחרוזת חיבור בטקסט גלוי, טעינה רשלנית עםLoadLibrary("foo.dll"), והרצת SQL בשרשור מחרוזות — כל אלה מוטב להימנע מהם גם ברף המינימלי. - אם רק חלק מהתהליכים דורשים הרשאות מנהל, בטוח יותר להפריד רק את אותו חלק לתהליך נפרד או לשירות, ולא להעלות את הרשאת כל היישום.
- כדאי להניח מראש שיישום שמופץ ב-Windows יזדקק לחתימה + חותמת זמן. זה נותן לא רק אמינות בעיני המשתמשים, אלא גם גילוי שיבוש והסבר ניהולי נוח יותר.
- מידע רגיש שנשמר משתמש לפי הצורך ב-DPAPI / ProtectedData או ב-Credential Locker. לפחות כדאי לצאת ממצב שבו הוא ב-
appsettings.jsonבטקסט גלוי. - לא כל כמות לוגים טובה. השארת טוקנים, סיסמאות, מחרוזות חיבור, מידע אישי וגוף בקשה מלא ביומן ישירות הופכת את היומן עצמו לגורם התקרית.
אבטחה מינימלית היא לא הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני.
flowchart TB
accTitle: התפיסה של אבטחה מינימלית
accDescr: תרשים המראה שאבטחה מינימלית אינה הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני.
add1["הוספת תכונה מיוחדת"] -.->|"זה לא המוקד המינימלי"| goal1["אבטחה מינימלית"]
rm1["לא להשאיר ברירת מחדל מסוכנת או מימוש רשלני"] -->|"זה המוקד"| goal1
איור 2: הרף המינימלי אינו הוספת תכונות, אלא הימנעות מהשארת ברירת מחדל מסוכנת או מימוש רשלני.
מפת הידע של המאמר
המאמר הזה מרכז רשימת תיוג של אמצעי אבטחה מינימליים שכדאי לא לוותר עליהם לפני שחרור, עבור יישומי Windows עסקיים ב-WPF, WinForms, WinUI, C++ ו-C#. במרכז הרשימה: לא להריץ את כל היישום כ-requireAdministrator אלא להפריד רק את הפעולות הדורשות הרשאות מנהל לתהליך או שירות נפרד; לחתום על קבצי ההפצה עם חותמת זמן ולהימנע מהפצה לא חתומה או מדילוג קבוע על אימות תעודות; ולהימנע משמירת סודות בתצורה בטקסט גלוי ולהגן עליהם למשל באמצעות DPAPI. בנוסף נדון האופן שבו שרשור מחרוזות ב-SQL מוביל להזרקת SQL וכיצד placeholders מונעים זאת, האופן שבו טעינת DLL לפי שם בלבד עלולה לגרום לחטיפת סדר חיפוש, והעובדה שהדפסת מידע רגיש ללוג הופכת את הלוג עצמו לערוץ דליפה.
flowchart LR
accTitle: מפת הידע של רשימת התיוג לאבטחה מינימלית ביישומי Windows
accDescr: תרשים המראה את היחסים בין הטיפול בהרשאות מנהל, חתימת קוד ואימות עדכונים, הגנה על סודות, סיכוני קלט וזמן ריצה כמו הזרקת SQL וטעינת DLL, לבין דליפת מידע רגיש דרך לוגים — כרכיבי מינימום של רשימת התיוג לאבטחה.
admin_rights["הרשאות מנהל"]
code_signing_cert["אישור לחתימת קוד"]
app_secrets["סודות היישום"]
execution_level_asinvoker["רמת הרצה asInvoker"]
execution_level_requireadministrator["רמת הרצה requireAdministrator"]
desktop_app["יישום שולחני עם משתמש אינטראקטיבי"]
admin_privilege_separation["הפרדת הפעולות הדורשות הרשאות מנהל"]
dll_search_order_hijacking["חטיפת סדר החיפוש של DLL"]
unvalidated_dll_loading["טעינת DLL לפי שם בלבד"]
unsigned_binary["בינארי לא חתום"]
code_signing_timestamp["חותמת זמן בחתימת קוד"]
certificate_expiry["פקיעת תוקף של אישור"]
update_integrity_verification["אימות חתימה או hash של העדכון"]
unverified_update_application["התקנת עדכון ללא אימות"]
certificate_pinning["הצמדת אישור (certificate pinning)"]
certificate_validation_bypass["דילוג קבוע על אימות האישור"]
certificate_revocation_check["בדיקת ביטול של אישור"]
sql_string_concatenation["שרשור מחרוזות SQL"]
sql_injection["הזרקת SQL"]
prepared_statement["פרמטר מוחלף (prepared statement)"]
secret_logging["כתיבת מידע רגיש ליומן"]
secret_log_exposure["חשיפת מידע רגיש דרך היומן"]
dpapi["DPAPI"]
plaintext_secret_storage["שמירת סודות בטקסט גלוי"]
windows_service["שירות Windows"]
admin_rights -->|"מוגדר באמצעות"| execution_level_asinvoker
admin_rights -->|"מוגדר באמצעות"| execution_level_requireadministrator
execution_level_requireadministrator -->|"שימוש לא מומלץ ל"| desktop_app
admin_privilege_separation -->|"מענה מומלץ ל"| execution_level_requireadministrator
execution_level_requireadministrator -.->|"עלול לגרום ל"| dll_search_order_hijacking
unvalidated_dll_loading -->|"עלול לגרום ל"| dll_search_order_hijacking
code_signing_cert -->|"מונע"| unsigned_binary
code_signing_timestamp -->|"מצמצם"| certificate_expiry
update_integrity_verification -->|"מונע"| unverified_update_application
code_signing_cert -->|"מענה מומלץ ל"| update_integrity_verification
certificate_pinning -->|"מענה מומלץ ל"| certificate_validation_bypass
certificate_pinning -.->|"מחייב"| certificate_revocation_check
certificate_validation_bypass -->|"שימוש לא מומלץ ל"| desktop_app
sql_string_concatenation -->|"עלול לגרום ל"| sql_injection
prepared_statement -->|"מונע"| sql_injection
secret_logging -->|"עלול לגרום ל"| secret_log_exposure
dpapi -->|"מענה מומלץ ל"| plaintext_secret_storage
app_secrets -.->|"מחייב"| dpapi
admin_privilege_separation -.->|"משתמש ב"| windows_service
code_signing_cert -.->|"מחייב"| code_signing_timestamp
prepared_statement -->|"מענה מומלץ ל"| desktop_app
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 21, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
2. היקף המאמר ומשמעות “מינימום”
2.1. ההיקף שהמאמר מטפל בו
המאמר מתייחס ליישומי Windows מהסוג הבא:
- יישומי שולחן עבודה ב-WPF / WinForms / WinUI
- יישומי Win32 ב-C++ / C#
- כלי הפצה פנים-ארגוניים, כלי שילוב מכשירים, כלי ניטור
- הרכב שכולל EXE נלווה, שירות Windows ומעדכן
- תוכנה עסקית שמופצת ב-EXE / MSI / MSIX
“המינימום” כאן אינו הצורה הסופית שעוברת ביקורת, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
flowchart TB
accTitle: משמעות "המינימום" במאמר הזה
accDescr: תרשים המראה שהמינימום במאמר הזה אינו הצורה הסופית שעוברת ביקורת, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
au1["הצורה הסופית שעוברת ביקורת"] -.->|"לא זו הכוונה כאן"| mn1["#quot;המינימום#quot; של המאמר הזה"]
ac1["סעיפים שהיעדרם גורם לתקלה בשגרה"] -->|"זו הכוונה"| mn1
איור 3: “המינימום” כאן אינו הצורה הסופית שעוברת ביקורת, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.
גם הנחות היסוד לדוגמאות הקוד מוגדרות מראש. דוגמאות C# מניחות .NET 8 ואילך, ודוגמאות C++ מניחות Win32 API. גם ב-.NET Framework 4.8 התפיסה נשארת זהה, אך יש מקומות שבהם הכתיבה המומלצת השתנתה. הדוגמה הבולטת היא סביבת ServicePointManager בסעיף 3.6, שבקוד חדש עבר לתפיסה שמשתמשת ב-IHttpClientFactory וב-HttpClient. כשקוראים מחדש קוד מדור ישן, שימו לב במיוחד לנקודה הזו.
2.2. מה לא נכלל
מצד שני, יש דברים שנשארים מחוץ למוקד המאמר.
- תכנון Zero Trust כלל-ארגוני
- תפעול כולל של EDR / SIEM / DLP / MDM
- חיזוק מפורט של דרייברי kernel
- תכנון הצפנה מאפס
- ניתוח איומים מתקדם או הליכי פורנזיקה
כלומר, לא “מדיניות אבטחה עצומה של הארגון כולו”, אלא קו יסוד שקשה למפתחי יישומי Windows להשמיט בכוחות עצמם לפני שחרור.
flowchart TB
accTitle: קו הגבול בין הנכלל למה שלא נכלל
accDescr: תרשים המראה שמדיניות אבטחה עצומה כלל-ארגונית אינה נכללת, ושהמאמר מתמקד בקו יסוד שקשה למפתחים להשמיט בכוחות עצמם לפני שחרור.
org1["מדיניות עצומה כלל-ארגונית"] -.->|"לא נכלל במאמר הזה"| sc1["ההיקף של המאמר הזה"]
dev1["קו יסוד שקשה למפתח להשמיט בכוחות עצמו"] -->|"זה נכלל"| sc1
איור 4: המאמר לא עוסק במדיניות כלל-ארגונית, אלא בקו היסוד שהמפתח יכול לתפוס בעצמו לפני שחרור.
3. קודם רשימת הבדיקה
לפני דיון מפורט, נציג טבלה שמאפשרת מבט כולל. כבר ממנה אפשר לזהות באילו מקומות כדאי לבדוק שוב.
3.1. התמונה הכוללת
| מה בודקים | מה עושים כמינימום | תקלה טיפוסית |
|---|---|---|
| הרשאת הרצה | asInvoker כברירת מחדל, ורק תהליכים שדורשים העלאה מופרדים |
הפיכת כל היישום ל-requireAdministrator |
| אמינות ההפצה | חתימת קוד ל-EXE / DLL / MSI / MSIX, כולל חותמת זמן | הפצה במצב לא חתום |
| עדכון | קיבוע מקור העדכון, וגילוי שיבוש דרך HTTPS ואימות חתימה | דריסה ישירה אחרי הורדה ב-HTTP |
| מידע רגיש | לא להחזיק סודות בקוד המקור או בתצורה גלויה, שימוש ב-DPAPI / Credential Locker וכדומה | הצבת מפתחות API או מחרוזות חיבור בקובץ תצורה בטקסט גלוי |
| תקשורת | שימוש ב-HTTPS ולא ביטול אימות אישורים | דילוג קבוע על אימות אישורים עם return true |
| קלט חיצוני | אימות מלא של SQL, קבצים, IPC, URI, CSV, JSON וכדומה | מעבר חופשי בטענה “זה כלי פנים-ארגוני” |
| טעינת DLL | נתיב מוחלט, SetDefaultDllDirectories, סדר חיפוש בטוח |
הסתמכות על הספרייה הנוכחית עם LoadLibrary("foo.dll") |
| לוגים | מיסוך טוקנים, סיסמאות, מידע אישי, והפרדה בין שגיאה למשתמש לשגיאה פנימית | הצגה או שמירה ישירה של פרטי חריגה או מחרוזת חיבור |
| תלויות | עדכון שוטף של SDK, NuGet, זמן ריצה של VC++, תלויות קוד פתוח | קיבוע לשנים ללא מעקב אחר מידע על פגיעויות |
3.2. הרשאה כברירת מחדל היא asInvoker
זו הנקודה הראשונה שכדאי לבחון מחדש ביישום Windows. הרצת כל היישום בהרשאות מנהל גורמת לכך שבאגים, החלפת DLL, קריאה שגויה של קובצי תצורה ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה.
flowchart TB
accTitle: הסכנה בהרצת כל היישום בהרשאות מנהל
accDescr: תרשים המראה שהרצת כל היישום בהרשאות מנהל גורמת לכך שבאגים, החלפת DLL ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה.
all1["הרצת כל היישום בהרשאות מנהל"] --> bug1["באגים"]
all1 --> swp1["החלפת DLL"]
all1 --> inp1["קריאה שגויה של תצורה / פגם בקלט"]
bug1 --> pw1["מתבצע ישירות בהרשאה חזקה"]
swp1 --> pw1
inp1 --> pw1
איור 5: כשמעלים הרשאה של כל היישום, כל פגם שהוא רץ בהרשאה חזקה.
המדיניות הבסיסית היא כזו:
- יישום UI רגיל יפעל ב-
asInvoker - רק תהליכים שדורשים הרשאות מנהל יופרדו לתהליך נפרד או לשירות
- ההעלאה בהרשאה תתבצע רק ברגע הנדרש
- גם הקלט שמועבר ל-EXE הנלווה או לשירות מאומת
אם מדובר ביישום שולחן עבודה שרגיל רק לצפייה ולעריכה, ורק ההתקנה או שינוי הגדרות חומת אש דורשים הרשאות מנהל, בטוח יותר לרכז את מה שדורש העלאה ב-broker, ולא להפוך את כל היישום ל-requireAdministrator.
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
“נוח כשהכול רץ כמנהל” בדרך כלל חוזר לפגוע בהמשך. הרצה בהרשאה מינימלית, עם הוצאה החוצה רק של הפעולות שבאמת דורשות זאת, מצמצמת משמעותית את רדיוס הפגיעה.
flowchart TB
accTitle: מבנה של הפרדת תהליכים שדורשים העלאה
accDescr: תרשים המראה שיישום UI רגיל רץ ב-asInvoker, שרק תהליכים שדורשים הרשאות מנהל מרוכזים ב-broker בתהליך נפרד או בשירות, ושההעלאה מתבצעת רק ברגע הנדרש.
ui1["יישום UI (asInvoker)"] -->|"פנייה רק ברגע הנדרש"| br1["broker (EXE נפרד / שירות)"]
br1 --> el1["ביצוע רק של פעולות שדורשות העלאה"]
ui1 -.-> vd1["גם הקלט ל-broker מאומת"]
איור 6: בדרך כלל מריצים ב-asInvoker, ורק פעולות שדורשות העלאה מרוכזות ב-broker, כך שרדיוס הפגיעה מצטמצם.
3.3. חתימה על הבינארי ועל ההתקנה
ב-Windows, אמינות קובצי ההפצה קובעת הכול. המשתמש נוגע לא בקוד המקור, אלא ב-EXE, DLL, MSI, MSIX ובמעדכן. אם אלה לא חתומים, גם גילוי השיבוש, גם האמינות בעיני המשתמש וגם ההסבר הניהולי נחלשים.
flowchart TB
accTitle: מה שהמשתמש נוגע בו הם קובצי ההפצה
accDescr: תרשים המראה שהמשתמש נוגע לא בקוד המקור אלא ב-EXE, DLL, מתקין ומעדכן, ושאם אלה לא חתומים גילוי השיבוש וההסבר הניהולי נחלשים.
us1["מה שהמשתמש נוגע בו"] --> bin1["EXE / DLL"]
us1 --> pkg1["MSI / MSIX"]
us1 --> upd1["מעדכן"]
bin1 --> ns1["בלי חתימה — גילוי שיבוש והסבר חלשים"]
pkg1 --> ns1
upd1 --> ns1
איור 7: משטח התקיפה הוא קובצי ההפצה עצמם, ובלי חתימה מאבדים את הבסיס לאמינות.
לכל הפחות, כדאי לבדוק את אלה:
- לחתום על EXE / DLL / MSI / MSIX
- לחתום לא רק על ההתקנה אלא גם על בינארים נלווים שמשמשים לעדכון
- להוסיף חותמת זמן
- לכלול בתהליך השחרור את פקיעת תוקף האישור ואת הליך החידוש
חתימה שאין לה חותמת זמן נוטה לגרום לקושי באימות אחרי פקיעת תוקף האישור. במקום “חתמנו וזהו”, יציב יותר לכלול בהליך השחרור חתימה + חותמת זמן יחד.
flowchart TB
accTitle: חתימה וחותמת זמן
accDescr: תרשים המראה שחתימה בלי חותמת זמן גורמת לקושי באימות אחרי פקיעת תוקף האישור, ושחתימה יחד עם חותמת זמן שנכללת בהליך השחרור יציבה יותר.
sg2["חתימה בלבד"] --> tr1["קושי באימות אחרי פקיעת תוקף האישור"]
ts1["חתימה + חותמת זמן"] --> st2["פחות קושי באימות אחרי הפקיעה"]
ts1 -.-> fl1["נכלל בהליך השחרור"]
איור 8: לא מסתפקים בחתימה בלבד — כוללים גם חותמת זמן בהליך השחרור.
אם משתמשים ב-MSIX, חתימת החבילה היא הנחת יסוד. גם בהפצת MSI / EXE, כדאי לפחות לחתום על גוף המתקין ועל הבינארים המרכזיים המורצים.
3.4. קיבוע מסלול העדכון והוספת גילוי שיבוש
ביישום Windows של היום, מסלול העדכון נמצא לרוב בשימוש ממושך יותר מההתקנה הראשונית. אם הוא נעשה ברשלנות, גם אם גוף היישום נבנה בקפידה, המעדכן נהיה החוליה החלשה ביותר.
flowchart TB
accTitle: מסלול העדכון נמצא בשימוש הארוך ביותר
accDescr: תרשים המראה שמסלול העדכון נמצא בשימוש ממושך יותר מההתקנה הראשונית, ושאם הוא נעשה ברשלנות המעדכן נהיה החוליה החלשה ביותר.
ins1["ההתקנה הראשונית"] -.->|"בשימוש רק פעם אחת"| ap1["אורך חיי היישום"]
up2["מסלול העדכון"] -->|"בשימוש ממושך לאורך זמן"| ap1
up2 --> wk2["ברשלנות הוא נהיה החוליה החלשה"]
איור 9: מסלול העדכון נמצא בשימוש ארוך יותר מההתקנה הראשונית, וברשלנות הוא נהיה החוליה החלשה.
סביב העדכון, כדאי לחשוב מראש לפחות על חמישה דברים:
- קבלת קובץ העדכון תמיד דרך HTTPS
- אימות חתימה או hash של קובץ העדכון שהתקבל
- מניעת החלפה חופשית של כתובת ה-URL של מקור העדכון בקוד או בתצורה
- חתימה גם על מודול העדכון עצמו
- קביעת נוהל rollback או שחזור במקרה כשלון
אם אפשר לבחור ב-MSIX + App Installer, קל יותר לרכז את מנגנון העדכון קרוב יותר למערכת ההפעלה. מצד שני, אם יש מעדכן עצמאי, יש לוודא גם את בטיחות התקשורת וגם את אמיתות קובץ ההפצה. HTTPS לבדו שומר על “ערוץ התקשורת”, אבל לא מבטיח “האם הקובץ הזה באמת פרסום שלנו”.
flowchart TB
accTitle: שני הדברים שמאמתים בעדכון
accDescr: תרשים המראה שבמעדכן עצמאי צריך לוודא גם את בטיחות התקשורת דרך HTTPS וגם את אמיתות קובץ העדכון דרך חתימה או hash.
dl1["קבלת קובץ העדכון ב-HTTPS"] --> vf1["אימות חתימה או hash"]
vf1 --> ap2["החלה רק אחרי מעבר האימות"]
dl1 -.-> lim1["HTTPS שומר רק על ערוץ התקשורת"]
vf1 -.-> own1["בדיקה האם זה פרסום שלנו"]
איור 10: גם אם מקבלים ב-HTTPS, האמיתות היא שאלה נפרדת — מיישמים רק אחרי מעבר אימות חתימה או hash.
3.5. לא להחזיק סודות בקוד המקור או בתצורה גלויה
זו נקודה שקל מאוד להיכשל בה בעבודה מעשית. בטענה “זה כלי פנים-ארגוני” או “בכל מקרה רק מפיצים exe”, נוטים להשאיר מחרוזת חיבור, מפתח API, אישורים לתיקייה משותפת וטוקן קבוע בקוד המקור או בקובץ תצורה.
לכל הפחות, כדאי להימנע מהצבות כאלה:
- מפתח API שכתוב ישירות בקוד המקור
- סיסמה בטקסט גלוי ב-
appsettings.jsonאו ב-app.config - מחרוזת חיבור שנכנסה למאגר הקוד
- תכנון שבו מפתח הפענוח והטקסט המוצפן נמצאים באותו מקום
- אישורים קבועים ומשותפים לכל המשתמשים, במקום פר-משתמש
באופן מעשי, ביישום Windows יש בעיקר ארבע אפשרויות:
- רוצים לשמור אישורי Windows ביישום packaged desktop app / משפחת WinUI, לשקול Credential Locker
- רוצים לשמור סוד מוצפן מקומית
ב-Win32 / .NET, להשתמש ב-DPAPI /
ProtectedData - היעד תומך באימות Windows או אימות משולב אם אפשר, לא להחזיק סיסמה ביישום כלל
- ניהול הסוד אפשרי בענן או בצד השרת להעדיף תכנון שלא מטמיע סוד ארוך-טווח בצד הלקוח
ב-C#, לפחות שימוש ב-DPAPI כמו להלן כבר עדיף בהרבה משמירה בטקסט גלוי. אם כותבים גם את שמירה וגם קריאה בהלוך-חזור, זה נראה כך:
// C# / .NET 8. ProtectedData מיועד ל-Windows בלבד, וב-.NET
// דרוש חבילת NuGet בשם System.Security.Cryptography.ProtectedData.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SecretStore
{
// גם בפענוח נדרש אותו ערך. אפשר גם null, אך יותר בטוח להוסיף ערך.
private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];
public static void Save(string path, string secretText)
{
byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
plaintext,
Entropy,
DataProtectionScope.CurrentUser);
// אפשר גם לשמור את מערך הבייטים ישירות לקובץ, אך אם מכניסים לקובץ תצורה, עדיף Base64.
File.WriteAllText(path, Convert.ToBase64String(ciphertext));
CryptographicOperations.ZeroMemory(plaintext);
}
public static string Load(string path)
{
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));
// אם לא אותו משתמש ואותו entropy כמו בזמן השמירה, יתקבל CryptographicException.
byte[] plaintext = ProtectedData.Unprotect(
ciphertext,
Entropy,
DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plaintext);
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
}
צד הקריאה נראה כך:
string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SampleApp",
"token.dat");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);
מה שחשוב כאן הוא לא “הצפנו אז זה בטוח”, אלא לקבוע בתכנון מי יכול לפענח.
יש הבדל משמעותי בין CurrentUser ל-LocalMachine. CurrentUser מאפשר פענוח רק למשתמש שביצע את השמירה, ו-LocalMachine מאפשר לכל מי שנמצא באותה מכונה. אם השירות רץ תחת חשבון נפרד, או שיש תפעול שמחליף בין משתמשים, חשוב לקבוע את זה מראש — אחרת ייתקעו אחר כך במצב של “לא ניתן לקרוא” או “ניתן לקרוא יותר מדי”.
flowchart TB
accTitle: מי יכול לפענח ב-DPAPI
accDescr: תרשים המראה שהגדרת ה-scope של DPAPI ל-CurrentUser מאפשרת פענוח רק למשתמש השומר, ואילו LocalMachine מאפשר פענוח לכל מי שבמכונה, ולכן צריך לקבוע זאת מראש בתכנון.
dc1["קביעה בתכנון: מי יכול לפענח"] -->|"CurrentUser"| cu1["רק המשתמש השומר יכול לפענח"]
dc1 -->|"LocalMachine"| lm1["כל מי שבמכונה יכול לפענח"]
dc1 -.-> lt1["בלי קביעה מראש — תקיעה מאוחר יותר"]
איור 11: ב-DPAPI, בחירת ה-scope קובעת מי יכול לפענח, ולכן קובעים זאת מראש.
חשוב גם לדעת שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן כשהפרופיל לא טעון (כגון בזמן impersonation), הפענוח עלול להיכשל — כך כתוב במפורש בתיעוד. אם מתכננים שימוש משירות, כדאי לבדוק גם את הנקודה הזו.
flowchart TB
accTitle: המפתח של DPAPI ופרופיל המשתמש
accDescr: תרשים המראה שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן במצבים כמו impersonation שבהם הפרופיל לא טעון, הפענוח נכשל.
ky1["המפתח של DPAPI"] --> pf1["נמצא בפרופיל המשתמש"]
pf1 -->|"פרופיל לא טעון (כגון impersonation)"| fe1["הפענוח נכשל"]
fe1 -.-> sv2["צריך לבדוק זאת בשימוש משירות"]
איור 12: מפתח ה-DPAPI נמצא בפרופיל המשתמש, וכשהפרופיל לא טעון לא ניתן לפענח.
בחיבור ל-SQL Server, בסביבה מקומית לפעמים אפשר להעדיף אימות Windows כברירה ראשונה.
אם בכל זאת חייבים לכלול אישורים במחרוזת החיבור, לפחות כדאי לשמור על Persist Security Info=False ולא להשאיר אותם בקובץ תצורה גלוי.
3.6. תקשורת מבוססת HTTPS, ולא לבטל אימות אישורים
פרצה שהוכנסה “רק לזמן הפיתוח” נשארת כמות שהיא בייצור. תקלות תקשורת הן בדרך כלל בדיוק הדפוס הזה.
במיוחד קוד ותצורה מהסוג הבא נוטים להישאר במוצר הסופי:
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.DangerousAcceptAnyServerCertificateValidator- הפצה עם בדיקת ביטול אישורים מבוטלת
- קוד שמניח אישור עצמי-חתום לפיתוח, שנשאר בייצור
המדיניות המינימלית פשוטה:
- תקשורת בייצור היא HTTPS
- לא לדלג באופן קבוע על אימות אישורים
- אם נדרשת הקלה חריגה באימות, להגביל אותה למארח ולאישור ספציפיים
- להסיר קוד עקיפה של פיתוח בוודאות דרך תנאי build או תצורה
- ב-.NET, לשים לב גם לבדיקת ביטול
הדוגמה השגויה הטיפוסית נראית כך:
ServicePointManager.ServerCertificateValidationCallback +=
(_, _, _, _) => true;
זה נראה נוח במבט ראשון, אבל בפועל זה קרוב ל”התקשורת הזו ב-HTTPS תתחבר לכל אחד”. כשמסירים אימות אישורים, גם אם משתמשים ב-HTTPS, התוכן חלול למדי.
הכתיבה משתנה לפי גרסת .NET
הצבת הגדרה גלובלית ב-ServicePointManager היא סגנון כתיבה מתקופת .NET Framework. בקוד חדש, טבעי יותר לקבל HttpClient מ-IHttpClientFactory, ואם צריך הגדרה סביב TLS, להחזיק אותה בצד SocketsHttpHandler או HttpClientHandler.
עם זאת, מסוכן להזניח בטענה “זה API ישן שכבר לא משפיע”. בתיעוד של Microsoft כתוב ש-ServicePointManager.ServerCertificateValidationCallback מ-.NET 9 ואילך ממופה אל RemoteCertificateValidationCallback של SocketsHttpHandler.SslOptions. כלומר, שורה בודדת של => true איפשהו עלולה להשפיע גם על התקשורת של HttpClient.
flowchart TB
accTitle: המסלול שבו callback ישן משפיע על תקשורת של היום
accDescr: תרשים המראה שה-ServerCertificateValidationCallback של ServicePointManager ממופה מ-.NET 9 ואילך לאימות של SocketsHttpHandler, ולכן שורה בודדת שמחזירה true עלולה להשפיע גם על תקשורת HttpClient.
old1["callback האימות של ServicePointManager"] -->|"ממופה מ-.NET 9 ואילך"| new1["האימות בצד SocketsHttpHandler"]
new1 --> ef1["משפיע גם על תקשורת HttpClient"]
ef1 -.-> rk2["שורה בודדת שמחזירה true עלולה לחלל את כל התקשורת"]
איור 13: דילוג אימות שהוצב ב-API ישן משפיע, דרך המיפוי, גם על תקשורת HttpClient של היום.
כשרוצים להקל חריגית על האימות, לא כדאי הגדרה גלובלית שמשפיעה על כל התהליך, אלא צורה שמוגבלת רק ל-handler הספציפי.
// C# / .NET 8. דוגמה שמטפלת חריגתית רק במארח ואישור ספציפיים.
// גם אם מקלים לצורך פיתוח, בלי להגביל את היעד זה כמו לבטל את האימות גלובלית.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
// טביעת האצבע של האישור המצופה. אפשר גם לקרוא מתצורה.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = true, // בודקים גם ביטול. ברירת המחדל היא false
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// מקבלים רק את המקרה שבו "המכשיר הזה לא בוטח ב-CA הפנים-ארגוני".
// אישור שלא הגיע או חוסר התאמה בשם מארח — לא מקבלים
if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
{
return false;
}
// מקבעים את הצד השני ואת האישור
if (request.RequestUri?.Host != "device.internal.example"
|| certificate is null
|| !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
// גם באישור המוצמד לא מקבלים פקיעת תוקף או ביטול.
// ברגע שמקבלים זאת, נפתח מסלול שממשיך להשתמש ב"אישור שבוטל אחרי דליפת מפתח"
return chain is not null
&& chain.ChainStatus.All(s =>
s.Status is X509ChainStatusFlags.NoError
or X509ChainStatusFlags.UntrustedRoot
or X509ChainStatusFlags.PartialChain);
},
};
using var client = new HttpClient(handler);
את ExpectedThumbprint נותנים כקבוע או מתוך תצורה, לפי טביעת האצבע של האישור הרלוונטי.
החשוב כאן הוא לא “להתעלם” מ-errors ומ-chain.ChainStatus. אם מחזירים true רק בגלל שטביעת האצבע תואמת, זה ימשיך לעבור גם אם האישור פג תוקף, וגם אחרי שהמפתח דלף וגרם לביטולו. הצמדת אישור (pinning) פירושה “מאמינים רק לעותק הזה”, לא “מאמינים לעותק הזה בכל מצב”. בקוד שלמעלה, מה שמותר הוא רק UntrustedRoot ו-PartialChain (כלומר ה-CA הפנים-ארגוני לא מותקן במכשיר הזה), בעוד ש-NotTimeValid (פג תוקף) ו-Revoked (בוטל) נדחים ישירות.
כדי לבדוק ביטול נדרש CheckCertificateRevocationList = true (ברירת המחדל היא false, ואז הביטול לא נבדק). מצד שני, אם ה-CA הפנים-ארגוני לא מפרסם לא CRL ולא OCSP, ייחסם עם RevocationStatusUnknown. זו התנהגות נכונה. אם אין אמצעי לבדוק ביטול, יש למלא את הפער באחת משתי דרכים: לקצר את תוקף האישור, או להכין מראש מסלול שמאפשר להפיץ מחדש את הערך המוצמד. המצב המסוכן ביותר הוא “הצמדת אישור לטווח ארוך בלי אפשרות לבטל אותו”.
flowchart TB
accTitle: זרימת ההחלטה בעת הצמדת אישור
accDescr: תרשים המראה שכשאין שגיאה מקבלים, שגיאה שאינה שגיאת שרשרת נדחית, ומתוך שגיאת שרשרת בלבד מאמתים שם מארח וטביעת אצבע, ואף מצב שרשרת של פקיעת תוקף או ביטול נדחה.
e0["בדיקת errors"] -->|"None"| pass1["מקבלים"]
e0 -->|"שגיאה שאינה שגיאת שרשרת"| rj1["דוחים"]
e0 -->|"רק שגיאת שרשרת"| hchk["התאמת שם מארח וטביעת אצבע"]
hchk -->|"חוסר התאמה"| rj1
hchk -->|"התאמה"| cchk["בדיקת מצב השרשרת"]
cchk -->|"פג תוקף / בוטל וכדומה"| rj1
cchk -->|"רק CA פנים-ארגוני לא מותקן"| pass1
איור 14: גם בהצמדת אישור לא מתעלמים — זרימת ההחלטה דוחה ישירות פקיעת תוקף וביטול.
3.7. להתייחס לכל קלט חיצוני כ”קלט לא מהימן”
יישום Windows אינו יישום Web, ולכן אימות הקלט (validation) נוטה להיות רופף. אבל בפועל, נקודות הכניסה לקלט חיצוני רבות יותר ממה שחושבים.
- נתיבי קבצים
- CSV / Excel / JSON / XML
- ארגומנטים משורת הפקודה
- named pipe / socket / COM / RPC / gRPC
- מחרוזת שמועברת ל-DB
- ערכי רישום
- לוח גזירים
- URL / deep link
- נתונים שחוזרים ממכשיר חיצוני או מ-SDK
בפרט, שלושה דברים שלא כדאי לפספס:
- תמיד לפרמט SQL לא לבנות SQL בשרשור מחרוזות.
- לנרמל נתיב קובץ לפני שימוש לא להשתמש בנתיב שהוזן על ידי המשתמש ישירות למחיקה, דריסה או חילוץ.
- להוסיף מגבלת גודל ובדיקת פורמט בקריאת קובץ חיצוני “נפתח בהצלחה” לא אומר “בטוח”.
בדוגמת SQL, זה מה שכדאי להימנע ממנו:
var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";
גם ברף המינימלי, כדאי להתקרב לזה:
using System.Data;
using Microsoft.Data.SqlClient;
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;
ההנחה “כלי פנים-ארגוני, אז הקלט מהימן” היא הנחת יסוד מסוכנת למדי. בפועל, CSV שבור, שמות קבצים בלתי צפויים, נתוני DB ישנים, טעויות הקלדה בתפעול, ו-JSON חצי-מוגמר שנכתב על ידי כלי אחר — כל אלה נכנסים כרגיל.
flowchart TB
accTitle: גם לכלי פנים-ארגוני מגיע קלט שבור
accDescr: תרשים המראה שגם בכלי פנים-ארגוני נכנס באופן שגרתי CSV שבור, שם קובץ בלתי צפוי, נתוני DB ישנים, טעויות הקלדה ו-JSON חצי-מוגמר, ולכן מתייחסים לכל קלט חיצוני כלא מהימן.
csv1["CSV שבור"] --> in2["קלט אל היישום"]
fn1["שם קובץ בלתי צפוי"] --> in2
hm1["טעות הקלדה / נתונים ישנים"] --> in2
in2 --> tr2["אימות כקלט לא מהימן, בכל נקודת כניסה"]
איור 15: גם לכלי פנים-ארגוני מגיע קלט שבור כעניין שגרתי, אז מאמתים בכל נקודת כניסה לפני שימוש.
3.8. לא להשאיר את מקור טעינת ה-DLL עמום
זו מלכודת אופיינית ל-Windows.
כשטוענים DLL רק לפי שם, כמו LoadLibrary("foo.dll"), ייתכן שבהתאם לסדר החיפוש ייטען DLL ממקום לא רצוי.
מה שצריך לעשות ידוע מראש:
- כשאפשר, לציין נתיב מוחלט ל-DLL
- להגדיר בשלב מוקדם את
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - להוסיף במפורש יעדי חיפוש עם
AddDllDirectory - להימנע מתכנון שמעביר את תוצאת
SearchPathישירות ל-LoadLibrary - לא להסתמך לחלוטין על safe DLL search mode
לדוגמה, ב-native code, תכנון שמכניס את הבא בשלב מוקדם של אתחול התהליך הוא אפשרות טובה:
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
ואז רושמים עם AddDllDirectory רק את תיקיות החיפוש הנוספות הנדרשות.
זה נוטה להישאר בלי טיפול כי “בדרך כלל זה עובד”, אבל אם ספריית העבודה משתנה ביעד ההפצה, או ש-DLL של מוצר אחר נכנס ל-PATH, זה נשבר בשקט. זה עוזר לא רק לאבטחה, אלא גם למניעת תקלות.
flowchart TB
accTitle: קיבוע מקור טעינת ה-DLL
accDescr: תרשים המראה שטעינה לפי שם בלבד עלולה לגרור DLL ממקום לא רצוי לפי סדר החיפוש, ושהגדרה מוקדמת של SetDefaultDllDirectories, הוספת יעד חיפוש עם AddDllDirectory וציון נתיב מוחלט כשאפשר, מונעים עמימות.
nm1["טעינה לפי שם בלבד"] --> pick2["גרירת DLL לא רצוי לפי סדר החיפוש"]
fix1["הגדרה מוקדמת של SetDefaultDllDirectories"] --> add2["הוספה מפורשת עם AddDllDirectory"]
add2 --> abs1["ציון נתיב מוחלט כשאפשר"]
abs1 --> safe1["מקור הטעינה לא נשאר עמום"]
איור 16: מפסיקים להסתמך על שם בלבד, מציינים במפורש יעדי חיפוש, וכך מקבעים את מקור הטעינה.
3.9. לא לחשוף מידע רגיש בלוגים ובחריגות
חשוב להגדיל את הלוגים לצורך חקירת תקלות. אבל הלוג עצמו נוטה גם להיות בית קברות למידע רגיש.
סביב הלוגים, כדאי לבחון מחדש לפחות את אלה:
- לא להוציא ללוג סיסמאות, Bearer token, מפתחות API
- לא להוציא מחרוזת חיבור מלאה
- למסך מידע אישי ותוכן נתונים עסקי
- להפריד בין פרטי חריגה למשתמש לבין לוג פנימי
- לא להפעיל בייצור לוג PII שמיועד ל-debug
- לבדוק מחדש הרשאות מיקום שמירה של dump או trace
ב-.NET העדכני, קל יותר גם לסדר את הנושא סביב redaction. לפחות כדאי לוותר על “מייצרים מחרוזת מכל דבר ורושמים כמו שהוא ללוג”.
כמה כשלים אופייניים:
- שמירת גוף בקשה/תגובה של HTTP במלואו
- הוצאת טוקן או כל הכותרות בעת כשלון אימות
- הצגת הודעת חריגה ישירות ב-MessageBox
- כריכת לוגים רגישים במלואם בקובץ ZIP לתחזוקה
תצוגת שגיאה מתחלקת, למשל, כך:
- למשתמש: “החיבור לשרת נכשל. בדקו את הגדרות הרשת ואת ה-URL.”
- ללוג פנימי: המארח שנכשל, סוג שגיאת TLS, מזהה קורלציה, stack trace, מספר ניסיונות חוזרים
רק ההפרדה הזו כבר משפרת משמעותית את האיזון בין דליפת מידע ליכולת חקירה.
flowchart TB
accTitle: הפרדת תצוגת השגיאה
accDescr: תרשים המראה שכשמתרחשת שגיאה, למשתמש מוצגת רק הנחיה תמציתית, ואילו ללוג הפנימי נשמר מידע חקירתי כמו המארח שנכשל, סוג השגיאה, מזהה קורלציה ו-stack trace.
er1["התרחשות שגיאה"] --> usr1["למשתמש: הנחיה תמציתית בלבד"]
er1 --> lg2["לוג פנימי: מארח, סוג, מזהה קורלציה וכדומה"]
lg2 -.-> bl1["שמירה על יכולת חקירה בלי לדלוף מידע"]
איור 17: רק הפרדה בין תצוגה למשתמש ללוג פנימי כבר משפרת את האיזון בין דליפה ליכולת חקירה.
3.10. לא להזניח ספריות תלויות וכלי פיתוח
הפריט האחרון צנוע במראה, אבל בעל השפעה גדולה. גם אם גוף היישום נבנה בקפידה, אם ממשיכים להחזיק זמן ריצה ישן או ספריית תלות עם פגיעות ידועה, יש חור מתחת לרגליים.
מספר הפריטים לבדיקה עצמו לא גדול.
- לשמור על SDK / זמן ריצה של .NET בגרסה נתמכת
- לבדוק תקופתית עדכוני תלויות NuGet / קוד פתוח
- ב-C++, לנהל גרסאות של רכיבי הפצה חוזרת של זמן ריצה ו-DLL חיצוניים
- לכלול בדיקת מידע פגיעויות בבדיקה שלפני שחרור
- להכין smoke test שלא ייהרס בעדכון תלויות
זו נקודה שבה “נעשה את זה בבת אחת אחר כך” מסוכנת ביותר. אם מזניחים חצי שנה או שנה, פער העדכון גדל מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.
flowchart TB
accTitle: מה קורה כשמזניחים עדכוני תלויות
accDescr: תרשים המראה שהזנחת עדכון תלויות למשך חצי שנה עד שנה גורמת לפער עדכון גדול מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.
pt1["#quot;נעשה את זה בבת אחת אחר כך#quot;"] --> ac2["הזנחה של חצי שנה עד שנה"]
ac2 --> df1["פער עדכון גדול מדי"]
df1 --> hw1["הטיפול עצמו הופך לעבודה כבדה"]
rg1["בדיקה תקופתית"] -.->|"מונע את זה"| hw1
איור 18: ככל שמזניחים את עדכון התלויות, הפער תופח, וגם הטיפול עצמו הופך לעבודה כבדה.
3.11. איך בודקים כל פריט
רשימת בדיקה מתפקדת רק כשהיא מלווה בשיטת אימות. עבור הפריטים בסעיפים 3.2 עד 3.10, הנה שיטות שאפשר להפעיל בפועל לפני שחרור.
| מה רוצים לבדוק | שיטת בדיקה |
|---|---|
| האם מבקשים העלאה שלא לצורך | לבדוק בקובץ המניפסט של היישום את הערך של requestedExecutionLevel. במקור — app.manifest, בקובץ ההפצה — לבדוק עם Sigcheck של Sysinternals או בעורך משאבים |
| חתימה וחותמת זמן | להריץ ב-PowerShell את Get-AuthenticodeSignature .\app.exe ולבדוק שה-Status הוא Valid ושיש TimeStamperCertificate. לבדוק אחד-אחד גם DLL נלווה וגם updater |
| האם ביטלנו אימות אישורים | לחפש בכל קוד המקור את ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback, CheckCertificateRevocationList |
| כתיבה ישירה של מידע רגיש | לחפש Password=, ApiKey, Secret, Token, ConnectionString. גם קוד נוכחי וגם היסטוריית מאגר הקוד |
| בניית SQL | לחפש שרשור מחרוזות שמכיל "SELECT, "INSERT, + , ולבדוק שהוא עובר דרך Parameters.Add |
| מקור טעינת ה-DLL | ב-Process Monitor, לצמצם לתהליך הרלוונטי, ולסנן Path ends with .dll ו-Result is NAME NOT FOUND. כך רואים לאן ובאיזה סדר חיפשו, ובודקים שלא נצפו תיקיות בלתי רצויות |
| פגיעויות ידועות בתלויות | להריץ dotnet list package --vulnerable --include-transitive |
| האם מידע רגיש מופיע בלוג | להריץ פעם אחת, ולחפש בלוג שיוצא Bearer , Password, Authorization |
חיפוש קוד יכול להתבצע עם rg (ripgrep) או עם חיפוש של Visual Studio. להרצה מרוכזת, זו הצורה:
Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
Select-Object Path, LineNumber, Line
חשוב לתעד את עצם הבדיקה. אם משאירים תיעוד גם של “בוצע חיפוש” וגם של “0 תוצאות”, בשחרור הבא צריך לבדוק רק את ההפרש.
flowchart TB
accTitle: התועלת בתיעוד הבדיקה
accDescr: תרשים המראה שכשמתעדים בדיקה שלפני שחרור גם כאשר התוצאה היא 0 תוצאות, בשחרור הבא צריך לבדוק רק את ההפרש.
chk2["ביצוע בדיקה לפני שחרור"] --> rec1["תיעוד: בוצע חיפוש · 0 תוצאות"]
rec1 --> nx1["בשחרור הבא מספיק לבדוק רק הפרש"]
איור 19: תיעוד עצם “הבדיקה שבוצעה” חוסך בשחרור הבא — מספיק לבדוק את ההפרש.
4. רשימת בדיקה שלפני שחרור
בצורה שאפשר להשתמש בה ישירות כתבנית לסקירה או להחלטת שחרור. כדי שיהיה נוח לבדוק על טבלה, מסדרים לפי קטגוריה את הפריטים שכדאי לבדוק כמינימום לפני שחרור.
4.1. הרשאות, אופן הרצה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
ההרצה הרגילה פועלת ב-asInvoker |
□ | |
| תהליכים שדורשים הרשאות מנהל מופרדים ל-EXE נפרד / שירות וכדומה | □ | |
| אם משתמשים בשירות, חשבון ההרצה אינו חזק מהנדרש | □ | |
האחריות מופרדת בין %ProgramFiles% לנתוני משתמש |
□ |
4.2. הפצה, חתימה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| EXE / DLL / MSI / MSIX / updater חתומים | □ | |
| החתימה כוללת חותמת זמן | □ | |
| פקיעת תוקף האישור והליך החידוש נכללים בזרימת השחרור | □ | |
| נקבעה שיטה לאימות hash ולגילוי שיבוש בקובצי ההפצה | □ |
4.3. עדכון
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| קבלת העדכון מתבצעת ב-HTTPS | □ | |
| אחרי הורדה מתבצע אימות חתימה או hash | □ | |
| התכנון מקשה על החלפה שרירותית של URL מקור העדכון | □ | |
| קיימת מדיניות rollback או ניסיון חוזר במקרה כשלון עדכון | □ |
4.4. מידע רגיש
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| סיסמה, מפתח API, מחרוזת חיבור אינם כתובים ישירות בקוד המקור | □ | |
| לא נשמר סוד בקובץ תצורה גלוי | □ | |
| סוד שנדרש לשמירה מקומית מוגן ב-DPAPI / Credential Locker וכדומה | □ | |
| היכן שאפשר, נעשה שימוש באימות Windows או באישורי המשתמש | □ |
4.5. תקשורת
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| תקשורת בייצור משתמשת ב-HTTPS | □ | |
DangerousAcceptAnyServerCertificateValidator או => true לא נשארו בקובץ ההפצה |
□ | |
| בדיקת ביטול ואימות שם מארח נלקחים בחשבון | □ | |
| קוד או תצורה שמניחים אישור פיתוח לא הוזנחו בייצור | □ |
4.6. קלט, גישה לנתונים
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| SQL מפורמט | □ | |
| לקלט משורת פקודה, קובץ, IPC, URI וכדומה יש מגבלה ובדיקת פורמט | □ | |
| טיפול בנתיבים מנורמל ומונע חריגה מהשורש | □ | |
| הודעת חריגה לא מוצגת ישירות למסך | □ |
4.7. DLL וסביבת הרצה
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| מקור טעינת ה-DLL מוגדר במפורש | □ | |
סדר החיפוש נשלט עם SetDefaultDllDirectories / AddDllDirectory וכדומה |
□ | |
| טעינת DLL לא מסתמכת על הספרייה הנוכחית או על PATH | □ | |
| ידוע אילו קבצים דרושים לטעינה דינמית ביעד ההפצה | □ |
4.8. לוגים, תפעול
| פריט לבדיקה | בדיקה | הערה |
|---|---|---|
| טוקנים, סיסמאות, מידע אישי אינם מופיעים בלוג | □ | |
| לוג פנימי מופרד מהודעה למשתמש | □ | |
| הרשאות מיקום שמירה של dump / trace / log נבדקו מחדש | □ | |
| מצב העדכון של SDK וספריות תלויות נבדק | □ |
5. הנחות שגויות נפוצות
בעבודה מעשית נתקלים בדרך כלל בהנחות שגויות מהסוג הזה.
5.1. “זה כלי פנים-ארגוני אז זה בסדר”
גם בכלי פנים-ארגוני, קבצים שבורים, תפעול שגוי, מכשירים חיצוניים, תיקיות משותפות, DLL ישן והגדרות הרשאה רשלניות — כל אלה מתקיימים כרגיל. גם ללא פרסום לאינטרנט, משטח התקיפה לא נעלם.
5.2. “זה HTTPS אז זה בטוח”
HTTPS חשוב, אבל ביטול אימות אישורים מרוקן את זה ממשמעות במידה רבה. בנוסף, בהפצת עדכונים לא מספיק HTTPS בלבד — נדרש גם אימות אמיתות קובץ ההפצה.
5.3. “הצפנו אז זה בטוח”
בלי סידור של מיקום מפתח הפענוח, הרשאת הפענוח, גבול המשתמש וגבול המכונה, ההצפנה לבדה לא מספיקה.
בפרט, אם משתמשים בערך שמוגן ב-LocalMachine בהנחה שהוא “סוד פר-משתמש”, זה יגרום בהמשך לבלבול.
5.4. “אם נגדיל את הלוגים נוכל לחקור”
אם הלוגים רבים אבל טוקנים ומידע אישי זולגים בהם, זו כשלעצמה תקרית. אם רוצים יכולת חקירה, קודם כדאי לקבוע מה נשמר ומה מוסתר.
5.5. “אם נריץ כמנהל זה ייפתר”
זה נוח בהתחלה, אבל בהמשך זה בדרך כלל מקשה סביב UAC, הפצה, תמיכה, גבולות הרשאה, טעינת DLL ומיקום שמירת קבצים. הרשאה מינימלית יציבה יותר לטווח הארוך.
flowchart TB
accTitle: הסוף של "אם נריץ כמנהל זה ייפתר"
accDescr: תרשים המראה שהרצה קבועה בהרשאות מנהל נוחה בהתחלה אך מקשה בהמשך סביב UAC, הפצה, תמיכה, גבולות הרשאה, טעינת DLL ומיקום שמירת קבצים, ושהרשאה מינימלית יציבה יותר לטווח הארוך.
ez1["#quot;אם נריץ כמנהל זה ייפתר#quot;"] --> ez2["נוח בהתחלה"]
ez2 --> pain1["מקשה סביב UAC, הפצה ותמיכה"]
ez2 --> pain2["מקשה סביב גבולות הרשאה ומיקום שמירה"]
lp1["הרצה בהרשאה מינימלית"] -->|"יציבה יותר לטווח הארוך"| ok2["נמנעים מהקושי"]
איור 20: הנוחות בהרצה קבועה כמנהל היא רק בהתחלה, ולטווח הארוך הרשאה מינימלית יציבה יותר.
6. סדר עדיפויות כללי
אם קשה לעשות הכול בבת אחת, סדר העדיפויות נראה בערך כך.
הבסיס לסידור הוא מכפלה בין גודל הנזק במקרה תקרית לבין נמוכות עלות התיקון. פרק 3 מסודר לפי זרימת התכנון “הרשאות ← הפצה ← מימוש ← תפעול”, אבל כאן הסדר הוא “מהמסוכן ביותר”, ולכן הוא לא זהה לסדר הפרקים. מצורף הסעיף המתאים לכל פריט.
- בחינה מחדש של הרשאות מנהל (3.2)
קודם כל, להפסיק את השימוש הקבוע ב-
requireAdministrator. היקף הנזק משתנה בצעד אחד, בעוד שינוי התכנון עצמו לרוב קטן. - חתימה וחותמת זמן (3.3) לסדר את אמינות קובצי ההפצה. מספיק לשלב בהליך, ואם נדחה לאחר מכן נדרשת הפצה מחדש.
- הוצאת מידע רגיש (3.5) להוציא סוד מקוד המקור ומתצורה גלויה. הנזק בעת דליפה גדול, ואחרי הדליפה אין דרך חזרה.
- תיקון HTTPS + אימות אישורים (3.6)
להסיר מקובצי ההפצה סוגים של
=> true. לרוב מספיקה הסרה בלבד, ובהזנחה כל התקשורת בלתי מהימנה. - בחינה מחדש של קלט SQL / קובץ / IPC (3.7) לצמצם שרשור מחרוזות וקלט לא מאומת. הכמות רבה, אך אפשר לתקן מקום אחד בכל פעם.
- קיבוע טעינת DLL (3.8) להפסיק טעינה לפי שם בלבד והסתמכות על PATH. זה תיקון של חלק מתהליך ההפעלה, ומועיל גם למניעת תקלות.
- מיסוך לוגים (3.9) לוודא שהלוג לא נהיה אסון משני בעת תקרית. יש הרבה מקומות פלט, ולכן זה לוקח זמן.
- קביעות של עדכון תלויות (3.10) להפוך לתהליך קבוע שנבדק בכל שחרור. לא נגמר בפעם אחת — העבודה היא להפוך את זה למנגנון.
מסלול העדכון (3.4) לא נכלל בסדר הזה כי לא לכל יישום יש עדכון אוטומטי. אם יש, כדאי לראות אותו באותה עדיפות כמו פריט 2, החתימה. מודול העדכון נמצא במעמד חלש יותר מגוף היישום, אך יש בכוחו לשכתב אותו.
flowchart TB
accTitle: איך קובעים סדר עדיפויות
accDescr: תרשים המראה שהסדר נקבע לפי מכפלה של גודל הנזק ונמוכות עלות התיקון, מהמסוכן ביותר, ושביישום עם עדכון אוטומטי מסלול העדכון נבחן באותה עדיפות כמו החתימה.
dmg1["גודל הנזק"] --> mul1["מכפלה שקובעת את הסדר"]
cst1["נמוכות עלות התיקון"] --> mul1
mul1 --> ord1["איטום לפי סדר מהמסוכן"]
ord1 -.-> upn1["ביישום עם עדכון אוטומטי — מסלול העדכון באותה עדיפות כמו החתימה"]
איור 21: סדר העדיפויות נקבע ממכפלת גודל הנזק ועלות התיקון, ומאטמים מהחור המסוכן ביותר.
בסדר הזה, נוח להתקדם במובן של “קודם לאטום את החורים שברור שהם מסוכנים”.
7. סיכום
אבטחת פיתוח יישומי Windows משתנה משמעותית כבר רק מסידור שבע הנקודות הרשאות, חתימה, מידע רגיש, תקשורת, קלט, DLL ולוגים — עוד לפני הכנסת מוצר מיוחד או מנגנון ענק.
הרף המינימלי, במשפט אחד לכל נקודה:
- לא להריץ את כל היישום בהרשאות מנהל
- לחתום על קובצי ההפצה והעדכון, כולל חותמת זמן
- לא להחזיק מידע רגיש בקוד המקור או בתצורה גלויה
- גם עם HTTPS, לא לבטל אימות אישורים
- לא לתת אמון בקלט חיצוני כמו SQL, קובץ, IPC וכדומה
- לא להשאיר את מקור טעינת ה-DLL עמום
- לא לחשוף מידע רגיש בלוגים
- לא להזניח ספריות תלויות
נושא האבטחה רחב, אבל לא חייבים לעשות הכול מההתחלה. עם זאת, שווה להשלים כבר בשלב מוקדם למדי את המינימום של לא להפיץ התנהגות ברירת מחדל מסוכנת כמו שהיא.
8. מקורות
- Administrator Broker Model - Win32 apps
- How User Account Control works
- Authenticode Digital Signatures
- Time Stamping Authenticode Signatures
- Sign a Windows app package
- Credential Locker for Windows apps
- CryptProtectData function (dpapi.h)
- CA5359: Do not disable certificate validation
- CA5399: Enable HttpClient certificate revocation list check
- Configuring parameters - ADO.NET Provider for SQL Server
- Connection String Syntax - ADO.NET
- Dynamic-Link Library Security - Win32 apps
- SetDefaultDllDirectories function (libloaderapi.h)
- Data redaction in .NET
- ProtectedData Class - הבהרה שמדובר במיועד ל-Windows בלבד, וכן אזהרה לגבי מצב שבו הפרופיל לא טעון.
- ServicePointManager.ServerCertificateValidationCallback - מ-.NET 9 ואילך ממופה להגדרות של SocketsHttpHandler.
- dotnet list package command - עם
--vulnerableאפשר לבדוק פגיעויות ידועות. - Get-AuthenticodeSignature
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
איך כותבים בפועל הפרדה של "רק תהליכים שדורשים הרשאות מנהל" ביישום Windows
מסדרים בפירוט, כולל UAC, runas, pipe בעל שם ואימות קלט, תכנון שבו ה-UI נשאר asInvoker ורק התהליכים שדורשים הרשאות מנהל מופרדים ל-helper ...
שמירת מידע רגיש ביישומי Windows - נמנעים מתצורה גלויה עם DPAPI
מסדרים כדי לא לשמור מחרוזות חיבור וטוקני API בטקסט גלוי בקובצי תצורה של יישום Windows, את התפיסה של DPAPI / ProtectedData, ההבדל בין Cur...
למה עדיף המתנה מונחית אירועים על פני Sleep(1) ב-Windows
ב-Windows הדיוק של timed wait קצר מושפע מרזולוציית שעון המערכת ומהתזמון. המאמר מסביר מדוע כשמחכים להגעת עבודה, השלמת קלט/פלט או בקשת עציר...
טבלת החלטה: לסיים או להמשיך אחרי חריגה בלתי צפויה
המאמר מסדר מתי כדאי לסיים אפליקציה ומתי אפשר להמשיך לפעול אחרי חריגה בלתי צפויה, מנקודת המבט של השחתת מצב, תופעות לוואי חיצוניות, threads...
למה להשתמש ב-Generic Host וב-BackgroundService של .NET באפליקציית desktop
בכלי Windows ובאפליקציות שרצות ברקע, מסודר כאן איך להשתמש ב-Generic Host וב-BackgroundService כדי לסדר הפעלה, עיבוד תקופתי, סיום, לוג, הג...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
מדובר בבחינה מחדש של כל יישום ה-Windows, כולל תכנון הרשאות, שיטת הפצה, שיטת עדכון ותכנון לוגים, ולכן זהו נושא שמתאים היטב לפיתוח יישומי Windows.
ייעוץ טכני וסקירת תכנון
אם רוצים להתחיל מבחינה מחדש של האבטחה ביישום קיים, מסידור גבולות ההרשאות או מתכנון מחדש של מדיניות המעדכן, אפשר לסדר זאת כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה כדאי לבדוק ראשון באבטחת יישומי Windows?
- ארבעה דברים: לא לדרוש הרשאות מנהל שלא לצורך, לחתום על הקוד, לא להחזיק סודות בטקסט גלוי, ולא לבטל אימות אישורים. אבטחה מינימלית היא לא הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני. דילוג קבוע על אימות אישורים, מחרוזות חיבור בטקסט גלוי, טעינת DLL שמסתמכת על הספרייה הנוכחית, והרצת SQL בשרשור מחרוזות — אלה דברים שכדאי להימנע מהם גם ברף המינימלי ביותר.
- האם אסור להריץ את כל היישום בהרשאות מנהל?
- כן, כדאי להימנע מכך. הרצת כל היישום בהרשאות מנהל גורמת לכך שבאגים, החלפת DLL, קריאה שגויה של קובצי תצורה ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה. המדיניות הבסיסית היא שיישום UI רגיל יפעל ב-asInvoker, שתהליכים שדורשים הרשאות מנהל יופרדו לתהליך נפרד או לשירות, ושההעלאה בהרשאה תתבצע רק ברגע הנדרש.
- איפה כדאי לשמור מפתחות API ומחרוזות חיבור?
- כדאי בהחלט לצאת ממצב שבו הם נמצאים בטקסט גלוי בקוד המקור או בקובצי תצורה כמו appsettings.json. מידע רגיש שנשמר משתמש לפי הצורך ב-DPAPI / ProtectedData או ב-Credential Locker. חשוב גם למסך ביומנים דברים כמו טוקנים, סיסמאות, מחרוזות חיבור ומידע אישי, כי אחרת היומן עצמו נהיה גורם התקרית.
- האם יישום להפצה פנים-ארגונית זקוק גם הוא לחתימת קוד?
- כדאי להתייחס לזה כאל הנחת יסוד. ביישומי Windows, קובצי ההפצה עצמם (EXE / DLL / MSI / MSIX / מודול עדכון אוטומטי) מהווים משטח תקיפה. חתימת קוד עם חותמת זמן מספקת גילוי שיבוש, אמינות בעיני המשתמשים, ונוחות בהסבר ניהולי. גם מנגנון העדכון צריך לקבע את מקור העדכון, ולזהות שיבוש דרך HTTPS ואימות חתימה.