NetRoc
http://www.DbgTech.net/
本文从主站点转贴过来的,附件和pdf请访问http://www.DbgTech.net/下载
一、简介
《Windows高级调试》第一章中提到了一个基于Microsoft Detours库的内存泄露检查工具LeakDiag。本文对这个库进行一些介绍。
一句话来说,Detours是一个用来在二进制级别上对程序中的函数(Function)或者过程(Procedure)进行修改的工具库。一般我们将这种技术称为"Hook"。Detours的实现原理是将目标函数的前几个字节改为jmp指令跳转到自己的函数地址,以此接管对目标函数的调用,并插入自己的处理代码。在现实中,这种技术可以应用在很多场景下。比如Hook某些Windows API,在实际调用到系统函数前进行一些过滤工作;软件中使用到了一些没有源代码的第三方库,但是又想增强其中某些函数的功能,等等。
图1 Hook前后的程序执行流程对比。
图2 Hook前后目标函数和跳板代码的改变
Detours相对其他一些Hook库和自己实现的代码来说,通常有以下这些优点:
考虑全面,代码非常稳定,并且经过了微软自己众多产品的验证。
可以简单的用纯C/C++代码实现对类的成员函数的Hook。
购买版权之后的Detours Professional还可以支持x64和IA64处理器。以此为基础编写的代码拥有更强的可移植性。
使用简单,不需要了解汇编指令以及技术细节。
二、使用方法
一般来说,使用Detours的代码都具有固定的模式。Detours 1.5和Detours 2.1的接口函数变了很多,这里按照2.1版本对基本的使用方法进行说明。
常用的函数有下面几个:
DetourTransactionBegin() :开始一次Hook或者Unhook过程。
DetourUpdateThread() :列入一个在DetourTransaction过程中要进行update的线程。这个函数的作用稍微有一些复杂,会在后面专门说明。
DetourAttach() :添加一个要Hook的函数。
DetourDetach () :添加一个要Unhook的函数。
DetourTransactionCommit() :执行当前的Transaction过程。在这个函数中才会真正进行Hook或者Unhook操作。前面三个函数都只是做一些记录工作。
在使用的时候,这几个函数的调用步骤基本上也是按照上面列出来的顺序。举例来说,现在想Hook掉API函数MessageBoxA,将消息框弹出的消息修改掉,可以按下面的方法做。
进行Hook的步骤:
首先需要定义目标函数的原型。如果目标函数是Windows API,可以到MSDN中查阅,但是需要注意ANSI版本和Unicode版本的区别。如果没有确切的原型声明,或者目标函数是通过逆向工程找出来的,那么需要定义一个和目标函数原型兼容的声明,即参数个数和调用约定要相同。如MessageBoxA的原型是:
int MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
使用typedef定义如下:
typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
声明一个指向目标函数的函数指针:
pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;
编写Hook函数的代码,用于替换目标函数。
调用DetourTransactionBegin开始一次Detours事务。
对进程中每个可能调用到目标函数的线程,都需要使用DetourUpdateThread加入到update队列中。这是因为Hook时修改目标函数的前几个字节,如果某个线程刚好执行到这几个字节的位置时,粗暴的修改掉会造成该线程出现异常。Detours事务处理时,会先枚举并暂停update队列中所有线程,获取它们的指令指针,如果发现这种情况,则将指令指针修改到跳板代码的对应字节上。这样就避免出现崩溃的问题。
对每个需要Hook的函数,调用DetourAttach加入到事务列表中。
调用DetourTransactionCommit进行实际的Hook操作。
Unhook的过程和上面的流程基本一样,只是第6步改为调用DetourDetach函数。
Hook MessageBoxA的完整示例代码如下:
//Hook函数的向前声明
int
WINAPI
Hook_MessageBoxA( HWND
hWnd, LPCSTR
lpText, LPCSTR
lpCaption, UINT
uType);
//目标函数原型声明
typedef
int (WINAPI *pfnMessageBoxA)( HWND
hWnd, LPCSTR
lpText, LPCSTR
lpCaption, UINT
uType);
//指向目标函数的指针
pfnMessageBoxA
g_pMessageBoxA = ::MessageBoxA;
BOOL
StartHook()
{//开始Hook
DetourTransactionBegin();
//只有一个线程,所以GetCurrentThread
DetourUpdateThread( GetCurrentThread());
//添加MessageBoxA的Hook
if( DetourAttach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)
{
printf( "Hook MessageBoxA fail.\n");
}
//完成事务
if( DetourTransactionCommit() != NO_ERROR)
{
printf( "DetourTransactionCommit fail\n");
return
FALSE;
}
else
{
printf( "DetourTransactionCommit ok\n");
return
TRUE;
}
}
BOOL
StopHook()
{//停止Hook
DetourTransactionBegin();
DetourUpdateThread( GetCurrentThread());
if( DetourDetach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)
{
printf( "Hook MessageBoxA fail.\n");
}
if( DetourTransactionCommit() != NO_ERROR)
{
printf( "DetourTransactionCommit fail\n");
return
FALSE;
}
else
{
printf( "DetourTransactionCommit ok\n");
return
TRUE;
}
}
int
WINAPI
Hook_MessageBoxA( HWND
hWnd, LPCSTR
lpText, LPCSTR
lpCaption, UINT
uType)
{
//需要调用原函数时,可以直接使用前面定义的指针变量
return
g_pMessageBoxA( hWnd, "MessageBox after hook.", "TestDetours", MB_OK);
}
在附件的示例代码中还包含了Hook类成员函数的代码。流程和上面基本一致,只是需要用一些强制转换来对付编译器的类型检查。
另外,Detours还包含一系列其他函数,如果需要使用的话,可以参考Detours安装目录下的示例。
三、使用Detours的注意事项
总体来说,Detours库的代码是非常稳定的,但是如果使用方法不对,会造成一些问题。有下面一些地方需要特别注意:
一定要枚举线程并调用DetourUpdateThread函数。否则可能出现很低几率的崩溃问题,这种问题很难被检查出来。
如果Hook函数在DLL中,那么绝大多数情况下不能在Unhook之后卸载这个DLL,或者卸载存在造成崩溃的危险。因为某些线程的调用堆栈中可能还包含Hook函数,这时卸载掉DLL,调用堆栈返回到Hook函数时内存位置已经不是合法的代码了。
Detours库设计时并没有考虑到卸载的问题,这是因为钩子的卸载本身是不安全的。当Detours库代码存在于DLL中的时候,即使Unhook了所有函数,清理了所有自己使用到的函数,还是会占用一些内存。卸载这个DLL会造成内存泄露,特别是反复的进行加载DLL->Hook->Unhook->卸载DLL的过程,会让这个问题变得非常严重。后面会用一篇专题文章来讨论Detours内存泄露问题的调试和解决。
有一些非常短的目标函数是无法Hook的。因为jmp指令需要占用一定空间,有些函数太过短小,甚至不够jmp指令的长度,自然是没有办法Hook掉的。
Detours不支持9x内核的Windows系统。因为9x内核下的内存模型和NT内核下有非常大的差别。
posted @
2009-05-28 22:42 NetRoc/cc682 阅读(728) |
评论 (0) |
编辑 收藏
NetRoc
http://www.DbgTech.net/
从Win XP开始,Windows的系统调用都是通过sysenter指令进入KiFastCallEntry或者_KiFastCallEntry2。最近稍微看了一下这部分的wrk代码实现,作了些笔记。
KiFastCallEntry中如何引用SSDT和SSDT Shadow
从sysenter进入KiFastCallEntry后,系统取得SSDT和SSDT Shadow的地址并不是直接通过内核导出的KeServiceDescriptorTable和未导出的KeServiceDescriptorTableShadow变量来引用的。而是从PCR取得_KTHREAD地址,通过KTHREAD中的ServiceTable来获得服务表地址的。I386处理器的KiFastCallEntry相关代码片段如下:
mov ebx, PCR[PcSelfPcr] ; 取得PRCB地址
push KGDT_R3_TEB OR RPL_MASK ; Push user mode FS
mov esi, [ebx].PcPrcbData+PbCurrentThread ; 获得当前线程地址
……
add edi, [esi]+ThServiceTable ; 计算出服务表的地址
通过这里,我们可以知道系统调用所使用的的服务表来自_KTHREAD这种"动态"数据,而非KeServiceDescriptorTable这种相对"静态"的数据。这一点在很多情况下都有较强的利用价值。比如XXX……
nt!_KTHREAD中ServiceTable字段的设置
NtCreateThread内部是通过PspCreateThread创建线程的。进行了线程的相关结构内存分配后,会调用KeInitThread初始化这些结构。KeInitThread中就会设置nt!_KTHREAD的ServiceTable字段。如下:
#if defined(_AMD64_)
Thread->ServiceTable = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Base;
Thread->KernelLimit = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Limit;
#else
Thread->ServiceTable = (PVOID)&KeServiceDescriptorTable[0];
#endif
I386下,线程的初始服务表就是KeServiceDescriptorTable。这可能会被改变。后面如果这个线程调用了ShadowTable中的服务,ServiceTable会被转变成ShadowTable。KiFastCallEntry会判断系统调用的服务号,如果发现是GUI服务,则调用PsConvertToGuiThread将线程转换成GUI线程。
PsConvertToGuiThread中如下处理:
#if defined(_AMD64_)
Thread->Tcb.Win32kTable = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base;
Thread->Tcb.Win32kLimit = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Limit;
#else
Thread->Tcb.ServiceTable = (PVOID)&KeServiceDescriptorTableShadow[0];
#endif
SSDT和SSDT Shadow的初始化
两个SDT的初始化在KiInitSystem函数中:
KeServiceDescriptorTable[0].Base = &KiServiceTable[0];
KeServiceDescriptorTable[0].Count = NULL;
KeServiceDescriptorTable[0].Limit = KiServiceLimit;
KeServiceDescriptorTable[0].Number = KiArgumentTable;
for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {
KeServiceDescriptorTable[Index].Limit = 0;
……
RtlCopyMemory(KeServiceDescriptorTableShadow,
KeServiceDescriptorTable,
sizeof(KeServiceDescriptorTable));
ShadowTable的第一项被在这个时候初始化成和KeServiceDescriptorTable一样。而第二项会在后面的初始化过程中通过调用KeAddSystemServiceTable被设置。
ShadowTable的设置时机
上面提到了,由于KiInitSystem的初始化,KeServiceDescriptorTableShadow[0]和KeServiceDescriptorTable[0]总是一样的,但是KeServiceDescriptorTableShadow[1]会被设置成win32k的服务。这应该是win32k加载之后调用KeAddSystemServiceTable添加的。
AMD64和i386中的区别
AMD64下的实现和i386不同,AMD64下的ShadowTable在_KTHREAD中专门有一个字段,ServiceTable保存KeServiceDescriptorTable,Win32kTable保存KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base。详见PsConvertToGuiThread等函数。
有什么意义?
从上面记录的这些实现来看,很直接导致的结果就是,系统服务表可以比较容易的替换掉。这种替换有下面这些特点:
只会影响指定的线程,对整个系统来说影响小。
由于和传统SSDT Hook一样只需要改掉一个指针,比inline hook更加稳定。
传统的SSDT Hook被用得太滥,大家都在搞,某些人还敢卸载掉并且恢复Hook之前的指针,很容易造成崩溃。这种方法因为只针对单独线程,被使用得也少,相对来说比传统SSDT Hook和Hook KiFastCallEntry都要安全。
对可能转变成GUI线程的线程进行替换就需要特别的处理了。比如构造自己的ShadowTable并Hook掉PsConvertToGuiThread之类的方法。
没有什么隐蔽性,呵呵。干坏事的同学慎用。当然,用的时候再改,用过之后马上改回去,或者配合内存隐藏之类的技术还是不错滴。干好事的同学也可以用,但是不适合对所有系统线程都用上去,因为要枚举所有线程,或者Hook线程创建函数之类比较麻烦,到不如直接去干KiFastCallEntry。用来保护自己线程的服务表安全倒是不错。
posted @
2009-04-08 00:18 NetRoc/cc682 阅读(743) |
评论 (0) |
编辑 收藏
驱动程序的源码调试
http://www.DbgTech.net
-
准备
Windows调试工具优于目前的其他内核调试器很重要的一点,就是能够非常方便的对自己编写的驱动程序进行源码调试。为了能够更好的说明,我们首先需要做一些准备工作,分别编写一个测试驱动程序和一个应用程序来使用驱动的功能。
-
驱动程序
首先实现一个最简单的驱动程序,除了DriverEntry等框架代码之外,我们添加一个IRP_MJ_READ的Dispatch例程,当应用程序调用ReadFile时返回一个值递增的字节。另外,实现两个DeviceIoControl Code,一个调用DbgPrint向调试器显示信息并返回,另一个访问非法指针造成崩溃。代码片断如下,完整的代码和编译出来的文件可以在附件中获取:
NTSTATUS DispatchRead (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PIO_STACK_LOCATION pstIrpStack;
PUCHAR pbyUserBuffer;
ULONG ulSize;
BOOLEAN blRtn = FALSE;
static UCHAR s_byCounter = 0;
pstIrpStack = IoGetCurrentIrpStackLocation( Irp);
pbyUserBuffer = (PUCHAR)Irp->UserBuffer;
ulSize = pstIrpStack->Parameters.Read.Length;
if ( ulSize == 1)
{
*pbyUserBuffer = s_byCounter++;
blRtn = TRUE;
}
if ( blRtn)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 1L;
IoCompleteRequest( Irp, 0 );
return STATUS_SUCCESS;
}
else
{
Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
Irp->IoStatus.Information = 0L;
IoCompleteRequest( Irp, 0 );
return STATUS_UNSUCCESSFUL;
}
}
NTSTATUS DispatchIoCtrl (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
PUCHAR pucCrash = NULL;
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
//////////////////////////////////////////////////////////////////////////
switch(irpStack->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_TEST_CTRL_CODE2:
//崩溃
*pucCrash = 1;
break;
case IOCTL_TEST_CTRL_CODE1:
DbgPrint( "Received IOCTL_TEST_CTRL_CODE1\n");
ntStatus = STATUS_SUCCESS;
break;
default:
ntStatus = STATUS_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
//
if(ntStatus == STATUS_SUCCESS)
Irp->IoStatus.Information = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
else
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = ntStatus;
IoCompleteRequest( Irp, 0 );
return ntStatus;
}
-
应用程序
应用程序主要用来控制驱动设备对象,显示返回的结果。
-
开始源码调试
-
设置
系列前面的几篇文章已经介绍过对符号路径和源码路径的设置。调试自己编写的驱动时,如果是主控机上编译,在目标机上运行,那么一般都不需要专门设置路径WinDbg就能找到正确的符号和源文件。如果驱动不是在主控机上编译的,或者编译之后移动了源码或符号文件就必须要进行设置。在这里我是这样设置的:
-
加载驱动程序
这里我们不自己写程序加载,而是通过附件中的KmdManager.exe工具来进行。
选择要加载的驱动程序之后,首先点击Register就能在注册表中注册该驱动程序的服务。然后点击Run加载并运行。按下Run之后,驱动的DriverEntry就会被调用,所以如果我们要想在驱动入口点中断下来的话,就要在这之前设置断点。Stop和Unregister按钮用来停止和卸载驱动服务。注意操作时要按照Register->Run->Stop->Unregister的顺序,并且Stop之前要在应用层关闭所有已打开的句柄,否则可能只有重起机器才能卸载了。
-
设置断点
初学者调试驱动时常常会问的一个问题就是,如何在DriverEntry运行之前中断下来。我们在实际的调试工作中也经常需要调试DriverEntry。这需要用到以前介绍过的未定断点,因为一般来说DriverEntry 运行之前,我们的驱动还没有被加载进来,这时在调试器是找不到这个驱动的任何符号的。
首先在调试器命令窗口输入bu SrcDbgKnlDrv!DriverEntry命令,F5运行起来。在目标机中用KmdManager.exe工具Register并Run。这时会发现目标机中断到了调试器中。
如果前面的路径设置没有问题的话,WinDbg中会自动弹出源码,可以看到目前中断在DriverEntry函数入口处。
从这时起,就可以使用各个WinDbg的调试窗口了,比如查看局部变量、查看内存等等,也可以打开各个源文件直接针对源码设置断点。
-
查看驱动设备的信息
运行到DriverEntry时,SrcDbgKnlDrv.sys已经被成功加载到系统中了。DriverEntry运行完成之后就能够看到我们的驱动创建的设备了。
使用gu命令,执行到DriverEntry返回处。使用!drvobj和!devobj命令可以查看驱动程序对象和设备对象的信息。
kd> !drvobj SrcDbgKnlDrv
Driver object (8219f5f0) is for:
\Driver\SrcDbgKnlDrv
Driver Extension List: (id , addr)
Device Object list:
82138030
kd> !devobj SrcDbgKnlDrv
Device object (82138030) is for:
SrcDbgKnlDrv \Driver\SrcDbgKnlDrv DriverObject 8219f5f0
Current Irp 00000000 RefCount 0 Type 00000022 Flags 000000c0
Dacl e10361f4 DevExt 00000000 DevObjExt 821380e8
ExtensionFlags (0000000000)
Device queue is not busy.
如果我们的设备有附加到某个设备栈上的话,可以用!devstack扩展命令显示设备栈的信息。
kd> !devstack 82138030
!DevObj !DrvObj !DevExt ObjectName
> 82138030 \Driver\SrcDbgKnlDrv00000000 SrcDbgKnlDrv
这里看到的输出说明SrcDbgKnlDrv没有附加到任何设备栈。
通过!devhandles命令可以查看设备被打开的句柄。目前在这个地方使用的话,由于没有句柄被打开,还看不到什么有用的信息,在后面进行演示。
-
Dispatch例程的调试
Dispatch例程是驱动程序响应应用层请求的地方。通常来说,驱动程序和应用程序的各种交互就是通过Dispatch例程实现。因此,要跟踪应用层请求处理的情况,可以在这些Dispatch例程上设置断点。
我们的测试程序只处理了IRP_MJ_READ和IRP_MJ_DEVICE_CONTROL两个请求。来试着调试一下。
接着上面中断下来的调试会话,首先用bp SrcDbgKnlDrv!DispatchRead命令和bp SrcDbgKnlDrv!DispatchIoCtrl命令在程序的两个Dispatch例程上下断。F5运行起来。在目标机打开SrcDbgKnlApp程序点击Open,会打开一个SrcDbgKnlDrv.sys驱动设备的句柄。这时中断目标机,使用!devhandles扩展命令就能看到有用的信息了:
kd> !devhandles 82138030
Checking handle table for process 0x823b97c0
Handle table at e1002000 with 241 Entries in use
省略掉一部分……
Checking handle table for process 0x82164da0
Handle table at e1137000 with 34 Entries in use
PROCESS 82164da0 SessionId: 0 Cid: 05e4 Peb: 7ffd6000 ParentCid: 05d0
DirBase: 07480240 ObjectTable: e11c1cd0 HandleCount: 34.
Image: SrcDbgKnlAppD.exe
008c: Object: 821cb028 GrantedAccess: 0012019f
命令显示EPROCESS地址为0x82164da0的进程打开了一个\\.\SrcDbgKnlDrv设备的句柄。再使用!process 82164da0扩展命令可以看到,这个进程正是我们的SrcDbgKnlApp。在调试驱动程序的时候,这是一种查看哪些应用程序在使用某个驱动设备对象的方法。
接下来继续,F5运行,点击SrcDbgKnlApp的Read按钮,可以看到中断到了DispatchRead例程上。这里就可以用单步、查看局部变量等等普通的方法调试我们的程序运行情况了。对于驱动程序来说,我们还可以做一些其他事情,例如查看IRP的信息,查看IRQL等。如:
kd> !irp @@(Irp)
Irp is active with 1 stacks 1 is current (= 0x8238d2d0)
No Mdl: No System Buffer: Thread 82191da8: Irp stack trace.
cmd flg cl Device File Completion-Context
>[ 3, 0] 0 0 82138030 821cb028 00000000-00000000
\Driver\SrcDbgKnlDrv
Args: 00000001 00000000 00000000 00000000
kd> !irql
nt!_KPRCB.DebuggerSavedIRQL not found, error : 0x4.
Saved IRQL not available prior to Windows Server 2003
!irql命令只能对Windows 2003之后的系统使用,我的机器上是Windows XP,因此这里提示了错误。
另外,也可以使用kd> !devobj @@(DeviceObject)这样的命令形式,直接用C++表达式将局部变量作为命令参数来使用。
用bc*命令清除断点,F5运行。每次点击Read都可以看到从驱动中读取出来的数据。
-
驱动异常和内核崩溃转储文件的源码调试
当驱动程序中发生异常时,如果有调试器附加上去,一般会中断到调试器中。如果没有附加调试器,系统通常会自动为我们生成内核dump文件。如果异常发生在我们的驱动中,那么也可以用WinDbg源码调试直观的看出问题所在。
在SrcDbgKnlApp程序中点击Crash。由于现在是在调试会话中,可以在调试器命令窗口中看到异常的信息:
kd> g
Access violation - code c0000005 (!!! second chance !!!)
SrcDbgKnlDrv!DispatchIoCtrl+0x3d:
f8d615ad c60101 mov byte ptr [ecx],1
源码窗口中可以看到崩溃处的代码:
pucCrash的值为NULL,造成了崩溃:
kd> ?? pucCrash
unsigned char * 0x00000000
""
如果是实际工作时进行调试的话,这时可以继续分析崩溃的原因。
再来尝试一下没有附加调试器的情况。重起目标机,在我的电脑->属性->高级中设置一下内核dump的类型和生成的路径:
再使用刚才的方法,加载驱动程序后,用SrcDbgKnlApp让目标机崩溃。发生蓝屏之后重起,在设置的路径下就可以找到dump文件了。这里我们生成的是minidump。将它拷贝到主控机上来,通过WinDbg菜单打开。如果驱动不是在本机上编译的话,因为还没有设置各种路径,现在看到的只有汇编代码。我们使用一下!analyze -v命令能够看到没有符号时对异常的分析结果,可以看到出问题的模块是SrcDbgKnlDrv.sys:
接下来设置好可执行映像路径、符号路径和源码路径,使用.reload命令重新加载符号,会自动弹出源码窗口,调试器中也会更新显示符号信息。如果再次使用!analyze -v,能够看到非常详细的分析报告。这时还可以查看异常的每个堆栈帧中的源码和符号化信息。对于调试Dump文件中的BUG来说,使用源码可以大大提高调试效率。
关于内核模式Dump文件的调试这里就不再介绍了。Windows调试工具的帮助文档中有相关技术较详细的资料。
posted @
2009-02-19 22:44 NetRoc/cc682 阅读(470) |
评论 (0) |
编辑 收藏
驱动程序的源码调试
http://www.DbgTech.net
准备
Windows调试工具优于目前的其他内核调试器很重要的一点,就是能够非常方便的对自己编写的驱动程序进行源码调试。为了能够更好的说明,我们首先需要做一些准备工作,分别编写一个测试驱动程序和一个应用程序来使用驱动的功能。
驱动程序
首先实现一个最简单的驱动程序,除了DriverEntry等框架代码之外,我们添加一个IRP_MJ_READ的Dispatch例程,当应用程序调用ReadFile时返回一个值递增的字节。另外,实现两个DeviceIoControl Code,一个调用DbgPrint向调试器显示信息并返回,另一个访问非法指针造成崩溃。代码片断如下,完整的代码和编译出来的文件可以在附件中获取:
NTSTATUS DispatchRead (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PIO_STACK_LOCATION pstIrpStack;
PUCHAR pbyUserBuffer;
ULONG ulSize;
BOOLEAN blRtn = FALSE;
static UCHAR s_byCounter = 0;
pstIrpStack = IoGetCurrentIrpStackLocation( Irp);
pbyUserBuffer = (PUCHAR)Irp->UserBuffer;
ulSize = pstIrpStack->Parameters.Read.Length;
if ( ulSize == 1)
{
*pbyUserBuffer = s_byCounter++;
blRtn = TRUE;
}
if ( blRtn)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 1L;
IoCompleteRequest( Irp, 0 );
return STATUS_SUCCESS;
}
else
{
Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
Irp->IoStatus.Information = 0L;
IoCompleteRequest( Irp, 0 );
return STATUS_UNSUCCESSFUL;
}
}
NTSTATUS DispatchIoCtrl (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
PUCHAR pucCrash = NULL;
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
//////////////////////////////////////////////////////////////////////////
switch(irpStack->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_TEST_CTRL_CODE2:
//崩溃
*pucCrash = 1;
break;
case IOCTL_TEST_CTRL_CODE1:
DbgPrint( "Received IOCTL_TEST_CTRL_CODE1\n");
ntStatus = STATUS_SUCCESS;
break;
default:
ntStatus = STATUS_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
//
if(ntStatus == STATUS_SUCCESS)
Irp->IoStatus.Information = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
else
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = ntStatus;
IoCompleteRequest( Irp, 0 );
return ntStatus;
}
应用程序
应用程序主要用来控制驱动设备对象,显示返回的结果。
开始源码调试
设置
系列前面的几篇文章已经介绍过对符号路径和源码路径的设置。调试自己编写的驱动时,如果是主控机上编译,在目标机上运行,那么一般都不需要专门设置路径WinDbg就能找到正确的符号和源文件。如果驱动不是在主控机上编译的,或者编译之后移动了源码或符号文件就必须要进行设置。在这里我是这样设置的:
加载驱动程序
这里我们不自己写程序加载,而是通过附件中的KmdManager.exe工具来进行。
选择要加载的驱动程序之后,首先点击Register就能在注册表中注册该驱动程序的服务。然后点击Run加载并运行。按下Run之后,驱动的DriverEntry就会被调用,所以如果我们要想在驱动入口点中断下来的话,就要在这之前设置断点。Stop和Unregister按钮用来停止和卸载驱动服务。注意操作时要按照Register->Run->Stop->Unregister的顺序,并且Stop之前要在应用层关闭所有已打开的句柄,否则可能只有重起机器才能卸载了。
设置断点
初学者调试驱动时常常会问的一个问题就是,如何在DriverEntry运行之前中断下来。我们在实际的调试工作中也经常需要调试DriverEntry。这需要用到以前介绍过的未定断点,因为一般来说DriverEntry 运行之前,我们的驱动还没有被加载进来,这时在调试器是找不到这个驱动的任何符号的。
首先在调试器命令窗口输入bu SrcDbgKnlDrv!DriverEntry命令,F5运行起来。在目标机中用KmdManager.exe工具Register并Run。这时会发现目标机中断到了调试器中。
如果前面的路径设置没有问题的话,WinDbg中会自动弹出源码,可以看到目前中断在DriverEntry函数入口处。
从这时起,就可以使用各个WinDbg的调试窗口了,比如查看局部变量、查看内存等等,也可以打开各个源文件直接针对源码设置断点。
查看驱动设备的信息
运行到DriverEntry时,SrcDbgKnlDrv.sys已经被成功加载到系统中了。DriverEntry运行完成之后就能够看到我们的驱动创建的设备了。
使用gu命令,执行到DriverEntry返回处。使用!drvobj和!devobj命令可以查看驱动程序对象和设备对象的信息。
kd> !drvobj SrcDbgKnlDrv
Driver object (8219f5f0) is for:
\Driver\SrcDbgKnlDrv
Driver Extension List: (id , addr)
Device Object list:
82138030
kd> !devobj SrcDbgKnlDrv
Device object (82138030) is for:
SrcDbgKnlDrv \Driver\SrcDbgKnlDrv DriverObject 8219f5f0
Current Irp 00000000 RefCount 0 Type 00000022 Flags 000000c0
Dacl e10361f4 DevExt 00000000 DevObjExt 821380e8
ExtensionFlags (0000000000)
Device queue is not busy.
如果我们的设备有附加到某个设备栈上的话,可以用!devstack扩展命令显示设备栈的信息。
kd> !devstack 82138030
!DevObj !DrvObj !DevExt ObjectName
> 82138030 \Driver\SrcDbgKnlDrv00000000 SrcDbgKnlDrv
这里看到的输出说明SrcDbgKnlDrv没有附加到任何设备栈。
通过!devhandles命令可以查看设备被打开的句柄。目前在这个地方使用的话,由于没有句柄被打开,还看不到什么有用的信息,在后面进行演示。
Dispatch例程的调试
Dispatch例程是驱动程序响应应用层请求的地方。通常来说,驱动程序和应用程序的各种交互就是通过Dispatch例程实现。因此,要跟踪应用层请求处理的情况,可以在这些Dispatch例程上设置断点。
我们的测试程序只处理了IRP_MJ_READ和IRP_MJ_DEVICE_CONTROL两个请求。来试着调试一下。
接着上面中断下来的调试会话,首先用bp SrcDbgKnlDrv!DispatchRead命令和bp SrcDbgKnlDrv!DispatchIoCtrl命令在程序的两个Dispatch例程上下断。F5运行起来。在目标机打开SrcDbgKnlApp程序点击Open,会打开一个SrcDbgKnlDrv.sys驱动设备的句柄。这时中断目标机,使用!devhandles扩展命令就能看到有用的信息了:
kd> !devhandles 82138030
Checking handle table for process 0x823b97c0
Handle table at e1002000 with 241 Entries in use
省略掉一部分……
Checking handle table for process 0x82164da0
Handle table at e1137000 with 34 Entries in use
PROCESS 82164da0 SessionId: 0 Cid: 05e4 Peb: 7ffd6000 ParentCid: 05d0
DirBase: 07480240 ObjectTable: e11c1cd0 HandleCount: 34.
Image: SrcDbgKnlAppD.exe
008c: Object: 821cb028 GrantedAccess: 0012019f
命令显示EPROCESS地址为0x82164da0的进程打开了一个\\.\SrcDbgKnlDrv设备的句柄。再使用!process 82164da0扩展命令可以看到,这个进程正是我们的SrcDbgKnlApp。在调试驱动程序的时候,这是一种查看哪些应用程序在使用某个驱动设备对象的方法。
接下来继续,F5运行,点击SrcDbgKnlApp的Read按钮,可以看到中断到了DispatchRead例程上。这里就可以用单步、查看局部变量等等普通的方法调试我们的程序运行情况了。对于驱动程序来说,我们还可以做一些其他事情,例如查看IRP的信息,查看IRQL等。如:
kd> !irp @@(Irp)
Irp is active with 1 stacks 1 is current (= 0x8238d2d0)
No Mdl: No System Buffer: Thread 82191da8: Irp stack trace.
cmd flg cl Device File Completion-Context
>[ 3, 0] 0 0 82138030 821cb028 00000000-00000000
\Driver\SrcDbgKnlDrv
Args: 00000001 00000000 00000000 00000000
kd> !irql
nt!_KPRCB.DebuggerSavedIRQL not found, error : 0x4.
Saved IRQL not available prior to Windows Server 2003
!irql命令只能对Windows 2003之后的系统使用,我的机器上是Windows XP,因此这里提示了错误。
另外,也可以使用kd> !devobj @@(DeviceObject)这样的命令形式,直接用C++表达式将局部变量作为命令参数来使用。
用bc*命令清除断点,F5运行。每次点击Read都可以看到从驱动中读取出来的数据。
驱动异常和内核崩溃转储文件的源码调试
当驱动程序中发生异常时,如果有调试器附加上去,一般会中断到调试器中。如果没有附加调试器,系统通常会自动为我们生成内核dump文件。如果异常发生在我们的驱动中,那么也可以用WinDbg源码调试直观的看出问题所在。
在SrcDbgKnlApp程序中点击Crash。由于现在是在调试会话中,可以在调试器命令窗口中看到异常的信息:
kd> g
Access violation - code c0000005 (!!! second chance !!!)
SrcDbgKnlDrv!DispatchIoCtrl+0x3d:
f8d615ad c60101 mov byte ptr [ecx],1
源码窗口中可以看到崩溃处的代码:
pucCrash的值为NULL,造成了崩溃:
kd> ?? pucCrash
unsigned char * 0x00000000
""
如果是实际工作时进行调试的话,这时可以继续分析崩溃的原因。
再来尝试一下没有附加调试器的情况。重起目标机,在我的电脑->属性->高级中设置一下内核dump的类型和生成的路径:
再使用刚才的方法,加载驱动程序后,用SrcDbgKnlApp让目标机崩溃。发生蓝屏之后重起,在设置的路径下就可以找到dump文件了。这里我们生成的是minidump。将它拷贝到主控机上来,通过WinDbg菜单打开。如果驱动不是在本机上编译的话,因为还没有设置各种路径,现在看到的只有汇编代码。我们使用一下!analyze -v命令能够看到没有符号时对异常的分析结果,可以看到出问题的模块是SrcDbgKnlDrv.sys:
接下来设置好可执行映像路径、符号路径和源码路径,使用.reload命令重新加载符号,会自动弹出源码窗口,调试器中也会更新显示符号信息。如果再次使用!analyze -v,能够看到非常详细的分析报告。这时还可以查看异常的每个堆栈帧中的源码和符号化信息。对于调试Dump文件中的BUG来说,使用源码可以大大提高调试效率。
关于内核模式Dump文件的调试这里就不再介绍了。Windows调试工具的帮助文档中有相关技术较详细的资料。
posted @
2009-02-19 22:30 NetRoc/cc682 阅读(264) |
评论 (0) |
编辑 收藏