תוכן proc/maps

זיהוי טעינה דינמית באפליקציות אנדרואיד עם /proc/maps

TL; DR: באמצעות טעינה דינמית, מחברי תוכנות זדוניות יכולים לטעון באופן סמוי קוד זדוני לאפליקציה שלהם על מנת למנוע זיהוי. אנו יכולים לזהות טעינה כזו דרך קובץ ה-/proc/[PID]/maps של האפליקציה שנוצר.
לאחרונה יצרנו סקריפט פשוט המאפשר לנו לזהות טעינה דינמית באפליקציות אנדרואיד. זה העניק לנו הזדמנות טובה לדון בטעינה דינמית באופן כללי בבלוג הזה. 

טעינה וקישור דינמיים:

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

מה זה קישור:

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

איך זה נעשה בפועל? לאחר הקומפילציה והרכבה, האסמבלר מוציא קבצי אובייקט, שבדרך כלל מתאימים לכל מודול בתוכנית שלך. קבצים כאלה יכולים להיות ניתנים למיקום מחדש או להפעלה. קבצי אובייקט מהזן הראשון צריכים לעבור קישור לפני שניתן יהיה להפעיל אותם, אבל קבצי אובייקט מהסוג השני ניתנים לביצוע באופן מיידי. במערכת האקולוגית של GNU/Linux, הסיומת הרגילה לקובץ אובייקט היא .o. קובצי אובייקטים משותפים, הידועים כספריות, הם קובצי אובייקט הניתנים להעברה, המיועדים לשימוש על ידי תוכניות רבות ושונות, ואינם ניתנים להפעלה בעצמם. ב-GNU/Linux, יש להם את הסיומת .so (Shared Object), ואילו ב-Windows יש להם את הסיומת .dll (Dynamic-link Library).

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

השמות הרבים השונים של קובץ ההפעלה:

גם ב-Linux וגם ב-Windows לאותם הרחבות שונות לכאורה יש את אותם פורמטים בסיסיים כמו קובצי הפעלה. ב-Windows, הם PEs (Portable Executable), ובדרך כלל יש להם את הסיומות .exe ו-.dll, וב-Linux, הם ELFs (קובץ ניתן להפעלה וניתן לקישור), ויש להם את הסיומות .bin, .so או .o (כן , קובצי האובייקט שהזכרנו קודם הם גם ELFs).

קישור דינמי:

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

אז למה זו בעיה?

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

אבל זה גם מציג דרך קלה למפתחי תוכנות זדוניות להסתיר את הקוד הזדוני שלהם. הם יכולים לשים את כל הקוד הלגיטימי שלהם ב-APK, ולהעביר את כל הקוד המרושע ל-DEX (Dalvik Executable) שהאפליקציה תוריד ולאחר מכן תטען באופן דינמי במהלך השימוש ובכך יגרום לאפליקציה שלהם להיראות תמימה בבדיקה סטטית בסיסית של APK.

כיצד נטענים שיעורי DEX:

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

// Init the loader
DexClassLoader dexClassLoader = new DexClassLoader(path_to_dex, null, null, parent_class);

// Load the class:
Class dynamic_class = dexClassLoader.loadClass("DynamicClass");

// Load a method we could call it
Method method = dynamic_class.getMethod("method1");

ואז נוכל להשתמש ב-invoke() של השיטה כדי להשתמש בשיטה.

הקובץ /proc/[PID]/maps:

ביוניקס, הכל קובץ, וגם אם זה לא באמת אחד, הוא מטופל ונגיש כמו אחד. זה כולל את מבני הנתונים של הקרנל, ולינוקס אינה יוצאת דופן לכלל. ליבת לינוקס מאפשרת לנו לגשת ולקרוא את מבני הנתונים שלו דרך /proc/ מערכת קבצים פסאודו. לאחר מכן לכל תהליך יש תיקיה משלו ב- /proc/[PID]. הקבצים ותתי התיקיות כאן מכילים שפע של מידע שימושי וחשוב על התהליך, אבל היום נתמקד בקובץ אחד בלבד: /proc/[PID]/maps.

/proc/[PID]/maps מציג תרשים של הזיכרון הממפה של תהליך. כשאומרים זיכרון ממופה, אנו מתכוונים לקטע זיכרון וירטואלי שיש לו התכתבות אחת לאחד עם קובץ. מיפוי זה מאפשר לאפליקציה לשנות ולגשת לקבצים על ידי קריאה וכתיבה ישירות לזיכרון. המשמעות היא שכאשר תוכנית ניגשת לקובץ, זה בסופו של דבר יירשם בקובץ /proc/[PID]/maps שלה.

/proc/[PID]/maps גם מראה לנו אילו הרשאות יש לתהליך עבור כל פלח. זה יכול לעזור לנו לקבוע אילו קבצים התהליך ערך ואילו קבצים הוא קרא.

כך נראה קטע קצר של קובץ /proc/PID/maps רגיל:

7f9cefbf7000-7f9cefbf8000 r--p 00000000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf8000-7f9cefbf9000 r-xp 00001000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf9000-7f9cefbfa000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfa000-7f9cefbfb000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfb000-7f9cefbfc000 rw-p 00003000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfc000-7f9cefc08000 r--p 00000000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc08000-7f9cefc1b000 r-xp 0000c000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc1b000-7f9cefc24000 r--p 0001f000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc24000-7f9cefc25000 r--p 00027000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc25000-7f9cefc26000 rw-p 00028000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc26000-7f9cefc27000 r--p 00000000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc27000-7f9cefc28000 r-xp 00001000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc28000-7f9cefc29000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc29000-7f9cefc2a000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2a000-7f9cefc2b000 rw-p 00003000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2b000-7f9cefc47000 r--p 00000000 103:03 1584005            /usr/lib/libX11.so.6.3.0
7f9cefc47000-7f9cefcd1000 r-xp 0001c000 103:03 1584005            /usr/lib/libX11.so.6.3.0

כל שורה בקובץ מתעדת קטע זיכרון בודד במרחב הכתובות הרציף של הזיכרון הווירטואלי שהוקצה לתהליך.

  • כתובת – העמודה הראשונה מציגה את כתובת ההתחלה והסיום של הקטע.
  • הרשאות – עמודה זו מציגה אילו הרשאות יש לתהליך עבור הפלח. r/w/x הם הקריאה/כתיבה/ביצוע הרגילה, בעוד שהאות האחרונה היא s or p, כלומר משותף או פרטי, בהתאמה.
  • לקזז – זהו ההיסט מתחילת הקובץ, על מנת שניתן יהיה לחשב את כתובת ההתחלה של הנתונים הממופים. לפעמים, קטע אינו ממופה מקובץ (במקרה זה לעמודת הנתיב יהיה מזהה לאופי הקטע, כפי שיוסבר להלן), ובמקרה זה, ההיסט פשוט נשאר כ-0.
  • מכשיר – כאשר הקטע ממופה מקובץ, המספר הקבוע של ההתקן שבו מאוחסן הקובץ מוצג בעמודה זו.
  • Inode (צומת אינדקס) – אם הקטע הגיע מקובץ, זהו ה- מספר קוד של הקובץ.
  • נתיב – זהו הנתיב לקובץ, אם יש כזה. זה יכול להיות [ערימה], [מחסנית] או [vsdo] אם הקטע הוא המבנה בעל השם.

התסריט הצנוע שלנו:

ביישומי אנדרואיד פוגעים, טעינה דינמית של קוד נעשית בדרך כלל מספריית הבית של האפליקציה, כך שקבצים שנטענו לזיכרון מאותו מילון (או מיקומים נפוצים אחרים) צריכים להופיע בקובץ המפות. כדי להפוך את המשימה של בדיקת הקובץ לאוטומטית, בנינו סקריפט פשוט מאוד שמשתמש ב-regex כדי לחפש את המחרוזת '/data/data' בקובץ המפות של PID נתון במכשיר המחובר ומחזיר את השורות התואמות. /data/data היא כמובן ספריית הבית של האפליקציות שבה מאוחסנים קבצים ונתונים. זהו המיקום היחיד שממנו אפליקציה יכולה לטעון קבצי DEX.

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

אתה יכול לראות את הסקריפט המלא שבו אנו משתמשים כאן תמצית גיתוב.

עבור לתוכן