CreateFileMapping: עריכת קבצי PE
כשהתחלתי לעבוד על פרוייקט קטן ב-C++ שכלל עריכת קבצי PE (קבצי הרצה, EXE או DLL בד"כ) נתקלתי בשיטה ממש נחמדה לעריכת קבצים תחת Windows, ובמיוחד קבצי PE. משימות מסובכות כמו שינוי מאפיינים (גרסה, חברה, וכו'), פאץ' לתוכנה, שינוי אייקון וכל מניפולציה אחרת הופכים להיות עניין של הכנסת וקריאת ערך ממשתנה. הסתכלו לדוגמה על השורות הבאות:
ntHeader->OptionalHeader.MajorImageVersion = 4; ntHeader->OptionalHeader.AddressOfEntryPoint = 0x53a8cd;
הייתם רוצים ששינוי משתנים בצורה כזו יגרור שינוי מיידי של הקובץ? זה אפשרי, והנה הדרך לעשות זאת.
מיפוי קובץ
ב-Windows API מיושמת שיטה בשם File Mapping הממפה קובץ לזיכרון הוירטואלי של התוכנה (ממפה, אך לאו דווקא טוענת. ברגע שיהיה צורך במידע הוא ייקרא מההארד-דיסק), ולאחר המיפוי, אפשר לגשת לחלקים ממנו עם מצביעים (Pointers) בדיוק כמו לזיכרון דינאמי (new, memalloc). בלי פונקציות קריאה\כתיבה\seek, הכל באופן אינטיאוטיבי כמו שרגילים בגישה לזיכרון.
וכך מתחילים:
HANDLE fileHandle = CreateFile("TestFile.exe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
אנו קוראים ל-CreateFile כדי לקבל Handle לקובץ. הפרמטר הראשון הוא כמובן נתיב ושם הקובץ, לאחריו אנו מודיעים שנרצה לקרוא ולכתוב אליו (אפשר לדוגמא לכתוב רק GENERIC_READ). את השימוש בשאר הפרמטרים אפשר לקרוא ב-MSDN.
HANDLE fileMapHandle = CreateFileMapping(fileHandle, NULL, PAGE_READWRITE, 0, 0, 0); char *fileContents = (char*)MapViewOfFile(fileMapHandle, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
בשורות אלו אנו ממפים את הקובץ. CreateFileMapping יוצרת את אובייקט המיפוי (עם הרשאות כתיבה) ו-MapViewOfFile ממפה את הקובץ למרחב הוירטואלי ומחזירה מצביע (void*) לתחילת תוכן הקובץ בזיכרון. במקרה הזה אני מתייחס אליו כאל מצביע ל-char, ועכשיו אפשר לקרוא ולשנות (והשינוי יתעדכן בקובץ אוטומטי) בחופשיות:
if (*fileContents == 0x30) *(++fileContents) = 'A'; fileContents++; *fileContents = *(fileContents + 20);
צריך להזהר שלא לעבור את גודל הקובץ או המיפוי (אפשר לקבל את גודל הקובץ בעזרת GetFileSizeEx).
בקריאה ל-CreateFileMapping אפשר לבחור את גודל הקובץ למיפוי, ואם ייבחר גודל גדול מגודל הקובץ בהארד-דיסק, גודל הקובץ יגדל.
היתרון הגדול
כמו שאמרתי בהתחלה, השתמשתי בטכניקה הזו כדי לערוך קבצי PE. ביחד עם ה-Windows SDK מגיעים קבצי Header של C המכילים הגדרות של מבני נתונים, בתוכם גם את מבני הנתונים שמהם קבצי PE בנויים. קובץ תוכנה מתחיל במבנה הנתונים IMAGE_DOS_HEADER שאחד מאבריו (האחרון) מבציע למבנה נתונים שנמצא במקום אחר בקובץ בשם IMAGE_NT_HEADERS (הנקרא PE Header שהוא מידע נוסף עבור קבצי PE של Windows). ומכיוון שאנו ניגשים לתוכן הקובץ כמו לזיכרון כל מה שצריך זה לעשות Cast לפוינטר:
IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER*)fileContents; if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) MSGRET("DOS Header magic != IMAGE_DOS_SIGNATURE", -1); IMAGE_NT_HEADERS *ntHeader = (IMAGE_NT_HEADERS*)(fileContents + dosHeader->e_lfanew); if (ntHeader->Signature != IMAGE_NT_SIGNATURE) MSGRET("NT Header signature != IMAGE_NT_SIGNATURE", -1);
בשורה הראשונה אנו לוקחים את הפוינטר, כמו שהוא, ואומרים לקומפיילר שזה באמת IMAGE_DOS_HEADER. אנו מוודאים את תקינות הקובץ (שהוא מתחיל ב-IMAGE_DOS_SIGNATURE ששווה ל-'MZ'), לאחר מכן אנו מוצאים את ה-PE Header בעזרת הפוינטר e_lfanew שב-DOS Header ומוודאים שגם הוא תקין (האם מתחיל ב-'PE').
אחרי ההכנה הזו אפשר לגשת לכל אחד מאיברי המבנים, לקרוא ולשנות אותם. ואפילו לשנות נקודת הכניסה של התוכנה (Entry Point):
ntHeader->OptionalHeader.AddressOfEntryPoint = 0x53a8cd;
או להציג רשימה של ה-sections:
IMAGE_SECTION_HEADER *section = (IMAGE_SECTION_HEADER*)((char*)ntHeader + sizeof(IMAGE_NT_HEADERS)); for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; i++, section++) { lstrcpyn(secName, (char*)section->Name, 8); cout << "Section: " << secName << endl; if (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) cout << " -- Code Section" << endl; }
ולבסוף נשאר רק לסגור את הכל:
UnmapViewOfFile(fileContents); CloseHandle(fileMapHandle); CloseHandle(fileHandle);
להורדת קוד מקור מלא (כולל בדיקת שגיאות) – main.cpp
תגיות: CreateFile, CreateFileMapping, EXE, MapViewOfFile, PE, Windows
פורסם בתאריך 10th מרץ 2011 ע"י vbCrLf
2 תגובות
יפה. מדריך חביב, ישר ולעניין – יוצא מנקודת הנחה שלקוראים יש כבר נסיון (או את היכולת לחפש בגוגל שניות אחדות) עם ה-Win32 API הקשורים ולא הייתי מצפה שיהיה אחרת.
כמו כן, גם שימושי ולא חופר מדי – אין צורך להסביר איך להמיר מבינארית במדריך שקהל היעד שלו כבר יודע. (להבדיל ממאמרים אחרים שמנסים לדחוף טקסט ובכך בעצם מאבדים פוקוס מהמאמר המקורי) בזמן האחרון יש תופעה שכל אחד רוצה לכתוב איזשהו מאמר, הבעיה היא שהוא לא בקיא בנושא ב-100% או שהוא לא כותב מאמר בצורה הכי מובנת וכך קוראים שבאים ללא ידע מסויים לא מוצאים את עצמם (או חושבים שהם כן – וטועים) וממה שראיתי עד כה, בלוג נחמד. תמשיכו כך.
תודה רבה Symbol.