המדריך למזריק (DLL) המתחיל – DLL INJECTION USING APC – מאמר שני בסדרת הזרקות קוד ו-HOOKING
הקדמה
ברוכים הבאים למאמר השני בסדרת המאמרים בנושא הזרקות קוד ו-Hooking. במאמר הבא נסקור שיטה שתאפשר לנו להריץ קוד בתהליך מרוחק (מרוחק = לא התהליך שמריץ את האפליקציה שלנו). במאמר הקודם תיארתי וממשתי שיטה להזרקת DLL לתהליך מרוחק עם הפונקציה CreateRemoteThread אולם במאמר זה נראה כיצד זה אפשרי גם מבלי השימוש בפונקציה הזו. מטרת המאמר היא להציג את הניצול של תור ה-APC על מנת להריץ קוד בתהליך מרוחק. על מנת לחתור לעבר המטרה, נעבור דרך מספר צמתים בניהם: פקודה סינכרונית מול פקודה א-סינכרונית, מה זה APC ו-APC Queue, איך ומתי ניתן לנצל את המנגנון הזה ולבסוף גם נבנה Injector משלנו שמנצל את המנגנון. ברצוני להדגיש למי שלא הזריק DLL מעולם – לכו וקראו את המאמר הקודם שלי בסדרה מכיוון שישנם קונספטים דומים שלא אסביר במאמר זה לעומק, מאחר והסברתי אותם במאמר הקודם.


פקודה סינכרונית VS פקודה א-סינכרונית
בשגרה שלנו כמתכנתים, אנו יודעים שכאשר אנו קוראים לפונקציה, הקוד שלנו יאלץ להמתין עד שהפונקציה תסיים את עבודתה. עד אשר היא תסיים, איננו יכולים להמשיך לרוץ מאחר וכל רגע רק פקודה אחת יכולה לרוץ. לכן איך יהיה ניתן גם להריץ את הפונקציה וגם להמשיך הלאה? התשובה לכך נמצאת בעובדה שכיום ברוב המחשבים, יש מספר ליבות וניתן להריץ מספר פקודות בו זמנית כאשר כל ליבה מעבדת פעולה אחרת. הדבר מוביל אותנו להבנה שכעת אולי לא נהיה חייבים לחכות לכל פונקציה שתסתיים ורק אז להמשיך הלאה בקוד שלנו. המצב הישן היה נקרא תכנות סינכרוני – כל פונקציה תסיים את עבודתה ורק אז נמשיך לפקודה הבאה. המצב החדש נקרא תכנות א-סינכרוני, שאומר שכעת לא נהיה חייבים לחכות לכל פונקציה וניתן יהיה להתקדם ובהמשך לקבל באיזושהי דרך אינדיקציה שהפונקציה סיימה את עבודתה וניתן להשתמש בערך שהיא החזירה. כדי להמחיש את זה מעט טוב יותר ננסה לעשות אנלוגיה למקרה פשוט יותר. תארו לכם שורה של ילדים שכל אחד צריך לעשות משימה. כאשר ילד ראשון יוצא למשימה שלו, בתכנות א-סינכרוני, הילד השני כבר יכול לצאת למשימה שלו ולא חייב להמתין שהילד הראשון יחזור מהמשימה שלו. אם הילד השני חייב את התוצר של המשימה של הילד הראשון אז הוא ימתין וכשהילד הראשון יחזור הוא יודיע לו שהוא סיים ואז הילד השני יוכל להמשיך במשימה שלו.
נראה דוגמה מ-Win32API: הפונקציה ReadFile, בצורתה הבסיסית ביותר, נועדה לרוץ כפונקציה סינכרונית. כלומר עד שהיא לא תסיים לרוץ, לא נמשיך הלאה לפקודה הבאה. אולם ישנה אפשרות להריץ את הפונקציה בצורה א-סינכרונית. לפני שאנו יכולים להריץ את ReadFile, עלינו לקבל איזשהו Handle ל-Device ממנו אנו רוצים לקרוא. לפישוט העניין, נניח שאנו מדברים על קריאה מקובץ טקסט רגיל. את ה-Handle ש-ReadFile צריכה לקבל ניצור עם הפונקציה CreateFile. ל-CreateFile יש דגל מיוחד בשם FILE_FLAG_OVERLAPPED שמציין ל-CreateFile, כי העבודה מול האובייקט שה-Handle שהיא מייצרת מנגיש לנו, תהיה בצורה א-סינכרונית. עכשיו שיצרנו את ה-Handle עם הדגל הנכון, אנחנו יכולים לקרוא ל-ReadFile בצורה א-סינכרונית. שניה לפני שהמשפט האחרון יהיה נכון לגמרי, יש לציין כי ברגע שעובדים מול ReadFile בצורה א-סינכרונית, נדרש לספק ל-ReadFile מצביע למבנה מסוג OVERLAPPED שיאפשר לסנכרן בין הסטטוס של ReadFile לבין שאר הקוד. המבנה הזה מכיל את השדה Internal שיכיל בעצמו, האם הסטטוס הוא Pending או Completed. אולם על פי התיעוד שח מיקרוסופט, השדה הזה שמור למערכת ולנו יש את השדה hEvent שיקבל Handle לאובייקט שנוצר מהפונקציה CreateEvent ובאמצעותו נוכל לסנכרן בין ReadFile בצורתה הא-סינכרונית לבין שאר הקוד שלנו. הדוגמה ממחישה לנו את צורת העבודה מול פעולות א-סינכרונית וכך ניתן להבין טוב יותר מה ההבדל בין פונקציה סינכרונית לבין פונקציה א-סינכרונית.
Asynchronous Procedure Call
APC, או בשמו המלא – Asynchronous Procedure Call, הוא הביטוי לפונקציה שרצה בצורה א-סינכרונית בקונטקסט של Thread מסוים. לכל Thread יש תור של פונקציות APC שנקרא APC Queue. כאשר מתווסף לתור הזה APC, בעת הרצת ה-Thread במעבד, ירוץ ה-APC. אולם זה נכון רק כאשר ה-Thread נכנס למצב Alertable. Thread יהיה במצב זה, כאשר נקראת פונקציה שמכניסה אותה למעיין מצב המתנה. לדוגמה הפונקציות SleepEx או WaitForSignalObject יגרמו לכך. כאשר ה-Thread יהיה במצב כזה ויהיה לו APC בתור ה-APC Queue, ה-APC הזה ירוץ. כאשר נכניס ל-APC Queue של Thread איזשהו APC, תתבצע Software Interrupt מהמערכת, כך שבפעם הבאה שה-Thread יתוזמן אכן ה-APC ירוץ בהנחה והוא ב-Alertable State.
ניצול APC Queue להרצת קוד בתהליך מרוחק
ניתן להוסיף לתור ה-APC של Thread גם אם הוא לא Thread של התהליך הנוכחי. כלומר אם אני Add.exe אני יכול להוסיף ל-APC Queue של Thread כלשהו בתהליך Target.ext. נכון! אלו שנופל להם האסימון ואומרים "אז למה שלא נוסיף ל-APC Queue של תהליך קורבן פונקציה שלנו וכך נזריק קוד" צודקים לחלוטין וזה מה שנעשה כעת. נתחיל בהזרקת DLL לתהליך קורבן. המתכון שלנו נראה כך:
- נאתר את ה-PID של התהליך קורבן באמצעות השם שלו.
- נפתח Handle לתהליך הזה.
- נפתח Handle ל-Thread קורבן.
- נאכלס מקום לנתיב של ה-DLL שלנו ונרשום את הנתיב לשם.
- נוסיף ל-APC Queue של ה-Thread קורבן את ה-APC שלנו שתהיה LoadLibrary עם הנתיב של ה-DLL כפרמטר.
- נקבל הזרקה.
את הקוד של שלב 4 אני לא הולך להסביר מאחר והסברתי אותו במאמר הקודם בסדרה. נתחיל מההתחלה. ראשית, על מנת לאתר את ה-PID של התהליך עלינו לקבל Snapshot של המצב הנוכחי במערכת. זה אומר שנכניס לתוך מבנה מסוים שלמטרה זו הוא נועד, את הפרטים של כל ה-Thread-ים והתהליכים במערכת. הפונקציה שתשמש אותנו לצורך זה היא CreateToolhelp32Snapshot. בתור פרמטרים נכניס לה את TH32CS_SNAPTHREAD ואת TH32CS_SNAPOROCESS בפרמטר הראשון ובפרמטר השני נכניס NULL. את הערך חזרה של הפונקציה נגדיר ל-Handle שימש אותנו בהמשך.
כעת נגדיר שני משתנים: הראשון – Handle לתהליך קורבן, השני מבנה מסוג PROCESSENTRY32 שישמש אותנו בלולאה שנראה בהמשך.
עכשיו שברשותנו Snapshot של כל התהליכים נוכל לרוץ על כולם ולבדוק בכל אחד אם השם של התהליך זהה לשם של התהליך שאנחנו מחפשים. התהליך שאנחנו מחפשים נקרא alertable.exe ונבנה אותו בהמשך המאמר. ברגע שנמצא את התהליך נשמור את ה-PID שלו.
שימו לב: הקוד שלי יוצא מנקודת הנחה שקיים תהליך בשם זה.
לאחר שמצאנו את ה-PID, נרוץ על כל ה-Thread-ים של התהליך הזה. למעשה נרוץ על כל ה-Thread-ים ב-Snapshot הקיים ונשמור את ה-Thread ID של כל Thread שה-PID שלו תואם ל-PID ששמרנו קודם לכן. נעשה זאת באמצעות הגדרה מצביע למבנה בשם tagTHREADENTRY32 ואכלוס מקום שלו ואז לולאה. כך נראה החלק הראשון בו אנו מגדירים את המשתנים שלנו:
עכשיו נשאר לבנות את הלולאה שתברור כל Thread ב-Snapshot ותוסף את ה-ID שלו במידה וה-PID שלו תואם לדרישה שלנו.
השלב הבא מוסבר בהרחבה במאמר הקודם – נפתח Handle לתהליך קורבן, נאכלס שם מקום ל-Path של ה-DLL שלנו ונכתוב את ה-Path לשם. פתיחת ה-Handle ואכלוס המקום:
כתיבת הנתיב והשגת הכתובת של LoadLibrary ב-Kernel32.dll:
ועכשיו לשלב האחרון – ניצול ה-APC Queue של התהליך הקורבן. נכתוב לולאה שתרוץ על כל ה-Thread-Ids שמצאנו ועברו כל אחד תקרא לפונקציה QueueUserAPC. הפונקציה הזו כותבת לתור ה-APC של Thread לפי ה-Handle שלו. הפרמטר ראשון שהיא מקבלת הוא מצביע ל-APC. הפרמטר השני הוא Handle ל-Thread הפתוח. הפרמטר השלישי הוא מצביע לפרמטרים שה-APC צריכה. שימו לב שאנחנו מוסיפים את ה-APC שלנו לכל Thread שמצאנו כדי להגדיל את הסיכוי שה-APC שלנו ירוץ. כך תראה הלולאה:
ה-Injector שלנו מוכן. את ה-DLL שאנו מזריקים בנינו במאמר הקודם מוזמנים לקחת משם את הקוד במידה וחסר לכם. נעבור לבניית ה-alertable.exe שלנו. ניצור פרויקט ריק ב-Visual Studio, נוסיף קובץ קוד לפרויקט ונכתוב לשם פונקציית main רגילה שתקרא ל-SleepEx. כך זה נראה:
קמפלו את שני הפרויקטים שלכם. הריצו את ה-alertable.exe ב-CMD. כעת הריצו את ה-Injector. זאת התוצאה:
הזרקת Shellcode במקום DLL
דיברנו עד עכשיו על הזרקת DLL. אולם בחלק מן המקרים נרצה להזריק Shellcode. גם להזרקת Shellcode מתאימה שיטה זו. למעשה, כדי להזריק Shellcode במקום DLL, נצטרך:
- להחליף את סוג ההרשאות על הדפים שאכלסנו עם VirtualAllocEx ל-PAGE_EXECUTE_READWRITE.
- במקום להשתמש ב-WriteMemoryEx על מנת לכתוב את הנתיב ל-DLL, נכתוב לשם את ה-Shellcode שלנו.
- במקום לתת את הכתובת של LoadLibrary ל-QueueUserAPC, ניתן את הכתובת של ה-Shellcode שלנו.
הקוד של ה-Injector:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <Windows.h> | |
#include <tchar.h> | |
#include <TlHelp32.h> | |
#ifdef UNICODE | |
#define LOAD_LIBRARY_VERSION "LoadLibraryW" | |
#else | |
#define LOAD_LIBRARY_VERSION "LoadLibraryA" | |
#endif | |
int _tmain(int argc, TCHAR * argv[]) { | |
// Create threads snapshot of the current system state | |
HANDLE handleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD | TH32CS_SNAPPROCESS, NULL); | |
HANDLE victimProcess = NULL; | |
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; | |
if (Process32First(handleSnapshot, &processEntry)) { | |
while (strcmp(processEntry.szExeFile, "alertable.exe") != 0) { | |
Process32Next(handleSnapshot, &processEntry); | |
} | |
} | |
int pid = processEntry.th32ProcessID; | |
// Allocate struct for threads iterating | |
tagTHREADENTRY32 * ptrThread = (tagTHREADENTRY32 *)calloc(1, sizeof(tagTHREADENTRY32)); | |
ptrThread->dwSize = sizeof(tagTHREADENTRY32); | |
// List of threads IDs in const max len | |
int tid_list[30]; | |
ZeroMemory(tid_list, sizeof(int) * 30); | |
if (Thread32First(handleSnapshot, ptrThread) != TRUE) { | |
_tprintf(TEXT("Cannot get first thread\r\n")); | |
} | |
// For each thread in snapshot | |
for (int i = 0; i < 30;) { | |
// If the thread's process ids is same as the process we want to inject to | |
if (ptrThread->th32OwnerProcessID == pid) { | |
// Save the thread's ID | |
tid_list[i] = ptrThread->th32ThreadID; | |
i++; | |
} | |
// Move to the next thread in Snapshot | |
if (Thread32Next(handleSnapshot, ptrThread) != TRUE) { | |
break; | |
} | |
} | |
// Open handle to process | |
HANDLE handleOpenThread; | |
HANDLE handleOpenProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, pid); | |
// Get DLL path len | |
int injectedDllPathLen = _tcslen(TEXT("D:\\C\\MyFirstDll\\Debug\\MyFirstDll.dll")) * sizeof(TCHAR) + sizeof(TCHAR); | |
LPVOID ptrArg; | |
if (handleOpenProcess == INVALID_HANDLE_VALUE) { | |
_tprintf("INVALID_HANDLE_VALUE for process id: %d\r\n", pid); | |
return 1; | |
} | |
// Allocate memory fot the dll path string and write the dll to the new memory | |
ptrArg = VirtualAllocEx(handleOpenProcess, NULL, injectedDllPathLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); | |
if (WriteProcessMemory(handleOpenProcess, | |
ptrArg, | |
TEXT("D:\\C\\MyFirstDll\\Debug\\MyFirstDll.dll"), | |
injectedDllPathLen, | |
NULL) == 0) { | |
_tprintf(TEXT("Cannot write to remote process memory\r\n")); | |
return 1; | |
} | |
// Get LoadLibrary address | |
LPVOID ptrAddr = (LPVOID)GetProcAddress(GetModuleHandle("Kernel32.dll"), LOAD_LIBRARY_VERSION); | |
// Foreach thread we stored | |
for (int i = 0; i < 30 && tid_list[i] != 0; i++) { | |
// Open thread handle in all access mode | |
handleOpenThread = OpenThread(THREAD_ALL_ACCESS, NULL, tid_list[i]); | |
if (handleOpenThread == INVALID_HANDLE_VALUE) { | |
_tprintf(TEXT("INVALID_HANDLE_VALUE for thread id: %d\r\n"), tid_list[i]); | |
} | |
// Add apc to call LoadLibrary with our process | |
else if (QueueUserAPC((PAPCFUNC)ptrAddr, handleOpenThread, (ULONG_PTR)ptrArg)) { | |
_tprintf(TEXT("QueueUserAPC for thread id: %d\r\n"), tid_list[i]); | |
} | |
else { | |
_tprintf(TEXT("Cannot Insert to APCQueue for thread id: %d\r\n"), tid_list[i]); | |
} | |
} | |
} |
הקוד של התהליך אליו נזריק (alertable.exe):
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Windows.h> | |
#include <stdio.h> | |
int main() { | |
printf("Start\r\n"); | |
SleepEx( | |
60 * 1000, | |
true | |
); | |
printf("Done\r\n"); | |
system("pause"); | |
} |
סיכום
הגענו לקיצו של מאמר שני בסדרת המאמרים שלי בנושא הזרקות קוד ו-Hooking. דיברנו על פונקציות סינכרוניות מול פונקציות א-סינכרוניות, דיברנו על APC Queue ועל APC ולבסוף גם הסברנו כיצד ניתן לנצל את המנגנון להזריק DLL או Shellcode ואף הוספנו הדגמה. מקווה שהצלחתי להעביר את הטכניקה הזו בצורה אופטימלית, להערות אפשר לכתוב פה בתגובות או לשלוח לי מייל: Orih90@gmail.com. מזמין אתכם להישאר מעודכנים במאמרים חדשים שאני מוציא באמצעות חשבון ה-Twitter שלי. נתראה במאמר הבא.
תגובות
הוסף רשומת תגובה