一、驱动程序实现思路
I/O请求包数据结构
MdlAddress(PMDL)域指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。为了访问用户模式缓冲区,驱动程序必须做一点额外工作。
Flags(ULONG)域包含一些对驱动程序只读的标志。但这些标志与WDM驱动程序无关。
AssociatedIrp(union)域是一个三指针联合。其中,与WDM驱动程序相关的指针是AssociatedIrp.SystemBuffer。 SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。对于IRP_MJ_READ和IRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志,则I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代码指出需要缓冲区,则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了METHOD_BUFFERED的I/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区,然后I/O管理器再把数据复制到用户模式的输出缓冲区。
oStatus(IO_STATUS_BLOCK)是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码,而IoStatus.Information的类型为ULONG_PTR,它将收到一个信息值,该信息值的确切含义要取决于具体的IRP类型和请求完成的状态。Information域的一个公认用法是用于保存数据传输操作,如IRP_MJ_READ,的流量总计。某些PnP请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。
RequestorMode将等于一个枚举常量UserMode或KernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。
PendingReturned(BOOLEAN)如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。
Cancel(BOOLEAN)如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。CancelIrql(KIRQL)是一个IQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。
CancelRoutine(PDRIVER_CANCEL)是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine函数设置这个域而不是直接修改该域。
UserBuffer(PVOID) 对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。
I/O堆栈
任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用(见图)。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。图中显示了堆栈单元的结构。
MajorFunction(UCHAR)是该IRP的主功能码。这个代码应该为类似IRP_MJ_READ一样的值,并与驱动程序对象中MajorFunction表的某个派遣函数指针相对应。如果该代码存在于某个特殊驱动程序的I/O堆栈单元中,它有可能一开始是,例如IRP_MJ_READ,而后被驱动程序转换成其它代码,并沿着驱动程序堆栈发送到低层驱动程序。
MinorFunction(UCHAR)是该IRP的副功能码。它进一步指出该IRP属于哪个主功能类。例如,IRP_MJ_PNP请求就有约一打的副功能码,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。
Parameters(union)是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数。这些子结构包括Create(IRP_MJ_CREATE请求)、Read(IRP_MJ_READ请求)、StartDevice(IRP_MJ_PNP的IRP_MN_START_DEVICE子类型),等等。
DeviceObject(PDEVICE_OBJECT)是与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。
FileObject(PFILE_OBJECT)是内核文件对象的地址,IRP的目标就是这个文件对象。驱动程序通常在处理清除请求(IRP_MJ_CLEANUP)时使用FileObject指针,以区分队列中与该文件对象无关的IRP。
CompletionRoutine(PIO_COMPLETION_ROUTINE)是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。
Context(PVOID)是一个任意的与上下文相关的值,将作为参数传递给完成例程。你绝对不要直接设置该域;它由IoSetCompletionRoutine函数自动设置,其值来自该函数的某个参数。
这里我们先定义一个控制码:
#define IOCTL_PROTECT_CONTROL CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
这里我们使用他来和应用程序通讯,Irp->AssociatedIrp.SystemBuffer这个结构中存放用户模式程序发送给驱动程序的数据。这里使用METHOD_BUFFERED方式时,I/O管理器创建一个足够大的内核模式拷贝缓冲区(与用户模式输入和输出缓冲区中最大的容量相同)。当派遣例程获得控制时,用户模式的输入数据被复制到这个拷贝缓冲区。在IRP完成之前,你应该向拷贝缓冲区填入需要发往应用程序的输出数据。当IRP完成时,你应该设置IoStatus.Information域等于放入拷贝缓冲区中的输出字节数。然后I/O管理器把数据复制到用户模式缓冲区并设置反馈变量:
要保护一个进程只用在调用ZwOpenProcess时返回一个STATUS_ACCESS_DENIED就可以了,相关的代码如下
这里我们要先定义相关宏:
这里通过windbg反汇编可以看出
lkd> u ZwOpenProcess
nt!ZwOpenProcess:
804e6044 b87a000000 mov eax,7Ah
804e6049 8d542404 lea edx,[esp+4]
804e604d 9c pushfd
804e604e 6a08 push 8
804e6050 e8dc150000 call nt!KiSystemService (804e7631)
804e6055 c21000 ret 10h
nt!ZwOpenProcessToken:
804e6058 b87b000000 mov eax,7Bh
804e605d 8d542404 lea edx,[esp+4]
eax后的7Ah就是索引号,便有了宏定义。
在卸载例程中对ssdt进行修复驱动的功能就基本完成了:
在应用程序中我们使用,CreateService来注册一个服务,由于驱动注册了符号链接这样我们就可以像打开一个文件一样打开驱动,我们用进程快照遍历进程获得ID然后传递给驱动,这样一个简单的保护功能就完成了。
二.驱动调试
下面我介绍下关于驱动的调试,这里以windbg为例:
1.配置windbg双机调试环境,(VMware Workstation)
(1) 创建windbg快捷方式在目标中加上-k com:port=//./pipe/com_1,baud=115200,pipe 例如:"E:/Program Files/Debugging Tools for Windows/windbg.exe" -k com:port=//./pipe/com_1,baud=115200,pipe
(2) 在虚拟机设置里添加一个串行端口(如图),点击下一步;
(3)点击输出到命名管道:
在高级选中,中轮询时主动放弃CPU占用.然后完成。
(4) 在虚拟机中系统的启动项中添加一个启动项:
multi(0)disk(0)rdisk(0)partition(1)/WINDOWS="Microsoft Windows XP Professional - debug" /fastdetect /debug /debugport=com1 /baudrate=115200
(5) 然后跟着我做:首先运行windbg,然后打开虚拟机客户机,选中调试模式
(6)进入要调试的系统后我们可以在windbg中用Debug命令中的Break对目标机进行中断,用F5我们又可以恢复客户机的运行。
我们设置windbg的Symbol path:我的设置如下
C:/MyCodesSymbols;SRV*C:/MyLocalSymbols*http://msdl.microsoft.com/download/symbols;C:/MyLocalSymbols
这里我把自己的.pdb文件放到C:/MyCodesSymbols下,从微软的符号文件服务器上下载符号文件到C:/MyLocalSymbols中。
现在我们来调试一个驱动试试,吧刚才写的驱动和应用文件拷贝到虚拟客户机中,在windbg中打开驱动源文件如下:
我们先下一个延迟断点:bu hook!DriverEntry 跟普通断点相比,推迟断点的特色是把对modules!names的操作延迟到模块加载时进行地址解析,而普通断点是对地址进行操作,或直接把modules!names转换为地址。推迟断点的另一个特性是系统重启之后还能记住它们(不是记住的已经转换的地址)。这个特性使得推迟断点在模块被卸载之后仍然被会记得,而普通断点就不行了,当模块被卸载之后断点同时会被移除。
延迟断点可以很方便的使我们的应用程序在特定的位置下断。
如果我们要下一个普通断点可以使用bp命令或者直接在源代码 要下断的地方按f9, 当你想查看已经设置的断点时,可以使用bl(“Breakpoint Line”)命令.下好断点后我们就可以按f5进行调试了,具体方法和其他调试器差不多我也就不多罗嗦了。关于windbg命令的详细描述可以参看帮助文档。
文章出处:http://www.diybl.com/course/3_program/c++/cppxl/2008515/116903.html