原文已发表黑客防线
蓝屏的调试艺术
1、前言
  当前,恶意程序和杀毒软件对系统控制权的争夺是愈演愈烈,各大杀毒厂商都是挖空心思在系统底层做文章。众多网友也是不甘示弱,都投入到系统底层开发之中,但是不少人是比葫芦画瓢,直接拿别人代码来用,包括我刚开始做的时候也是这样。但是由于种种原因,别人运行正常的代码到你电脑上它就是不能正常运行了,蓝屏了。遇到这种情况,可能就傻眼了,盯着代码不知所措,问高手也不知道什么时候高手心情高兴给解释了,或者高手直接来句“根据dump文件双机调试就解决了“。但是我们是菜鸟,我们没有高手所修的内功的,我们不知道怎么做“根据dump文件双机调试就解决了”,我们也双机调试了,但是我们没解决。其实很多时候,高手所谓的简单也是在拥有了几年的开发经验之后才觉得简单。最近看黑防有些读者写驱动遇到问题,不知所措,有感于我的学习经历,“授人以鱼不如授人以渔”,因此作此文,一来解惑二来共勉。
2、Windbg调试环境搭建
2.1、Loacl Kernel Debug(本地调试):有时候我们只是看看内核的某些数据结构,直接利用本地调试就行。
环境配置:
1、  打开windbg的kernel debug,选择第四个Local 点确定
2、  设置本地调试的符号路径,打开windbg的symbol file path选项
srv*C:\LocalSymbols*http://msdl.microsoft.com/download/symbols 然后点reload。Windbg从微软符号服务器下载跟你操作系统相对应的符号文件,因此需要联网操作。
符号文件跟操作系统正确对应的话,就出现Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
lkd> .reload
Unable to read head of debugger data list
Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
Loading Kernel Symbols
............................................................................................................................
Loading User Symbols
Loading unloaded module list

之后,我们就可以开始本地调试了。比如看EPROCESS的结构,直接本地dt _eprocess就出来了。比如反汇编函数ZwCreateProcessEx
lkd> uf ZwCreateProcessex
ntdll!NtCreateProcessEx:
7c92d140 b830000000      mov     eax,30h
7c92d145 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d14a ff12            call    dword ptr [edx]
7c92d14c c22400          ret     24h
2.2、双机调试:主要用于单步调试程序,动态调试,定位驱动程序出现蓝屏的原因或者逆向动态分析。
环境配置:
1、设置虚拟机(以Vmware汉化版、Windows  XP系统为例)
  打开Vmware中安装的XP虚拟机,右键点击“设置”,出现虚拟机硬件设置项
 
点击下面的“添加”,下一步选择“串口,下一步选择“输出到重命名管道”


按照上边设置,点击完成就OK。这样双机调试的时候,虚拟机就作为服务器让Windbg来调试了。
2、Vmware 中XP设置
编辑C盘根目录文件 boot.int(隐藏只读文件,编辑的时候去掉右键去掉只读属性)
[boot loader]
timeout=5
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect 
将最后一行复制,加上新的启动参数就完成的虚拟机的设置了。
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional Debug" /fastdetect /debugport=com1 /baudrate=115200

3、Windbg参数设置
  我一般建立2个Windbg快捷方式,分别对应Local调试和双机调试。
双机调试Windbg参数设置如下:
右键点击双机调试对应的快捷方式,点“属性”,在“目标”一栏设置下面就OK
"C:\Program Files\Debugging Tools for Windows\windbg.exe" 
-b -k com:port=\\.\pipe\com_1,baud=115200,pipe
之后双机调试的时候选择双机调试的快捷方式,Local调试用另一个,这样设置后就方便多了。

4、dump文件设置
  蓝屏发生后,依据dump文件查找程序错误。
右键“我的电脑”,选择系统属性,点击“高级”,对“启动和故障恢复”进行设置
将事件写入系统日志打钩,选择小内存存储就可以了,填上保存dump的目录。按照我下图设置就OK了,这样当蓝屏发生的时候windows就会自动把蓝屏记录的信息保存到这个文件夹里了。

5、设置虚拟机符号链接
  启动虚拟机之后,启动双机调试windbg快捷方式,选择Debug模式的操作系统,连接上之后就可以设置虚拟机对应的符号链接了
srv*C:\VmSymbols*http://msdl.microsoft.com/download/symbols 然后点reload。Windbg从微软符号服务器下载跟你操作系统相对应的符号文件,因此需要联网操作。
  这一部分,原本并不想这么详细的介绍,网上也有详细资料。但是许多资料都是偏于一方面,导致刚开始环境都配置错误,双机调试更别提了,因此我在这这里加以详细介绍,我也几多想删去这一部分,考虑到双击调试环境的重要性及诸多问题,最终还是保留下来了。




3、  Windbg双机源码级调试常用命令
源码级双机调试常用命令分为3类:
1、  下断点
2、  跟踪代码:
3、  查看数据
基本用法见表一,详细用法介绍,请参考windbg 帮助文档。
Windbg源码级双机调试常用命令
命令  用法  描述
bp  bp 驱动名!DriverEntry  下函数中断
bl   bl  查看所下断点
bc   bc 断点号  取消断点
F9  选中某行,按下F9下断,再按一下取消断点,相当于OD中的F2,源码随机下断
F10  单步跳过,遇到CALL函数调用跳过函数,相当于OD中的F8
F8或者F11  单步进入,遇到CALL函数调用进入函数,相当于OD中的F7
F7  运行到执行行,相当于OD中的F4
Shift + F11  跳出当前函数
dd   dd 地址 或 dd 变量名  查看内存地址数据
dt   dt  结构名 地址  查看结构中的数据



4、源码双机调试详细步骤(环境搭建成功的前提下,环境没搭建成功,一切白搭)
1、成功连接上虚拟机
2、设置符号路径
srv*C:\VmSymbols*http://msdl.microsoft.com/download/symbols;G:\SAE\20100107\objchk_wnet_x86\i386
虚拟机符号+我们驱动符号
3、设置源代码路径
G:\SAE\20100107
4、下函数断点      bp sae!DriverEntry
5、加载驱动       
6、中断在所下函数入口,然后跟踪代码
7、查看一些变量的数据,是否正确,确定程序问题
5、基于源码的双机蓝屏调试
常见蓝屏问题:
1、内存问题:溢出,指针指向错误地址、地址无效等
2、参数无效:参数指向错误,无效等
3、系统问题:线程、DPC、中断级等
解决蓝屏步骤:
1、依据dump文件,结合符号文件定位出错所在模块及所在函数
2、依据dump分析的结果,进行代码注释、审查,试图找出问题
3、若无法确定问题,采用双机调试,准确定位出错所在代码
4、依据定位到的错误进行修改(可以利用搜索引擎查找相关信息,然后再进行修改)
下面就分别介绍2个蓝屏实例进行介绍:
1、内存越界
代码如下:
//=======函数声明=============================================
NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObject, PUNICODE_STRING pRegString);
VOID DriverUnload(PDRIVER_OBJECT pDrvObject);
//创建一个系统线程
BOOLEAN CreateLogThread(PKSTART_ROUTINE StartRoutine,ULONG Flags,PVOID Thread);
//写数据
VOID WriteProcessLog(PVOID context);

NTSTATUS PsLookupThreadByThreadId(
                  IN HANDLE ThreadId,    
                  OUT PETHREAD *Thread);

ULONG pFlags=0;
PVOID pThread=NULL;
//==========================================
NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObject, PUNICODE_STRING pRegString)
{
  NTSTATUS status = STATUS_SUCCESS;
  KdPrint(("[1] DriverEntry: %S\n",pRegString->Buffer));
  pDrvObject->DriverUnload = DriverUnload;
  //创建线程
  if(CreateLogThread(WriteProcessLog,pFlags,pThread))       //线程
  {
    KdPrint(("CreateThread Success\n"));
  }

  return STATUS_SUCCESS;
}


VOID DriverUnload(PDRIVER_OBJECT pDrvObject)
{  
  if(pThread!=NULL)
  {
    pFlags = 0;
    KeWaitForSingleObject(pThread,Executive,KernelMode,0,0);
  }
  KdPrint(("[1] Unloaded\n"));
}
//
//写数据
VOID WriteProcessLog(PVOID context)
{
  do
  {
    KdPrint(("[WriteProcessLog] OK\n"));

  }while(pFlags);
  //退出循环,线程结束
  PsTerminateSystemThread(STATUS_SUCCESS);
}

//创建一个系统线程
BOOLEAN CreateLogThread(PKSTART_ROUTINE StartRoutine,PULONG Flags,PVOID Thread)
{
  NTSTATUS status;
  CLIENT_ID cid;
  HANDLE ThreadHandle = NULL;
  status = PsCreateSystemThread(
                  &ThreadHandle,
                  0L,
                  NULL,
                  NULL,
                  &cid,
                  StartRoutine,
                  NULL);
  if(!NT_SUCCESS(status))
  {
    KdPrint(("[CreateLogThread]PsCreateSystemThread error\n"));
    KdPrint(("[CreateLogThread] NTSTATUS error:0x%x\n",status));
    return 0;
  }
  status = PsLookupThreadByThreadId(cid.UniqueThread, (PETHREAD *)Thread);
  if(!NT_SUCCESS(status))
  {
    KdPrint(("[CreateLogThread]PsCreateSystemThread error\n"));
    KdPrint(("[CreateLogThread] NTSTATUS error:0x%x\n",status));
    ZwClose(ThreadHandle);
    return 0;
  }
  ZwClose(ThreadHandle);
  Flags = 1;
  return 1;
}
蓝屏分析过程
1、1.sys驱动加载的时候,蓝屏发生,因此可以初步猜测蓝屏发生在DriverEntry中
2、Windbg打开蓝屏对应的dump文件,配置正确符号文件,点分析获取详细信息
3、dump文件反馈的信息,我们重点关注
(1)蓝屏错误号
(2)蓝屏发生的模块及所在函数
(3)蓝屏发生在哪一行代码(这个有时候不准确的)
蓝屏错误号:SYSTEM_THREAD_EXCEPTION_NOT_HANDLED_M (1000007e)
蓝屏发生的模块:IMAGE_NAME:  1.sys
                SYMBOL_NAME:  1!CreateLogThread+61
蓝屏发生的代码:
FOLLOWUP_IP: 
1!CreateLogThread+61 [d:\\\2010.2\code\1\1.c @ 76]
f9ca6271 8945f4          mov     dword ptr [ebp-0Ch],eax

FAULTING_SOURCE_CODE:  
    72:     KdPrint(("[CreateLogThread]PsCreateSystemThread error\n"));
    73:     KdPrint(("[CreateLogThread] NTSTATUS error:0x%x\n",status));
    74:     return 0;
    75:   }
>   76:   status = PsLookupThreadByThreadId(cid.UniqueThread, (PETHREAD *)Thread);
    77:   if(!NT_SUCCESS(status))
    78:   {
    79:     KdPrint(("[CreateLogThread]PsCreateSystemThread error\n"));
    80:     KdPrint(("[CreateLogThread] NTSTATUS error:0x%x\n",status));
    81:     ZwClose(ThreadHandle);
依据dump反馈的信息,我们可以得到下面得猜测:1.sys的加载导致蓝屏发生,问题是出在函数CreateLogThread中的这一句
status = PsLookupThreadByThreadId(cid.UniqueThread, (PETHREAD *)Thread);
假如我们没有想到原因,那我们可以采用双机调试进行调试。
1、windbg连接上虚拟机,设置符号路径,源文件路径
2、对DriverEntry下断点  bp 1!DriverEntry
3、在虚拟机加载1.sys文件,系统中断在DriverEntry首行,单步进入CreateLogThread函数中
5、先看下参数dd pThread发现数值为0,因此发现问题出现在参数传递中,也就是说我们本来要出入的是pThread在内存中的地址,却不小心传入了pThread的值,也就是0,因此导致地址无效,蓝屏发生,我们修改
(CreateLogThread(WriteProcessLog,pFlags,pThread)为
(CreateLogThread(WriteProcessLog,pFlags,&pThread)也就是传入pThread所在内存的地址而不是pThread的值。因此有时候dump文件显示的错误代码并不代表错误就一定是这里导致的,也可能是前面参数问题。至此,我们已经找出了加载驱动蓝屏发生的原因,我们修改编译后继续测试,一切正常。
另外我提供了一道练习题,大家可以自己动手分析一下。 
2、Hook  ZwTerminateProcess遇到的问题
代码如下:
//My函数
NTSTATUS MyZwTerminateProcess(  
                   IN HANDLE  ProcessHandle,    
                   IN NTSTATUS  ExitStatus )
{
  NTSTATUS status;
  PCHAR path = (PCHAR)ExAllocatePool(NonPagedPool, 256);
  //得到路径
  GetTerminateProcessPath(ProcessHandle, path);
  KdPrint(("[MyZwTerminateProcess]path:%s\n",path));
  //执行函数
  status = OrigZwTerminateProcess( 
                                  ProcessHandle,    
                        ExitStatus);
  return status;
}
//原理
/*Eprocess->sectionobject(0x138)->Segment(0x014)->ControlAera(0x000)->FilePointer(0x024)->(FileObject->FileName,FileObject->DeviceObject)*/
VOID GetProcessPath(ULONG eprocess,PCHAR ProcessImageName)
{
  ULONG object;
  PFILE_OBJECT FileObject;
  UNICODE_STRING FilePath; 
  UNICODE_STRING DosName; 
  STRING AnsiString; 

  FileObject = NULL; 
  FilePath.Buffer = NULL; 
  FilePath.Length = 0; 
  *ProcessImageName = 0;  
  
  if(MmIsAddressValid((PULONG)(eprocess+0x138)))//Eprocess->sectionobject(0x138)
  {
    object=(*(PULONG)(eprocess+0x138));
        //KdPrint(("[GetProcessFileName] sectionobject :0x%x\n",object));
    if(MmIsAddressValid((PULONG)((ULONG)object+0x014)))
    {
      object=*(PULONG)((ULONG)object+0x014);
      //KdPrint(("[GetProcessFileName] Segment :0x%x\n",object));
      if(MmIsAddressValid((PULONG)((ULONG)object+0x0)))
      {
        object=*(PULONG)((ULONG_PTR)object+0x0);
        //KdPrint(("[GetProcessFileName] ControlAera :0x%x\n",object));
        if(MmIsAddressValid((PULONG)((ULONG)object+0x024)))
        {
          object=*(PULONG)((ULONG)object+0x024);
          //KdPrint(("[GetProcessFileName] FilePointer :0x%x\n",object));
        }
        else
          return ;
      }
      else
        return ;
    }
    else
      return ;
  }
  else
    return ;
    FileObject=(PFILE_OBJECT)object;

  FilePath.Buffer = ExAllocatePool(PagedPool,0x200);
  FilePath.MaximumLength = 0x200; 
    //KdPrint(("[GetProcessFileName] FilePointer :%wZ\n",&FilePointer->FileName));
  ObReferenceObjectByPointer((PVOID)FileObject,0,NULL,KernelMode);//引用计数+1,操作对象
  
  RtlVolumeDeviceToDosName(FileObject-> DeviceObject, &DosName); 
  RtlCopyUnicodeString(&FilePath, &DosName); 
  RtlAppendUnicodeStringToString(&FilePath, &FileObject->FileName); 
  ObDereferenceObject(FileObject); 
   
  RtlUnicodeStringToAnsiString(&AnsiString, &FilePath, TRUE); 
  if ( AnsiString.Length >= 216 ) 
  { 
    memcpy(ProcessImageName, AnsiString.Buffer, 0x100u); 
    *(ProcessImageName + 215) = 0; 
  } 
  else 
  { 
    memcpy(ProcessImageName, AnsiString.Buffer, AnsiString.Length); 
    ProcessImageName[AnsiString.Length] = 0; 
  } 
  RtlFreeAnsiString(&AnsiString); 
  ExFreePool(DosName.Buffer); 
  ExFreePool(FilePath.Buffer); 
}

//根据ProcessHandle得到EPROCESS  然后得到结束进程全路径
VOID GetTerminateProcessPath( HANDLE ProcessHandle, char *ProcessPath)
{
  NTSTATUS status;
  PVOID ProcessObject;
  ULONG eprocess;
  status = ObReferenceObjectByHandle( ProcessHandle ,0,*PsProcessType,KernelMode, &ProcessObject, NULL);
  if(!NT_SUCCESS(status))   //失败
  {
    DbgPrint("Object Error");
    KdPrint(("[GetTerminateProcessPath] error status:0x%x\n",status));
  }
  KdPrint(("[GetTerminateProcessPath] Eprocess :0x%x\n",(ULONG)ProcessObject));
  //Object转换成EPROCESS: object低二位清零
  eprocess = ((ULONG)ProcessObject) & 0xFFFFFFFC;
  ObDereferenceObject(ProcessObject);
  GetProcessPath( eprocess ,ProcessPath);
}                                                    
蓝屏分析报告:
1、1.sys驱动加载后,当结束程序的蓝屏发生,因此怀疑MyZwTerminateProcess导致的蓝屏
2、依据dump反馈的信息,我们得到:
蓝屏代号:PAGE_FAULT_IN_NONPAGED_AREA
蓝屏发生的模块:IMAGE_NAME:  1.sys
                SYMBOL_NAME:  1!GetTerminateProcessPath+4b
蓝屏发生在:1.c @ 201    GetProcessPath( eprocess ,ProcessPath);
依据dump反馈的信息,我们可以猜测GetProcessPath函数导致的蓝屏发生。
3、对GetTerminateProcess下断点,单步执行,当执行到
status = ObReferenceObjectByHandle( ProcessHandle ,
0,
*PsProcessType,
KernelMode, 
&ProcessObject, 
NULL);
时发现,这个函数执行失败,返回Error NTSTATUS 是:0xC00000008为什么失败:
#define   STATUS_INVALID_HANDLE        ((NTSTATUS)0xC0000008L)
无效句柄导致的函数执行失败,ProcessObject错误导致GetProcessPath执行发生错误。
4、为什么这里会有无效句柄呢,利用搜索引擎搜索,发现要结束自身进程的时候,ProcessHandle为空,因此这里执行失败。那怎么办呢?
5、参考WRK中的ZwTerminateProcess的代码,发现MS对句柄有进行修正之后,才调用的ObReferenceObjectByHandle。
if (ARGUMENT_PRESENT (ProcessHandle)) //利用ARGUMENT_PRESENT宏对句柄是否为空进行了判断
{
        ProcessHandleSpecified = TRUE;
    
} else 
{
        ProcessHandleSpecified = FALSE;
        ProcessHandle = NtCurrentProcess();   //为空得到当前的进程句柄
}
找出了原因,我们也仿效WRK做法,利用宏对ProcessHandle进行处理。这里我需要说明一下,这段代码是来源于帮别人分析蓝屏原因得到的,里面有些地方,值得引起我们的注意。
1、既然进行容错处理,就要进行完善的容错处理啊,这里既然对
ObReferenceObjectByHandle是否执行成功进行了容错处理,却缺少一句return跳出函数。
2、对函数一些参数我们要进行检查,确保参数有问题时及早的中止代码执行。

蓝屏原因无花八门,但是只要我们掌握规范的的内核编程,并学会利用windbg双机调试,我们就能够解决很大部分蓝屏,而不是处处求人。最近有人让我帮着解决蓝屏问题,经过我分析找到蓝屏原因,发现都是很小的问题,很多都是细节问题,因此觉得有必要在帮助解决问题的同时把方法也教给别人,特作此文,希望以后读者都能做到“自救”。
6、减少蓝屏的做法
由于内核内存空间是共享的,稍有不慎就会导致蓝屏发生,因此我们写内核模块时思路要清晰,代码尽量规范,采用一些正确的编程方法,依然能够帮助我们尽可能的减少蓝屏的几率。
可以采用的下做法如下:

1、拥有完整的函数容错处理代码,便于找问题(这个一定要重视)
2、利用KdPrint不断验证代码执行后的结果是否与预期相符,及早发现异常问题,避免小问题到后面被放大化,增加分析难度
3、内存分配与回收要异常小心,利用try-except处理
4、不要随便用别人的内核代码,要自己理解后,自己写,安全才能把握,一味的复制、粘贴程序只会问题越来越多
5、写完后,进行代码自我审查
依据我的经验,如果读者注意了以上五点,许多蓝屏都能在初期就被扼杀住,而不至于扩大范围,影响分析。
7、总结
  由于蓝屏原因实在太多了,无法一一分析介绍,特地选择几个有代表性的分析一下,重在介绍方法。我也尽力把方法说清楚,但是由于这个需要真实的去实践,有些地方说的不是很明白,大家有什么问题直接发邮件问我,或者观看随文提供的录像。希望大家看完这篇文章,学会调试步骤、技巧、方法,以后遇到蓝屏,能够有勇气说:一切“蓝屏”都是纸老虎。这也是我本人想告诉大家的,只要掌握调试蓝屏的方法,蓝屏就是“纸老虎”。
  自己动手丰衣足食,守株待兔一无所获。当我们遇到问题的时候,不妨试着自己解决下,自己靠双手花10个小时解决问题也比请别人花10分钟解决问题收获更多。最后,我用一句话来和所有从事底层开发的读者共勉:Blue Screen of Death is inevitable.But to believe yourself, it is more reliable.Never give up,everything is possible!(蓝屏是不可避免的,与其靠别人不如靠自己,坚持下去,一切皆有可能)。