SDT Hooking 무력화에대한연구 By Dual5651 (http://dualpage.muz.ro) 요약 : 이문서는 Windows 2000/XP/2003 환경에서의 SSDT Hooking 을무력화시키는방법에대한필자의연구내용을다루고있습니다. 이문서는독자가 SSDT Hooking 에대하여알고있다는전제하에쓰여졌습니다.
1. 소개 - SSDT Hooking SSDT 란 System Service Descriptor Table 의약자로써 Window 의 API 들의실제함수의주소들이저장되어있는 Table 입니다. Kernel 단에서의처리가필요한 API 들은해당 API 의 Service Index 를이용하여 SSDT 에서해당함수의실제주소를얻어호출합니다. SSDT 는 Kernel 단의 Memory 에존재하는데, SSDT 는 Process 독립적인부분이아닙니다. 즉어떠한조작을하지않는한모든 Application 들은모두같은 Table 을참조하고있는것입니다. 그럼으로 SSDT 를수정하여주면시스템전역적인 API Hooking 을할수있습니다. 이러한이유로수많은 Rootkit 들이 SSDT Hooking 을사용하고있으며, Virus Engine 의동작기반 Virus 탐지기술에도사용되고있습니다. 2. Windows Internals Ring3 에서 Ring0 로 Service 호출을할때, Windows 2000 까지는 int 0x2e 를사용하였으며, Windows XP 부터는 sysenter 를사용하여 Ring0 로전환하고있습니다. Kernel 단의처리가필요한 API 가실제로어떠한호출과정을거치는지 CreateFile() 의호출과정을통해보도록하겠습니다. --------------- Ring 3 --------------- Application 에서 CreateFile 호출 Kernel32.dll 의 CreateFileA() 호출됨. Kernel32.dll 의 CreateFileW() 호출됨.
ntdll.dll 의 NtCreateFile() 호출됨. 위의코드는 ntdll 의 NtCreateFile() 를 Disassemble 한것인데, eax 의값은호출하고자하는 Native API 의 Service Index 이고, edx 의값은 KiSystemCallEntry() 의주소를가진포인터값입니다. ntdll.dll 의 KiFastSystemCall() 호출됨. 현재 ESP 값을 EDX 에저장시키고 SYSENTER 명령어를실행시킨다. SYSENTER 명령어는 MSR 중 IA32_SYSENTER_EIP(0x176) 번에저장되어있는함수를실행시킵니다. --------------- Ring 0 --------------- ntoskrnl.exe 의 KiFastCallEntry() 호출됨. 어떠한조작을가하지않는한 IA32_SYSENTER_EIP 에는 KiFastCallEntry() 의주소가저장되어있습니다. KiFastCallEntry() 는 Kernel 단에서필요한작업을처리하기위한준비작업을합니다. KiFastCallEntry() 는현재 Thread 의 ETHREAD 구조체의 ServiceTable 를읽어서 Table 의주소를구합니다. 조작을가하지않는한 EPROCESS 의 ServiceTable 에는 System Thread 라면 ntoskrnl.exe 에의해 export 되어지는 KeServiceDescriptorTable 의주소가, GUI Thread 라면 KeServiceDescriptorTableShadow 의주소가담겨있습니다.
ntoskrnl.exe 의 ZwCreateFile() 호출됨. eax 레지스터로넘어온 Service Index 를참조하여 KiFastCallEntry() 는 SSDT 에서실제함수의주소를구해호출합니다. --------------- Ring 3 --------------- ntdll.dll 의 KiSystemCallRet() 호출됨 KiFastCallEntry() 에의해실제함수의 Call 처리가완료된후, SYSEXIT 명령을이용하여 Ring3 로돌아오게됩니다. ntdll.dll 의 NtCreateFile() 로리턴됨. kernel32.dll 의 CreateFileW() 로리턴됨. kernel32.dll 의 CreateFileA() 로리턴됨. Application 의코드로리턴됨. Kernel 단의처리가필요한 API 들은대부분위와같은과정을통해수행되어집니다. 이떄 Hooking 할수있는기회는다음과같습니다. Ring 3 : 1) Application 의 Import Descriptor Table 에서 CreateFileA() 의주소수정 2) kernel32.dll 의 Export Descriptor Table 에서 CreateFileA() 의주소수정 3) kernel32.dll 의 Import Descriptor Table 에서 NtCreateFile() 의주소수정 3) kernel32.dll 의 CreateFileA() Inline Hooking 4) kernel32.dll 의 CreateFileW() Inline Hooking 5) ntdll.dll 의 Export Descriptor Table 에서 NtCreateFile() 의주소수정 6) ntdll.dll 의 NtCreateFile() Inline Hooking or HotByte Hooking 7) ntdll.dll 의 KiFastSystemCall() Inline Hooking
Ring 0 : 1) MSR 의 IA32_SYSENTER_EIP(0x176) 번수정 - Sysenter Hooking or IDT 의 0x2e 번수정 - KiSystemService() Hooking 2) KiFastCallEntry() Inline Hooking 3) KiSystemService() Inline Hooking 4) SSDT Hooking 5) NTAPI Inline Hooking 위와같이공격자입장에서는 Hooking 할기회가많이있습니다. 이글에서위의모든내용을다루는방법이나방어법에관하여모두다를수는없습니다. 하지만이미 NET 상에는해당방법에관한많은문서들이올라와있음으로어렵지않게자료를구하실수있을겁니다. 3. SSDT Hooking 무력화의원리와코드 1) SDT-RESTORE by Chew Keong TAN SIG^2 G-TEC Lab 에의해발표된방법입니다. 핵심적인것은 Ring3 에서 \device\physicalmemory Section 을 NtOpenSection() 로열어 Kernel Memory 에접근할수있다는점과, NtQuerySystemInformation() 을이용하여 ntoskrnl.exe 가로드되어있는 Base 주소를구할수있다는점, ntdll.dll 의 Export Table 에는 KiServiceTable 에존재하는함수들의이름이모두존재한다는것입니다. 코드의내용중중요한부분을살펴보도록하겠습니다. BOOL getnativeapis(void) HMODULE hntdll; hntdll = GetModuleHandle("ntdll.dll"); *(FARPROC *)&_RtlAnsiStringToUnicodeString = GetProcAddress(hntdll, "RtlAnsiStringToUnicodeString"); *(FARPROC *)&_RtlInitAnsiString = GetProcAddress(hntdll, "RtlInitAnsiString"); *(FARPROC *)&_RtlFreeUnicodeString = GetProcAddress(hntdll, "RtlFreeUnicodeString"); *(FARPROC *)&_NtOpenSection = GetProcAddress(hntdll, "NtOpenSection"); *(FARPROC *)&_NtMapViewOfSection =
GetProcAddress(hntdll, "NtMapViewOfSection"); *(FARPROC *)&_NtUnmapViewOfSection = GetProcAddress(hntdll, "NtUnmapViewOfSection"); *(FARPROC *)&_NtQuerySystemInformation = GetProcAddress(hntdll, "ZwQuerySystemInformation"); if(_rtlansistringtounicodestring && _RtlInitAnsiString && _RtlFreeUnicodeString && _NtOpenSection && _NtMapViewOfSection && _NtUnmapViewOfSection && _NtQuerySystemInformation) return TRUE; return FALSE; GetModuleHandle() 을이용하여현재 Application 에 Load 되어있는 ntdll.dll 의 Base 주소를구해온후, GetProcAddress() 를사용하여사용할함수들의주소를구해오는함수입니다. HANDLE openphymem() HANDLE hphymem; OBJECT_ATTRIBUTES oattr; ANSI_STRING astr; _RtlInitAnsiString(&aStr, "\\device\\physicalmemory"); UNICODE_STRING ustr; if(_rtlansistringtounicodestring(&ustr, &astr, TRUE)!= STATUS_SUCCESS) return INVALID_HANDLE_VALUE; oattr.length = sizeof(object_attributes); oattr.rootdirectory = NULL; oattr.attributes = OBJ_CASE_INSENSITIVE; oattr.objectname = &ustr; oattr.securitydescriptor = NULL; oattr.securityqualityofservice = NULL; if(_ntopensection(&hphymem, SECTION_MAP_READ SECTION_MAP_WRITE, &oattr )!= STATUS_SUCCESS) return INVALID_HANDLE_VALUE;
return hphymem; ntdll 의 NtOpenSection() 를이용하여 \\device\\physicalmemory 를 READ & WRITE 권한으로열어옵니다. LPVOID loaddll(char *dllname) char modulefilename[max_path + 1]; LPVOID ptrloc = NULL; MZHeader mzh2; PE_Header peh2; PE_ExtHeader pexh2; SectionHeader *sechdr2; GetSystemDirectory(moduleFilename, MAX_PATH); if((mystrlena(modulefilename) + mystrlena(dllname)) >= MAX_PATH) return NULL; strcat(modulefilename, dllname); // load this EXE into memory because we need its original Import Hint Table HANDLE fp; fp = CreateFile(moduleFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if(fp!= INVALID_HANDLE_VALUE) BY_HANDLE_FILE_INFORMATION fileinfo; GetFileInformationByHandle(fp, &fileinfo); DWORD filesize = fileinfo.nfilesizelow; //printf("size = %d\n", filesize); if(filesize) LPVOID exeptr = HeapAlloc(GetProcessHeap(), 0, filesize); if(exeptr) DWORD read; if(readfile(fp, exeptr, filesize, &read, NULL) && read == filesize) if(readpeinfo((char *)exeptr, &mzh2, &peh2, &pexh2, &sechdr2))
int imagesize = calctotalimagesize(&mzh2, &peh2, &pexh2, sechdr2); //ptrloc = VirtualAlloc(NULL, imagesize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); ptrloc = HeapAlloc(GetProcessHeap(), 0, imagesize); if(ptrloc) loadpe((char *)exeptr, &mzh2, &peh2, &pexh2, sechdr2, ptrloc); HeapFree(GetProcessHeap(), 0, exeptr); CloseHandle(fp); return ptrloc; 시스템디렉토리에서인자로넘어온프로그램을열어메모리를할당하여 EXCUTE & READ & WRITE 권한으로메모리를할당하고로드하는역할을합니다. DWORD procapiexportaddr(dword hmodule, char *apiname) if(!hmodule!apiname) return 0; char *ptr = (char *)hmodule; ptr += 0x3c; // offset 0x3c contains offset to PE header ptr = (char *)(*(DWORD *)ptr) + hmodule + 0x78; // offset 78h into PE header contains addr of export table ptr = (char *)(*(DWORD *)ptr) + hmodule; // ptr now points to export directory table // offset 24 into the export directory table == number of entries // in the Export Name Pointer Table // table DWORD numentries = *(DWORD *)(ptr + 24); //printf("numentries = %d\n", numentries);
DWORD *ExportNamePointerTable = (DWORD *) (*(DWORD *)(ptr + 32) + hmodule); // offset 32 into export directory contains offset to Export Name Pointer Table DWORD ordinalbase = *((DWORD *)(ptr + 16)); //printf("ordinalbase is %d\n", ordinalbase); WORD *ExportOrdinalTable = (WORD *) ((*(DWORD *)(ptr + 36)) + hmodule); // offset 36 into export directory contains offset to Ordinal Table DWORD *ExportAddrTable = (DWORD *) ((*(DWORD *)(ptr + 28)) + hmodule); // offset 28 into export directory contains offset to Export Addr Table for(dword i = 0; i < numentries; i++) char *exportname = (char *)(ExportNamePointerTable[i] + hmodule); if(mystrcmpa(exportname, apiname) == TRUE) WORD ordinal = ExportOrdinalTable[i]; //printf("%s (i = %d) Ordinal = %d at %X\n", // exportname, i, ordinal, ExportAddrTable[ordinal]); return (DWORD)(ExportAddrTable[ordinal]); return 0; 첫번쨰인자로넘어온 Base 주소에 Load 되어있는 PE 포맷을가진파일로부터두번쨰인자로넘어온이름을 Export Descriptor Table 에서찾아서리턴하여줍니다. DWORD getkernelbase(void) HANDLE hheap = GetProcessHeap(); NTSTATUS Status; ULONG cbbuffer = 0x8000; PVOID pbuffer = NULL; DWORD retval = DEF_KERNEL_BASE; do pbuffer = HeapAlloc(hHeap, 0, cbbuffer);
if (pbuffer == NULL) return DEF_KERNEL_BASE; Status = _NtQuerySystemInformation(SystemModuleInformation, pbuffer, cbbuffer, NULL); if(status == STATUS_INFO_LENGTH_MISMATCH) HeapFree(hHeap, 0, pbuffer); cbbuffer *= 2; else if(status!= STATUS_SUCCESS) HeapFree(hHeap, 0, pbuffer); return DEF_KERNEL_BASE; while (Status == STATUS_INFO_LENGTH_MISMATCH); DWORD numentries = *((DWORD *)pbuffer); SYSTEM_MODULE_INFORMATION *smi = (SYSTEM_MODULE_INFORMATION *)((char *)pbuffer + sizeof(dword)); for(dword i = 0; i < numentries; i++) if(strcmpi(smi->imagename, "ntoskrnl.exe")) //printf("%.8x - %s\n", smi->base, smi->imagename); retval = (DWORD)(smi->Base); break; smi++; HeapFree(hHeap, 0, pbuffer); return retval; NtQuerySystemInformation() 을이용하여 Process 목록을열거하고그중, "ntoskrnl.exe" 를찾아보고, 찾는데성공하면 Base 주소를리턴하여줍니다. BOOL mapphymem(handle hphymem, DWORD *phyaddr, DWORD *length, PVOID *virtualaddr) NTSTATUS ntstatus; PHYSICAL_ADDRESS viewbase; *virtualaddr = 0;
viewbase.quadpart = (ULONGLONG) (*phyaddr); ntstatus = _NtMapViewOfSection(hPhyMem, (HANDLE)-1, virtualaddr, 0, *length, &viewbase, length,viewshare, 0, PAGE_READWRITE ); if(ntstatus!= STATUS_SUCCESS) printf("failed to map physical memory view of length %X at %X!", *length, *phyaddr); return FALSE; *phyaddr = viewbase.lowpart; return TRUE; 할당했던메모리를물리적주소를얻기위해메핑하는역할을합니다. BOOL buildnativeapitable(dword hmodule, char *nativeapinames[], DWORD numnames) if(!hmodule) return FALSE; char *ptr = (char *)hmodule; ptr += 0x3c; // offset 0x3c contains offset to PE header ptr = (char *)(*(DWORD *)ptr) + hmodule + 0x78; // offset 78h into PE header contains addr of export table ptr = (char *)(*(DWORD *)ptr) + hmodule; // ptr now points to export directory table // offset 24 into the export directory table == number of entries in the //Name Pointer Table // table DWORD numentries = *(DWORD *)(ptr + 24); DWORD *ExportNamePointerTable = (DWORD *)(*(DWORD *) (ptr + 32) + hmodule); // offset 32 into export directory contains offset to Export Name Pointer Table DWORD ordinalbase = *((DWORD *)(ptr + 16)); WORD *ExportOrdinalTable = (WORD *)((*(DWORD *) (ptr + 36)) + hmodule); // offset 36 into export directory contains offset to Ordinal Table DWORD *ExportAddrTable = (DWORD *)((*(DWORD *)
(ptr + 28)) + hmodule); // offset 28 into export directory contains offset to Export Addr Table for(dword i = 0; i < numentries; i++) // i now contains the index of the API in the Ordinal Table // ptr points to Export directory table WORD ordinalvalue = ExportOrdinalTable[i]; DWORD apiaddr = (DWORD)ExportAddrTable[ordinalValue] + hmodule; char *exportname = (char *)(ExportNamePointerTable[i] + hmodule); // Win2K if(gwinversion == 0 && *((unsigned char *)apiaddr) == 0xB8 && *((unsigned char *)apiaddr + 9) == 0xCD && *((unsigned char *)apiaddr + 10) == 0x2E) DWORD servicenum = *(DWORD *)((char *)apiaddr + 1); if(servicenum < numnames) nativeapinames[servicenum] = exportname; //printf("%x - %s\n", servicenum, exportname); // WinXP else if(gwinversion == 1 && *((unsigned char *)apiaddr) == 0xB8 && *((unsigned char *)apiaddr + 5) == 0xBA && *((unsigned char *)apiaddr + 6) == 0x00 && *((unsigned char *)apiaddr + 7) == 0x03 && *((unsigned char *)apiaddr + 8) == 0xFE && *((unsigned char *)apiaddr + 9) == 0x7F) DWORD servicenum = *(DWORD *)((char *)apiaddr + 1); if(servicenum < numnames) nativeapinames[servicenum] = exportname; //printf("%x - %s\n", servicenum, exportname); return TRUE; NativeAPI들의 Name배열을만듭니다.
for(dword i = 0; i < sdtcount; i++) if((servicetable[i] - PROT_MEMBASE - kerneloffset)!= fileservicetable[i]) printf("%-25s %3X --[hooked by unknown at %X]--\n", (nativeapinames[i]? nativeapinames[i] : "Unknown API"), i, servicetable[i]); hookcount++; 메모리에새롭게메핑한 ntoskrnl.exe 의함수주소와기존의 ntoskrnl.exe 의함수주소가같은지한개씩비교합니다. for(dword i = 0; i < sdtcount; i++) if((servicetable[i] - PROT_MEMBASE - kerneloffset)!= fileservicetable[i]) servicetable[i] = fileservicetable[i] + PROT_MEMBASE + kerneloffset; printf("[+] Patched SDT entry %.2X to %.8X\n", I, fileservicetable[i] + PROT_MEMBASE + kerneloffset); 기존의 ntoskrnl.exe 의함수주소를한개씩비교하면서새로할당한 ntoskrnl.exe 의함수주소와다르다면, 새로할당한 ntoskrnl.exe 의함수주소로바꿔줍니다. 정리 실행순서를다음과같이정리하여볼수있습니다. 1) Kernel Memory 를 Open 한다. 2) Memory 에 ntoskrnl.exe 를 Load 한다. 3) Load 한메모리에서 KeServiceDescriptorTable 의주소를구한다. 4) 커널의베이스주소를구해온다. 5) Load 한메모리를메핑한다. 6) NativeAPI Name 배열을만든다. 7) Hooking 된함수가있는지검사한다. 8) Hooking 된함수를정상주소로돌린다. 장점 Ring3 에서 ntdll 의함수들을이용하여 Kernel Memory 에존재하는 SSDT 를검사하고, 고쳐준다는점에서획기적입니다. 단점
Rootkit 에의해 \\device\\physicalmemory 을 Open 하는것이이전에막혀있을경우나 Administrator 의권한이아닐경우작동할수없습니다. 2) ServiceTable Relocation by Yeori System Thread 의경우 ServiceTable 로써, KeServiceDescriptorTable 을, GUI Thread 의경우 KeServiceDescriptorTableShadow 을사용한다고하였습니다. 이를확인해보기위해 System Process 의 Thread 와 A.exe 라는 User Application 의 Thread 에 ServiceTable 이각각가르키는주소를덤프하여보았습니다. KeServiceDescriptorTable : 8055B480 A8 46 4E 80 00 00 00 00 1C 01 00 00 B8 1A 51 80 8055B490 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8055B4A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8055B4B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 KeServiceDescriptorTableShadow : 8055B440 A8 46 4E 80 00 00 00 00 1C 01 00 00 B8 1A 51 80 8055B450 00 83 99 BF 00 00 00 00 9B 02 00 00 10 90 99 BF 8055B460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8055B470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 KeServiceDescriptorTableShadow 의경우두번째줄에 KeServiceDescriptor Table 의경우 0 으로되어있는부분에값이더들어있는것을볼수있습니다. 이는 GUI 관련처리를하기위해필요한 WINAPI Table 에대한것입니다. KeServiceDescriptorTable 과 KeServiceDescriptorTableShadow 를각각 C 구조체로나타내보면다음과같습니다. typedef struct ServiceDescriptorEntry unsigned int *ServiceTableBase; // 서비스테이블 unsigned int *ServiceCounterTableBase; //Debug 모드에만사용됨 unsigned int NumberOfServices; // 서비스의갯수 unsigned char *ParamTableBase; // 각서비스당 Param 의총크기 ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t; typedef struct ServiceDescriptorShadowEntry unsigned int *ServiceTableBase; // 서비스테이블 unsigned int *ServiceCounterTableBase; //Debug 모드에만사용됨 unsigned int NumberOfServices; // 서비스의갯수 unsigned char *ParamTableBase; // 각서비스당 Param 의총크기 unsigned int *Win32kTableBase; //WINAPI 서비스테이블
unsigned int *Win32kCounterTableBase; //Debug 모드에만사용됨 unsigned int NumberofWin32kServices; //WINAPI 서비스의갯수 unsigned char *Win32kParamTableBase; // 각서비스당 Param 의총크기 ServiceDescriptorTableShadowEntry_t, *PServiceDescriptorTableShadowEntry_t; KiFastCallEntry() 는요구된 NativeAPI 를처리하기위해직접적으로위의 ServiceDescriptorTable 들을사용하는것이아니라, 요청한 ETHREAD 의 ServiceTable 를참조한다고하였습니다. 그럼으로 ServiceDescriptorTable 을별도로만들고, ETHREAD 의 ServiceTable 을별도로만든 ServiceDescriptor Table 로연결시킨다면, SSDT Hooking 을해당 Thread 에한하여무력화시킬수있을것입니다. 이가정을전제로만든코드의핵심부분을보겠습니다. SDT = &KeServiceDescriptorTable; NewServiceTable = ExAllocatePool(NonPagedPool,(SDT->NumberOfServices) * 4); memcpy(newservicetable,sdt->servicetablebase,(sdt->numberofservices) * 4); Service 개수 * 4 의크기를가진메모리를할당하고, 그곳에본래 ServiceTable 에있던값들을전부복사하여넣습니다. OrgSDT = (PServiceDescriptorTableEntry_t)* (ethread + ThrdOffset_ServiceTable); //Thread 가가지고있던 Original ServiceTable 주소저장. DbgPrint("Original SDT : 0x%X",OrgSDT); SDT_s[ThreadCount] = OrgSDT; // 관리를위한테이블에저장해둔다. ETHREAD_s[ThreadCount] = ethread; // NewSDT[ThreadCount] = ExAllocatePool(NonPagedPool,128); // 새로운메모리할당 memcpy(newsdt[threadcount],orgsdt,128); // 복사한다. ServiceTable[ThreadCount] // 서비스테이블을위한메모리할당. = ExAllocatePool(NonPagedPool,(SDT->NumberOfServices) * 4); memcpy(servicetable[threadcount],newservicetable, (SDT->NumberOfServices) * 4); // 메모리를복사한다. DbgPrint("New Service Table : 0x%X",ServiceTable[ThreadCount]); NewSDT[ThreadCount]->ServiceTableBase = ServiceTable[ThreadCount]; // 관리를위한테이블에저장해둔다. g_pmdl_keservicetable[threadcount] = MmCreateMdl(NULL,NewSDT[ThreadCount],128); //MDL 을만든다.
MmBuildMdlForNonPagedPool(g_pmdl_KeServiceTable[ThreadCount]); g_pmdl_keservicetable[threadcount]->mdlflags =g_pmdl_keservicetable[threadcount]->mdlflags MDL_MAPPED_TO_SYSTEM_VA MDL_WRITE_OPERATION MDL_IO_PAGE_READ; //MDL 속성을변환한다. Mapped_KeServiceTable[ThreadCount] = MmMapLockedPages(g_pmdl_KeServiceTable[ThreadCount], KernelMode); DbgPrint("New SDT : 0x%X",Mapped_KeServiceTable[ThreadCount]); pmdl_sdt_pointer = MmCreateMdl(NULL, SDT_Pointer, 4); MmBuildMdlForNonPagedPool(pmdl_SDT_Pointer); pmdl_sdt_pointer->mdlflags = pmdl_sdt_pointer->mdlflags MDL_MAPPED_TO_SYSTEM_VA MDL_WRITE_OPERATION MDL_IO_PAGE_READ; Mapped_SDT_Pointer = MmMapLockedPages(pmdl_SDT_Pointer, KernelMode); *Mapped_SDT_Pointer = Mapped_KeServiceTable[ThreadCount]; // 새로만든 ServiceTable 을가르키도록한다. MmUnmapLockedPages(Mapped_SDT_Pointer,pmdl_SDT_Pointer); IoFreeMdl(pmdl_SDT_Pointer); //MDL 을해제한다. 위의코드가 SDT Relocation 의핵심적인부분입니다. 위의코드는완성되지는않았습니다. 그이유는 Thread 의생성단계에서는 Thread 의 ETHREAD 에 ServiceTable 은모두 KeServiceDescriptorTable 을포인트하다가, KiFastCallEntry() 에의해 Service 를처리할떄, KiFastCallEntry() 내에서내부적으로호출하는 PsConvertToGuiThread() 라는함수에의해서 WINAPI 의처리가필요할경우그때에 KeServiceDescriptorTable Shadow 를포인트하도록변하기때문입니다. 정리 실행순서를다음과같이정리하여볼수있습니다. 1) 기존의 ServiceTable 로부터함수들의주소들을복사한새로운 ServiceTable 을만든다. 2) 지정한 Process 의 Thread 들을트레이싱하면서각 ETHREAD 에대해새로운 SDT 를만들어준다. 3) PsSetCreateThreadNotifyRoutine() 에의해 THREAD 가동적생성이 Notify 되면, 해당 Thread 에대한 SDT 를만들어준다. 4) 사용자가기능을중지하기원하는경우, 각 Thread 의 ServiceTable 을저장해두었던, 것으로복구시키고, 사용했던메모리들을모두 Release 시킨다. 장점 ServiceTable 을새로만드는것임으로, SSDT Hooking 에영향을받지않습니다.
단점현재 Code 로는동적으로생성된 GUI Thread 에대해서는처리할수없습니다. 3) KiFastCallEntry Imitation by Dual Kernel 단의처리가필요한함수들은 SYSENTER 에의해 Ring0 로변환된후, 처리된다고하였습니다. 이떄 SYSENTER 는 MSR 중, IA32_SYSENTER_EIP 를참조하여실행할함수를결정하는데, 이 IA32_SYSENTER_EIP 의값을 WRMSR 이라는명령어를통해덮어쓸수있습니다. 만약덮어씀으로써, 새롭게연결된함수에서 KiFastCallEntry() 의기능을수행할수있지만, ETHREAD 의 ServiceTable 을참조하여, 그 ServiceTable 을사용하는것이아니라, 내부적인 ServiceTable 을사용한다면, SSDT Hooking 을전역적으로무력화시킬수있을것입니다. 그럼핵심코드를보도록하겠습니다. VOID InitializeEngine() UNICODE_STRING y; IDTINFO idt_info; IDTENTRY* idt_entries; unsigned int i; DbgPrint("KiFastCallEntry Imitation coded by Dual"); DbgPrint("ProcessorCount : 0x%X",KeNumberProcessors); // 프로세서의갯수를구해온다. gdpcp1 = ExAllocatePool(NonPagedPool,sizeof(KDPC)); KeInitializeDpc(gDPCP1,WRMSRDPC,NULL); gdpcp2 = ExAllocatePool(NonPagedPool,sizeof(KDPC)); KeInitializeDpc(gDPCP2,RestoreMSR,NULL); //DPC 를위한메모리들을할당해둔다. GetProcessNameOffset(); //EPROCESS 에서 Name 까지의 Offset DbgPrint("ProcessName Offset : 0x%X",gProcessNameOffset); DbgPrint("Engine_KiFastCallEntry : 0x%8X",MyKiFastCallEntry); asm mov ecx, 0x176 rdmsr mov d_origkifastcallentry, eax //KiFastCallEntry 의주소를구해온다. DbgPrint("KiFastCallEntry : 0x%8X",d_origKiFastCallEntry);
PsConvertToGuiThread = findaddressofpsconverttoguithread(); //PsConvertToGuiThread 의주소를구해온다. DbgPrint("PsConvertToGuiThread : 0x%8X",PsConvertToGuiThread); _MmUserProbeAddress = *MmUserProbeAddress; DbgPrint("MmUserProbeAddress : 0x%8X",_MmUserProbeAddress); DbgPrint("KeServiceDescriptorTable : 0x%8X",&KeServiceDescriptorTable); DbgPrint("KiServiceTable : 0x%8X",KeServiceDescriptorTable.ServiceTableBase); DbgPrint("ArguementTable : 0x%8X",KeServiceDescriptorTable.ParamTableBase); DbgPrint("Service Limit : 0x%X",KeServiceDescriptorTable.NumberOfServices); //Make New Service Table OrgServiceTable = KeServiceDescriptorTable.ServiceTableBase; NewServiceTable = ExAllocatePool(NonPagedPool, (KeServiceDescriptorTable.NumberOfServices) * 4); memcpy(newservicetable, KeServiceDescriptorTable.ServiceTableBase, (KeServiceDescriptorTable.NumberOfServices) * 4); DbgPrint("NewServiceTableBase : 0x%8X",NewServiceTable); KeServiceDescriptorTableShadow = (PServiceDescriptorTableShadowEntry_t)findAddressofShadowTable(); ShadowSDT = (ULONG)KeServiceDescriptorTableShadow; KeServiceDescriptorTableShadow += 0x1; DbgPrint("KeServiceDescriptorTableShadow : 0x%8X", KeServiceDescriptorTableShadow); DbgPrint("Win32KServiceTable : 0x%8X", KeServiceDescriptorTableShadow->Win32kTableBase); DbgPrint("Win32kArguementTable : 0x%8X", KeServiceDescriptorTableShadow->Win32kParamTableBase); DbgPrint("Win32kService Limit : 0x%X", KeServiceDescriptorTableShadow->NumberofWin32kServices); RtlInitUnicodeString(&y,L"KeRaiseIrql"); _KeRaiseIrql = MmGetSystemRoutineAddress(&y); DbgPrint("KeRaiseIrql : 0x%8X",_KeRaiseIrql); RtlInitUnicodeString(&y,L"KeLowerIrql"); _KeLowerIrql = MmGetSystemRoutineAddress(&y); DbgPrint("KeLowerIrql : 0x%8X",_KeLowerIrql); RtlInitUnicodeString(&y,L"KiDeliverApc"); _KiDeliverApc = MmGetSystemRoutineAddress(&y); DbgPrint("KiDeliverApc : 0x%8X",_KiDeliverApc); RtlInitUnicodeString(&y,L"KeGetCurrentIrql"); _KeGetCurrentIrql = MmGetSystemRoutineAddress(&y);
DbgPrint("KeGetCurrentIrql : 0x%8X",_KeGetCurrentIrql); asm sidt idt_info idt_entries = (IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase); _KiTrap6 = MAKELONG(idt_entries[0x6].LowOffset,idt_entries[0x6].HiOffset); DbgPrint("KiTrap06 : 0x%8X",_KiTrap6); KiFastCallEntry() 를구현하는데있어서필요한함수들의주소를구해오는기능을합니다. Case StartHook: for(i = 0; i < KeNumberProcessors; i++) KeSetTargetProcessorDpc(gDPCP1,i); KeInsertQueueDpc(gDPCP1,&tmp,&tmp); 미리구해둔프로세서의개수만큼반복하면서, 각프로세서의 MSR 에 IA32_SYSENTER_EIP 을새로만든 KiFastCallEntry() 의주소로덮어씁니다. 이유는멀티프로세서환경에서각프로세서마다별도의 MSR 들을가지고있기때문에정상적인처리를위해선전부처리해주어야합니다. VOID WRMSRDPC(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID sys1, IN PVOID sys2) ULONG ProcessorFastCallEntry; DbgPrint("Processor Number : 0x%X",KeGetCurrentProcessorNumber()); asm mov ecx,0x176 mov edx,0 mov eax,mykifastcallentry wrmsr asm mov ecx,0x176 rdmsr mov ProcessorFastCallEntry,eax
if(processorfastcallentry == (ULONG)MyKiFastCallEntry) DbgPrint("Install Success"); else DbgPrint("Install Faild"); VOID RestoreMSR(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID sys1, IN PVOID sys2) ULONG ProcessorFastCallEntry; DbgPrint("Processor Number : 0x%X",KeGetCurrentProcessorNumber()); asm mov ecx,0x176 mov edx,0 mov eax,d_origkifastcallentry wrmsr asm mov ecx,0x176 rdmsr mov ProcessorFastCallEntry,eax if(processorfastcallentry == d_origkifastcallentry) DbgPrint("Restore Success"); else DbgPrint("Restore Faild"); 위에것은설치함수이고, 밑에것은복구함수입니다. DWORD findaddressofshadowtable(void) int i; unsigned char *p; DWORD val;
p = (unsigned char *)KeAddSystemServiceTable; for (i = 0; i < PAGE_SIZE; i++, p++) try val = *(unsigned int *)p; except (EXCEPTION_EXECUTE_HANDLER) return 0; if (MmIsAddressValid((PVOID)val)) if (memcmp((pvoid)val, &KeServiceDescriptorTable, 16) == 0) if((pvoid)val!= &KeServiceDescriptorTable) return val; return 0; DWORD findaddressofpsconverttoguithread(void) int i; unsigned char *p; ULONG val; static char Adr[4]; char CodePattern[3] = 0x52,0x53,0xE8; char PsCovertPattern[3] = 0x6A,0x38,0x68; p = (unsigned char *)d_origkifastcallentry - PAGE_SIZE; for(i = 0; i < PAGE_SIZE*2; i++,p++) if(!memcmp(p,codepattern,3)) val = *(ULONG *)(p + 3) + (ULONG)p + 7; if(mmisaddressvalid((pvoid)val)) if(!memcmp((pvoid)val,pscovertpattern,3)) return val; else
return 0; return 0; 위에것은 ShadowTable 의주소를구해오는함수이고, 밑에것은 PsConvertToGuiThread 의주소를구해오는함수입니다. KeServiceDescriptorTableShadow 와 PsConverToGui Thread 의경우 export 되지않기떄문에, 재가아는한에서는위와같은방법으로구할수밖에없었습니다. 더좋은방법을알고계시다면저에게연락해주세요. :) declspec(naked) MyKiFastCallEntry() asm // 세그 s 먼트셀렉터값지정 mov ecx,0x23 push 0x30 pop fs //FS 를 PCR 로지정 mov ds,cx mov es,cx // 현스택을커널스택으로변경 mov ecx,dword ptr fs:0x40 ////KPCR_TSS mov esp, ss:[ecx+0x4] // 가짜 INT 스택을만든다. push 0x23 //KGDT_R3_DATA + RPL_MASK(0x3) push edx //Ring3 ESP pushfd //Ring3 EFLAGS push 2 //Ring 0 EFLAGS add edx,8 //Skip user param popfd //Set EFLAGS or byte ptr [esp+0x1],0x2 // 가짜 INT 로 IRQ 재활성화 push 0x1B //RGDT_R3_CODE + RPL_MASK push KiFastSystemCallRet //sysenter리턴어드레스 push 0 push ebp push ebx push esi
push edi // 우리의 PCR 로포인터저장 mov ebx,dword ptr fs:0x1c push 0x38 + 0x3 //KGDT_R3_TEB + RPL_MASK // 현재스레드의포인터구함 mov esi,[ebx+0x124] // 예외핸들러체인종료자지정 push dword ptr[ebx] mov dword ptr[ebx],-1 // 스레드의스택사용 mov ebp,[esi+0x18] //KTHREAD_INITIAL_STACK push 0x1 //Usermode //push dword ptr[esi+0x140] //KTHREAD_PREVIOUS_MODE // 다른레지스터들을넘긴다. sub esp,0x48 //mov dword ptr [esp+0x38], 0x23 //mov dword ptr [esp+0x34], 0x23 // 스택에우리의공간을만든다. sub ebp,0x29c // 모드를써넣는다. mov byte ptr[esi+0x140],0x1 //KTHREAD_PREVIOUS_MODE DEBUG_STATUS: //Sanity check cmp ebp,esp jne BadStack //Flush DR7 and dword ptr[ebp+0x2c],0 // 스레드가디버깅당하는중인가? test byte ptr[esi+0x2c],0xff //KTHREAD_DEBUG_ACTIVE // 스레드의트렙프레임설정 mov [esi+0x134],ebp //KTHREAD_TRAP_FRAME 0x134? or 0x110? jnz Dr_FastCallDrSave
// 트렙프레임디버그헤드설정 mov ebx,dword ptr[ebp+0x60] mov edi,dword ptr[ebp+0x68] //KTRAP_FRAME_EBP //KTRAP_FRAME_EIP // 커널데이터기록 mov dword ptr[ebp+0xc],edx //KTRAP_FRAME_DEBUGPOINTER mov dword ptr[ebp+0x8],0xbadb0d00 //KTRAP_FRAME_DEBUGARGMARK mov dword ptr[ebp],ebx //stack 세이브 mov dword ptr[ebp+0x4],edi //KTRAP_FRAME_DEBUGEIP // 인터럽트활성화 sti /* SysCallEntry: 여기서 SDT를이용하여함수호출 */ ///////////////////////////////////////////////////////////// mov edi,eax shr edi,0x8 //SERVICE_TABLE_SHIFT and edi,0x30 //SERVICE_TABLE_MASK mov ecx,edi //add thread`s base system table to offset add edi,dword ptr[esi+0xe0] //KTHREAD_SERVICE_TABLE //GetSyscallID mov ebx,eax and eax,0xfff //SERIVCE_NUMBER_MASK //check syscallid cmp eax,dword ptr[edi+0x8] //SERVICE_DESCRIPTOR_LIMIT //Invalid? jnb UnexpectedRange //Check Win32K cmp ecx,0x10 //SERVICE_TABLE_TEST jnz NotWin32K //Get TEB mov ecx,dword ptr fs:0x18 //KPCR_TEB //check flush? xor ebx,ebx or ebx,dword ptr[ecx+0xf70] //TEB_GDI_BATCH_COUNT
je NotWin32K //=jz //Flust it push edx push eax call NtGdiFlushUserBatch pop eax pop edx NotWin32K: //inc dword ptr fs:[0x638] inc dword ptr fs:0x638 //KPCR_SYSTEM_CALLS //KPCR_SYSTEM_CALLS //NoCountTable: mov esi,edx // 인자들공간확보 mov ebx,dword ptr[edi+0xc] //SERVICE_DESCRIPTOR_NUMBER xor ecx,ecx mov cl,byte ptr [eax+ebx] //Get the function point mov edi,dword ptr[edi] //Service_Descriptor_Base //Blocking SSDT Hooking cmp edi,orgservicetable jne Shadow Shadow: NotShadow: mov edi,newservicetable jmp NotShadow mov edi,newshadowtable mov ebx,dword ptr[edi+eax*4] // 우리스택공간확보 sub esp,ecx // 인자 & 목적지크기지정 shr ecx,2 mov edi,esp //MmUserProbeAddress인지확인 cmp esi,_mmuserprobeaddress jnb AccessViolation //0x7FFF0000 CopyParams:
rep movsd //call syscall call ebx AfterSyscall: mov esp,ebp ///////////////////////////////////////////////////////////// KeReturnFromSystemCall: mov ecx,dword ptr fs:0x124 //KPCR_CURRENT_THREAD // 프레임포인터복구 mov edx,dword ptr[ebp+0x3c] mov dword ptr[ecx+0x134],edx //KTHREAD_TRAP_FRAME 0x110? 0x134? //KTRAP_FRAME_EDX // 인터럽트비활성화 cli test dword ptr[ebp+0x70],0x20000 //KTHREAD_COMBINED_APC_DISABLE(0x70),EFLAGS_V86_MASK(0x20000) jnz SKIP_CS_TEST test byte ptr[ebp+0x6c],0x1 //KTRAP_FRAME_CS je SKIP_SAVE_FRAME SKIP_CS_TEST: // 현재스레드구함 //mov ebx,dword ptr fs:edx+0x124 //KPCR_CURRENT_THREAD mov ebx,dword ptr fs:0x124 mov byte ptr [ebx+0x2e],0 //KTHREAD_ALERTED 0x5E? 0x2E? cmp byte ptr[ebx+0x4a],0 je SKIP_SAVE_FRAME //KTHREAD_PENDING_USER_APC // 스텍포인터를트렙프레임에저장 mov ebx,ebp mov dword ptr[ebx+0x44],eax //KTRAP_FRAME_EAX mov dword ptr[ebx+0x50],0x3b //KTRAP_FRAME_FS<-KGDT_R3_TEB + RPL_MASK mov dword ptr[ebx+0x38],0x23 //KTRAP_FRAME_DS<-KGDT_R3_DATA + RPL_MASK mov dword ptr[ebx+0x34],0x23 //KTRAP_FRAME_ES<-KGDT_R3_DATA + RPL_MASK
mov dword ptr[ebx+0x30],0x0 //KTRAP_FRAME_GS //IRQL 을 APC LEVEL 로 mov ecx,1 // 과거 IRQL 저장 call _KeGetCurrentIrql //Hack for Hyper Thread push eax push offset TmpOldIrql //Temp for OldIrql push 1 //APC_LEVEL call _KeRaiseIrql // 인터럽트활성화 sti push ebx push 0x0 push 0x1 // 유저모드 call _KiDeliverApc // 과거 IRQL 로복귀 pop ecx push ecx //:) call _KeLowerIrql //EAX 복구 mov eax,dword ptr[ebx+0x44] //KTRAP_FRAME_EAX // 인터럽트비활성화 cli jmp SKIP_CS_TEST SKIP_SAVE_FRAME: //mov ds,[esp+0x38] //mov es,[esp+0x34] mov edi,edi mov edx,dword ptr[esp+0x4c] //KTRAP_FRAME_EXCEPTION_LIST mov ebx,dword ptr fs:[0x50] //KTRAP_FRAME_FS mov dword ptr fs:[0],edx mov ecx,dword ptr[esp+0x48] //KTRAP_FRAME_PREVIOUS_MODE mov esi,dword ptr fs:[0x124] //KPCR_CURRENT_THREAD mov byte ptr[esi+0x140],cl //KTHREAD_PREVIOUS_MODE test ebx,0xff //MAXIMUM_IDTVECTOR? jnz FullIDTVector
CHECK_FOR_V86: //Check for V86 test dword ptr[esp+0x70],0x20000 //KTRAP_FRAME_EFLAGS,EFLAGS_V86_MASK jnz V86_EXIT test word ptr[esp+0x6c],0xfff8 //KTRAP_FRAME_CS,FRAME_EDITED jz RestoreCS cmp word ptr[esp+0x6c],0x1b bt word ptr[esp+0x6c],0 cmc ja RestoreEAX //KGDT_R3_CODE + RPL_MASK cmp dword ptr[ebp+0x6c],0x8 //KTRAP_FRAME_CS,KGDT_R0_CODE jz SkipDebugInformation RestoreFS: //Restore FS lea esp,dword ptr[ebp+0x50] //KTRAP_FRAME_FS pop fs SkipDebugInformation: lea esp,dword ptr[ebp+0x54] //KTRAP_FRAME_EDI pop edi pop esi pop ebx pop ebp //ABIOS 를위한체크? cmp word ptr[esp+0x8],0x80 ja ABIOS_Exit //Pop Error Code add esp,4 //Previous CS from usermode? test dword ptr[esp+4],1 jnz FastExit IntReturn: pop edx pop ecx popfd jmp edx iretd
FastExit: test byte ptr[esp+0x9],0x1 jnz IntReturn // 클린업스택 pop edx add esp,4 and byte ptr[esp+1],0xfd popfd pop ecx jmp short $+3 mov eax,90350ffbh //sti sysexit iretd BadStack: mov ecx,fs:0x40 //KPCR_TSS mov esp,dword ptr[ecx+0x4] //KTSS_ESP0 //V86 stack push 0 push 0 push 0 push 0 push 0x23 //KGDT_R3_DATA + RPL_MASK push 0 push 0x20202 push 0x1B //KGDT_R3_CODE + RPL_MASK push 0 jmp _KiTrap6 //KiTrap06 Dr_FastCallDrSave: test dword ptr[ebp+0x70],20000 ////KTRAP_FRAME_EFLAGS,EFLAGS_V86_MASK jnz SKIP_CS_TEST3 test dword ptr[ebp+0x6c],1 ////KTRAP_FRAME_CS je DEBUG_STATUS SKIP_CS_TEST3: mov ebx,dr0 mov ecx,dr1 mov edi,dr2 mov dword ptr[ebp+0x18],ebx mov dword ptr[ebp+0x1c],ecx mov dword ptr[ebp+0x20],edi mov ebx,dr3 mov ecx,dr6
mov edi,dr7 mov dword ptr[ebp+0x24],ebx mov dword ptr[ebp+0x28],ecx xor ebx,ebx mov dword ptr[ebp+0x2c],edi mov DR7,ebx mov edi,dword ptr fs:0x20 mov ebx,dword ptr[edi+0x2f8] mov ecx,dword ptr[edi+0x2fc] mov DR0,ebx mov DR1,ecx mov ebx,dword ptr[edi+0x300] mov ecx,dword ptr[edi+0x304] mov DR2,ebx mov DR3,ecx mov ebx,dword ptr[edi+0x308] mov ecx,dword ptr[edi+0x30c] mov DR6,ebx mov DR7,ecx jmp DEBUG_STATUS UnexpectedRange: cmp ecx,0x10 //SERVICE_TABLE_TEST jnz InvalidCall //Setup Win32K Table push edx push ebx call PsConvertToGuiThread // 리턴코드체크 or eax,eax // 레지스터리스토어 pop eax pop edx // 트렙프레임리셋 mov ebp,esp mov [esi+0x134],ebp //KTHREAD_TRAP_FRAME // 다시콜 jz SysCallEntry // 테이블 limit 과베이스구함 lea edx, KeServiceDescriptorTable + 0x10
mov ecx,dword ptr[edx+0x8] //SERVICE_DESCRIPTOR_LIMIT mov edx,dword ptr[edx] //SERVICE_DESCRIPTOR_BASE // 테이블주소와인덱스값더한주소구함 lea edx,dword ptr[edx+ecx*4] and eax,0xfff //SERVICE_NUMBER_MASK add edx,eax // 리턴값구함 movsx eax,byte ptr[edx] or eax,eax jle KeReturnFromSystemCall // 잘못된서비스로리턴값넘김 mov eax, 0xC000001CL //STATUS_INVALID_SYSTEM_SERVICE jmp KeReturnFromSystemCall InvalidCall: mov eax, 0xC000001CL //STATUS_INVALID_SYSTEM_SERVICE jmp KeReturnFromSystemCall AccessViolation: // 커널모드로부터의엑세스인가? test byte ptr [ebp+0x6c],0x1 //KTRAP_FRAME_CS,MODE_MASK // 괞찮다면계속진행 je CopyParams // 실패잘못된인자 mov eax,0xc0000005l jmp AfterSyscall FullIDTVector: test dword ptr[ebp+0x70],0x20000 //KTRAP_FRAME_EFLAGS,EFLAGS_V86_MASK jnz SkIP_CS_TEST2 test dword ptr[ebp+0x6c],0x1 je CHECK_FOR_V86 SKIP_CS_TEST2: xor ebx,ebx mov esi,dword ptr[ebp+0x18] mov edi,dword ptr[ebp+0x1c] mov DR7,ebx mov DR0,esi //KTRAP_FRAME_CS
mov ebx,dword ptr[ebp+0x20] mov DR1,edi mov DR2,ebx mov esi,dword ptr[ebp+0x24] mov edi,dword ptr[ebp+0x28] mov edi,dword ptr[ebp+0x2c] mov DR3,esi mov DR6,edi mov DR7,ebx jmp CHECK_FOR_V86 RestoreEAX: mov eax,[esp+0x44] //KTRAP_FRAME_EAX //skip registers add esp,0x30 //Restore segments pop gs pop es pop ds pop edx pop ecx //Jmp back to mainline jmp RestoreFS RestoreCS: mov ebx,[esp+0x10] //KTRAP_FRAME_TEMPCS mov [esp+0x6c],ebx //KTRAP_FRAME_CS mov ebx,[esp+0x14] //KTRAP_FRAME_TEMPESP sub ebx,0xc mov [esp+0x64],ebx //KTRAP_FRAME_ERROR_CODE //Copy Interrupt Stack mov esi,[esp+0x70] //KTRAP_FRAME_EFLAGS mov [ebx+8],esi mov esi,[esp+0x6c] //KTRAP_FRAME_CS mov [ebx+4],esi mov esi,[esp+0x68] //KTRAP_FRAME_EIP mov [ebx],esi // 리턴 add esp,0x54 pop edi pop esi //KTRAP_FRAME_EDI
pop ebx pop ebp mov esp,[esp] iretd V86_Exit: add esp,0x3c //KTRAP_FRAME_EDX //Restore pop edx pop ecx pop eax lea esp,[ebp+0x54] //KTRAP_FRAME_EDI pop edi pop esi pop ebx pop ebp cmp word ptr [esp+8],0x80 ja ABIOS_Exit SKIP_LSS: ABIOS_Exit: //skip error code add esp,4 iretd cmp word ptr[esp+0x2],0 je SKIP_LSS cmp word ptr[esp],0 jnz SKIP_LSS shr dword ptr[esp],0x10 mov word ptr[esp+0x2],0xf8 lss sp,dword ptr[esp] movzx esp,sp iretd 위의코드는 Windows XP SP2 의 KiFastCallEntry() 를 Reverse Engineering 하여만든것이기떄문에다른 Windows 에서는동작하지않을것입니다. 코드의내용중, 기존의 KiFastCallEntry() 와다른부분이있는데, 밑의코드입니다. //Blocking SSDT Hooking cmp edi,orgservicetable //edi 가 KeServiceDescriptorTable 인가? jne Shadow // 아니라면 KeServiceDescriptorShadow 로인식 mov edi,newservicetable // 프로그램내부의새 ServiceTable 을사용
Shadow: NotShadow: jmp NotShadow mov edi,newshadowtable //Shadow 일경우 WINAPI 에관한정보가 // 추가된 SDT 사용 위의코드를통해서프로그램내부의 ServiceTable 을사용하도록변경됨으로, SSDT Hooking 을전역적으로무력화시킬수있습니다. SYSENTER 를사용하지않는 int 0x2e 에대해서도 IDT 에 0x2e 번을다음함수의주소로수정하여처리하여줄수있습니다. declspec(naked) MyKiSystemService() asm push 0 push ebp push ebx push edi push fs mov ebx,0x30 mov fs,bx push dword ptr fs:0 mov dword ptr fs:0,-1 mov esi,dword ptr fs:0x124 push dword ptr[esi+0x140] sub esp,0x48 mov ebx,dword ptr[esp+0x6c] and ebx,0x1 mov byte ptr[esi+0x140],bl mov ebp,esp mov ebx,dword ptr[esi+0x134] mov dword ptr[ebp+0x3c],ebx mov dword ptr[esi+0x134],ebp cld mov ebx,dword ptr[ebp+0x60] mov edi,dword ptr[ebp+0x68] mov dword ptr[ebp+0xc],edx mov dword ptr[ebp+0x8],0xbadb0d00 mov dword ptr[ebp],ebx mov dword ptr[ebp+0x4],edi test byte ptr[esi+0x2c],0xff jnz DebugStatus ExitService: sti
jmp SyscallEntry DebugStatus: test dword ptr [ebp+0x70],0x20000 jnz SkipCheck test dword ptr [ebp+0x6c],1 je ExitService SkipCheck: mov ebx,dr0 mov ecx,dr1 mov edi,dr2 mov dword ptr [ebp+0x18],ebx mov dword ptr [ebp+0x1c],ecx mov dword ptr [ebp+0x20],edi mov ebx, DR3 mov ecx, DR6 mov edi, DR7 mov dword ptr [ebp+0x24],ebx mov dword ptr[ebp+0x28],ecx xor ebx,ebx mov dword ptr [ebp+0x2c],edi mov DR7,ebx mov edi,dword ptr fs:[0x20] mov ebx,dword ptr[edi+0x2f8] mov ecx,dword ptr[edi+0x2fc] mov DR0,ebx mov DR1,ecx mov ecx,dword ptr[edi+0x304] mov DR2,ebx mov DR3,ecx mov ebx,dword ptr[edi+0x308] mov ecx,dword ptr[edi+0x30c] mov DR6,ebx mov DR7,ecx jmp ExitService 정리 실행순서를다음과같이정리하여볼수있습니다. 1) 구현에필요한함수들의주소를구해온다. 2) 각프로세서의 MSR 에 0x176 번을새로만든함수로연결하여준다. 3) 사용자가기능중지를원할경우 MSR 의 0x176 번을기존의 KiFastCallEntry() 주소로복구시킨다. 장점시스템전역적으로 SSDT Hooking 을무력화시킬수있으며, 프로그램내부적으로는 Hooking 을사용할수있음으로, 경쟁상태에서우위를차지할수있습니다.
단점 0x176 번이다른프로세스에의해덮어쓰여질경우, 재기능을발휘할수없습니다. 4. 작동여부테스트 실제위에서다루었던내용들이작동하는지에대한테스트를하겠습니다. 시나리오는간단합니다. SSDT Hooking 을사용하여 notepad.exe 를숨긴후, 3 번에서다루었던무력화방법들을사용하여어떤작용을하는지보겠습니다. 후킹하는함수들은다음과같습니다.. ZwOpenProcess() ZwWriteVirtualMemory() ZwReadVirtualMemory() ZwQuerySystemInformation() 4-1. SDT-RESTORE Hooking 을하기전에는프로세스목록에서 notepad.exe 를볼수있습니다.
Hooking 을시작하니 Process 목록에서 notepad.exe 가사라졌습니다. SDT-RESTORE 를위와같이이용하여 SDT 들을복구시켜주었습니다. 다시 notepad.exe 를볼수있게되었습니다.
4-2. SDT RELOCATION Hooking 전이라 notepad.exe 를목록에서볼수있습니다. Hooking 을시작하여서 notepad.exe 가목록에서사라진것을볼수있습니다.
SDT RELOCATION 시키자, SSDT Hooking 여부에상관없이, notepad.exe 를프로세스목록에서다시볼수있게되었습니다. 4-3. KiFastCallEntry() Imitation 현재의 KiFastCallEntry() Imitation 에는 ServiceTable 을기존시스템의것을그대로사용하고있습니다. 그럼으로현재의코드에서는 KiFastCallEntry() Imitation 을 Hooking 하는녀석보다먼저실행시켜주어야합니다. 만약 ServiceTable 을하드코딩을하거나, SDT-RESTORE 처럼재메핑하여꺠끗한 ServiceTable 을제공해준다면보다강력해질것입니다. Hooking 전이라 notepad.exe 를프로세스목록에서볼수있습니다.
KiFastCallEntry() Imitation 을작동시켰습니다. Hooking 중인상태이나, notepad.exe 를프로세스목록에서볼수있습니다. 위의테스트를통해 SSDT Hooking 무력화가실제로가능한것을알수있습니다. 재가다루었던이방법외에도훨씬강력한방법이존재할수있습니다. 읽고게시는분께서무언가만들고자하는의지와열정만있으시다면분명획기적인방법을고안해내실수있으리라믿습니다. 그럼이만이글을마치도록하겠습니다.
5. References 참고문헌 [1] Greg Hoglund, James Bulter, Rootkits : Subverting the Windows Kernel [2] 정덕영, Windows 구조와원리그리고 CODES [3] Rootkit.com - http://rootkit.com [4] 여리의홈페이지 - http://zap.pe.kr - End