简单驱动编写与windbg调试
【51CTO.com 专家特稿】 诚信网安--小麒麟
一.驱动编写
随着对windows系统的深入研究,越来越多的内核方面的知识被挖掘出来了,今天我们讨论下如何写一个简单的驱动,并使用现在比较新的windbg调试器进行调试。首先写驱动要对驱动有一个比较全面的认识。
一个简单的驱动一般有以下几个部分组成:
1,一个入口点(DriverEntry):用于创建设备对象及符号连接,以及其它初使化操作,如分配池内存等.
2,一个出口(DriverUnload):删除符号连接与设备对象,并释放已经分配的各种资源,如池内存等
3,几个派遣例程:用于响应Ring3程序的请求及其它驱动事件,并做相关处理。
2,一个出口(DriverUnload):删除符号连接与设备对象,并释放已经分配的各种资源,如池内存等
3,几个派遣例程:用于响应Ring3程序的请求及其它驱动事件,并做相关处理。
我用一个挂钩SSDT的简单驱动来详细介绍如何写一个驱动和驱动的结构。
在驱动入口点,我们需要使用RtlInitUnicodeString来初始化一个UnicodeString结构:
例如:
RtlInitUnicodeString(&DeviceName,NT_DEVICE_NAME);
RtlInitUnicodeString(&DeviceLinkString,DOS_DEVICE_NAME);
NT_DEVICE_NAME和 DOS_DEVICE_NAME,往往在头文件中用define进行定义。
RtlInitUnicodeString(&DeviceLinkString,DOS_DEVICE_NAME);
NT_DEVICE_NAME和 DOS_DEVICE_NAME,往往在头文件中用define进行定义。
(这里有一点要注意如:
#define NT_DEVICE_NAME L"http://www.cnblogs.com/adylee/admin/file://device//HookSSDT"
这里的L并不是说明”\\Device\\HookSSDT”是个Unicode字符串,只是表明他是一个宽字符,这里很多人都搞混。)
我们初始化设备名后就可以调用IoCreateDevice创建一个设备对象了,这里我给大家一个小例子:
#define NT_DEVICE_NAME L"http://www.cnblogs.com/adylee/admin/file://device//HookSSDT"
这里的L并不是说明”\\Device\\HookSSDT”是个Unicode字符串,只是表明他是一个宽字符,这里很多人都搞混。)
我们初始化设备名后就可以调用IoCreateDevice创建一个设备对象了,这里我给大家一个小例子:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, |
这里IoCreateSymbolicLink用来创建一个符号链接以便在应用程序中可以方便的打开驱动对象,与其通讯。
DriverObject->MajorFunction[IRP_MJ_CREATE] =
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DevCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DevCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;
这里是典型的派遣例程,他把IRP的处理过程交给我们的派遣例程去执行,其实我们常常要做的是把IRP交给下一层的驱动处理。
在这里的DispatchDeviceControl中我们处理由应用程序传来的控制码,首先我们要了解IRP和IRP堆栈的结构:
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请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。
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管理器把数据复制到用户模式缓冲区并设置反馈变量
NTSTATUS DispatchDeviceControl(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp) |
要保护一个进程只用在调用ZwOpenProcess时返回一个STATUS_ACCESS_DENIED就可以了,相关的代码如下
NTSTATUS NewZwOpenProcess( |
这里我们要先定义相关宏:
#pragma pack(1) |
这里通过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就是索引号,便有了宏定义。
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就是索引号,便有了宏定义。
NTSYSAPI
NTSTATUS
NTAPI
ZwOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
typedef NTSTATUS (*ZWOPENPROCESS)(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
ZWOPENPROCESS OldZwOpenProcess;
/////////////////////////////////////////////////////////////////////
NTSTATUS NewZwOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
在DriverEntry中对CR0进行修改然后修改 SSDT表。
_asm
{
CLI
MOV EAX, CR0
AND EAX, NOT 10000H
MOV CR0, EAX
}
OldZwOpenProcess = (ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess)) ;
(ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess)) = NewZwOpenProcess;
_asm
{
MOV EAX, CR0
OR EAX, 10000H
MOV CR0, EAX
STI
}
在卸载例程中对ssdt进行修复驱动的功能就基本完成了,
VOID OnUnload(IN PDRIVER_OBJECT DriverObject) |
在应用程序中我们使用,CreateService来注册一个服务,由于驱动注册了符号链接这样我们就可以像打开一个文件一样打开驱动,我们用进程快照遍历进程获得ID然后传递给驱动,这样一个简单的保护功能就完成了。
TCHAR name[256]; 下面我介绍下关于驱动的调试,这里以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命令的详细描述可以参看帮助文档。
这篇文章我就写到这里,有机会的话,我会给大家更详细的介绍windbg调试器的用法.谢谢大家。
|