离奇事件薄之Irp死亡未遂

白天在外面耗了一整天,和各色朋友们讨论了一整天的新公司初创、经营、账务、税收等相关的问题,竟也蛮有兴致,也费了相当的口水,等回到家时间已很晚了,可精神却还是异常兴奋,只得爬起来找点事打发一下过剩的精力,第一直觉上便想到了前阵子曾遇到的一个稍许离奇的问题。

内容上可接承以前的一篇日志《离奇死亡事件薄之CoCreate篇》,于是继续取题《离奇事件薄》。细节如下:

驱动程序中要转向IRP_MJ_DIRECTORY_CONTROL/IRP_MN_QUERY_DIRECTORY,即Directory Enumeration操作,以方便处理此目录下所有的文件项。

由于会涉及文件I/O操作,不得不将操作移至系统线程中来做(至于为什么,请读者自己思考),所以在用户请求线程中不得不将原Irp请求的完成延后(pending),函数原型可简化成如下二个函数:

/* 用户线程环境:直接处理 IRP_MJ_DIR_CTRL / IRP_MN_QUERY_DIRECTORY */

NTSTATUS MyQueryDirectory(DevObj, Irp)

{

        ……
        MyQueueToSystemThread(DevObj, Irp);
        return STATUS_PENDING:

}

/* 系统线程,用以处理面向新路径(Dcb)的IRP_MN_QUEYR_DIRECTORY操作 */

NTSTATUS MySystemThreadHandler(DevObj, Irp)

{
        PIRP newIrp = IoAllocateIrp(newDevObj);
        ……
        IoCallDriver(newDevObj, newIrp);
        KeWaitForSingleObject(…):

        /* 处理返回的目录项 */
        ……
        /* 完成原用户的IRP请求 */
       IoCompleteRequest(irp, IO_NO_INCREMENT);

}

代码在逻辑上并没有问题,MySystemThreadHandler()正确获取目录项内容并实施对目标项的逐一检查,之后再完成用户所发的Irp请求,Irp的状态被系统(I/O Manager)修改为完成状态,此过程中并没有异常。但最早发出IRP_MJ_DIR_CTRL / IRP_MN_QUERY_DIRECTORY请求的用户线程却被永远地挂起来了,原Irp内容依然有效,虽然其状态已被标记为“完成”。

调试发现,由于系统线程优先级稍高,MySystemThreadHandler()完成Irp的操作竟然先于MyQueryDirectory()返回至I/O Manager。

执行顺序如下:
时间1:MyQueryDirectory() 添加一个新任务至系统线程队列
时间2:MyQueryDirectory() 被系统挂起了
时间3:系统线程执行MySystemThreadHandler()来处理时刻时间1所添加的任务
时间4:MySystemThreadHandler()执行完毕
时间5:原用户线程MyQueryDirectory()被唤醒,并返回STATUS_PENDING给I/O Manager

看到此序列,对于熟悉Irp操作规则的开发者来说,问题已经明了:显然是对此Irp的第二阶段用户线程环境相关的处理没有进行(Stage 2),也就是说,添加Kernel Apc的部分被跳过了。

一般Irp的完成要经过两个过程(可参见本文末所附文档链接):
Stage 1: IofCompleteRequest(): 完成与请求线程环境无关的操作
Stage 2: IopCompleteRequest(): 与Kernel Apc中执行,用以完成与请求线程相关的操作

之所以Stage 2的IopCompleteRequest会被跳过,有两方面的原因:

  1. Windows内核在处理IRP_MJ_DIR_CTRL/IRP_MN_QUERY_DIRECTORY请求时会默认加上标志位IRP_DEFER_IO_COMPLETION。据我所知,仅对DeviceIoControl操作不加此标志。当此标志位被设置的同时,如果Irp的Pending位没有设置,IoCompleteRequest()(此函数会调用IofCompleteRequest())则会直接返回,从面不在继续处理第二阶段(stage 2)。
  2. 原用户线程IoCallDriver()返回之后,由于返回值为STATUS_PENDING,I/O Manager将不在调用IopCompleteRequest()

所以二者同时跳过Stage 2,导致Irp的Stage 2处理不会再有机会执行,从而原用户线程也得不到被唤醒的机会了。

解决办法却是异常简单,在MyQueryDirectory()调用IoCallDriver()之前,或MySystemThreadHandler()调用IoCompleteRequest()之前将Irp设为PendingReturened非状态即可,此操作由系统支持函数IoMarkIrpPending()来做的。

最后,不妨再复习一下OSR介绍Irp操作的经典之作吧:《Secrets of the Universe Revealed! - How NT Handles I/O Completion

阴沟里翻船之KeSetEvent

KeSetEvent是个使用频率很高的内核支持函数,但经常使用未必意味着确实了解它。上周就曾遇到一件怪事,系统线程在调用KeSetEvent后线程IRQL竟然从PASSIVE_LEVEL提升至DISPATCH_LEVEL,以至后续的操作出错:Bug Check 0xA: IRQL_NOT_LESS_OR_EQUAL。

先看看它的函数声明:

LONG
KeSetEvent(
    IN PRKEVENT Event,
    IN KPRIORITY Increment,
    IN BOOLEAN Wait
);

比较简单,一共只有3个参数:

  • Event: 准备被激活的事件
  • Increment: 预备给被唤醒线程临时提升线程优先级的增量。一般情况下均为0,但针对不同的子系统及对响应快慢的不同要求,会有不同的取值。如网络相关:IO_NETWORK_INCREMENT=2;键盘、鼠标涉及用户界面交互部分:取值为6 (IO_KEYBOARD_INCREMENT, IO_MOUSE_INCREMENT);声音相关,IO_SOUND_INCREMENT=8
  • Wait: 这个参数名字起的太不妥贴,非常容易误导。我就曾以为将此参数设为TRUE表示KeSetEvent将等待睡眠在此Event上的线程被唤醒,直到KeWaitForXXXObject返回。现在看来这种自以为是的默认是全然没有根据的。先看DDK中怎么说这个参数吧:
  • Specifies whether the call to KeSetEvent is to be followed immediately by a call to one of the KeWaitXxx routines. If TRUE, the KeSetEvent call must be followed by a call to KeWaitForMultipleObjects, KeWaitForMutexObject, or KeWaitForSingleObject.

    由此来看,此参数就是为了避免不必要的锁开销及可能的线程调度开销。你可能会认为,调用者先调用KeSetEvent(Event, 0, TRUE),然后直接跟上了KeWaitForXXXObject(Event …),那岂不是多此一举?控制权本就在自己手中,何必再搞个假放权真夺权的无用功呢?再说即便是有用功,也有点任人唯亲或肥水不流外人田的嫌疑吧?

    但反过来想想,如果KeSetEvent和KeWaitForXXXObject所操作的Event对象不是同一个的话,那所有的疑惑便都得到解答了:放的是权A,要夺的则是权B ,但无论如何,作为特权一层,还是可以享受些优待的,至少进入权B争夺之门时不用再去排队了。至于什么时间能得到权B,那是调度器说了才算的,你或许不由得去想,若去公关调度器话,那后门该如何走呢?!

    至此,就Wait参数的使用上,应该不会再有”To be or not to be, that’s a question”的疑惑了吧?

说了这么多,但对KeSetEvent为什么会导致IRQL的提升还没有解答,但其实答案已经明了:KeSetEvent执行时首先会调用KiLockDispatcherDatabase()来提升IRQL以阻止调度器的调度,在Wait = TRUE的情况下,返回时KeSetEvent会保持着高IRQL,而降低IRQL的工作将由后续的KeWaitForXXXObject来做。考虑到两个函数所操作的Event对象并不一定是同一个,所以二者都要通线程结构(KTHREAD)来进行通讯。

最后再说说返回值吧,DDK中是这样说的:If the previous state of the event object was signaled, a nonzero value is returned. 这句话说得还要让你玩“我猜猜”,其隐含的意思就是说如果此Event先前没有被激活,则返回0值。所以,简言之,KeSetEvent的返回值就是此Event先前的状态:Signaled or Not Signaled !

离奇死亡事件薄之CoCreate篇

“周节棍的双杰伦,砌里侉叉 …”

一阵吵杂的手机铃声将正在发呆的瓶瓶吵醒,他呆滞的眼神中蓦地闪出亮光,心想:大买卖又来了?

果不其然,又是一桩离奇案件:CoCreate在密镇莫名死亡,临终前嘴里还含糊地说了句“The file xxx failed to load. could not unpack the file xxx. Either this file is invalid, you do not have sufficient memory, or no space is left. (Error 398)” ,然后便咽气了。

密镇是凶杀案频发的地方,因其本身治案环境不好,很多案件都成了无头案。这也是为什么作为私家侦探的瓶瓶会被委托查找真凶的原因。

瓶瓶深思了一会,便开始着手重构死亡现场。这里还要多说一句,随着科技的进步,新时代的侦探们也有了更高效的破案方式方法,虽然原始的从验尸(Postmortem,Crash dump analysis)开始的福氏方式依然盛行,但越来越多的人都开始采用这种重建虚拟现场的方式,毕竟新的方法可以让你一遍一遍地推演,并在推演中不断地发现新的线索,直到真相大白。

重构虚拟现场需要相关的工具和设备才行。每位侦探都有自己偏爱的工具组合,当然也逃不脱其时代的限制与烙印。比如两位同姓宋的侦探前辈:宋慈因处宋朝,手中也只有手术刀之类的工具,纵使锋利,但作用毕竟有限;再看19世纪英国的宋戴克,他的方形绿皮箱里可谓是包罗万象,从放大镜,显微镜,到酒精灯,试剂等一应俱全,俨然福尔摩斯的化学实验室了。

但对于21世纪像瓶瓶一样的侦探们来说,一切又化繁为简,一般一台电脑足矣,有时也需要多台并加上若干互联设备。瓶瓶的书房便是充斥着各色各样的电子设备,相互间连接着的五颜六色的电缆全绞缠在一起。

瓶瓶最得心应手的工具包还是windbg,外加一根1394线,便足以对付大多数棘手的凶案。从业之初,瓶瓶用得可是debug工具包,后来经过softice,最后才用上windbg,前几年还是windbg第五代,当时用得还是串口线,现在已升级至第六代了。

不到一个小时,瓶瓶便已连接好所有设备,并设定妥了虚拟现场环境。随着“滴”的一声响,模拟系统开始启动,windbg上开始显现一串串的字符。此时,瓶瓶圆睁的双眼透过厚厚的近似镜片直盯盯地注视着快速翻滚的屏幕,生怕错过每一个细节。同时又暗自庆幸,CoCreate并没有进行反模拟手术,否则,如果像3ds max那样完全依赖log来分析地话苦头可就大了。

忽然“铛”地一声响,屏幕跳出了CoCreate的临终遗言。此次模拟相当成功,但在细致分析windbg记录之前还没找到任何明显线索,在浏览了所有记录后,也未能找到什么重要的线索。不得不又架设好procmon工具,重新又模拟了一次。procmon工具可以协助记录CoCreate死亡前的所有活动记录,及所有与他接触的相关人员等等。在procmon记录中,发现最后一个与死者接触的人是unzip。

“CoCreate死前找unzip干什么呢?”,瓶瓶陷入深思,忽然间,他想到了死者的遗言中有说 "unpack”,“难道真和unzip有关”,瓶瓶一边思考着一边努力寻找着证据。

瓶瓶打开了死者临死前正翻阅的文件,题为xxx。这是一个加密的文件,密镇定了法律为了保护所有信息,必须对各种重要文件进行加密,否则不能随便交换或传阅。瓶瓶当然知道怎么将此加密文件解密成明文,毕竟如果没有点关系,还真干不成侦探这个行当。

刚打开明文文件,瓶瓶就发现文件头竟赫然是 “504B0304”, ”504B”正是zip压缩法发明者Phil Katz的名字缩写: PK。基本可以肯定xxx文件为zip文件格式,所以CoCreate死前才会去见unzip。若如此地话,那unzip倒底做了些什么呢?

瓶瓶大感意外的是:等找到unzip时,发现unzip也离奇死亡了。难道是…连环谋杀?看来此案件内情确实不简单,搞不好会惹火上身!想到此,瓶瓶不禁觉得背后发寒。

但事已至此,由不得自己了。瓶瓶先勘察了unzip的死亡现场,但另他失望的是,unzip临死留下的遗言更让人摸不着头脑:  ”End-of-central-directory signature not found. Either this file is not a zipfile, or it constitutes one disk of a multi-part archive…”。这倒底什么意思?进行了一番搜索后,瓶瓶发现如此死亡的案件竟有很多,只是,众多老案件至今还未结案。

多次探索均无功而返,瓶瓶决定先从模拟unzip死亡现场开始。好在unzip也是位名人,有关他的材料可谓是汗牛充栋,特别是有关他的生平传记(unzip60.zip),更是给现场模拟提供了最好的背景材料。

模拟中果然有新的发现,unzip在通过公开渠道获取的加密文件信息竟是错的。此公开渠道是_stat64调用:

int _stat64( const char path, struct __stat64 buffer );

_stat64会返回文件大小、时间戳等信息。关键的地方是:_stat64获取这些信息的方法不是通过GetFileInformationByHandle (IRP_MJ_QUERY_INFORMATION/FileStandardInformation),而是通过FindFirstFileEx (即IRP_MJ_DIRECTORY_CONTROL/IRP_MN_QUERY_DIRECTORY)得来的。这样操作如果在平时是没有问题的,但在密镇却会出错。

密镇会对所有文件进行过滤并加密,即便是授权的用户在用第二种办法查询文件时,所返回的文件大小与实际大小也是不相符的,因为密镇的所有文件都要附载加密信息而被增厚了。故unzip用错误的文件页码信息来校验文件时,必然会发生了意外。

这个问题当然是出在密镇身上,但令人可气的是,unzip在调用_stat64后又调用了GetFileTime (IRP_MJ_QUERY_INFORMATION/FileBasicInformation),既然都动用私人关系了,为什么不一次性到位,直接将文件大小也一并取了呢?!

案件侦测至此,案情已基本大白。只是善后工作该怎样去做,真是个让瓶瓶头痛的事情:

最彻底的办法,当然是改良密镇,虽然此方案涉及面太广。瓶瓶经过好一番努力,透过多个渠道才将此事办妥,本以为可以长舒一口气的当口,黑老大M$的代表Office Word发飙了,无论如何都反对。此事竟然黑老大也参与其中?这让瓶瓶不得其解,在没有搞清楚其中奥妙之前,也只能放弃此番所有努力而另辟奇径了,毕竟太岁头上的土动不得。

既然unzip已经死亡,那就不妨再推出个新个unzip吧。让新的unzip用新的私人渠道来获取加密文件的信息不就完了呗。

说到便做到,等将新的unzip装上设备,又进行新的一轮模拟,一切正常了。

连日的探查已让瓶瓶厚厚的镜片蒙上了一层灰尘,正好遮挡住了他更加疲惫的眼神。但无论如何,最后还是以最小的代价将善后工作完成了,瓶瓶终于能够安坐在弹簧椅上,惬意地随着电视哼起了 《游击队歌》。

密镇的电视共有200个频道,只是所有频道上都是相同的节目,而且都是红歌大比拼。红歌声中,瓶瓶淡定地等待着下一件凶杀案的发生。他知道,用不着等太久 ……

(本故事部分纯属虚构,请勿胡乱对号入座)

Pages:  1 2 3 4 5