zoukankan      html  css  js  c++  java
  • 蓝屏的调试艺术[转]

    作 者: 竹君
         原文链接:http://bbs.pediy.com/showthread.php?t=113285

    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 FilesDebugging Tools for Windowswindbg.exe" 
    -b -k com:port=\.pipecom_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:SAE20100107objchk_wnet_x86i386
    虚拟机符号+我们驱动符号
    3、设置源代码路径
    G:SAE20100107
    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 ",pRegString->Buffer));
      pDrvObject->DriverUnload = DriverUnload;
      //创建线程
      if(CreateLogThread(WriteProcessLog,pFlags,pThread))       //线程
      {
        KdPrint(("CreateThread Success "));
      }
      return STATUS_SUCCESS;
    }
    VOID DriverUnload(PDRIVER_OBJECT pDrvObject)
    {  
      if(pThread!=NULL)
      {
        pFlags = 0;
        KeWaitForSingleObject(pThread,Executive,KernelMode,0,0);
      }
      KdPrint(("[1] Unloaded "));
    }
    //
    //写数据
    VOID WriteProcessLog(PVOID context)
    {
      do
      {
        KdPrint(("[WriteProcessLog] OK "));
      }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 "));
        KdPrint(("[CreateLogThread] NTSTATUS error:0x%x ",status));
        return 0;
      }
      status = PsLookupThreadByThreadId(cid.UniqueThread, (PETHREAD *)Thread);
      if(!NT_SUCCESS(status))
      {
        KdPrint(("[CreateLogThread]PsCreateSystemThread error "));
        KdPrint(("[CreateLogThread] NTSTATUS error:0x%x ",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.2code11.c @ 76]
    f9ca6271 8945f4          mov     dword ptr [ebp-0Ch],eax
    FAULTING_SOURCE_CODE:  
        72:     KdPrint(("[CreateLogThread]PsCreateSystemThread error "));
        73:     KdPrint(("[CreateLogThread] NTSTATUS error:0x%x ",status));
        74:     return 0;
        75:   }
    >   76:   status = PsLookupThreadByThreadId(cid.UniqueThread, (PETHREAD *)Thread);
        77:   if(!NT_SUCCESS(status))
        78:   {
        79:     KdPrint(("[CreateLogThread]PsCreateSystemThread error "));
        80:     KdPrint(("[CreateLogThread] NTSTATUS error:0x%x ",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 ",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 ",object));
        if(MmIsAddressValid((PULONG)((ULONG)object+0x014)))
        {
          object=*(PULONG)((ULONG)object+0x014);
          //KdPrint(("[GetProcessFileName] Segment :0x%x ",object));
          if(MmIsAddressValid((PULONG)((ULONG)object+0x0)))
          {
            object=*(PULONG)((ULONG_PTR)object+0x0);
            //KdPrint(("[GetProcessFileName] ControlAera :0x%x ",object));
            if(MmIsAddressValid((PULONG)((ULONG)object+0x024)))
            {
              object=*(PULONG)((ULONG)object+0x024);
              //KdPrint(("[GetProcessFileName] FilePointer :0x%x ",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 ",&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 ",status));
      }
      KdPrint(("[GetTerminateProcessPath] Eprocess :0x%x ",(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!(蓝屏是不可避免的,与其靠别人不如靠自己,坚持下去,一切皆有可能)。

  • 相关阅读:
    linux 下怎么看postgresql安装到哪个目录了?
    sqlserver 存储image 语句
    thinkphp5.1 配置使用
    百度车牌识别demo
    elastticsearch 安装
    InDB开发
    John爆破密码
    域传送漏洞
    新远程下载方式(IME)
    SSH端口转发
  • 原文地址:https://www.cnblogs.com/yuqiao-ray-vision/p/3714099.html
Copyright © 2011-2022 走看看