源代码下载地址:挂钩SSDT源代码
据微软所言,服务描述符表是一个由四个结构组成的数组,其中的每一个结构都是由四个双字项组成。因此,我们可以将服务描述符表表示为:
typedef struct ServiceDescriptorTable
{
SDE ServiceDescriptor[4];
}SDT;
其中,其中的每一个服务描述符呈现出四个双字的形式,它的结构为:
#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase; //Used only in checked build
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} SDE,*PSDE;
#pragma pack()
我们寻找的数据结构SSDT是由第一个域所引用的调用表(可以在调试器中用命令dps nt!KiServiceTable查看)
一、 禁用WP位
如果我们能够简单的将值换人并换出SSDT该有多好,然而障碍在于SSDT存在于只读内存中。因此,为了挂钩由SSDT引用的例程,我们一般的策略看起来应该类似于以下形式(以伪代码表示):
DisableReadProtection();
ModifySSDT();
EnableReadProtection();
英特尔的文档写明:“如果CR0.WP=1,访问类型由页目录和页表项的R/W标志位决定。如果CR0.WP=0,超级用户权限允许读写访问。”因此,为破坏SSDT上的写保护,我们需要临时清除写保护(Write Protect,WP)标志。
通过分配自己的MDL来描述SSDT,我们可以禁用读保护。MDL与存储SSDT内容的物理内存页相关联,MDL元素结构在WDK附带的wdm.h头文件中的定义为:
typedef struct _MDL
{
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
}MDL,*PMDL;
对于物理内存的这一区域,一旦我们添加了自己的私有描述,就可以使用按位OR以及MDL_MAPPED_TO_SYSTEM_VA宏调整MDL的权限。再一次,我们可以成功实现,因为我们拥有自己的MDL对象。最后,我们使用SSDT在物理内存中的位置与MDL之间的映射正式化。
然后,锁定我们在线性空间创建的MDL缓冲区。作为回报,我们得到一个新的线性地址,它也指向SSDT,而且能够对其进行修改。
简而言之,使用MDL,我们在系统的线性地址空间中创建了新的可写缓冲区,它恰好解析成存储SSDT的物理内存。只要两个区域解析成同样的物理内存区域,它就没什么不同。它只是一个数字游戏,纯粹而简单。如果你不能写入线性内存的给定区域,那么就创建自己的区域并且向其写入。
WP_GLOBALS disableWP_MDL(unsigned int *ssdt,unsigned int nServices)
{
WP_GLOBALS wpGbs;
DbgPrint("[disableWP_MDL] SSDT=%\n",ssdt);
DbgPrint("[disableWP_MDL] nServices=%\n",nServices);
wpGbs.pMDL = MmCreateMdl(NULL, (PVOID)ssdt, (SIZE_T)nServices*4);
if(wpGbs.pMDL==NULL)
{
DbgPrint("[disableWP_MDL] %\n","call to MmCreateMdl failed");
return (wpGbs);
}
MmBuildMdlForNonPagedPool(wpGbs.pMDL);
wpGbs.pMDL->MdlFlags = wpGbs.pMDL->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;
wpGbs.callTable = (unsigned char*)MmMapLockedPages(wpGbs.pMDL, KernelMode);
if(wpGbs.callTable==NULL)
{
DbgPrint("[disableWP_MDL] %\n","call to MmMapLockedPages failed");
return (wpGbs);
}
return (wpGbs);
}
这个例程返回一个结构,它只是一个指向我们MDL和SSDT指针的包装器。
typedef struct _WP_GLOBALS
{
unsigned char *callTable;
PMDL pMDL;
}WP_GLOBALS;
我们通过前面的函数返回该结构,以便访问可写版本的SSDT,而且当不再需要MDL缓冲区时,我们可以复原事务的原始状态。为复原系统状态,我们使用以下函数:
void enableWP_MDL(PMDL mdlPtr,unsigned char *callTable)
{
if(mdlPtr != NULL)
{
MmUnmapLockedPages((PVOID)callTable, mdlPtr);
IoFreeMdl(mdlPtr);
}
}
二、 挂钩SSDT项
一旦禁用了写保护,我们可以使用以下例程将新的函数地址换人SSDT
unsigned char* hookSSDT(unsigned char *apiCall,unsigned char *newAddr,unsigned int *callTable)
{
PLONG target;
unsigned int indexValue;
indexValue = getSSDTIndex(apiCall);
target = (PLONG)&(callTable[indexValue]);
return ((unsigned char*)InterlockedExchange(target,(LONG)newAddr));
}
该例程接收挂钩例程的地址、现有例程的地址以及指向SSDT的指针,返回现有例程的地址(以便完成任务后恢复SSDT)。
给定某个Nt*()函数,他的地址在SSDT中的什么位置?我们在调试器中使用u nt!Zw*命令查看汇编代码,发现这种函数开始处的汇编代码都是这种格式:mov eax,xxxH,这个xxx就是系统调用的索引号。
unsigned int getSSDTIndex(unsigned char *address)
{
unsigned char *addressOfIndex;
unsigned int indexValue;
addressOfIndex = address + 1;
indexValue = *((PULONG)addressOfIndex);
return (indexValue);
}
一旦有了索引值,定位表项的地址并且将其换出就是一件简单的事情。但是请注意,我们必须使用InterlockedExchange()锁定对此项的访问,以便我们暂时拥有独占的访问权。与IDT或GDT这样基于处理器的结构不同,不论多少个处理器正在运行,只有一个单独的SSDT。
对SSDT中的系统调用脱钩使用同样的基本机制。唯一的不同之处在于我们不向调用例程返回值。
void unHookSSDT(unsigned char *apiCall,unsigned char *oldAddr,unsigned int *callTable)
{
PLONG target;
unsigned int indexValue;
indexValue = getSSDTIndex(apiCall);
target = (PLONG)&(callTable[indexValue]);
InterlockedExchange(target,(LONG)oldAddr);
}
三、 SSDT示例
既然已经分析了组成这首乐曲的各种和弦,让我们将其演奏起来听听看。挂钩ZwTerminateProcess系统调用。
NTSTATUS DriverEntry (
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
DbgPrint("Load SSDT Driver \n");
DriverObject ->DriverUnload = UnLoad;
wpGlobals = disableWP_MDL(KeServiceDescriptorTable.ServiceTableBase, KeServiceDescriptorTable.NumberOfServices);
if(wpGlobals.pMDL==NULL || wpGlobals.callTable==NULL)
{
return (STATUS_UNSUCCESSFUL);
}
pMDL = wpGlobals.pMDL;
systemCallTable = (PVOID *)wpGlobals.callTable;
Old_ZwTerminateProcess =(ZwTerminateProcessPtr)hookSSDT((unsigned char*)ZwTerminateProcess, (unsigned char*)NewZwTerminateProcess, (unsigned int*)systemCallTable);
return STATUS_SUCCESS;
}
KeServiceDescriptorTable是由ntoskrnl.exe导出的符号。要想访问它,我们必须为声明加上__declspec(dllimport)前缀,以便编译器知道我们在做什么。导出的内核符号为我们提供了内存中一个位置的地址。我们提供的数据类型定义(即typedef struct ServiceDescriptorEntry)把某个复合结构强加于这个地址的内存。通过这种通用方法,你能够修改由操作系统导出的任意变量。
我们将返回值保存到三个全局变量中(pMDL,systemCallTable,Old_ZwTerminateProcess),以便能够脱钩系统调用,并且重新启用写保护。
VOID
UnLoad (
IN PDRIVER_OBJECT DriverObject
)
{
DbgPrint("Unload SSDT Driver \n");
unHookSSDT((unsigned char*)ZwTerminateProcess, (unsigned char*)Old_ZwTerminateProcess, (unsigned int*)systemCallTable);
enableWP_MDL(pMDL,(unsigned char*)systemCallTable);
}
每当ZwTerminateProcess被调用,我们挂钩的函数都会被调用。为了存储实现此接口的现有系统调用的地址,定义以下的函数指针数据类型。
typedef NTSTATUS(*ZwTerminateProcessPtr)(
IN HAndLE ProcessHAndle OPTIONAL,
IN NTSTATUS ExitStatus );
要做的事情只剩下实现挂钩例程。我们通过打印输出字符串跟踪调用,然后调用原始的系统调用。
NTSTATUS NewZwTerminateProcess(
IN HAndLE ProcessHAndle OPTIONAL,
IN NTSTATUS ExitStatus )
{
NTSTATUS ntStatus;
DbgPrint("NewZwTerminateProcess Called \n");
ntStatus = ((ZwTerminateProcessPtr)(Old_ZwTerminateProcess))(ProcessHAndle,ExitStatus);
if(!NT_SUCCESS(ntStatus))
{
DbgPrint("[NewZwTerminateProcess] %\n","Call to Old_ZwTerminateProcess failed");
}
return STATUS_SUCCESS;
}
我们在这个示例中建立的是挂钩SSDT的标准操作程序。不论我们正在拦截哪个例程,挂钩与脱钩的技术细节保持相同。从现在起,无论我们何时想要跟踪或过滤系统调用,必须做的所有工作只有以下几点
- 声明原始系统调用原型(例如ZwTerminateProcess())
- 声明相应的函数指针数据类型(例如ZwTerminateProcessPtr)
- 定义函数指针(例如Old_ZwTerminateProcess)
- 实现挂钩例程(例如NewZwTerminateProcess())
四、加载驱动
我们用INSTDRV加载编译好的驱动程序DCSSSDT.sys
我们用PCHunter32查看挂钩是否成功
我们用Dbgview查看调试信息
没有问题都成功了。
转自 http://www.dcscms.com/article/content.php?seq=13