分享
 
 
 

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

王朝system·作者佚名  2006-01-31
窄屏简体版  字體: |||超大  

第五章 监控Native API调用

翻译:Kendiv( fcczj@263.net )

更新:Thursday, March 24, 2005

声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。

本书设计的hook机制的最大特色就是它是完全数据驱动的(data-driven)。只需简单的增加一个新的API符号表,该hook dispatcher就可适应Windows 2000的新版本。而且,通过向apdSdtFormats[]数组中加入新的API函数的格式化字符串就可在任何时候记录对这些附加的API函数的调用。这并不需要编写任何附加的代码---API Spy的动作可完全由一组字符串来确定!不过,在定义新的格式化字符串是必须要小心,因为w2k_spy.sys是运行于内核模式的驱动程序。因为在这一系统层次上,系统不能温和的处理发生错误。给Win32 API函数提供了一个无效的参数并不是问题-----你会收到一个错误提示窗口,同时程序会被系统自动终止。在内核模式下,一个微小的访问违规都会引发系统蓝屏。因此,一定要小心。在需要的地方如果没有出现一个正确的格式化控制ID或缺失了这一ID都会使你的系统彻底崩溃。即使一个简单的字符串有时都是致命的!

现在仅剩SpyHookInitializeEx()中的那一大块ASM代码还未讨论,这段代码由SpyHook2和SpyHook9标识。这段代码的一个有趣的特性是:在SpyHookInitializeEx()被调用的时候,它们从来都不会被执行。在进入SpyHookInitializeEx()后,函数代码将跳过这一整段代码,然后在SpyHook9标签处开始恢复执行,此处包含aSpyHooks[]数组的初始化代码。这一大块ASM代码只有通过aSpyHooks[]数组中的Handler成员才能进入。稍候,我将展示这些进入点是如何连接到SDT的。

在设计这段ASM代码时,我的重要目标之一就是使其是完全非侵入式的。截获操作系统调用非常危险,因为你从来不会知道被调用的代码是否会依赖调用上下文(calling context)的某些未知特性。理论上来说,这些ASM代码完全符合__stdcall约定,但仍存在出错的可能性。我不得不选择将原始的Native API处理例程放入几乎完全相同的环境中,这意味着这些原始函数将使用最初的参数堆栈并且可以访问所有的CPU寄存器,就像它们被正常调用一样。当然,必须接受由于插入hook所带来的最低限度的危险,否则,监控将不可能实现。在这里,有意义的改动就是维护堆栈中的返回地址。如果你翻回到图5-3,你会发现在进入函数时,调用者的返回地址并不位于堆栈的顶部。SpyHookInitializeEx()中的hook dispatcher占用了此地址,将它自己的SpyHook6标签的地址写在了这里。因此,原始Native API处理例程将被打断,然后进入SpyHook6中,这样hook dispatcher才能检查原始Native API处理例程的参数和它要返回的数据。

在调用原始处理例程之前,dispatcher将建立一个SPY_CALL(参见列表5-3)控制块,该控制块中包含它稍候将会用到的参数。其中的一些参数在正确记录API调用时会用到,另外一些则提供了有关调用者的信息,因此dispatcher可以在写完log后,把控制返回给调用者,就像什么都没有发生一样。Spy设备在它的全局数据块DEVICE_CONTEXT中维护着一个SPY_CALL结构的数组,可通过全局变量gpDeviceContext来访问。Hook Dispatcher通过检查SPY_CALL结构中的InUse成员来在数组中找到一个空的SPY_CALL。Hook Dispatcher使用CPU的XCHG指令来加载和设置该成员的值(译注:XCHG指令可以保证此操作为原子操作)。这一点非常重要,因为当代码运行于多线程环境中时,读写全局数据时必须采取保护措施以避免条件竞争。如果在数组中找到了一个空的SPY_CALL,dispatcher就会将调用者的线程ID(通过PsGetCurrentThreadId()获取)、与当前API函数相关的SPY_HOOK_ENTRY结构的地址以及整个参数堆栈保存到该SPY_CALL结构中。需要复制的参数的字节数取自KiArqumentTable数组,该数组保存在系统的SDT中。如果所有的SPY_CALL都被使用了,原始的API函数处理例程将被调用而不会产生任何日志记录。

必须采用SPY_CALL数组是因为Windows 2000的多线程本性。当Native API函数被暂停(suspended)时,这种情况就会经常出现----此时,另一个线程将获得控制权,然后在它自己的时间片(time slice)内调用另一个Native API函数。这意味着Spy设备的Hook Dispatcher必须允许在任何时间和任何执行点上的重进入(reenter)。如果Hook Dispatcher有单一的全局SPY_CALL存储区域,它就可能在处于等待状态的线程使用完之前被当前运行的线程覆写(overwritten)。而这种情况正是蓝屏的最佳候选人。为了进一步了解Native API的嵌套,我在Spy的DEVICE_CONTEXT结构中增加了dLevel和dMisses成员。无论何时只要重进入hook dispatcher(如,向SPY_CALL数组中增加一个新的SPY_CALL)dLevel都不会累加一个1。如果超过了最大嵌套层数(如,SPY_CALL数组已满),dMisses就会累加一个1,来标识丢失了一个日志记录。根据我的观察,在实际环境下,可以很容易的发现嵌套层达到4。这表示即时在高负载(heavy-load)的情况下,Native API也会被重进入,因此,我将嵌套层数的上限设为256。

在调用原始的API处理例程之前,Hook Dispatcher会保存所有的CPU寄存器(包括EFLAGS),随后执行路径将导向函数的进入点。这会在列表5-3中的SpyHook5标签之前立即完成。此时,SpyHook6将位于栈顶,仅随其后的是调用者的参数。一旦API处理例程推出了,控制将被传回到hook dispatcher的SpyHook6标签。从此处开始执行的代码也被设计为非入侵的。此时,主要目标是允许调用者可以看到调用上下文,这和原始API函数建立的上下文几乎完全一致。Dispatcher的主要问题是要能立即找到保存有当前API调用信息的SPY_CALL结构。唯一可以依赖的就是调用者的线程ID,该ID保存在SPY_CALL结构的hThread成员中。因此,Dispatcher循环遍历整个SPY_CALL数组以寻找匹配的线程ID。注意,代码不会关心fmuse标志的值;这并不是必须的,因为数组中所有未使用的SPY_CALL结构的hThread都被设为了0,这是系统空闲线程的ID。循环会在到达数组结尾时终止。否则的话(译注:即没有找到匹配的线程ID),Dispatcher不会将控制返回给调用者,因为这样做将是致命的。在这种情况下,代码的选择余地很小,因此,它会进入KeBugCheck(),这样做的结果当然是使系统以受控的方式终止。不过这种情况应该从来不会发生,但如果它发生了,那表示系统必然出现了很严重的错误,因此,使系统终止是最佳解决方案。

如果发现了匹配的SPY_CALL,Hook Dispatcher将结束它的工作。最后的动作是调用日志记录函数SpyHookProtocol(),需要给该函数传入一个指向SPY_CALL结构的指针。日志记录所需的信息都保存在该结构中。当SpyHookProtocol()返回后,Dispatcher就释放它刚才使用的SPY_CALL,恢复所有的CPU寄存器,然后返回到调用者。

API HOOK协议

一个好的API Spy应该可以在原始函数被调用后还能察看它使用的参数,因为函数可能会通过传入的缓冲区返回附加的数据。因此,日志函数SpyHookProtocol()在hook例程结束时将被调用,而此时API函数还未返回到调用者。在讨论它的实现秘诀之前,请先看看下面给出的两个示例性的协议(Protocol),它们会为你提供一个大概的方向。图5-6是在命令行下执行dir c:\时产生的日志文件的快照。

请对比图5-6中列出的日志项和列表5-6给出的协议格式化字符串。在示列5-1中,NtOpenFile()和NtClose()的格式化字符串分别对应图5-6中的第一行和第四行。它们有着惊人的相似处;每一个格式化控制ID都紧随在一个%号后(参考表5-2),与其相关的参数项将包含在协议中。不过,协议还包含一些附加的信息,这些信息明显不属于格式字符串。稍后我将解释这种差异的原因。

示例5-2给出了一个协议项的一般格式。每一项包含相同个数的域,这些域采用分隔符隔开。这样分隔可以使程序很容易的解析它。这些域按照如下的一组简单的基本规则来构建:

l 所有的数字都已十六进制表示,没有0前缀或常见的前缀“0x”

l 函数的多个参数由逗号隔开

l 字符串参数将位于一对双引号中

l 结构体成员的值由“.”符号隔开

图5-6. 命令dir c:\的示列协议

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

18:sO=NtOpenFile(+46C.18,nl00001,o"\??\C:\",i0.1,n3,n4021)lBFEE5AE05B6710,278,2

"%s=NtClose(%-l)"

lB:sO=NtClose(-46C.18="\??\C:\")lBFEE5AE05B6710,278,l

示列5-1. 比较格式化字符串和协议项

<#> : <status>=<function> (<arguments>) <time> , <thread>, <handles>

示列5-2. 协议项的一般格式

l 与句柄相关的对象名称和句柄的值采用“=”进行分割。

l 日期/时间的stamp为1601-01-01至今逝去的毫秒数,其格式依赖Windows 2000的基本时间格式,精度可达到1/10毫秒。

l 线程ID是调用API函数的线程的唯一数字标识。

l 句柄计数的状态表示当前注册到Spy设备句柄列表中的句柄的数量。协议函数使用该列表查找与对象名称相关的句柄。

图5-7. 命令type c:\boot.ini的示列协议

图5-7是在控制台中执行:type c:\boot.ini命令产生的API Spy协议结果。下面给出日志项中的某些列的含义:

l 在0x31行,调用了NtCreateFile()来打开\??\c:\boot.ini文件。(o”\??\c:\boot.ini”)该函数返回的NTSTATUS的值为0(s0),即STATUS_SUCCESS,并分配了一个新的文件句柄,其值为0小8,该句柄属于进程0x46c(+46C.18)。因此,句柄计数从1增加到2。

l 在0x36行,type命令将文件\??\c:\boot.ini的前512个字节(n200)读入位于线性地址0x0012F5B4处的缓冲区中,并把从NtCreateFile()获取的句柄解析给NtReadFile()函数。系统成功的返回512字节(io.200)。

l 在0x39行,将处理另一块512个字节的文件块。这一次,将到达文件的末尾,因此NtReadFile()仅返回了75个字节(io.4B)。显然,我的boot.ini文件的大小为:512+75=587字节。

l 在0x3C行,NtClose()成功的释放了指向\??\c:\boot.ini的文件句柄(-46.18=”\??\c:\boot.ini”),因此,句柄计数将从2减少为1。

现在,你应该已经明白Spy协议的API是如何构建的了,这会帮助你掌握协议生成机制的细节,接下来我们将讨论这一机制。在前面我曾提及过,用于日志记录的主要API函数是SpyHookProtocol()。列表5-7给出了该函数,它将使用SPY_CALL结构中的数据来为每个API函数生成一个协议记录并将其写入一个环形缓冲区中,这里的SPY_CALL结构由Hook Dispatcher传入。一个Spy设备的客户端可以通过IOCTL调用来读去这一协议。每个记录项都是一行文本,每行都由单个行结束符(即C语言中的”\n”)表示行的结束。通过使用内核的Mutext KMUTEX kmProtcol来实现串行读去协议缓冲区,kmProtocol位于Spy设备的全局结构DEVICE_CONTEXT中。列表5-7中的SpyHookWait()和SpyHookRelease()函数用于请求和释放此Mutext对象。所有对协议缓冲区的访问都必须由SpyHookWait()预处理并在结束时由SpyHookRelease()处理,SpyHookProtocol()函数展示了这种行为。

NTSTATUS SpyHookWait (void)

{

return MUTEX_WAIT (gpDeviceContext->kmProtocol);

}

// -----------------------------------------------------------------

LONG SpyHookRelease (void)

{

return MUTEX_RELEASE (gpDeviceContext->kmProtocol);

}

// -----------------------------------------------------------------

// <#>:<status>=<function>(<arguments>)<time>,<thread>,<handles>

void SpyHookProtocol (PSPY_CALL psc)

{

LARGE_INTEGER liTime;

PSPY_PROTOCOL psp = &gpDeviceContext->SpyProtocol;

KeQuerySystemTime (&liTime);

SpyHookWait ();

if (SpyWriteFilter (psp, psc->pshe->pbFormat,

psc->adParameters,

psc->dParameters))

{

SpyWriteNumber (psp, 0, ++(psp->sh.dCalls)); // <#>:

SpyWriteChar (psp, 0, ':');

// <status>=

SpyWriteFormat (psp, psc->pshe->pbFormat, // <function>

psc->adParameters); // (<arguments>)

SpyWriteLarge (psp, 0, &liTime); // <time>,

SpyWriteChar (psp, 0, ',');

SpyWriteNumber (psp, 0, (DWORD) psc->hThread); // <thread>,

SpyWriteChar (psp, 0, ',');

SpyWriteNumber (psp, 0, psp->sh.dHandles); // <handles>

SpyWriteChar (psp, 0, '\n');

}

SpyHookRelease ();

return;

}

列表5-7. 主要的Hook协议函数SpyHookProtocol()

如果你比较一下列表5-7给出的SpyHookProtocol()函数的主要部分和示列5-2给出的协议项的一般格式,将很容易找出那个语句生成了协议项中的哪一个域(field)。这样一来一切就很清楚了为什么列表5-6中的协议字符串没有说明整个数据项---有些独立于功能的数据将由SpyHookProtocol()添加,而这将不需要格式字符串的帮助。SpyHookProtocl()的核心调用是SpyWriteFormat(),该函数生成<status>=<function>[<arguments>]部分,这依赖于与要记录的当前API函数相关的格式字符串。请参考位于随书光盘的\src\w2k_spy目录下的源文件w2k_spy.c和w2k_spy.h,以获取Spy设备驱动程序中使用的SpyWrite*()函数的更多实现信息。

请注意,这些代码稍微有些危险。这些代码编写与1997年是针对Windows NT 4.0的。在移植到Windows 2000之后,当hook工作一段较长时间后会偶尔引发蓝屏。更糟糕的是,有些特殊的操作将立即引发蓝屏,例如,在My Favoriter文本编辑器的File\Open对话框中打开我的电脑时。在分析过多过crash dump后,我发现是由于将NULL指针传递给了某些函数从而导致了系统崩溃。一但Spy设备试图使用这些指针中的某个来记录该指针引用的数据时,系统就会崩溃。典型的就是,指向IO_STATUS_BLOCK结构的指针,在UNICODE_STRING和OBJECT_ATTRIBUTES结构中存在无效的字符串指针。我还发现某些有Buffer成员的UNICODE_STRING结构没有\0结束符。因此,我再次强调你不应该假定所有的UNICODE_STRING结构都以\0结束。在不能确定时,请使用Length成员,它总能正确地告诉你在Buffer中存放的有效的字节数。

为了修正这一问题,我为所有使用客户指针的日志函数增加了指针有效性检查。在结束时,我使用第四章讨论过的SpyMemoryTestAddress()函数来检验一个线性地址指针是否指向一个有效的页表项(PTE)。更详细的信息请参考列表4-22和列表4-24。另一种可能的替代方案是使用结构化异常(__try/__except)。

…………….待续………………

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有