热门搜索:Photoshop 平面设计 Linux Vista Windows ASP.NET qq word 病毒 XP Excel 标志设计 

《Undocumented Windows 2000 Secrets》翻译 --- 第五章(2)

来源:佚名(读取中...) 2005-8-17 【字体: 】 切换为
通用解决方案的主要障碍是 C 语言的典型参数传递机制。就像你知道的, C 通常在调用函数的入口点之前会将函数参数传递到 CPU 堆栈中。根据函数需要的参数数量,参数堆栈的大小将有很大的差别。 windows 2000 的 248 个 Native API 函数需要的参数堆栈的大小位于 0 到 68 字节。

SpyHook1:

push eax

mov eax, offset SpyHook2

call eax

push eax

mov eax, offset SpyHook2

call eax

;244 boring repetitions cimitted

push eax

mov eax, offset SpyHook2

call eax

push eax

mov eax, offset SpyHook2

call eax

SpyHook2:

pop eax

列表 5-4. 扩充 SpyHook 宏调用

例如, 列表 5-4 中第一个 CALL EAX 指令的返回地址是其下一个语句的地址。通常,第 N 个 CALL EAX 指令的返回地址是第 N+1 个语句的地址,但最后一个除外,最后这个将返回 SpyHook2 。因此,从 0 开始的所有进入点的索引可以由 5-4 中的通用公式计算出来。这三条规则中的潜在规则是: SDT_SYMBOLS_MAX 进入点符合内存块 SpyHook2---SpyHook1 。那么有多少个进入点符合 ReturnAddress---SpyHook1 呢?因为计算结果是位于 0 到 SDT_SYMBOLS_MAX 中的某一个数值,所以,肯定要使用该数值来获取一个从 0 开始的索引。

《Undocumented

图 5-4. 通过 Hook 进入点的返回地址确定一个 Hook 进入点

图 5-4 所示公式的实现方式可以在 列表 5-3 中找到,在汇编标签 SpyHook2 的右边。在 5-5 的左下角也给出了该公式的实现代码,它展示了 Hook Dispatcher 机制的基本原理。注意, i386 的 mul 指令会在 EDX:EAX 寄存器中产生一个 64 位的结果值,这正是其后的 div 指令所期望的,因此,这里没有整数溢出的危险。在 5-5 的左上角,是对 KiServiceTable 的描述,该表将被 SpyHook 宏生成的进入点地址修改。在图的中部展示了展开后的宏代码(来自 列表 5-4 中)。进入点的线性地址位于图的右手边。为了完全一致,每个进入点的大小都是 8 字节,因此,通过将 KiServiceTable 中每个函数的索引值乘以 8 ,然后再将乘积加上 SpyHook1 的地址就可得出进入点的地址。

事实上,每个进入点并不都是纯粹的 8 字节长。我花费了大量的时间来寻找最佳的 hook 函数的实现方式。尽管按照 32 位边界对齐代码并不是必须的,但这从来都不是个坏主意,因为这会提高性能。当然,能提升的性能十分有限。你或许会奇怪:为什么我要通过 EAX 寄存器间接的调用 SpyHook2 ,而不是直接使用 CALL SpyHook2 指令,这不是更高效吗?是的!不过,问题是 i386 的 CALL (还有 jmp )指令可以有多种实现方式,而且都具有相同的效果,但是产生的指令大小却不相同。请参考: Intel's Instruction Set Reference of the Pentium CPU family ( Intel 199c )。因为最终的实现方式要由编译器 / 汇编器来确定,这不能保证所有的进入点都会有相同的编码。换句话说, MOV EAX 和一个 32 位常量操作数总是以相同的方式编码,同样的,这也适用于 CALL EAX 指令。

《Undocumented

图 5-5. Hook Dispatcher 的功能原理

列表 5-3 中还有一点需要澄清。让我们从 SpyHook9 标签后的最后一快 C 代码段开始。紧随 SpyHook9 之后的汇编代码将 SpyHook1 和 SpyHook2 的线性地址保存在 dHook1 和 dHook2 变量中。接下来,变量 n 被设为每个进入点的大小(由进入点数组的大小除以进入点的个数而得出)。当然,这个值将是 8 。 列表 5-3 的剩余部分是一个循环语句,用来初始化全局数组 aSpyHooks[] 中的所有项。这个数组所包含的 SPY_HOOK_ENTRY 结构定义于列 5-3 的顶部,该数组中的每一项都对应一个 Native API 函数。要理解该结构中的 Handler 和 pbFormat 成员是如何被设置的,就必须进一步了解传递给 SpyHookInitializeEx() 的 ppbSymbols 和 ppbFormats 参数, 列表 5-5 给出了外包函数 SpyHookInitialize() ,该函数会选择适合当前 OS 版本的参数来调用 SpyHookInitializeEx() 。前面已经提示过,我使用的代码不直接测试 OS 版本或 Build Number ,而是用常量 SPY_SYMBOLS_NT4 、 SPY_SYMBOLS_NT5 和 SDT 中与 ntoskrnl.exe 相关的 ServiceLimit 成员的值进行比较。如果没有一个匹配, Spy 设备将把 aSpyHooks[] 数组内容全部初始化为 NULL ,从而有效的禁止 Native API Hook 机制。

BOOL SpyHookInitialize (void)

{

BOOL fOk = TRUE;

switch (KeServiceDescriptorTable->ntoskrnl.ServiceLimit)

{

case SDT_SYMBOLS_NT4:

{

SpyHookInitializeEx (apbSdtSymbolsNT4, apbSdtFormats);

break;

}

case SDT_SYMBOLS_NT5:

{

SpyHookInitializeEx (apbSdtSymbolsNT5, apbSdtFormats);

break;

}

default:

{

SpyHookInitializeEx (NULL, NULL);

fOk = FALSE;

break;

}

}

return fOk;

}

列表 5-5. SpyHookInitialize() 选择匹配当前 OS 版本的符号表

将全局数组: apbSdtSymbolsNT4[] 和 apbSdtSymbolsNT5[] 传递给 SpyHookInitializeEx() 函数作为其第一个参数 ppbSymbols ,这两个数组只是简单的字符串数组,包含 Windows NT 4.0 和 windows 2000 的所有 Native API 函数的名称,按照它们在 KiServiceTable 中的索引顺序来存储,最后以 NULL 结束。 列表 5-6 给出了 apbStdFormats[] 字符串数组。这个格式字符串列表也是 hook 机制中很重要的一部分,因为它确定了记录了那个 Native API 调用,以及每个记录项的格式。显然,这些字符串的结构借鉴了 C 运行时库中的 printf() 函数,但针对 Native API 经常使用的数据类型进行了修改。 5-2 列出了所有可被 API Logger 识别的格式化 ID 。

PBYTE apbSdtFormats [] =

{

"%s=NtCancelIoFile(%!,%i)",

"%s=NtClose(%-)",

"%s=NtCreateFile(%+,%n,%o,%i,%l,%n,%n,%n,%n,%p,%n)",

"%s=NtCreateKey(%+,%n,%o,%n,%u,%n,%d)",

"%s=NtDeleteFile(%o)",

"%s=NtDeleteKey(%-)",

"%s=NtDeleteValueKey(%!,%u)",

"%s=NtDeviceIoControlFile(%!,%p,%p,%p,%i,%n,%p,%n,%p,%n)",

"%s=NtEnumerateKey(%!,%n,%n,%p,%n,%d)",

"%s=NtEnumerateValueKey(%!,%n,%n,%p,%n,%d)",

"%s=NtFlushBuffersFile(%!,%i)",

"%s=NtFlushKey(%!)",

"%s=NtFsControlFile(%!,%p,%p,%p,%i,%n,%p,%n,%p,%n)",

"%s=NtLoadKey(%o,%o)",

"%s=NtLoadKey2(%o,%o,%n)",

"%s=NtNotifyChangeKey(%!,%p,%p,%p,%i,%n,%b,%p,%n,%b)",

"%s=NtNotifyChangeMultipleKeys(%!,%n,%o,%p,%p,%p,%i,%n,%b,%p,%n,%b)",

"%s=NtOpenFile(%+,%n,%o,%i,%n,%n)",

"%s=NtOpenKey(%+,%n,%o)",

"%s=NtOpenProcess(%+,%n,%o,%c)",

"%s=NtOpenThread(%+,%n,%o,%c)",

"%s=NtQueryDirectoryFile(%!,%p,%p,%p,%i,%p,%n,%n,%b,%u,%b)",

"%s=NtQueryInformationFile(%!,%i,%p,%n,%n)",

"%s=NtQueryInformationProcess(%!,%n,%p,%n,%d)",

"%s=NtQueryInformationThread(%!,%n,%p,%n,%d)",

"%s=NtQueryKey(%!,%n,%p,%n,%d)",

"%s=NtQueryMultipleValueKey(%!,%p,%n,%p,%d,%d)",

"%s=NtQueryOpenSubKeys(%o,%d)",

"%s=NtQuerySystemInformation(%n,%p,%n,%d)",

"%s=NtQuerySystemTime(%l)",

"%s=NtQueryValueKey(%!,%u,%n,%p,%n,%d)",

"%s=NtQueryVolumeInformationFile(%!,%i,%p,%n,%n)",

"%s=NtReadFile(%!,%p,%p,%p,%i,%p,%n,%l,%d)",

"%s=NtReplaceKey(%o,%!,%o)",

"%s=NtSetInformationKey(%!,%n,%p,%n)",

"%s=NtSetInformationFile(%!,%i,%p,%n,%n)",

"%s=NtSetInformationProcess(%!,%n,%p,%n)",

"%s=NtSetInformationThread(%!,%n,%p,%n)",

"%s=NtSetSystemInformation(%n,%p,%n)",

"%s=NtSetSystemTime(%l,%l)",

"%s=NtSetValueKey(%!,%u,%n,%n,%p,%n)",

"%s=NtSetVolumeInformationFile(%!,%i,%p,%n,%n)",

"%s=NtUnloadKey(%o)",

"%s=NtWriteFile(%!,%p,%p,%p,%i,%p,%n,%l,%d)",

NULL

};

列表 5-6. Native API Logger 使用的格式化字符串

这里要特别提出的是:每个格式字符串要求必须提供函数名的正确拼写。 SpyHookInitializeEx() 遍历它接受到的 Native API 符号列表(通过 ppbSymbols 参数),并试图从 ppbFormats 列表中找出与函数名匹配的格式字符串。由帮助函数 SpySearchFormat() 来进行比较工作, 列表 5-3 底部的 if 语句中调用了该函数。因为要执行大量的字符串查找操作,我使用了一个高度优化的查找引擎,该引擎基于“ Shift/And ”搜索算法。如果你想更多的学习它的实现方式,请察看随书 CD 的 \src\w2k_spy\w2k_spy.c 源文件中的 SpySearch*() 函数。当 SpyHookInitializeEx() 推出循环后, aSpyHooks[] 中的所有 Handler 成员都将指向适当的 Hook 进入点, pbFormat 成员提供与之匹配的格式字符串。对于 Windows NT 4.0 ,所有索引值在 0xD3---0xF8 的数组成员都将被设为 NULL ,因为在 NT4 中,它们并没有被定义。

表 5-2. 可识别的格式控制 ID

ID

%+

句柄(登记)

将句柄和对象名写入日志,并将其加入句柄表。

%!

句柄(检索)

将句柄写入日志,并从句柄表中检索其对应的对象名。

%-

句柄(撤销登记)

将句柄和对象名写入日志,并将其从句柄表移除

%a

ANSI 字符串

将一个由 8 位 ANSI 字符构成的字符串写入日志

%b

BOOLEAN

将一个 8 位的逻辑值写入日志

%c

CLIENT_ID*

将 CLIENT_ID 结构的成员写入日志

%d

DWORD *

将该 DWORD 所指变量的值写入日志

%i

IO_STATUS_BLOCK *

将 IO_STATUS_BLOCK 结构的成员写入日志

%l

LARGE_INTEGER *

将一个 LARGE_INTEGER 的值写入日志

%n

数值 (DWORD)

将一个 32 位无符号数写入日志

%o

OBJECT_ATTRIBUTES *

将对象的 ObjectName 写入日志

%p

指针

将指针的目标地址写入日志

%s

状态 (NTSTATUS)

将 NT 状态代码写入日志

%u

UNICODE_STRING *

将 UNICOD_STRING 结构的 Buffer 成员写入日志

%w

宽字符串

将一个由 16 位字符构成的字符串写入日志

%%

百分号转义符

将一个“ % ”号写入日志

上一页  [1] [2] [3] 

关注此文的读者还看过:
    用户评论
评论内容:不能超过100字,需审核,请自觉遵守互联网相关政策法规。
发表评论: 匿名发表 用户名: loading 位网友发表了评论 查看评论
(0/100)