文件系统过滤水力驱动过滤,怎么安全解决IRP

File System Minifilter Drivers(文件系统微型过滤驱动)
时间: 12:54:05
&&&& 阅读:351
&&&& 评论:
&&&& 收藏:0
标签:问题:
公司之前有一套文件过滤驱动,但是在实施过程中经常出现问题,现在交由我维护。于是在边看代码的过程中,一边查看官方资料,进行整理。
这套文件过滤驱动的目的只要是根据应用层下发的策略来控制对某些特定文件的控制,例如根据后缀名来决定是否允许查看,是否允许查看指定目录啊之类的功能。
MSDN上对可安装的文件系统驱动介绍:其中树形结构菜单可以看出可安装的文件驱动类型种类:
文件系统驱动那说的应该就是最基本的驱动了,过滤驱动则是完全基于文件驱动中的操作进行手动过滤,可以理解成你hook了系统的create函数,然后如果你要拦截这个操作你就按要求返回不允许,否则就传给系统下一层去继续就好了。
而minifilter驱动则是通过向过滤管理器(Filter Manager)驱动进行注册自己需要过滤的一些操作,提供指定格式的回调函数让过滤管理器来进行调用即可。对于每一种操作,minifilter都可以注册一个“过滤前”和“过滤后”被调用的回调函数。(有点类似于Linux内核上probe调试技术)。(preCreate和postCreate用来作为后面“过滤前”和“过滤后”被调用的回调函数的例子)。
要注意minifilter的加载顺序是根据一个Altitude来决定的,可以理解成驱动所处的维度。例如,当FilterManager检测到要执行create的时候,就去调用注册了对该操作进行过滤的minifilter的回调函数,根据Altitude的值从大到小的顺序先调用preCreate函数。而FilterManager检测到create执行完成后,便按照Altitude的值从小到大的顺序去调用postCreate函数。
minifilter是基于FilterManager所提供的类似插件一样的模块,它可以调用FilterManager所提供很多功能:
加载和卸载:
FilterLoad和FilterUnload可用于在用户态程序控制minifilter驱动的加载和卸载。
这里要注意的是minifilter还存在一个instance的概念,这个instance应该就是指某个特定的minifilter,比如有三个minifilter A、B、C。就代表三个minifilter,每一个instance有特定的名字、altitude和flags。当在instance里面调用FltStartFiltering的时候就会去通知FilterManager去将这个insance关联到磁盘以及相关的I/0操作过滤表上。如果在FLtStartFiltering返回之前,FilterManager检测新挂上的磁盘的话,就会自动通知这个instance(通过调用回调函数InstanceSetupCallBack),所以minifilter注册时提供的Instance开头的回调函数,主要是用于FilterManager对这个驱动的一些反馈操作。(比如关联磁盘啊、取消关联啊之类的)。
此外,minifilter自己主动去卸载的话,当然是调用函数了。
英语能力确实不太好,在看后面的I/O操作的介绍的时候看的实在费解。幸好有搜到中文资料:。还是挺感慨很多高深底层的东西都只有英文资料。我就不多做口舌甚至好有可能误导了。
WDK示例:在WDK开发包示例中的IFSK Samples类别下。源码目录是src/filesys/minifilter。中文介绍:
可能根据我的需求,比较拥有的应该是以上三个例子了,其中swapBuffers那个,用来做加解密比较适合参考。
记录下我自己遇到的问题:
1、想要做个最简单的测试,通过vs来直接调试,于是简单的仅仅过滤IRP_MJ_CREATE操作,并且在Pre函数里得到操作的文件名,然后输出。而对于FLT_REGISTRATION结构体中,其他可选的回调函数全部置NULL。效果是根本不会进入到的我的pre函数中,原因是我压根没有attach我的驱动的instance到相关卷标上。(这里可以增进对instance这概念的理解,也就是说必须在相关卷标上attach了对应的instance才会生效)。所以如果在系统启动通过fltmc load命令或者FilterLoad和FltLoadFilter来手动加载驱动的,需要手动去关联minifilter到本地磁盘上。可以用fltmc attach命令、FilterAttach、FltAttachVolume、FilterAttachAtAltitude、FltAttachVolumeAtAltitude来手动加载。
2、原来的过滤驱动代码中,是在InstanceSetup中根据需要关联的卷来获取磁盘信息等,保存到Instance的上下文中。如果开启了磁盘控制,还要对USB类型的磁盘设备名称进行记录。话说这里为什么不在磁盘驱动里面记录呢??
<font color="#、原来的代码里创建上下文的地方,既调用了FltAllocateContext也调用了FltSet***Context函数的,如果都成功的话,需要调用两次FltReleaseContext,而实际只调用了一次。
上下文(context):
流上下文(stream context):为文件流所设置的上下文,需要和打开了的文件对象关联,所以不能在pre-create回调函数中直接关联,但是可以在pre-create中创建,然后通过pre-create的最后一个参数传递到post-create中进行调用FltSetStreamContext进行关联。
pre回调函数(PFLT_PRE_OPERATION_CALLBACK)
FLT_PREOP_COMPLETE:表示当前的过滤驱动完成了本次I/O操作,过滤管理器就不再往下发送本次I/O请求,而是依次向上调用post回调函数。这种情况下IoStatus.Status的值就是最终I/O操作的执行结果(不能是STATUS_PENDING)。
FLT_PREOP_SUCCESS_NO_CALLBACK/FLT_PREOP_SUCCESS_WITH_CALLBACK:这个返回值表示处理成功,让过滤管理器去做自己的事,区别在于WITH_CALLBACK的返回值会标明需要回调post函数。而NO_CALLBACK的则标明不需要。
FLT_PREOP_PENDING:顾名思义,表明当前过滤驱动将本次I/O操作挂起了,过滤管理器需要等待当前驱动调用FltCompletePendedPreOperation函数后才会继续本次I/0操作处理流程。注意只有对于基于IRP中断的I/O操作(用FLT_IS_IRP_OPERATION宏测试)才可以挂起。
FLT_PREOP_DISALLOW_FASTIO:只有操作是fast I/O操作(用FLT_IS_FASTIO_OPERATION(Data)进行测试)时才可以返回这个值,表明过滤驱动不允许fast I/O操作继续执行。因此过滤管理器不会再下发该请求,而是依次向上调用post回调函数。这种情况下不需要设置IoStatus.Status的值,过滤管理器会自动设置这个值。
FLT_PREOP_SYNCHRONIZE:这个返回值表明处理未完成,保持当前过滤驱动上下文线程环境,交由过滤管理器继续下发后调用post回调函数后继续处理。也只对基于IRP中断的操作有效,并且必须有post函数,如果不是基于IRP中断的,就会和FLT_PREOP_SUCCESS_WITH_CALLBACK一样。注意:对于Create操作,不应该返回这个值,因为文件管理器已经为这个操作进行同步了。此外对于同步的读和写操作,如果返回这个值会严重影响驱动和系统性能。
如果在pre和post函数中更改了Data的内容,必须调用FltSetCallbackDataDirty函数(更改IoStatus除外)。
Post回调函数(PFLT_POST_OPERATION_CALLBACK)
这个回调函数执行的中断等级为IRQL &= DISPATCH_LEVEL。所以需要注意以下几点:1、不能安全调用必须低于IRQL级别的任何内核模式的派遣函数。2、在这个函数内开辟的任何数据结构必须位于非页内存。3、该函数不可分页。4、不能请求资源(resource)、信号量(mutextes)和快速信号量(fast mutexes),只能获取互斥锁(spin lock)。5、不能获取、设置或者删除上下文,但可以释放上下文。
相对于Pre回调函数多了最后一个参数Flags,这个参数如果存在FLTFL_POST_OPERATION_DRAINING标记位,则表明当前过滤驱动实例正在被取消关联,本次调用是为了清理pre回调函数传入的completion context,返回值必须是FLT_POSTOP_FINISHED_PROCESSING,并且这个回调是在IRQL&=APC_LEVEL执行的。
FLT_POSTOP_FINISHED_PROCESSING:表明本过滤驱动完成了对I/0操作的处理并将控制交还给过滤管理器。
FLT_POSTOP_MORE_PROCESSING_REQUIRED:只有当微过滤驱动将本次I/O操作发送到工作队列中时,才能返回这个值,微过滤驱动最后必须负责完成这个I/O操作。过滤管理器会继续等待FltCompletePendedPostOperation函数被调用后才继续执行控制。注意:这个返回值也必须对基于IRP中断的操作执行。
任何要被执行在IRQL&DISPATCH_LEVEL的I/O完成处理都不能在这个回调函数内直接执行。取而代之,可以使用FltDoCompletionRpocessingWhenSafe或者FltQueueDeferredIoWorkItem之类的函数发送到工作队列中。
除了以下情况外,要确保在Flags参数没有FLTFL_POST_OPERATION_DRAINING标记位的时候才能调用FltDoCompletionRpocessingWhenSafe函数:
1、如果微过滤驱动的pre回调函数为一个基于IRP中断的操作返回FLT_PREOP_SYNCHRONIZE,那么对应的post回调要保证和pre回调都处在IRQL&=APC_LEVEL的线程上下文中。
2、对于create操作的post回调要保证中断级别为IRQL_PASSIVE_LEVEL(原始IRP_MJ_CREATE操作所处的线程上下文)。标签:
&&国之画&&&& &&
版权所有 京ICP备号-2
迷上了代码!lwglucky 的BLOG
用户名:lwglucky
文章数:250
评论数:85
访问量:679015
注册日期:
阅读量:5863
阅读量:12276
阅读量:414077
阅读量:1101819
51CTO推荐博文
看了 ChuKuangRen 的第二版《文件过滤驱动开发教程》后,颇有感触。我想,交流都是建立在平等的基础上,在抱怨氛围和环境不好的同时应该先想一想自己究竟付出了多少?只知索取不愿付出的人也就不用抱怨了,要怪也只能怪自己。发自己心得的人无非是两种目的,一是引发一些讨论,好纠正自己错误的认识,以便从中获取更多的知识使自己进步的更快。二是做一份备忘,当自己遗忘的时候能够马上找到相关资料。我这里也总结了近几年做文件过滤驱动时所积累下来的一些小小经验,这分笔记也是看了 ChuKuangRen 的教程后,临时想到的一小部分而已,是想到哪写到哪,不是很全,如果以后再回想起什么也会不断补充。因其工作原因,近段时间在 SOLARIS 驱动与 Linux 内核方面投入的精力比较多,Windows 下的文件过滤驱动一直也没有怎么去碰,所以最后还是那句老话 FIXME。
1、获得文件全路径以及判断时机
除在所有 IRP_MJ_XXX 之前自己从头创建 IRP 发送到下层设备查询全路径外,不要尝试在 IRP_MJ_CREATE 以外的地方获得全路径,因为只有在 IRP_MJ_CREATE
中才会使用 ObCreateObject() 来建立一个有效的 FILE_OBJECT。而在 IRP_READ IRP_WRITE 中它们是直接操作 FCB (File Control Block)的。
2、从头建立 IRP 发送关注点
无论你建立什么样的 IRP,是 IRP_MJ_CREATE 也好还是 IRP_MJ_DIRECTORY_CONTROL也罢,最要提醒的就是一些标志。不同的标志会代来不同的结果,有些结果是直接返回失败。这里指的标志不光是 IRP-&Flags,还要考虑 IO_STACK_LOCATION-&Flags还有其它等等。尤其是你要达到一些特殊目的,这时候更需要注意,如 IRP_MN_QUERY_DIRECTORY,不同的标志结果有很大的不同。
3、从头建立 IRP 获取全路径注意点
自己从头建立一个 IRP_MJ_QUERY_INFORMATION 的 IRP 获取全路径时需要注意,不仅在 IRP_MJ_CREATE 要做区别处理,在 IRP_MJ_CLOSE 也要做同样的处理,否则如果目标是 NTFS 文件系统的话可能产生 deadlock。如果是 NTFS 那么在 IRP_MJ_CLEANUP 的时候也需要对 FO_STREAM_FILE 类型的文件做同样处理。
4、获得本地/远程访问用户名(域名/SID)
方法只有在 IRP_MJ_CREATE 中才可用,那是因为 IO_SECURITY_CONTEXT 只有在 IO_STACK_LOCATION-&Parameters.Create.SecurityContext 才会有效。这样你才有可能从 IO_SECURITY_CONTEXT-&SecurityContext-&AccessState-&SubjectSecurityContext.XXXToken 中获得访问 TOKEN,从而进一步得到用户名或 SID。记得 IFS 中有一个库,它的 LIB 导出一个函数可以让你在获得以上信息后得到用户名与域名。但如果你想兼容 NT4 的话,只能自己分析来得出本地和远程的 SID。
5、文件与目录的判断
正确的方法在楚狂人的文档里已经说过了,再补充一句。如果你的文件过滤驱动要兼容所有文件系统,那么不要十分相信从 FileObject-&FsContext 里取得的数据。正确的方法还是在你传递下去 IRP_MJ_CREATE 后从最下层文件系统延设备栈返回到你这里后再获得。
6、加/解密中判断点
只判断 IRP_PAGING_IO,IRP_SYNCHRONOUS_PAGING_IO,IRP_NOCACHE 是没错的。如果有问题,相信是自己的问题。关于有人提到在 FILE_OBJECT-&Flags中的 FO_NO_INTERMEDIATE_BUFFERING 是否需要判断,对此问题的回答是只要你判断了 IRP_NOCACHE 就不用再判断 FILE_OBJECT 中的,因为它最终会设置 IRP-&Flags 为 IRP_NOCACHE。关于你看到的诸如 IRP_DEFER_IO_COMPLETION 等 IRP 不要去管它,因为它只是一个过程。最终读写还是如上所介绍。至于以上这些 IRP 哪个是由 CC MGR 发送的,哪些是由 I/O MGR 发送和在什么时候发送的,这个已经有很多讨论了,相信可以找到。
7、举例说明关于 IRP 传递与完成注意事项
只看 Walter Oney 的那本 《Programming the Microsoft Windows driver model》里介绍的流程,自己没有实际的体会还是不够的,那里只介绍了基础概念,让自己有了知识。知道如何用,在什么情况下用,用哪种方法,能够用的稳定这叫有了技术。我们从另一个角度出发,把问题分为两段来看,这样利于总结。一个 IRP 在过滤驱动中,把它分为需要安装 CompleteRoutine 的与无需安装 CompleteRoutine 的。那么在不需要安装 CompleteRoutine 的有以下几类情况。
(1) 拿到这个 IRP 后什么都不做,直接调用 IoCompleteRequest() 来返回。
(2) 拿到这个 IRP 后什么都不做,直接传递到底层设备,使用IoSkipCurrentIrpStackLocation() 后调用 IoCallDriver() 传递。
(3) 使用 IoBuildSynchronousFsdRequest() 或 IoBuildDeviceIoControlRequest()来建立 IRP 的。
以上几种根据需要直接使用即可,除了一些参数与标志需要注意外,没有什么系统机制相关的东西需要注意了。那么再来看需要安装 CompleteRoutine 的情况。我们把这种情况再细分为两种,一是在 CompleteRoutine 中返回标志为STATUS_MORE_PROCESSING_REQUIRED 的情况。二是返回处这个外的标志,需要使用函数IoMarkIrpPending() 的情况。在 CompleteRoutine 中绝大多数就这么两种情况,你需要使用其中的一种情况。那么为什么需要安装 CompleteRoutine 呢?那是因为我们对其 IRP 从上层驱动,经过我们驱动,在经过底层设备栈返回到我们这一层驱动时需要得到其中内容作为参考依据的,还有对其中内容需要进行修改的。再有一种情况是没有经过上层驱动,而 IRP 的产生是在我们驱动直接下发到底层驱动,而经过设备栈后返回到我们这一层,且我们不在希望它继续向上返回的,因为这个 IRP 本身就不是从上层来的。综上所述,先来看下 IoMarkIrpPending() 的情况。
(1) 在 CompleteRoutine 中判断 Irp-&PendingReturned 并使用 IoMarkIrpPending()然后返回。这种方法在没有使用 KeSetEvent() 的情况下,且不是自建 IRP 发送到底层驱动返回时使用。也就是说有可能我所做的工作都是在 CompleteRoutine 中进行的。比如加/解密时,我在这里对下层驱动返回数据的判断并修改。修改后因为没有使用 STATUS_MORE_PROCESSING_REQUIRED 标志,它会延设备堆一直向上返回并到用户得到数据为止。这里一定要注意,在这种情况下 CompleteRoutine返回后,不要在碰这个 IRP。也就是说如果这个时候你使用了 IoCompleteRequest()的话会出现一个 MULTIPLE_IRP_COMPLIETE_REQUEST 的 BSOD 错误。
(2) 在 CompleteRoutine 中直接返回 STATUS_MORE_PROCESSING_REQUIRED 标志。这种情况在使用了 KeSetEvent() 的函数下出现。这里又有两个小小的分之。
1) 出现于上层发送到我这里,当我这里使用 IoCallDriver() 后,底层返回数据经过我这一层时,我想让它暂时停止继续向上传递,让这个 IRP 稍微歇息一会,等我对这个 IRP 返回的数据操作完成后(一般是没有在 CompleteRoutine中对返回数据进行操作情况下,也就是说等到完成例程返回后再进行操作),由我来调用 IoCompleteRequest() 让它延着设备栈继续返回。这里要注意,我们是想让它返回的,所以调用了 IoCompleteRequest()。这个可不同于下面所讲的自己从头分配 IRP 时在 CompleteRoutine 中已经调用 IoFreeIrp() 释放了当前IRP 的情况。比如我在做一个改变文件大小,向文件头写入加密标志的驱动时,在上层发来了 IRP_MJ_QUERY_INFORMATION 查询文件,我想在这个时候获得文件信息进行判断,然后根据我的判断结果再移动文件指针。注意:上面是两步,第一步是先获得文件大小,那么在这个时候我就需要用到上述办法,先让这个 IRP传递下去,得到我想要的东西后在进行对比。等待适当时机完成这个 IRP,让数据继续传递,直到用户收到为止。第二步我会结合下面小节来讲。
2) 出现于自己从头建立 IRP,当使用 IoAllocate() 或 IoBuildAsynchronousFsdRequest()创建 IRP 调用 IoCallDriver() 后,底层返回数据到我这一层时,我不想让这个 IRP 继续向上延设备栈传递。因为这个 IRP 就是在我这层次建立的,上层本就不知道有这么一个 IRP。那么到这里我就要在 CompleteRoutine 中使用 IoFreeIrp()来释放掉这个 IRP,并不让它继续传递。这里一定要注意,在 CompleteRoutine函数返回后,这个 IRP 已经释放了,如果这个时候在有任何关于这个 IRP 的操作那么后果是灾难性的,必定导致 BSOD 错误。前面 1) 小节给出的例子只完成了第一步这里继续讲第二步,第一步我重用这个 IRP 得到了文件大小,那么这个时候虽然知道大小,但我还是无法知道这个文件是否被我加过密。这时,我就需要在这里自己从头建立一个 IRP_MJ_READ 的 IRP 来读取文件来判断是否我加密过了的文件,如果是,则要减少相应的大小,然后继续返回。注意:这里的返回是指让第一步的IRP 返回。而不是我们自己创建的。我们创建的都已经在CompleteRoutine 中销毁了。
8、关于完成 IRP 的动作简介
当一个底层驱动调用了 IoCompleteRequest() 函数时,基本上所有设备栈相关 IRP 处理工作都是在它那里完成的。包括 IRP-&Flags 的一些标志的判断,对 APC 的处理,抛出MULTIPLE_IRP_COMPLETE_REQUESTS 错误等。当它延设备栈一直调用驱动所安装的 CompleteRoutine时,如果发现 STATUS_MORE_PROCESSING_REQUIRED 这个标志,则会停止向上继续回滚。这也是为什么在 CompleteRoutine 中使用这个标志即可暂停 IRP 的原因。
9、关于 ObQueryNameString 的使用
这个函数的使用,在有些环境下会有问题。它的上层函数是 ZwQueryObject()。在某些情况下会导致系统挂起,或者直接 BSOD。它是从 对象管理器中的 ObpRootDirectoryObject开始遍历,通过 OBJECT_HEADER_TO_NAME_INFO 获得对象名称。今天问了下 PolyMeta好象是在处理 PIPE 时会挂启,这个问题出现在 2000 系统。在 XP 上好象补丁了。
10、关于重入问题
其实这个问题在很久前的 IFS FAQ 里已经介绍的很清楚,包括处理方法以及每种方法可能带来的问题。IFS FAQ 里的 Q34 一共介绍了四种方法,包括自己从头建立 IRP发送,使用 ShadowDevice,使用特征字符串,根据线程 ID,在 XP 下使用IoCreateFileSpecifyDeviceObjectHint() 函数。并且把以上几种在不同环境下使用要处理的问题也做了简单的介绍。且在 Q33 里介绍了在 CIFS 碰到的 FILE_COMPLETE_IF_OPLOCKED 问题的解决方法。
650) this.width=650;" onclick='window.open("/viewpic.php?refimg=" + this.src)' alt="" src="http://hi.bccn.net/image/click/luguo.gif" />
650) this.width=650;" onclick='window.open("/viewpic.php?refimg=" + this.src)' alt="" src="http://hi.bccn.net/image/click/jidan.gif" />
650) this.width=650;" onclick='window.open("/viewpic.php?refimg=" + this.src)' alt="" src="http://hi.bccn.net/image/click/xianhua.gif" />
650) this.width=650;" onclick='window.open("/viewpic.php?refimg=" + this.src)' alt="" src="http://hi.bccn.net/image/click/woshou.gif" />
650) this.width=650;" onclick='window.open("/viewpic.php?refimg=" + this.src)' alt="" src="http://hi.bccn.net/image/click/leiren.gif" />
评论 (3 个评论)
windows服务调用机制
Windows系统服务调用是存在于Windows系统中的一个关键接口,常常称作SystemCall,SysemServiceCall或SystemServiceDispatching等,在此我们就权且称之为Windows系统服务调用,它提供了操作系统环境由用户态切换到内核态的功能。虽然在国外关于Windows系统服务调用的讨论比较多,但却很少看到比较详细的中文资料,希望本文能够为和作者一样对Windows底层感兴趣并且是刚刚接触的朋友提供一些帮助。文章中将以一个内核级的进程监视/隐藏工具T-ProcMon为例来详细讨论Windows系统服务调用的相关技术细节。另需注意本文讨论的技术仅适用于基于WindowsNT内核的操作系统,并以Windows2000为例。
二、Windows2000系统体系结构
微软Windows2000是一个主要面向网络服务器的操作系统,因此它和以前大家比较熟悉的Windows9x有很大的区别。但是对于讨论一个因商业策略而出现的个人桌面操作系统的确没有太大的价值。所以我们将主要介绍一些关于NT系统内部结构的细节。Windows2000在实现其自身目标的过程中,我们有必要讲解一些它的特性。
1.可扩展性(Extensibility)
Windows2000操作系统是一个面向未来的系统,所以它非常注重自身的扩展性,因为在将来可能有许多市场等方面的原因导致我们必须添加或删除目前操作系统的一些组件,这就必须要求操作系统有较强的可扩展性。为了满足扩充/删除的各种需求,Windows2000提供了一个重要的设计思想就是子系统(Subsystem)。我们可以将一些需要扩展的操作系统功能作为一个子系统添加到Windows2000内,就像OS/2,POSIX等一样。当然还有一个特性就是,我们可以通过为系统服务调用添加钩子来修改系统的各项行为,这就为我们提供了一个了解系统内部并扩展系统功能的机会。
2.可靠性和健壮性(ReliabilityandRobust)
一个系统存在的最基本的要求就是它的稳定性,没有稳定的环境就做不出任何满意的产品。为了满足这项要求,Windows2000提出了基于对象的访问控制权限的措施。现代的大多数微处理器都支持两种模式:用户模式(User/Normal)和内核模式(Kernel/Privileged)。操作系统组件和关键的系统组件处于内核模式,而一般用户模式的程序只能访问私有地址空间和执行非特权等级的指令。如果用户要调用一些内核组件的功能,就得通过系统服务调用来实现。
3.兼容性(Compatibility)
Intel和Microsoft能够做到今天的一个很重要的因素就是他们支持对过去存在系统的兼容。这一点非常的关键,没有人愿意三天两头的更换系统,当然也很少有人有这个经济实力。Windows2000为了实现对其他系统的兼容,如Dos,16位Windows等,出现了环境子系统。而在Windows2000中必须存在的环境子系统是Win32,它是其他子系统的基础,其他子系统都是一些表面的接口,而实际上是调用了Win32提供的接口,而Win32最终也是通过系统服务调用来与内核联系的。虽然操作系统为各种环境子系统提供了不同的动态链接库,而且其中的API函数名称往往也是不同的,不过这个函数的最终都是通过相同的系统服务调用进入内核来实现的。
(NextPage)
4.易维护性(Maintainability)
作为一个大型的项目,Windows2000的维护也成为了一个大型的工程。而如此巨大的项目没有很好的维护性是无法发展下去的。为此,Windows2000使用了分层的思想,这也是一种操作系统体系结构模型。其中,系统服务调用将系统的内核模式代码和用户模式代码隔离开来,子系统使用系统服务调用为用户提供应用程序编程接口(API),而系统服务调用向下调用执行体实现各项功能。
就像在上文我们提到的操作系统存在的两种模式,这是建立在处理器的基础之上的。按理说,一般处理器可以提供从Ring0到Ring3的四种处理器模式,但是它们必须提供至少两种,那就是Ring0和Ring3。而一些特殊处理器指令只能在内核模式执行,而一些地址空间必须在内核模式才可以被访问。Windows2000就利用了这个特点,将操作系统和其他关键组件保护起来,只有在内核模式才可以访问执行,而一般的用户程序就只能在用户态执行咯,这样就可以避免一些用户程序对操作系统代码的破坏,也就是大家看到的Windows2000明显比Windows9x稳定得多的主要原因。下面我们给出了Windows2000的体系结构简图:
&& 系统支持进程,服务进程,应用程序,环境子系统
&& 应用程序编程接口
&& 基于NTDLL.dll的本地系统服务 (用户模式)
&& -----------------------------------------
&& 系统服务调用 (内核模式)
&& 系统内核,设备驱动程序
&& 硬件抽象层
三、Windows2000本机系统服务(NativeAPI)
Windows2000本机系统服务又称为Windows本机应用程序编程接口,它是由执行体(Executive)为用户模式和内核模式的程序提供的系统服务集。它包含两种类型的函数:Windows执行系统服务的系统服务调度占位程序;子系统,子系统DLL和其他本机映像使用的内部支持函数。
从用户模式调用本机系统服务是通过NTDLL.dll来实现的。表面上,Win32函数为编程人员提供了很多接口来实现我们想要的功能,但是这些Win32函数只不过是本机应用程序编程接口的一个包装器而已,它们将本机API包装起来,调用本机系统服务来实现用户期望的功能。也就是说NTDLL.dll只是系统服务调用接口在用户模式下的一个外壳。关于用户模式下的Windows本机系统服务的相关信息,请参见我以前写的一篇文章《探测Windows2K/XP/2003本机系统信息》。
(NextPage)
我们再谈谈从内核模式调用系统服务吧,这时就不是由NTDLL.dll导出系统服务调用的函数接口了,而是由ntoskrnl.exe来实现的,它会提供两种形式的函数:ZwXxx和NtXxx,在此我们就不多说了。大家应该注意到了,在上面我们介绍的Windows2000系统体系结构中的系统服务调用,执行体和内核都是存在于ntoskrnl.exe(在多处理器中为ntkrnlmp.exe)之中,并且是分层的。
四、Windows2000系统服务调用机制
Windows2000的陷阱调度(TrapDispatching)机制包括了:中断(Interrupt),延迟过程调用(DeferredProcedureCall),异步过程调用(AsynchronousProcedureCall),异常调度(ExceptionDispatching)和系统服务调用。在Intelx86的Windows2000系统中,处理器执行int0x2e指令来激活Windows系统服务调用;在Intelx86的WindowsXP系统中处理器却是通过执行sysenter指令使系统陷入系统服务调用程序中;而在AMD的WindowsXP中使用了指令syscall来实现同样的功能。我们暂时使用x86的Windows2000为例来演示。我们先给出一个系统服务调用的模型:
moveax,ServiceId
leaedx,ParameterTable
retParamTableBytes
其中,ServiceId清楚的说明了传递给系统服务调用的系统服务号,内核使用这个标识符来查找系统服务调度表(SystemServiceDispathTable)中的对应系统服务信息。在系统服务调度表中的每一项包含了一个指向系统服务程序的指针,我们Hook时就是修改这个指针使其指向我们自定义的系统服务的地址。ParameterTable是传递的参数,系统服务调用程序KiSystemService必须严格校验传递的每一个参数,并将其参数从线程的用户堆栈中复制到系统的核心堆栈以备使用。由于执行int指令会导致陷阱发生,所以在Windows2000内的中断描述表(IDT=InterruptDescriptorTable)中的0x2e项指向了系统服务调用程序。最后返回的ParamTableBytes是关于参数个数的信息。
现在我们已经看得出来了,系统服务调用只是一个接口,它提供了将用户模式下的请求转发到Windows2000内核的功能,并引发处理器模式的切换。在用户看来,系统服务调用接口就是Windows内核组件功能实现对外的一个界面。系统服务调用接口定义了Windows内核提供的大量服务。
五、Windows2000系统服务调用类型
在Windows2000中默认存在两个系统服务调度表,它们对应了两类不同的系统服务。这两个系统服务调度表分别是:KeServiceDescriptorTable和KeServiceDescriptorTableShadow。
(NextPage)
Windows2000执行程序服务对应于NTDLL.dll为我们提供的系统服务调用。子系统通过调用NTDLL.dll中的函数接口来实现它们需要的功能。系统服务调度表KeServiceDescriptorTable定义了在ntoskrln.exe中实现的系统服务,通常在kernel32.dll/advapi32.dll中提供的函数接口均是调用的这个系统服务调度表中。
同时存在于Windows2000操作系统中还有在Win32k.sys中实现的相关Win32USER和GDI函数,它们是属于另一类系统服务调用。与之对应的系统服务调度表为KeServiceDescriptorTableShadow,它提供了内核模式实现的USER和GDI服务。函数KeAddSystemServiceTable允许Win32.sys和其他设备驱动程序添加系统服务表。除了Win32k.sys服务表外,使用KeAddSystemServiceTable添加的服务表会被同时复制到KeServiceDescriptorTable和KeServiceDescriptorTableShadow中去。
我们可以看出这两类函数实现在服务调度上的区别:Win32内核API经过Kernel32.dll/advapi32.dll进入NTDLL.dll后使用int0x2e中断进入内核,最后在Ntoskrnl.exe中实现了真正的函数调用;Win32USER/GDIAPI直接通过User32.dll/Gdi32.dll进入了内核,最后却是在Win32k.sys中实现了真正的函数调用。在此我们只讨论与NTDLL.dll相关的函数,也就是我们例子中处理的函数。
六、Hook系统服务调用的作用
钩子(Hooking)是一种拦截/监听可执行代码在执行过程中相关信息的一种通用机制。它使我们了解系统内部结构,运作机制甚至修改系统行为的想法成为可能。在一个像M$存在的世界里,Windows的很多内部信息我们都是无法得知的,因为Windows不是Linux,但这并不意味着我们就此放弃!只要开动你的大脑,很多事情都会变成可能。
1.事件追踪
你想知道Windows在什么时候会打开一个进程吗?你想知道Windows任务管理器中进程相关信息的获取调用了哪些函数吗?我们都可以使用Hook技术来实现这些你想要的信息。我们可以追踪ZwOpenProcess的执行情况,我们同样也可以追踪ZwQueryInformationProcess的执行情况,包括传递的参数和返回的结果。大家可以看到本文相关的程序T-ProcMon就是一个进程监视工具,它会追踪系统中与进程相关的各种信息。在某些我们期望的事件发生时,程序会通知用户发生了什么,这也是我们期望看到的结果。
2.修改系统行为
操作系统为我们提供了一些通用的功能,如查询系统进程信息ZwQuerySystemInformation(SystemInformationClass==5),它会返回系统中当前所有进程/线程的相关信息。如果我们希望隐藏一些特殊的进程那该怎么办呢?那就是修改系统服务调用,也就是修改ZwQuerySystemInformation的行为。在查询系统进程时,系统会返回一个进程信息队列,每个单元对应一个进程,如果我们想隐藏其中的某个进程,只须修改队列中的某些数据,然后返回给上层函数,它们就不会发现Xxx.exe进程存在于系统之中了。
(NextPage)
3.研究系统内部机制
微软提供的Windows操作系统是一个&封闭&的系统,很多内部资料都没有公布,我们可以通过Hook技术来探测系统的内部数据结构和运行机制,学习操作系统内部的操作方式。基于Hook的Windows内核黑客技术(KernelHacking)是非常之流行和有效,在我们探测系统的一些未公开,未文档化的技术细节时我们都可以使用钩子技术。
其他如我们要调试一个非常麻烦的程序时就可以使用Hook技术,这样就可以更好的帮助我们追踪系统的行动,更好的了解程序内部的执行过程。同样,为了获取系统的一些特殊性能数据,我们也可以在特定的情况下使用Hook技术。
七、Hook系统服务调用的实现
在此我们讨论Hook的对象仅限于由Windows2000的ntoskrnl.exe提供的系统服务调用。Windows2000系统服务调用为内核模式的代码,所以我们必须书写设备驱动程序来访问系统服务调度表。如果你对Windows2000下基本设备驱动程序的书写不太清楚,请查阅相关的书籍,此处不做介绍。我们先回顾一下Win32内核API的实现流程。
Windows2000系统服务调用向用户提供了经过包装的用户模式的函数接口(由NTDLL.dll提供)。当Kernel32.dll/Advapi32.dll中的函数执行时,先调用NTDLL.dll中对应的相关接口,经过参数检查后使用int0x2e指令进入内核模式,传递相关的服务号和参数列表。在ntoskrnl.exe中维护着两个表系统服务调度表(SystemServiceDispathTable)和系统服务参数表(SystemServiceParameterTable),其中int0x2e指令就是通过服务号在SSDT中查询相关系统服务程序指针的。现在我们已经清楚了每个系统服务调用都对应一个服务号,同时也对应一个服务程序的地址!如果我们修改SSDT中的某个系统服务程序的入口地址为指向我们自定义的函数地址,在执行完我们的代码后再执行原始系统服务地址处的代码,这不就实现了对系统服务调用的了Hook吗?
对我们来说,定位系统服务调度表是实现Hook的关键。在Windows2000中有一个未公开的由ntoskrnl.exe导出的单元:KeServiceDescriptorTable,我们可以通过它来完成对SSDT的访问与修改。KeServiceDescriptorTable对应于一个数据结构,定义如下:
typedefstructSystemServiceDescriptorTable
UINT *ServiceTableB
UINT *ServiceCounterTableB
UINT NumberOfS
UCHAR *ParameterTableB
}SystemServiceDescriptorTable,*PSystemServiceDescriptorT
(NextPage)
其中ServiceTableBase指向系统服务程序的地址,ParameterTableBase则指向SSPT中的参数地址,它们都包含了NumberOfService这么多个单元。我们只要由KeServiceDescriptorTable找到了我们关注的系统服务调用程序,就可以修改它的ServiceTableBase参数来实现对相关系统服务调用的Hook了!
八、T-ProcMon-1.0关键源码分析
1.基于CUI的用户模式控制程序
由于在此之前我已经对Win32的系统服务进行了详细的介绍,现在就不做多说了,大家如果有什么疑问请参阅我以前写的文章,你可以到FZ5FZ的主页阅读相关文章,或下载相关源代码。
2.基于设备驱动的Hook代码
定义在用户模式与内核模式程序间通信的命令代码:
#definePROCMON_MONITOR(ULONG)CTL_CODE(FILE_DEVICE_PROCMON,0x01,METHOD_BUFFERED,FILE_ANY_ACCESS)
#definePROCMON_HIDDEN (ULONG)CTL_CODE(FILE_DEVICE_PROCMON,0x02,METHOD_BUFFERED,FILE_ANY_ACCESS)
#definePROCMON_HOOK (ULONG)CTL_CODE(FILE_DEVICE_PROCMON,0x03,METHOD_BUFFERED,FILE_ANY_ACCESS)
#definePROCMON_UNHOOK (ULONG)CTL_CODE(FILE_DEVICE_PROCMON,0x04,METHOD_BUFFERED,FILE_ANY_ACCESS)
将KeServiceDescriptorTable与相关数据结构联系起来,定义系统调用:
__declspec(dllimport) ServiceDescriptorTableEntryKeServiceDescriptorT
#defineSYSCALL(_function)KeServiceDescriptorTable.ServiceTableBase(*(PULONG)((PUCHAR)_function+1))
定义各种未公开的函数,如ZwQuerySystemInformation:
(*ZWQUERYSYSTEMINFORMATION)(
IN ULONG SystemInformationClass,
INOUTPVOID SystemInformation,
IN ULONG SystemInformaitonLength,
OUT PULONG ReturnLengthOPTIONAL);
&& 修改系统服务调用,保存原始的入口地址,修改为我们自定义的程序入口地址,如ZwQuerySystemInformation:
OldZwQuerySystemInformation =(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation));
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation))& =NewZwQuerySystemI
解除钩子,还原系统服务调用:
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation))& =OldZwQuerySystemI
(NextPage)
调用原始的系统服务程序代码:
NtStatus=(OldZwQuerySystemInformation)
(SystemInformationClass,
SystemInformation,
SystemInformaitonLength,
ReturnLength);
隐藏进程,既是修改系统返回的数据队列中相关项的偏移量使起指向需要隐藏进程的下一个单元,也就是说跳过我们需要隐藏进程的单元:
if(RtlCompareUnicodeString(&pCurrentNK-&Name,&ProcCur-&ProcessName,TRUE)==0)
RtlUnicodeStringToAnsiString(&ProcNameA,&pCurrentNK-&Name,TRUE);
DbgPrint(&HiddenProcessName:%s\n&,ProcNameA.Buffer);
if(ProcPre!=NULL)
if(ProcCur-&NextEntryDelta!=0)
ProcPre-&NextEntryDelta+=ProcCur-&NextEntryD
ProcPre-&NextEntryDelta =0;
if(ProcCur-&NextEntryDelta!=0)
SystemInformation=(PSYSTEM_PROCESSES)((PTSTR)ProcCur+ProcCur-&NextEntryDelta);
SystemInformation=NULL;
WINDOWS下的磁盘还原及穿透技术内幕
&&& 还原精灵、冰点、影子系统等还原产品在关键的技术原理上没有本质区别,都是在磁盘过滤驱动一层对特定的扇区IO进行了重定位处理。本文将对这种技术进行简略分析,并提供一套实用的实现源代码。多点还原的实现方式与前面几者区别较大,这里不作讨论。
&&& 我们都知道,当系统里安装了磁盘&还原&类软件并激活后,所有应用层文件的写操作&&创建、修改、删除,以及各种系统设置、注册表的变化,甚至包括分区格式化,在系统重启后都被完全恢复到激活还原软件时的状态。这种&恢复&的表象背后,并不是真的有什么机制在帮我们把文件系统的各种操作回退到最初的状态。事实上,一旦还原软件被激活后,我们的对文件的所有写操作就不再是有效的,全都被写到磁盘上的一些临时空间中,比如未使用的簇或预先分配的特定存储文件内,而磁盘分区内已分配给文件占用的有效扇区,一个也不会被写入!由于文件系统下层的还原驱动程序通过内存中的一个重定位表,帮我们维护着这些新写入的脏数据快与上层文件系统结构的链接关系,所以我们仍然觉得各种文件操作都在正常进行。但是当系统重启后,内存中非持久的重定位表消失了,驱动程序也处于初始化状态,更关键的是,文件系统的原来分配的有效扇区一个也没变!所以我们通过文件系统看到的磁盘分区结构也是没有变化(其实是有变化的,你在系统重启前写入的那些文件都在磁盘未分配簇或临时转储文件里)。
&&& 弄明白上述过程,就可以分析出磁盘过滤还原驱动的关键技术点了:
&&& 一、分析磁盘分区结构,你得知道什么是MBR、DBR、文件簇、扇区相对偏移(分区内)、扇区绝对偏移(磁盘内)、磁盘分区结构表等,还得对FAT\NTFS文件系统结构有一定的了解,至少你得知道FAT的簇分配表以及NTFS的$MFT和$BITMAP。
&&& 二、截获扇区IO(文件系统是以簇为单位操作,簇到了磁盘驱动这一层就变成了连续的扇区块),然后根据扇区的偏移做出不同的处理。
&&&&&&& 下面为了叙述方便,我们把在还原状态下被写过的扇区称为&脏&扇区,未写过的扇区称为净扇区。
&&&&&&& 读操作&&&&&&&&&&&&
&&&&&&&&&&&&& 读净扇区&&直接下传该操作的IRP;
&&&&&&&&&&&&& 读脏扇区&&查询重定位表,找到脏扇区数据的实际存储位置,转换IRP的偏移,分割后再下传(这里如何分割并创建子IRP是个难点);
&&&&&&& 写操作
&&&&&&&&&&&&& 写净扇区&&在未使用的扇区里分配出同样大小的空间,设置重定位表,转换IRP的偏移,分割后再下传;
&&&&&&&&&&&&& 写脏扇区&&查询重定位表,转换IRP的偏移,分割后再下传;
&&& 三、高效的重定位算法及数据结构,位图是个比较直观方式,但是当磁盘空间很大时,也许你在物理内存中无法放置一个映射全盘扇区的位图,而且位图的操作效率就是最好的吗?
&&& 四、从磁盘分区信息中解析出文件的簇链,这个并不是还原驱动所必须的,但如果你要做一些相对高级的功能,就会需要以文件为单位来进行操作而不是以扇区为单位来进行操作。使用内核文件API?磁盘驱动中怎么能使用文件API呢!
&&& 五、穿透,有很多方式,这里举一个例子:即动态切换磁盘的还原/非还原状态,然后在非还原状态下把我们需要更新的文件写入磁盘扇区。虽然更新的时间通常只是一会儿,为了稳妥起见,在非还原状态下还是要使用某种临时保护机制(比如文件系统只读),禁止其它进程写文件。切换分区还原状态一定要锁定分区,因为未锁定的分区在内存中可能有各种未决的操作、中间状态及未刷新的缓存,是不稳定的。锁定分区和临时的保护方式,会带来一些应用上的不便。目前有一种不锁定分区,映射出虚拟分区后两次写操作的更新方式。
&&& 六、其它一些细节。比如pagefile.sys\hiberfil.SYS文件的特殊处理、BOOT文件标志的设置、阻止WINDOWS磁盘分区管理程序的操作,以及对MBR\DBR的保护等等。
&&& 七、磁盘过滤驱动的安装及防护。对于那些写ROOTKIT、病毒、木马的混蛋们来说,要绕过一个标准的磁盘过滤驱动是很容易的。所以在驱动安装及防止非法穿透上要不断地改进,做还原驱动做到这步就跟做杀毒软件的有点类似了,道高一尺魔高一丈啊!
比较 windows和OS2位图的24真彩结构
windows位图24真彩结构:
第10字节=54
第18-21字节(长整型)位图宽度
第22-25字节(长整型)位图高度
第26-27字节(整型)调色板=1
第28-29字节(整型)单个色点占位=24
第54字节-第(宽*高*3+53)字节象素数据(排行方式为r,g,b,r,g,b,r,g,b、、、、、、)
OS2位图24真彩结构:
第10字节=26
第18-19字节(整型)位图宽度
第20-21字节(整型)位图高度
第22-23字节(整型)调色板=1
第24-25字节(整型)单个色点占位=24
第26字节-第(宽*高*3+25)字节象素数据(排行方式为r,g,b,r,g,b,r,g,b、、、、、、)
下面是一段不完全的代码实现:
typedef unsigned char&&&& BYTE;
struct rgbColor
&&&&&&& int usedTimes[4096] = {0};
&&& int color[4096];
&&& for (int i = 0; i & 4096; i++)
&&&&&&& color[i] =
&&& rgbColor PointC
&&& {// 读每个点的颜色值,存在PointColor中
&&&&&&& int iRed&&& = PointColor.red&&4;
&&&&&&& int iGreen&&& = PointColor.green&&4;
&&&&&&& int iBlue&&& = PointColor.blue&&4;
&&&&&&& int iColor&&& = (iRed&&8) + (iGreen&&4) + iB
&&&&&&& assert(iColor & 4096);
&&&&&&& usedTimes[iColor]++;
&&& int numberOfColors = 0;//原来的24位图中实际用到的颜色数
&&& for (i = 0; i & 4096; i++)
&&&&&&& if (usedTimes[i] & 0)
&&&&&&&&&&& numberOfColors++;
&&&&&& //对usedTimes进行排序,由大到小,排序过程中color数组也作与useTimes数组&&&
&&&&&& // 相似的交换
&&& int color8[256] = {0};//保存8bit位图用到的256种颜色
&&& for (i = 0; i & 256; i++)
&&&&&&& color8[i] = color[i];
&&& int *colorIndex = usedT//用原来的useTimes数组来保存索引值
&&& memset(colorIndex, 0, sizeof(int) * 4096);
&&& if (numberOfColors &= 256)
&&&&&&& for (i = 0; i & numberOfC i++)
&&&&&&&&&&& colorIndex[color[i]] =
&&&&&&& for (i = 0; i & 256; i++)
&&&&&&&&&&& colorIndex[color[i]] =
&&&&&&& int index, tmp, tmp1;
&&&&&&& for (i = 256; i & numberOfC i++)
&&&&&&&&&&& tmp&&&&& = PFC(color8[0], color[i]);
&&&&&&&&&&& index = 0;
&&&&&&&&&&& for (int j = 1; j & 256; j++)
&&&&&&&&&&& {
&&&&&&&&&&&&&&& tmp1 = PFC(color8[j], color[i]);
&&&&&&&&&&&&&&& if (tmp & tmp1)
&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&& tmp = tmp1;
&&&&&&&&&&&&&&&&&&& index =
&&&&&&&&&&&&&&& }
&&&&&&&&&&& }
&&&&&&&&&&& colorIndex[color[i]] =
&&& //现在对于24bit位图中某一点的颜色PointColor,有
&&& //&&& int iRed&&& = PointColor.red&&4;
&&& //&&& int iGreen&&& = PointColor.green&&4;
&&& //&&& int iBlue&&& = PointColor.blue&&4;
&&& //&&& int iColor&&& = (iRed&&8) + (iGreen&&4) + iB
&&& //& colorIndex[iColor]即为8bit位图中对应点的调色盘索引值
&&&&&&& //& 8bit位图调色盘用到的256种颜色保存在color8数组中
/***********
位图文件头
该结构的长度是固定的,为14个字节,各个域的说明如下:
&&& bfType:指定文件类型,必须是0x4d42,即字符串&BM&。
&&& bfSize:指定整个文件大小,4字节。
&&& bfReserved1,bfReserved2:保留字,为0,各2字节。
&&& bfOffBits:从文件头到实际的位图图像数据的偏移字节数。
*************/
struct BmpHead
&&& long imageS
&&& long startP
&&& void show(void)
&&&&&&& printf(&BMP Head:\n&);
&&&&&&& printf(&Image Size:%d\n&,imageSize);
&&&&&&& printf(&Image Data Start Position : %d\n&,startPosition);
/*********************
&&& 位图信息头
该结构的长度也是固定的,为40个字节,各个域的说明如下:
?&&& biSize:指定这个结构的长度,为40个字节。
?&&& biWidth:指定图像的宽度,单位是象素。
?&&& biHeight:指定图像的高度,单位是象素。
?&&& biPlanes:必须是1。
?&&& biBitCount:指定表示颜色时用到的位数,常用的值为1(黑白二色图)、4(16色图)、8(256色图)、24(真彩色图)。
?&&& biCompression:指定位图是否压缩,有效值为BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS。Windows位图可采用RLE4和RLE8的压缩格式,BI_RGB表示不压缩。
?&&& biSizeImage:指定实际的位图数据占用的字节数,可用以下的公式计算出来:
&&&&&&&&&&&& biSizeImage = biWidth'& biHeight
&&& 要注意的是:上述公式中的biWidth'必须是4的整数倍(不是biWidth,而是大于或等于biWidth的最小4的整数倍)。如果biCompression为BI_RGB,则该项可能为0。
?&&& biXPelsPerMeter:指定目标设备的水平分辨率。
?&&& biYPelsPerMeter:指定目标设备的垂直分辨率。
?&&& biClrUsed:指定本图像实际用到的颜色数,如果该值为0,则用到的颜色数为2的biBitCount次幂。
?&&& biClrImportant:指定本图像中重要的颜色数,如果该值为0,则认为所有的颜色数都是重要的。
***********************************/
struct InfoHead
&&& long&&& L
&&& long&&&
&&& long&&&
&&& WORD&&& colorP
&&& WORD&&& bitC
&&& long&&& zipF
&&& long&&& realS
&&& long&&& xP
&&& long&&& yP
&&& long&&& colorU
&&& long&&& colorI
&&& void show(void)
&&& {&&&&&&
&&&&&&& printf(&infoHead Length:%d\n&,Length);
&&&&&&& printf(&width&height:%d*%d\n&,width,height);&&
&&&&&&& printf(&colorPlane:%d\n&,colorPlane);
&&&&&&& printf(&bitColor:%d\n&,bitColor);
&&&&&&& printf(&Compression Format:%d\n&,zipFormat);
&&&&&&& printf(&Image Real Size:%d\n&,realSize);
&&&&&&& printf(&Pels(X,Y):(%d,%d)\n&,xPels,yPels);
&&&&&&& printf(&colorUse:%d\n&,colorUse);&&&&&&
&&&&&&& printf(&Important Color:%d\n&,colorImportant);
/***************************
调色盘结构
如果颜色位数为4,需2^4=16组调色盘
&&& rgbBlue:&& 该颜色的蓝色分量。
&&& rgbGreen:& 该颜色的绿色分量。
&&& rgbRed:&&& 该颜色的红色分量。
&&& rgbReserved:保留值。
************************/
struct RGBMixPlate
&&&&&&&& BYTE&& rgbB
&&&&&&&& BYTE&& rgbG
&&&&&&&& BYTE&& rgbR
&&&&&&&& BYTE&& rgbR
&&&&&&&& void show(void)
&&&&&&&& {
&&&&&&&&&&& printf(&Mix Plate B,G,R:%d %d %d\n&,rgbBlue,rgbGreen,rgbRed);
&&&&&&&& }
/****************************
图像数据区
&&& 用到调色板的位图,图像数据就是该象素颜色在调色板中的索引值;
&&& 对于真彩色图,图像数据就是实际的R、G、B值。
&&&&&&& 2色图,用1位就可以表示该象素的颜色,所以1个字节可以表示8个象素。
&&&&&&& 16色图,用4位可以表示一个象素的颜色,所以1个字节可以表示2个象素。
&&&&&&& 256色图,1个字节刚好可以表示1个象素。
&&&&&&& 真彩色图,3个字节才能表示1个象素。&&&&&&&&
****************************/
一个读bmp文件并转换图像数据为RGB的例子:
int __cdecl main(int argc,char* argv[])
&&& if (argc & 2)
&&&&&&& printf(&Usage:\n&);
&&&&&&& printf(&\%s filename.bmp&, argv[0]);
&&&&&&& return -1;
&&& BmpHead& headBMP;
&&& InfoHead infoH
&&& char* filename = argv[1];
&&& p = fopen(filename,&rb&);
&&& if(p!=NULL)
&&&&&&& printf(&file %s open success.\n&,filename);
/**********&& read BMP head ********************/&&&&&&&&&&&&&&&
&&&&&&& fseek(p,2,SEEK_CUR);& // &BM&
&&&&&&& fread(&headBMP,1,12,p);
&&&&&&& headBMP.show();
&&&&&&& fread(&infoHead,1,40,p);
&&&&&&& infoHead.show();
/************ read Mix color Plate *************/&&
&&&&&&& RGBMixPlate* pMixP&&
&&&&&&& if(infoHead.bitColor & 24)
&&&&&&& {&&&
&&&&&&&&&&& long nMixPlate = long(pow(2,infoHead.bitColor));&& //& Mix color Plant N
&&&&&&&&&&& pMixPlate = new RGBMixPlate[nMixPlate];
&&&&&&&&&&& assert((headBMP.startPosition-54)==nMixPlate*4);
&&&&&&&&&&& fread(pMixPlate,4,nMixPlate,p);
&&&&&&&&&&& printf(&Mix Color Plate Number: %d\n&,nMixPlate);
/***********& read Image Date& **************/
&&&&&&& long nData = infoHead.realS
&&&&&&& BYTE* pColorData = new BYTE[nData];
&&&&&&& fread(pColorData,1,nData,p);
&&&&&&& printf(&last 4 is Data:%d%d%d%d\n&,pColorData[nData-4],pColorData[nData-3],
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& pColorData[nData-2],pColorData[nData-1]);
&&&&&&& BYTE t=0;
&&&&&&& while(!feof(p))
&&&&&&&&&&& fread(&t,1,1,p);
&&&&&&&&&&& printf(&other data in file:%d\n&,t);
&&&&&&& }&&&
&&&&&&& printf(&read file over.\n&);&&&
&&&&&&& if(!fclose(p))
&&&&&&&&&&& printf(&file close.\n&);
/*************** transfer bmp data to RGB data&& *********************/
//&& bmp数据中,每行图像数据的byte数必须是4的倍数
&&&&&&& int width = infoHead.
&&&&&&& int height = infoHead.
&&&&&&& int l_width& = infoHead.realSize/
&&&&&&& unsigned char* bmpRGB = new unsigned char[width*height*3];
&&&&&&& if(infoHead.bitColor&24)&&&
&&&&&&&&&&&
&&&&&&&&&&& int index = 0;
&&&&&&&&&&& int mixI
&&&&&&&&&&& if(infoHead.bitColor==1)
&&&&&&&&&&&&&&& for(int i=0;i&i++)
&&&&&&&&&&&&&&&&&&& for(int j=0;j&j++)
&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&&&&&& k = i*l_width + j/8;
&&&&&&&&&&&&&&& int t = pColorData[k]&&(7-j%8);
&&&&&&&&&&& if(t%2==0)
&&&&&&&&&&&&&&& mixIndex = 0;
&&&&&&&&&&& else
&&&&&&&&&&&&&&& mixIndex = 1;
&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbR
&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbG
&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbB
&&&&&&&&&&& }
&&&&&&&&&&& if(infoHead.bitColor == 4)
&&&&&&&&&&& {
&&&&&&&&&&&&&&& for(int i=0;i&i++)
&&&&&&&&&&&&&&&&&&& for(int j=0;j&j++)
&&&&&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&&&&&& k = i*l_width + j/2;
&&&&&&&&&&&&&&&&&&&&&&& if(j%2==0)
&&&&&&&&&&&&&&&&&&&&&&&&&&& mixIndex = pColorData[k] % 16;
&&&&&&&&&&&&&&&&&&&&&&& else
&&&&&&&&&&&&&&&&&&&&&&&&&&& mixIndex = pColorData[k] / 16;
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbR
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbG
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbB
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&& }
&&&&&&&&&&& if(infoHead.bitColor == 8)
&&&&&&&&&&& {
&&&&&&&&&&&&&&& for(int i=0;i&i++)
&&&&&&&&&&&&&&&&&&& for(int j=0;j&j++)
&&&&&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&&&&&& k = i*l_width +
&&&&&&&&&&&&&&&&&&&&&&& mixIndex = pColorData[k];
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbR
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbG
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbB
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&& }
&&&&&&&&&&& if(infoHead.bitColor == 16)
&&&&&&&&&&& {
&&&&&&&&&&&&&&& for(int i=0;i&i++)
&&&&&&&&&&&&&&&&&&& for(int j=0;j&j++)
&&&&&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&&&&&& k = i*l_width + j*2;
&&&&&&&&&&&&&&&&&&&&&&& mixIndex = pColorData[k] + pColorData[k+1]*256;
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbR
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbG
&&&&&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pMixPlate[mixIndex].rgbB
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&& }
&&&&&&& else
&&&&&&&&&&&
&&&&&&&&&&& int index = 0;
&&&&&&&&&&& for(int i=0;i&i++)
&&&&&&&&&&&&&&& for(int j=0;j&j++)
&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&& k = i*l_width + j*3;
&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pColorData[k+2];&& // R
&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pColorData[k+1];&& // G
&&&&&&&&&&&&&&&&&&& bmpRGB[index++] = pColorData[k];&&&& // B
&&&&&&&&&&&&&&& }&&&&&&&&&&&&
&&&&&&& delete []pColorD
&&&&&&& printf(&transfer to RGB finished.\n&);
&&&&&&& if(infoHead.bitColor&24)
&&&&&&&&&&& delete []pMixP
&&&&&&& delete []
&&&&&&& delete []bmpRGB;
&&& } // end if open file success.
&&&&&&& printf(&file %s open fail.\n&,filename);
&&& return 0;
前面程序中这一段:
/***********& read Image Date& **************/
&&&&&&& long nData = infoHead.realS
&&&&&&& BYTE* pColorData = new BYTE[nData];
&&&&&&& fread(pColorData,1,nData,p);
24bitBMP图像的颜色数据就保存在pColorData指向的数组中
pColorData[0], pColorData[1], pColorData[2]分别是第一个点的RGB值,依次类推。
了这篇文章
类别:┆阅读(0)┆评论(0)}

我要回帖

更多关于 文件过滤驱动 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信