רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows

· עודכן בתאריך: · · פיתוח Windows, אבטחה, תכנון, C# / .NET, Win32

הורידו את רשימת הבדיקה בגרסת 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, מעשי יותר להתחיל מאיטום פרצות שברור שהן מסוכנות, ולא לנסות להשלים הכול בבת אחת.

כאן נסדר לפי סדר תכנון, מימוש, הפצה ותפעול, את הנקודות שברור שלא כדאי לפספס.

אופן ההתקדמות של המאמרתרשים המראה שקודם אוטמים פרצות בסיסיות ורק אז עוברים להגנה מתקדמת, ושהמאמר מסדר את הנקודות המינימליות לפי תכנון, מימוש, הפצה ותפעול.קודם לכןהגנה מתקדמת (Zero Trust וכדומה)איטום הפרצות הבסיסיותתכנוןמימושהפצהתפעול

איור 1: לפני הגנה מתקדמת אוטמים את הפרצות הבסיסיות, ולאחר מכן עוברים לפי הסדר על תכנון, מימוש, הפצה ותפעול.

1. קודם המסקנה

  • הדבר הראשון שלא כדאי לפספס: לא לדרוש הרשאות מנהל שלא לצורך, לחתום, לא להחזיק סודות בטקסט גלוי, ולא לבטל אימות אישורים.
  • ביישומי Windows, קובצי ההפצה עצמם מהווים משטח תקיפה. בטוח יותר להתייחס גם ל-EXE /‏ DLL /‏ MSI /‏ MSIX / מודול עדכון אוטומטי.
  • ServerCertificateValidationCallback => true, מחרוזת חיבור בטקסט גלוי, טעינה רשלנית עם LoadLibrary("foo.dll"), והרצת SQL בשרשור מחרוזות — כל אלה מוטב להימנע מהם גם ברף המינימלי.
  • אם רק חלק מהתהליכים דורשים הרשאות מנהל, בטוח יותר להפריד רק את אותו חלק לתהליך נפרד או לשירות, ולא להעלות את הרשאת כל היישום.
  • כדאי להניח מראש שיישום שמופץ ב-Windows יזדקק לחתימה + חותמת זמן. זה נותן לא רק אמינות בעיני המשתמשים, אלא גם גילוי שיבוש והסבר ניהולי נוח יותר.
  • מידע רגיש שנשמר משתמש לפי הצורך ב-DPAPI /‏ ProtectedData או ב-Credential Locker. לפחות כדאי לצאת ממצב שבו הוא ב-appsettings.json בטקסט גלוי.
  • לא כל כמות לוגים טובה. השארת טוקנים, סיסמאות, מחרוזות חיבור, מידע אישי וגוף בקשה מלא ביומן ישירות הופכת את היומן עצמו לגורם התקרית.

אבטחה מינימלית היא לא הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני.

התפיסה של אבטחה מינימליתתרשים המראה שאבטחה מינימלית אינה הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני.זה לא המוקד המינימליזה המוקדהוספת תכונה מיוחדתאבטחה מינימליתלא להשאיר ברירת מחדל מסוכנת או מימוש רשלני

איור 2: הרף המינימלי אינו הוספת תכונות, אלא הימנעות מהשארת ברירת מחדל מסוכנת או מימוש רשלני.

מפת הידע של המאמר

המאמר הזה מרכז רשימת תיוג של אמצעי אבטחה מינימליים שכדאי לא לוותר עליהם לפני שחרור, עבור יישומי Windows עסקיים ב-WPF,‏ WinForms,‏ WinUI, C++ ו-C#. במרכז הרשימה: לא להריץ את כל היישום כ-requireAdministrator אלא להפריד רק את הפעולות הדורשות הרשאות מנהל לתהליך או שירות נפרד; לחתום על קבצי ההפצה עם חותמת זמן ולהימנע מהפצה לא חתומה או מדילוג קבוע על אימות תעודות; ולהימנע משמירת סודות בתצורה בטקסט גלוי ולהגן עליהם למשל באמצעות DPAPI. בנוסף נדון האופן שבו שרשור מחרוזות ב-SQL מוביל להזרקת SQL וכיצד placeholders מונעים זאת, האופן שבו טעינת DLL לפי שם בלבד עלולה לגרום לחטיפת סדר חיפוש, והעובדה שהדפסת מידע רגיש ללוג הופכת את הלוג עצמו לערוץ דליפה.

מפת הידע של רשימת התיוג לאבטחה מינימלית ביישומי Windowsתרשים המראה את היחסים בין הטיפול בהרשאות מנהל, חתימת קוד ואימות עדכונים, הגנה על סודות, סיכוני קלט וזמן ריצה כמו הזרקת SQL וטעינת DLL, לבין דליפת מידע רגיש דרך לוגים — כרכיבי מינימום של רשימת התיוג לאבטחה.מוגדר באמצעותמוגדר באמצעותשימוש לא מומלץ למענה מומלץ לעלול לגרום לעלול לגרום למונעמצמצםמונעמענה מומלץ למענה מומלץ למחייבשימוש לא מומלץ לעלול לגרום למונעעלול לגרום למענה מומלץ למחייבמשתמש במחייבמענה מומלץ להרשאות מנהלאישור לחתימת קודסודות היישוםרמת הרצה asInvokerרמת הרצה requireAdministratorיישום שולחני עם משתמש אינטראקטיביהפרדת הפעולות הדורשות הרשאות מנהלחטיפת סדר החיפוש של DLLטעינת DLL לפי שם בלבדבינארי לא חתוםחותמת זמן בחתימת קודפקיעת תוקף של אישוראימות חתימה או hash של העדכוןהתקנת עדכון ללא אימותהצמדת אישור (certificate pinning)דילוג קבוע על אימות האישורבדיקת ביטול של אישורשרשור מחרוזות SQLהזרקת SQLפרמטר מוחלף (prepared statement)כתיבת מידע רגיש ליומןחשיפת מידע רגיש דרך היומןDPAPIשמירת סודות בטקסט גלוישירות Windows

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 21, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

2. היקף המאמר ומשמעות “מינימום”

2.1. ההיקף שהמאמר מטפל בו

המאמר מתייחס ליישומי Windows מהסוג הבא:

  • יישומי שולחן עבודה ב-WPF /‏ WinForms /‏ WinUI
  • יישומי Win32 ב-C++‏ / C#
  • כלי הפצה פנים-ארגוניים, כלי שילוב מכשירים, כלי ניטור
  • הרכב שכולל EXE נלווה, שירות Windows ומעדכן
  • תוכנה עסקית שמופצת ב-EXE /‏ MSI /‏ MSIX

“המינימום” כאן אינו הצורה הסופית שעוברת ביקורת, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.

משמעות "המינימום" במאמר הזהתרשים המראה שהמינימום במאמר הזה אינו הצורה הסופית שעוברת ביקורת, אלא הסעיפים שהיעדרם גורם לתקלה בשגרה.לא זו הכוונה כאןזו הכוונההצורה הסופית שעוברת ביקורת"המינימום" של המאמר הזהסעיפים שהיעדרם גורם לתקלה בשגרה

איור 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 להשמיט בכוחות עצמם לפני שחרור.

קו הגבול בין הנכלל למה שלא נכללתרשים המראה שמדיניות אבטחה עצומה כלל-ארגונית אינה נכללת, ושהמאמר מתמקד בקו יסוד שקשה למפתחים להשמיט בכוחות עצמם לפני שחרור.לא נכלל במאמר הזהזה נכללמדיניות עצומה כלל-ארגוניתההיקף של המאמר הזהקו יסוד שקשה למפתח להשמיט בכוחות עצמו

איור 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, קריאה שגויה של קובצי תצורה ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה.

הסכנה בהרצת כל היישום בהרשאות מנהלתרשים המראה שהרצת כל היישום בהרשאות מנהל גורמת לכך שבאגים, החלפת DLL ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה.הרצת כל היישום בהרשאות מנהלבאגיםהחלפת DLLקריאה שגויה של תצורה / פגם בקלטמתבצע ישירות בהרשאה חזקה

איור 5: כשמעלים הרשאה של כל היישום, כל פגם שהוא רץ בהרשאה חזקה.

המדיניות הבסיסית היא כזו:

  • יישום UI רגיל יפעל ב-asInvoker
  • רק תהליכים שדורשים הרשאות מנהל יופרדו לתהליך נפרד או לשירות
  • ההעלאה בהרשאה תתבצע רק ברגע הנדרש
  • גם הקלט שמועבר ל-EXE הנלווה או לשירות מאומת

אם מדובר ביישום שולחן עבודה שרגיל רק לצפייה ולעריכה, ורק ההתקנה או שינוי הגדרות חומת אש דורשים הרשאות מנהל, בטוח יותר לרכז את מה שדורש העלאה ב-broker, ולא להפוך את כל היישום ל-requireAdministrator.

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    </requestedPrivileges>
  </security>
</trustInfo>

“נוח כשהכול רץ כמנהל” בדרך כלל חוזר לפגוע בהמשך. הרצה בהרשאה מינימלית, עם הוצאה החוצה רק של הפעולות שבאמת דורשות זאת, מצמצמת משמעותית את רדיוס הפגיעה.

מבנה של הפרדת תהליכים שדורשים העלאהתרשים המראה שיישום UI רגיל רץ ב-asInvoker, שרק תהליכים שדורשים הרשאות מנהל מרוכזים ב-broker בתהליך נפרד או בשירות, ושההעלאה מתבצעת רק ברגע הנדרש.פנייה רק ברגע הנדרשיישום UI (asInvoker)broker (EXE נפרד / שירות)ביצוע רק של פעולות שדורשות העלאהגם הקלט ל-broker מאומת

איור 6: בדרך כלל מריצים ב-asInvoker, ורק פעולות שדורשות העלאה מרוכזות ב-broker, כך שרדיוס הפגיעה מצטמצם.

3.3. חתימה על הבינארי ועל ההתקנה

ב-Windows, אמינות קובצי ההפצה קובעת הכול. המשתמש נוגע לא בקוד המקור, אלא ב-EXE,‏ DLL,‏ MSI,‏ MSIX ובמעדכן. אם אלה לא חתומים, גם גילוי השיבוש, גם האמינות בעיני המשתמש וגם ההסבר הניהולי נחלשים.

מה שהמשתמש נוגע בו הם קובצי ההפצהתרשים המראה שהמשתמש נוגע לא בקוד המקור אלא ב-EXE, DLL, מתקין ומעדכן, ושאם אלה לא חתומים גילוי השיבוש וההסבר הניהולי נחלשים.מה שהמשתמש נוגע בוEXE / DLLMSI / MSIXמעדכןבלי חתימה — גילוי שיבוש והסבר חלשים

איור 7: משטח התקיפה הוא קובצי ההפצה עצמם, ובלי חתימה מאבדים את הבסיס לאמינות.

לכל הפחות, כדאי לבדוק את אלה:

  • לחתום על EXE /‏ DLL /‏ MSI /‏ MSIX
  • לחתום לא רק על ההתקנה אלא גם על בינארים נלווים שמשמשים לעדכון
  • להוסיף חותמת זמן
  • לכלול בתהליך השחרור את פקיעת תוקף האישור ואת הליך החידוש

חתימה שאין לה חותמת זמן נוטה לגרום לקושי באימות אחרי פקיעת תוקף האישור. במקום “חתמנו וזהו”, יציב יותר לכלול בהליך השחרור חתימה + חותמת זמן יחד.

חתימה וחותמת זמןתרשים המראה שחתימה בלי חותמת זמן גורמת לקושי באימות אחרי פקיעת תוקף האישור, ושחתימה יחד עם חותמת זמן שנכללת בהליך השחרור יציבה יותר.חתימה בלבדקושי באימות אחרי פקיעת תוקף האישורחתימה + חותמת זמןפחות קושי באימות אחרי הפקיעהנכלל בהליך השחרור

איור 8: לא מסתפקים בחתימה בלבד — כוללים גם חותמת זמן בהליך השחרור.

אם משתמשים ב-MSIX, חתימת החבילה היא הנחת יסוד. גם בהפצת MSI /‏ EXE, כדאי לפחות לחתום על גוף המתקין ועל הבינארים המרכזיים המורצים.

3.4. קיבוע מסלול העדכון והוספת גילוי שיבוש

ביישום Windows של היום, מסלול העדכון נמצא לרוב בשימוש ממושך יותר מההתקנה הראשונית. אם הוא נעשה ברשלנות, גם אם גוף היישום נבנה בקפידה, המעדכן נהיה החוליה החלשה ביותר.

מסלול העדכון נמצא בשימוש הארוך ביותרתרשים המראה שמסלול העדכון נמצא בשימוש ממושך יותר מההתקנה הראשונית, ושאם הוא נעשה ברשלנות המעדכן נהיה החוליה החלשה ביותר.בשימוש רק פעם אחתבשימוש ממושך לאורך זמןההתקנה הראשוניתאורך חיי היישוםמסלול העדכוןברשלנות הוא נהיה החוליה החלשה

איור 9: מסלול העדכון נמצא בשימוש ארוך יותר מההתקנה הראשונית, וברשלנות הוא נהיה החוליה החלשה.

סביב העדכון, כדאי לחשוב מראש לפחות על חמישה דברים:

  • קבלת קובץ העדכון תמיד דרך HTTPS
  • אימות חתימה או hash של קובץ העדכון שהתקבל
  • מניעת החלפה חופשית של כתובת ה-URL של מקור העדכון בקוד או בתצורה
  • חתימה גם על מודול העדכון עצמו
  • קביעת נוהל rollback או שחזור במקרה כשלון

אם אפשר לבחור ב-MSIX +‏ App Installer, קל יותר לרכז את מנגנון העדכון קרוב יותר למערכת ההפעלה. מצד שני, אם יש מעדכן עצמאי, יש לוודא גם את בטיחות התקשורת וגם את אמיתות קובץ ההפצה. HTTPS לבדו שומר על “ערוץ התקשורת”, אבל לא מבטיח “האם הקובץ הזה באמת פרסום שלנו”.

שני הדברים שמאמתים בעדכוןתרשים המראה שבמעדכן עצמאי צריך לוודא גם את בטיחות התקשורת דרך HTTPS וגם את אמיתות קובץ העדכון דרך חתימה או hash.קבלת קובץ העדכון ב-HTTPSאימות חתימה או hashהחלה רק אחרי מעבר האימותHTTPS שומר רק על ערוץ התקשורתבדיקה האם זה פרסום שלנו

איור 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 מאפשר לכל מי שנמצא באותה מכונה. אם השירות רץ תחת חשבון נפרד, או שיש תפעול שמחליף בין משתמשים, חשוב לקבוע את זה מראש — אחרת ייתקעו אחר כך במצב של “לא ניתן לקרוא” או “ניתן לקרוא יותר מדי”.

מי יכול לפענח ב-DPAPIתרשים המראה שהגדרת ה-scope של DPAPI ל-CurrentUser מאפשרת פענוח רק למשתמש השומר, ואילו LocalMachine מאפשר פענוח לכל מי שבמכונה, ולכן צריך לקבוע זאת מראש בתכנון.CurrentUserLocalMachineקביעה בתכנון: מי יכול לפענחרק המשתמש השומר יכול לפענחכל מי שבמכונה יכול לפענחבלי קביעה מראש — תקיעה מאוחר יותר

איור 11: ב-DPAPI, בחירת ה-scope קובעת מי יכול לפענח, ולכן קובעים זאת מראש.

חשוב גם לדעת שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן כשהפרופיל לא טעון (כגון בזמן impersonation), הפענוח עלול להיכשל — כך כתוב במפורש בתיעוד. אם מתכננים שימוש משירות, כדאי לבדוק גם את הנקודה הזו.

המפתח של DPAPI ופרופיל המשתמשתרשים המראה שהמפתח של DPAPI נמצא בפרופיל המשתמש, ולכן במצבים כמו impersonation שבהם הפרופיל לא טעון, הפענוח נכשל.פרופיל לא טעון (כגון impersonation)המפתח של DPAPIנמצא בפרופיל המשתמשהפענוח נכשלצריך לבדוק זאת בשימוש משירות

איור 12: מפתח ה-DPAPI נמצא בפרופיל המשתמש, וכשהפרופיל לא טעון לא ניתן לפענח.

בחיבור ל-SQL Server, בסביבה מקומית לפעמים אפשר להעדיף אימות Windows כברירה ראשונה. אם בכל זאת חייבים לכלול אישורים במחרוזת החיבור, לפחות כדאי לשמור על Persist Security Info=False ולא להשאיר אותם בקובץ תצורה גלוי.

3.6. תקשורת מבוססת HTTPS, ולא לבטל אימות אישורים

פרצה שהוכנסה “רק לזמן הפיתוח” נשארת כמות שהיא בייצור. תקלות תקשורת הן בדרך כלל בדיוק הדפוס הזה.

במיוחד קוד ותצורה מהסוג הבא נוטים להישאר במוצר הסופי:

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.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.

המסלול שבו callback ישן משפיע על תקשורת של היוםתרשים המראה שה-ServerCertificateValidationCallback של ServicePointManager ממופה מ-.NET 9 ואילך לאימות של SocketsHttpHandler, ולכן שורה בודדת שמחזירה true עלולה להשפיע גם על תקשורת HttpClient.ממופה מ-.NET 9 ואילךcallback האימות של ServicePointManagerהאימות בצד SocketsHttpHandlerמשפיע גם על תקשורת HttpClientשורה בודדת שמחזירה 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. זו התנהגות נכונה. אם אין אמצעי לבדוק ביטול, יש למלא את הפער באחת משתי דרכים: לקצר את תוקף האישור, או להכין מראש מסלול שמאפשר להפיץ מחדש את הערך המוצמד. המצב המסוכן ביותר הוא “הצמדת אישור לטווח ארוך בלי אפשרות לבטל אותו”.

זרימת ההחלטה בעת הצמדת אישורתרשים המראה שכשאין שגיאה מקבלים, שגיאה שאינה שגיאת שרשרת נדחית, ומתוך שגיאת שרשרת בלבד מאמתים שם מארח וטביעת אצבע, ואף מצב שרשרת של פקיעת תוקף או ביטול נדחה.Noneשגיאה שאינה שגיאת שרשרתרק שגיאת שרשרתחוסר התאמההתאמהפג תוקף / בוטל וכדומהרק CA פנים-ארגוני לא מותקןבדיקת errorsמקבליםדוחיםהתאמת שם מארח וטביעת אצבעבדיקת מצב השרשרת

איור 14: גם בהצמדת אישור לא מתעלמים — זרימת ההחלטה דוחה ישירות פקיעת תוקף וביטול.

3.7. להתייחס לכל קלט חיצוני כ”קלט לא מהימן”

יישום Windows אינו יישום Web, ולכן אימות הקלט (validation) נוטה להיות רופף. אבל בפועל, נקודות הכניסה לקלט חיצוני רבות יותר ממה שחושבים.

  • נתיבי קבצים
  • CSV /‏ Excel /‏ JSON /‏ XML
  • ארגומנטים משורת הפקודה
  • named pipe / socket / COM / RPC / gRPC
  • מחרוזת שמועברת ל-DB
  • ערכי רישום
  • לוח גזירים
  • URL / deep link
  • נתונים שחוזרים ממכשיר חיצוני או מ-SDK

בפרט, שלושה דברים שלא כדאי לפספס:

  1. תמיד לפרמט SQL לא לבנות SQL בשרשור מחרוזות.
  2. לנרמל נתיב קובץ לפני שימוש לא להשתמש בנתיב שהוזן על ידי המשתמש ישירות למחיקה, דריסה או חילוץ.
  3. להוסיף מגבלת גודל ובדיקת פורמט בקריאת קובץ חיצוני “נפתח בהצלחה” לא אומר “בטוח”.

בדוגמת 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 חצי-מוגמר שנכתב על ידי כלי אחר — כל אלה נכנסים כרגיל.

גם לכלי פנים-ארגוני מגיע קלט שבורתרשים המראה שגם בכלי פנים-ארגוני נכנס באופן שגרתי CSV שבור, שם קובץ בלתי צפוי, נתוני DB ישנים, טעויות הקלדה ו-JSON חצי-מוגמר, ולכן מתייחסים לכל קלט חיצוני כלא מהימן.CSV שבורקלט אל היישוםשם קובץ בלתי צפויטעות הקלדה / נתונים ישניםאימות כקלט לא מהימן, בכל נקודת כניסה

איור 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, זה נשבר בשקט. זה עוזר לא רק לאבטחה, אלא גם למניעת תקלות.

קיבוע מקור טעינת ה-DLLתרשים המראה שטעינה לפי שם בלבד עלולה לגרור DLL ממקום לא רצוי לפי סדר החיפוש, ושהגדרה מוקדמת של SetDefaultDllDirectories, הוספת יעד חיפוש עם AddDllDirectory וציון נתיב מוחלט כשאפשר, מונעים עמימות.טעינה לפי שם בלבדגרירת DLL לא רצוי לפי סדר החיפושהגדרה מוקדמת של SetDefaultDllDirectoriesהוספה מפורשת עם AddDllDirectoryציון נתיב מוחלט כשאפשרמקור הטעינה לא נשאר עמום

איור 16: מפסיקים להסתמך על שם בלבד, מציינים במפורש יעדי חיפוש, וכך מקבעים את מקור הטעינה.

3.9. לא לחשוף מידע רגיש בלוגים ובחריגות

חשוב להגדיל את הלוגים לצורך חקירת תקלות. אבל הלוג עצמו נוטה גם להיות בית קברות למידע רגיש.

סביב הלוגים, כדאי לבחון מחדש לפחות את אלה:

  • לא להוציא ללוג סיסמאות, Bearer token, מפתחות API
  • לא להוציא מחרוזת חיבור מלאה
  • למסך מידע אישי ותוכן נתונים עסקי
  • להפריד בין פרטי חריגה למשתמש לבין לוג פנימי
  • לא להפעיל בייצור לוג PII שמיועד ל-debug
  • לבדוק מחדש הרשאות מיקום שמירה של dump או trace

ב-‎.NET העדכני, קל יותר גם לסדר את הנושא סביב redaction. לפחות כדאי לוותר על “מייצרים מחרוזת מכל דבר ורושמים כמו שהוא ללוג”.

כמה כשלים אופייניים:

  • שמירת גוף בקשה/תגובה של HTTP במלואו
  • הוצאת טוקן או כל הכותרות בעת כשלון אימות
  • הצגת הודעת חריגה ישירות ב-MessageBox
  • כריכת לוגים רגישים במלואם בקובץ ZIP לתחזוקה

תצוגת שגיאה מתחלקת, למשל, כך:

  • למשתמש: “החיבור לשרת נכשל. בדקו את הגדרות הרשת ואת ה-URL.”
  • ללוג פנימי: המארח שנכשל, סוג שגיאת TLS, מזהה קורלציה, stack trace, מספר ניסיונות חוזרים

רק ההפרדה הזו כבר משפרת משמעותית את האיזון בין דליפת מידע ליכולת חקירה.

הפרדת תצוגת השגיאהתרשים המראה שכשמתרחשת שגיאה, למשתמש מוצגת רק הנחיה תמציתית, ואילו ללוג הפנימי נשמר מידע חקירתי כמו המארח שנכשל, סוג השגיאה, מזהה קורלציה ו-stack trace.התרחשות שגיאהלמשתמש: הנחיה תמציתית בלבדלוג פנימי: מארח, סוג, מזהה קורלציה וכדומהשמירה על יכולת חקירה בלי לדלוף מידע

איור 17: רק הפרדה בין תצוגה למשתמש ללוג פנימי כבר משפרת את האיזון בין דליפה ליכולת חקירה.

3.10. לא להזניח ספריות תלויות וכלי פיתוח

הפריט האחרון צנוע במראה, אבל בעל השפעה גדולה. גם אם גוף היישום נבנה בקפידה, אם ממשיכים להחזיק זמן ריצה ישן או ספריית תלות עם פגיעות ידועה, יש חור מתחת לרגליים.

מספר הפריטים לבדיקה עצמו לא גדול.

  • לשמור על SDK / זמן ריצה של ‎.NET בגרסה נתמכת
  • לבדוק תקופתית עדכוני תלויות NuGet / קוד פתוח
  • ב-C++, לנהל גרסאות של רכיבי הפצה חוזרת של זמן ריצה ו-DLL חיצוניים
  • לכלול בדיקת מידע פגיעויות בבדיקה שלפני שחרור
  • להכין smoke test שלא ייהרס בעדכון תלויות

זו נקודה שבה “נעשה את זה בבת אחת אחר כך” מסוכנת ביותר. אם מזניחים חצי שנה או שנה, פער העדכון גדל מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.

מה קורה כשמזניחים עדכוני תלויותתרשים המראה שהזנחת עדכון תלויות למשך חצי שנה עד שנה גורמת לפער עדכון גדול מדי, וטיפול האבטחה עצמו הופך לעבודה כבדה.מונע את זה&quot;נעשה את זה בבת אחת אחר כך&quot;הזנחה של חצי שנה עד שנהפער עדכון גדול מדיהטיפול עצמו הופך לעבודה כבדהבדיקה תקופתית

איור 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 תוצאות”, בשחרור הבא צריך לבדוק רק את ההפרש.

התועלת בתיעוד הבדיקהתרשים המראה שכשמתעדים בדיקה שלפני שחרור גם כאשר התוצאה היא 0 תוצאות, בשחרור הבא צריך לבדוק רק את ההפרש.ביצוע בדיקה לפני שחרורתיעוד: בוצע חיפוש · 0 תוצאותבשחרור הבא מספיק לבדוק רק הפרש

איור 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 ומיקום שמירת קבצים. הרשאה מינימלית יציבה יותר לטווח הארוך.

הסוף של "אם נריץ כמנהל זה ייפתר"תרשים המראה שהרצה קבועה בהרשאות מנהל נוחה בהתחלה אך מקשה בהמשך סביב UAC, הפצה, תמיכה, גבולות הרשאה, טעינת DLL ומיקום שמירת קבצים, ושהרשאה מינימלית יציבה יותר לטווח הארוך.יציבה יותר לטווח הארוך&quot;אם נריץ כמנהל זה ייפתר&quot;נוח בהתחלהמקשה סביב UAC, הפצה ותמיכהמקשה סביב גבולות הרשאה ומיקום שמירההרצה בהרשאה מינימליתנמנעים מהקושי

איור 20: הנוחות בהרצה קבועה כמנהל היא רק בהתחלה, ולטווח הארוך הרשאה מינימלית יציבה יותר.

6. סדר עדיפויות כללי

אם קשה לעשות הכול בבת אחת, סדר העדיפויות נראה בערך כך.

הבסיס לסידור הוא מכפלה בין גודל הנזק במקרה תקרית לבין נמוכות עלות התיקון. פרק 3 מסודר לפי זרימת התכנון “הרשאות ← הפצה ← מימוש ← תפעול”, אבל כאן הסדר הוא “מהמסוכן ביותר”, ולכן הוא לא זהה לסדר הפרקים. מצורף הסעיף המתאים לכל פריט.

  1. בחינה מחדש של הרשאות מנהל (3.2) קודם כל, להפסיק את השימוש הקבוע ב-requireAdministrator. היקף הנזק משתנה בצעד אחד, בעוד שינוי התכנון עצמו לרוב קטן.
  2. חתימה וחותמת זמן (3.3) לסדר את אמינות קובצי ההפצה. מספיק לשלב בהליך, ואם נדחה לאחר מכן נדרשת הפצה מחדש.
  3. הוצאת מידע רגיש (3.5) להוציא סוד מקוד המקור ומתצורה גלויה. הנזק בעת דליפה גדול, ואחרי הדליפה אין דרך חזרה.
  4. תיקון HTTPS + אימות אישורים (3.6) להסיר מקובצי ההפצה סוגים של => true. לרוב מספיקה הסרה בלבד, ובהזנחה כל התקשורת בלתי מהימנה.
  5. בחינה מחדש של קלט SQL / קובץ / IPC (3.7) לצמצם שרשור מחרוזות וקלט לא מאומת. הכמות רבה, אך אפשר לתקן מקום אחד בכל פעם.
  6. קיבוע טעינת DLL (3.8) להפסיק טעינה לפי שם בלבד והסתמכות על PATH. זה תיקון של חלק מתהליך ההפעלה, ומועיל גם למניעת תקלות.
  7. מיסוך לוגים (3.9) לוודא שהלוג לא נהיה אסון משני בעת תקרית. יש הרבה מקומות פלט, ולכן זה לוקח זמן.
  8. קביעות של עדכון תלויות (3.10) להפוך לתהליך קבוע שנבדק בכל שחרור. לא נגמר בפעם אחת — העבודה היא להפוך את זה למנגנון.

מסלול העדכון (3.4) לא נכלל בסדר הזה כי לא לכל יישום יש עדכון אוטומטי. אם יש, כדאי לראות אותו באותה עדיפות כמו פריט 2, החתימה. מודול העדכון נמצא במעמד חלש יותר מגוף היישום, אך יש בכוחו לשכתב אותו.

איך קובעים סדר עדיפויותתרשים המראה שהסדר נקבע לפי מכפלה של גודל הנזק ונמוכות עלות התיקון, מהמסוכן ביותר, ושביישום עם עדכון אוטומטי מסלול העדכון נבחן באותה עדיפות כמו החתימה.גודל הנזקמכפלה שקובעת את הסדרנמוכות עלות התיקוןאיטום לפי סדר מהמסוכןביישום עם עדכון אוטומטי — מסלול העדכון באותה עדיפות כמו החתימה

איור 21: סדר העדיפויות נקבע ממכפלת גודל הנזק ועלות התיקון, ומאטמים מהחור המסוכן ביותר.

בסדר הזה, נוח להתקדם במובן של “קודם לאטום את החורים שברור שהם מסוכנים”.

7. סיכום

אבטחת פיתוח יישומי Windows משתנה משמעותית כבר רק מסידור שבע הנקודות הרשאות, חתימה, מידע רגיש, תקשורת, קלט, DLL ולוגים — עוד לפני הכנסת מוצר מיוחד או מנגנון ענק.

הרף המינימלי, במשפט אחד לכל נקודה:

  • לא להריץ את כל היישום בהרשאות מנהל
  • לחתום על קובצי ההפצה והעדכון, כולל חותמת זמן
  • לא להחזיק מידע רגיש בקוד המקור או בתצורה גלויה
  • גם עם HTTPS, לא לבטל אימות אישורים
  • לא לתת אמון בקלט חיצוני כמו SQL, קובץ, IPC וכדומה
  • לא להשאיר את מקור טעינת ה-DLL עמום
  • לא לחשוף מידע רגיש בלוגים
  • לא להזניח ספריות תלויות

נושא האבטחה רחב, אבל לא חייבים לעשות הכול מההתחלה. עם זאת, שווה להשלים כבר בשלב מוקדם למדי את המינימום של לא להפיץ התנהגות ברירת מחדל מסוכנת כמו שהיא.

8. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה כדאי לבדוק ראשון באבטחת יישומי Windows?
ארבעה דברים: לא לדרוש הרשאות מנהל שלא לצורך, לחתום על הקוד, לא להחזיק סודות בטקסט גלוי, ולא לבטל אימות אישורים. אבטחה מינימלית היא לא הוספת תכונות מיוחדות אלא הימנעות מהשארת התנהגות ברירת מחדל מסוכנת או מימוש רשלני. דילוג קבוע על אימות אישורים, מחרוזות חיבור בטקסט גלוי, טעינת DLL שמסתמכת על הספרייה הנוכחית, והרצת SQL בשרשור מחרוזות — אלה דברים שכדאי להימנע מהם גם ברף המינימלי ביותר.
האם אסור להריץ את כל היישום בהרשאות מנהל?
כן, כדאי להימנע מכך. הרצת כל היישום בהרשאות מנהל גורמת לכך שבאגים, החלפת DLL, קריאה שגויה של קובצי תצורה ופגמים בקלט חיצוני מתבצעים ישירות בהרשאה חזקה. המדיניות הבסיסית היא שיישום UI רגיל יפעל ב-asInvoker, שתהליכים שדורשים הרשאות מנהל יופרדו לתהליך נפרד או לשירות, ושההעלאה בהרשאה תתבצע רק ברגע הנדרש.
איפה כדאי לשמור מפתחות API ומחרוזות חיבור?
כדאי בהחלט לצאת ממצב שבו הם נמצאים בטקסט גלוי בקוד המקור או בקובצי תצורה כמו appsettings.json. מידע רגיש שנשמר משתמש לפי הצורך ב-DPAPI /‏ ProtectedData או ב-Credential Locker. חשוב גם למסך ביומנים דברים כמו טוקנים, סיסמאות, מחרוזות חיבור ומידע אישי, כי אחרת היומן עצמו נהיה גורם התקרית.
האם יישום להפצה פנים-ארגונית זקוק גם הוא לחתימת קוד?
כדאי להתייחס לזה כאל הנחת יסוד. ביישומי Windows, קובצי ההפצה עצמם (EXE /‏ DLL /‏ MSI /‏ MSIX / מודול עדכון אוטומטי) מהווים משטח תקיפה. חתימת קוד עם חותמת זמן מספקת גילוי שיבוש, אמינות בעיני המשתמשים, ונוחות בהסבר ניהולי. גם מנגנון העדכון צריך לקבע את מקור העדכון, ולזהות שיבוש דרך HTTPS ואימות חתימה.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג