我们的函数实现注册表的监控,那么在NT下是否可以找到类似的函数来实现相应的功能呢, 很遗憾,
在微软公开的文档里并没有此部分的说明. 但方法还是有的, 就实现注册表的监控来说, NT下有两种
方法, 一种是驱动级的, 另一种是应用程序级的.
在驱动部分实现监控, 通常称为system call hooking,这是一种非公开的技术, 得不到正式
支持, 但微软经常这么干, 大家也都习以为常, 对于这种非公开技术的可靠性并不打折扣, MS PRESS
就经常出版类似技术的书籍供程序员参考.
据说有关system call hooking的技术在 "undocument windows nt"一书中有些说明, 假如
谁能提供E版, 将不胜感激.
如同9X, 监控注册表还是拦截几个系统服务:
Begin_Hook_table:
ZwOpenKey
ZwQueryKey
ZwQueryValueKey
ZwEnumerateValueKey
ZwEnumerateKey
ZwClose
ZwFlushKey
ZwDeleteKey
ZwSetValueKey
ZwCreateKey
ZwDeleteValueKey
ZwLoadKey
ZwUnloadKey
End_Hook_table:
这几个服务例程在NTOSKRNL.EXE里实现, 可以在NTDDK里找到说明. NTOSKRNL.EXE对这些系统
服务有很好的组织, 类似与DOS下的中断向量表, 几乎所有的函数起始地址组织在一张
KeServiceDescriporTable表中, 模拟一下大概的结构就是:
KeServiceTable_Begin :
...
pZwOpenKey dd 0
pZwQueryKey dd 0
pZwQueryValueKey dd 0
pZwEnumerateValueKey dd 0
pZwEnumerateKey dd 0
...
KeServiceTable_End :
所有对Zw***(或NT***等)的调用都会跳转到此表所指向的地址, 那么现在就很明显, 只要我们
能在这张表中找到需要接管的函数, 把它所指向的地址转向我们的例程就一切OK了.
对NTOSKNRL.LIB进行输出, 可以看到一个KeServiceDescriptorTable的指针, 虽然没有公开的
声明, 但就其名字也可联想到上面所说的那张表, 大概可以用C语言描述该指针如下:
typedef struct SERVICE_TABLE {
PVOID table; file://service/ table pointer
PVOID reserved; //?? always 0, 猜测是第一个服务顺序号。
ULONG srv_end_number; file://最/后一个服务顺序号。
PVOID table_end; //?? end service table pointer = service_table_pointer + srv_end_number * 4;
} *PSERVICE_TABLE;
PSERVICE_TABLE KeServiceDescriptorTable;
(如前所述, 由于此部分不公开, 除了某些人了解之外, 而象我们, 就只有借助于SOFTICE之类
的工具进行破解了, 因此, 所有不能肯定的部分都会有说明, 也希望大家共同探讨.)
有了这张表的位置, 剩下的就是该如何确定里面的函数的排列方式了. 这一点就好办多了, 我们
有种种办法来刺探里面每个函数的位置, 也有人早已告诉我们, 举例说明:
ZwOpenKey, 它指向一个16字节的结构, 其第1-4字节就是它在service_table中的顺序号!
至此, 假设接管函数ZwOpenKey:
在驱动里声明:
typedef struct SERVICE_TABLE {
PVOID table;
PVOID reserved;
ULONG srv_end_number;
PVOID table_end;
} *PSERVICE_TABLE;
extern PSERVICE_TABLE KeServiceDescriptorTable;
PULONG *pKeServiceTable;
NTSTATUS (*OldZwOpenKey)( OUT PHANDLE, IN OUT ACCESS_MASK, IN POBJECT_ATTRIBUTES );
NTSTATUS MyZwOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
)
{
ntstatus = OldZwOpenKey( KeyHandle, DesiredAccess, ObjectAttributes);
...
return ntstatus;
}
BOOL GetKeServiceTable()
{
pKeServiceTable = KeServiceDescriptorTable->table;
return TRUE;
}
void hook()
{
OldZwOpenKey = (PVOID)InterlockedExchange(
(PLONG)pKeServiceTable[*(ULONG *)((UCHAR *)ZwOpenKey+1))],
(LONG)MyZwOpenKey);
}
一些解释:
NT的内存的管理想来大家应该很熟, 在IRQL >= DISPATCH_LEVEL时,是不会触发PAGE FAULT的,
因此若此时访问了一个不存在的页会导致系统崩溃,通常的做法是先锁定再操作, 而我之所以没有锁定
KeServiceDescriptorTable->table来进行操作, 是因为它本来就处于核心态, 而且存在于NT里唯一的一个达
4M的内存页里, 但是锁定也有锁定的好处, 对于编商业软件的朋友可能就觉得不锁定心里会不塌实。下面
就是锁定的操作, 但对应也增加了一个释放MDL的函数.
PMDL KeServiceTableMdl;
BOOL GetKeServiceTable()
{
ULONG tablesize;
tablesize = KeServiceDescriptorTable->srv_end_number * 4;
KeServiceTableMdl = MmCreateMdl(0, KeServiceDescriptorTable->table, tablesize);
MmBuildMdlForNonPagedPool(KeServiceTableMdl);
pKeServiceTable = MmMapLockedPages(KeServiceTableMdl, 0);
return TRUE;
}
VOID ReleaseMdl()
{
MmUnmapLockedPages(pKeServiceTablePointers, KeServiceTableMdl);
ExFreePool(KeServiceTableMdl);
}