← חזרה למאגר המאמרים

Breaking the Compilation Boundary: From Suspected Zero-Day to Common Lisp Trust Model

מאת: שי מרדכי | תאריך פרסום: 5 ביוני 2026 (המחקר בוצע ב-31 ביולי 2025) | קטגוריה: Vulnerability Research, Compiler Internals
סטטוס מחקר: דווח ל-CERT-IL לצורך תיעוד ובחינת סיווג. נכון למועד הפרסום לא הונפק CVE.
הקדמה: ההתנגשות בין התיאוריה למציאות
המחקר הזה נולד מתוך התנגשות בין שני עולמות תוכן שלמדתי במקביל: הנדסה לאחור (Reverse Engineering) ופיתוח קומפיילרים (בקורס nand2tetris, שם בניתי קומפיילר בשפת Raku).

ב-RE, בעזרת כלי ה-Patching של IDA Pro, למדתי איך אפשר "לשבור חוקים" בקוד בינארי – למשל, לשמור משתנה מקומי בפונקציה A ולהשתמש בו בפונקציה B דרך אוגרים. לעומת זאת, בקורס קומפיילרים למדתי על התיאוריה הנוקשה של מתחמי הכרה (Lexical Scopes), שלכאורה מבודדים לחלוטין את המשתנים זה מזה. ההתנגשות הזו עוררה אצלי סקרנות: האם ניתן לפרוץ את החוקים הסטטיים האלה כבר בשלב ההידור?

בסיוע AI Agent שבניתי אשר תפקד כ-Fuzzer סמנטי, יצאתי לבדוק את גבולות הבידוד של קומפיילר SBCL (שפת LISP).

יישור קו: מושגי יסוד בארכיטקטורת Lisp

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

משתנה לקסיקלי (Lexical Variable)
משתנה שקיים אך ורק בתוך גבולות גזרה פיזיים מוגדרים בקוד (כמו בלוק let). מפתחים מניחים אינטואיטיבית שזהו "קיר מגן" אבטחתי בזמן קומפילציה.
מאקרו (Macro)
ב-Lisp מאקרו הוא קוד דינמי שרץ בתוך הקומפיילר בזמן הקימפול (Compile-time) ופועל בהרשאות מלאות (Trusted Code).
proclaim (הכרזה גלובלית)
פונקציית מערכת המשמשת להנחיית הקומפיילר לגבי מאפיינים של משתנים או פונקציות ברמה הגלובלית.
intern (רישום בטבלת הסמלים)
פונקציה המקבלת מחרוזת טקסט ומחזירה מצביע ישיר לאובייקט הסמל (Symbol) המרכזי במערכת.
shadow (הסתרת שמות בחבילה)
מנגנון ניהול מרחבי השמות, המנחה את המערכת להסתיר סמל קיים וליצור במקומו סמל מקומי חדש.

פרק א': ההשערה והווקטורים — אשליית ה-CVE

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

1. שינוי מאפייני קישור דינמי (Dynamic Binding Modification)

שימוש בפקודה proclaim בתוך המאקרו כדי לשנות את המטא-דאטה הגלובלי של הסמל, וכפייה על הקומפיילר לייצר קריאה דינמית (Dynamic Runtime Lookup) במקום הפניה מוגנת למחסנית.

/* Step 1: Front-End */ Read & Parse (AST Generation) | v /* Step 2: Lexical Analysis */ Register "secret-token" as LOCAL in LEXENV | v /* Step 3: Macro Expansion */ -> Runs macro modifying global state * Evaluates: (proclaim '(special secret-token)) | v /* Step 4: Code Generation */ * Compiler resolves the symbol according to the updated compilation environment.
;; Vector 1 PoC
(defmacro isolation-bypass-attack (target new-value)
  `(progn
     (proclaim '(special ,target))
     (setf ,target ,new-value)))
למה זה נראה כמו פגיעות (Why It Looked Like a Vulnerability):
במבט ראשון נראה שהמאקרו מצליח "להשתלט" על משתנה שהקומפיילר כבר סימן כמשתנה לקסיקלי מקומי. עבור מפתח המגיע מ-Java או C#, עצם האפשרות שקוד שרץ בזמן קומפילציה משנה את אופן הפתרון של הסמל לאחר שלב הניתוח הלקסיקלי נראית כהפרת בידוד קלאסית. בפועל, לפי מודל השפה, המאקרו פועל כחלק מתהליך הקומפילציה עצמו ולכן רשאי לעדכן את סביבת הקומפילציה.

2. זיהום טבלת הסמלים (Global Symbol Table Pollution)

שימוש ב-intern כדי להשיג מצביע גלובלי למשתנה המקומי, ושתילת נתונים על גבי ה-Property List שלו, מה שמאפשר חילוץ מידע לסביבה החיצונית.

;; Vector 2 PoC
(defmacro pollute-symbol-table (sym-name)
  (let ((global-sym (intern (string sym-name))))
    (setf (get global-sym 'compromised-leak) t)
    `(quote ,global-sym)))
למה זה נראה כמו פגיעות:
היכולת לקבל גישה לאובייקט Symbol גלובלי בשם זהה למשתנה מקומי עלולה להיראות כחילוץ מידע מתוך Scope פרטי אל מרחב ציבורי. בבדיקה מעמיקה התברר שהמאקרו אינו "פורץ" למשתנה הלקסיקלי עצמו, אלא פועל על אובייקט הסמל הגלובלי שמוגדר כחלק ממודל הנתונים של Common Lisp.

3. בלבול מרחבי שמות (Cross-Package Namespace Confusion)

שימוש בפקודת shadow במהלך Macro Expansion מאפשר לשנות את מרחב השמות (Package Namespace) באופן שעלול לסתור את ההנחות האינטואיטיביות של המפתח לגבי זהות הסמל הנפתר בזמן קומפילציה.

;; Vector 3 PoC
(defmacro hijack-namespace (target-sym)
  (let ((shadow-pkg (make-package "EPHEMERAL-ATTACK-PKG")))
    (shadow (string target-sym) shadow-pkg)
    nil))
למה זה נראה כמו פגיעות:
החלפת רזולוציית שמות במהלך הקומפילציה יוצרת מצב שבו שם זהה עשוי להתייחס לאובייקטים שונים בהתאם למרחב השמות הפעיל. עבור מפתח שאינו רגיל למערכת ה-Packages של Lisp, ההתנהגות עשויה להיראות כמו השתלטות על משתנה מקומי, בעוד שבפועל מדובר במנגנון Namespace תקני.

פרק ב': מבחן המציאות — Trust but Verify

בשלב זה, נראה היה שיש בידי פגיעות קלאסית. החשד גבר כאשר זיהיתי קומיטים היסטוריים ודחופים בקוד המקור של SBCL, כגון הוספת מנגנוני pseudo-atomic סביב פעולות ה-dynbind. זה נראה כמו Silent Patch מושלם.

באותו שלב, נתקלתי בפוסט של מהנדס המערכות הוותיק איירה ג'יי פרלו (Ira J. Perlow), שכתב על גילוי באגים בקומפיילרים וסיכם בעצה: "Trust but Test and Verify!". המשפט הזה נתן לי את הדרייב לא לעצור בחשדות. החלטתי לשפוט את מפתחי SBCL לכף זכות, לאתגר את ההנחות שלי, ולבדוק את ההשערה עד תום.

אחרי הכל, חוקר אבטחה נמדד ביכולתו להפריך את עצמו.
לקחתי את ה-PoC המקורי שלי (שנבדק תחילה על מערכת Windows) והרצתי אותו באופן דיפרנציאלי (Differential Testing) תחת סביבת לינוקס, מול הגרסה העדכנית ביותר דאז (31 ביולי 2025), ולאחר מכן מול הגרסאות שלאחר סדרת הקומיטים ה"חשודה". התוצאה הייתה חד-משמעית: הפגיעות עבדה בשתי הגרסאות ללא דופי.

המסקנה (The Pivot): ההתנהגות היא By Design ומעוגנת במפרט ה-ANSI. הפאצ'ים ב-cell.lisp נועדו לפתור בעיות סנכרון (Thread Safety), ולא ליצור Sandbox אבטחתי שאינו קיים בשפה. במילים אחרות, ה-Lexical Scope ב-Common Lisp הוא מנגנון סמנטי עבור התוכנית, אך אינו מהווה גבול אבטחתי מול קוד מאקרו שרץ בזמן קומפילציה.

פרק ג': איפה עובר גבול האמון?

מה שהמפרט אומר — ומה שמפתח מודרני עלול לפספס:
לאחר הבדיקה התברר ש-SBCL מתנהג בצורה תקינה ביחס למפרט ANSI Common Lisp. המאקרו ב-Lisp מעולם לא תוכנן להיות Sandbox אבטחתי, אלא חלק בלתי נפרד מתהליך הקומפילציה עצמו.

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

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

המשמעות האבטחתית:
בסביבות CI/CD מודרניות, ספרייה חיצונית שמכילה Macro זדוני מקבלת בפועל הרשאות מלאות בזמן ה-Build.

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

אז האם זו חולשה?

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

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

אבל מבחינת אבטחה, המחקר מציף נקודה חשובה: לא כל גבול סמנטי הוא גבול אבטחתי.

העובדה שמנגנון Macro מקבל הרשאות מלאות בזמן Build היא החלטת תכנון לגיטימית, אך היא הופכת אותו לחלק ממשטח התקיפה כאשר משתמשים בו בסביבות מודרניות של Dependency ו-Supply Chain.


🔗 משאבי מחקר וקוד מקור

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

📂 מאגר ה-GitHub הרשמי: AI-Augmented-SBCL-Vulnerability-Research