分类 调试 下的文章

今天我想谈谈一个常见的错误,我们在日常工作钟许多案例中都看到了这一点。它涉及到驱动程序占用内核堆栈上的过多空间,从而导致内核堆栈溢出,然后将通过以下错误检查之一使系统崩溃:

1. STOP 0x7F: UNEXPECTED_KERNEL_MODE_TRAP当参数1设置为EXCEPTION_DOUBLE_FAULT时,这是由于覆盖内核堆栈的末尾而导致的。

2. STOP 0x1E: KMODE_EXCEPTION_NOT_HANDLED, 0x7E: SYSTEM_THREAD_EXCEPTION_NOT_HANDLED, or 0x8E: KERNEL_MODE_EXCEPTION_NOT_HANDLED, 异常代码为STATUS_ACCESS_VIOLATION,表示内存访问冲突。

3. STOP 0x2B: PANIC_STACK_SWITCH, 这通常发生在内核模式驱动程序使用太多堆栈空间时。

内核堆栈概述

系统中的每个线程都分配有一个内核模式堆栈。运行在任何内核模式线程(无论是系统线程还是驱动程序创建的线程)上的代码都使用该线程的内核模式堆栈,除非该代码是DPC,在这种情况下,它在某些平台上使用处理器的DPC堆栈。堆栈负增长。这意味着堆栈的开始(底部)的地址高于堆栈的结束(顶部)。例如,让我们保持堆栈的开头是0x80f1000,这是堆栈指针(ESP)指向的位置。如果将一个DWORD值推送到堆栈上,它的地址将是0x80f0ffc。下一个DWORD值将存储在0x80f0ff8,以此类推,直到分配的堆栈的限制(顶部)。堆栈顶部以保护页为边界,以检测溢出。

内核模式堆栈的大小因不同的硬件平台而异。例如:

  • 在基于x86的平台上,内核模式堆栈是12K。
  • 在基于x64的平台上,内核模式堆栈为24K(基于x64的平台包括使用AMD64体系结构的处理器和使用Intel EM64T体系结构的处理器的系统)。
  • 在基于安腾的平台上,内核模式堆栈是32K,有一个32K后备存储。(如果处理器的寄存器文件中的寄存器用完,它将使用后备存储器来保存寄存器的内容,直到分配函数返回为止。这不会直接影响堆栈分配,但操作系统在基于安腾的平台上使用的寄存器比在其他平台上使用的寄存器多,这使得驱动程序可以使用的堆栈相对更多。)

上面列出的堆栈大小是系统施加的硬限制,所有驱动程序都需要保守地使用空间,以便它们能够共存

0×01 介绍

一些文章已经介绍过通过检测异常来对抗调试器的技术。这个思想很简单:根据设计本意,调试器会处理特定的异常。如果一个异常包裹在try块中,只有当没有附加调试器的时候,异常处理程序才会执行。因此,可以得出结论,只要异常块没有执行,那么程序就正在被一个调试器调试。

0×02 一个字节的Interrupt 3中断(0xCC)

在所有会被调试器处理的异常中,interrupt 3中断算是一个,它会生成一个单字节的断点。

BOOL IsDebuggerPresent_Int3()
{
      __try
      {
            __asm int 3
      }
      __except(1)
      {
            return FALSE;
      }
 
      return TRUE;
}

0×03 两个字节的Interrupt 3中断(0xCD 0×03)

使用Visual C++内联汇编器的_emit伪指令可以生成一个两字节的interrupt 3指令。在测试的所有调试器中,只有OnllyDbg调试器识别这个异常。

 

 

BOOL IsDebuggerPresent_Int3_2Byte()
{
      __try
      {
            __asm
            {
                  __emit 0xCD
                  __emit 0x03
            }
      }
      __except(1)
      {
            return FALSE;
      }
 
      return TRUE;
}

 

0×04 Interrupt 0x2C中断

Interrupt 0x2C引起一个调试断言异常。WinDbg会响应这个异常,但是其它的调试器都不会处理它。这个指令可以使用_int 2c来生成。这个中断只在Vista以及之后的系统上有效。因为这一点,需要动态检查windows的版本,如果版本号小于6就返回false。

 

BOOL IsDebuggerPresent_Int2c()
{
      OSVERSIONINFO osvi;
 
      ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
      osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
 
      GetVersionEx(&osvi);
 
      if (osvi.dwMajorVersion < 6)
            return FALSE;
 
      __try
      {
            __int2c();
      }
      __except(1)
      {
            return FALSE;
      }
 
      return TRUE;
}

 

0×05 Interrupt 0x2D

如果执行了interrupt 0x2D,Windows将抛出一个断点异常

BOOL IsDebuggerPresent_Int2d()
{
__try
{
__asm int 0x2d
}
__except(1)
{
return FALSE;
}

return TRUE;
}

0×06 ICEBP(0xF1)

ICEBP是一个没有文档化的指令,可以作为一个单字节的interrupt 1中断,产生一个单步异常。

BOOL IsDebuggerPresent_IceBp()
{
__try
{
__asm __emit 0xF1
}
__except(1)
{
return FALSE;
}

return TRUE;
}

0×07 陷阱标志位

EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。

BOOL IsDebuggerPresent_TrapFlag()
{
__try
{
__asm
{
pushfd
or word ptr[esp], 0x100
popfd
nop
}
}
__except(1)
{
return FALSE;
}

return TRUE;
}

0×08 抛出异常

RaiseException函数产生的若干不同类型的异常可以被调试器捕获。在RaiseException的基础上,OutputDebugString,该函数常被用来检测附加的调试器。有效的异常码如下所示:

STATUS_BREAKPOINT                                   (0x80000003)
STATUS_SINGLE_STEP                                   (0x80000004)    
DBG_PRINTEXCEPTION_C                            (0x40010006)
DBG_RIPEXCEPTION                                       (0x40010007)
DBG_CONTROL_C                                           (0x40010005)
DBG_CONTROL_BREAK                                 (0x40010008)
DBG_COMMAND_EXCEPTION                   (0x40010009)
ASSERTION_FAILURE                                     (0xC0000420)
STATUS_GUARD_PAGE_VIOLATION       (0x80000001)
SEGMENT_NOTIFICATION                           (0x40000005)
EXCEPTION_WX86_SINGLE_STEP             (0x4000001E)
EXCEPTION_WX86_BREAKPOINT              (0x4000001F)
BOOL TestExceptionCode(DWORD dwCode)
{
__try
{
RaiseException(dwCode, 0, 0, 0);
}
__except(1)
{
return FALSE;
}

return TRUE;
}

BOOL IsDebuggerPresent_RipException()
{
return TestExceptionCode(DBG_RIPEXCEPTION);
}

0×09 兼容性表

我最近在处理一个问题,服务器在几天内耗尽了NonPagedPool。通常,我们只需要使用像PoolMon这样的工具来识别有问题的pool标记,然后使用本文中的方法找到使用该pool标记的驱动程序。然而,让这个案例有趣的是pool标记,而且我们无法使用常规方法识别驱动程序。你一会儿就会明白我的意思了。支持给我提供了服务器处于状态时的内核转储,这就是我发现的。
让我们先看看虚拟内存的使用情况:

2: kd> !vm*** Virtual Memory Usage ***Physical Memory:851420 ( 3405680Kb)

Page File: \
??\C:\pagefile.sys

Current:
3584000 Kb Free Space: 3568552Kb

Minimum:
3584000 Kb Maximum: 3584000Kb

Available Pages:
573277 ( 2293108Kb)

ResAvail Pages:
800628 ( 3202512Kb)

Locked IO Pages:
1067 ( 4268Kb)

Free System PTEs:
25102 ( 100408Kb)

Free NP PTEs:
335 ( 1340Kb)

Free Special NP:
0 ( 0Kb)

Modified Pages:
22 ( 88Kb)

Modified PF Pages:
22 ( 88Kb)

NonPagedPool Usage:
31369 ( 125476Kb) ß Very high

NonPagedPool Max:
31986 ( 127944Kb)********** Excessive NonPaged Pool Usage *****PagedPool0 Usage: 19071 ( 76284Kb)

PagedPool
1 Usage: 735 ( 2940Kb)

PagedPool
2 Usage: 747 ( 2988Kb)

PagedPool
3 Usage: 720 ( 2880Kb)

PagedPool
4 Usage: 746 ( 2984Kb)

PagedPool Usage:
22019 ( 88076Kb)

PagedPool Maximum:
38912 ( 155648Kb)********** 3 pool allocations have failed **********

当我在别人的机器上调试问题时,我做的第一件事就是查看modules窗口。按版本排序并看到一个不属于的dll可以帮助立即诊断配置问题,并节省许多调试痛苦。

 

 下面介绍下各列的意思:

  • Name:
    模块名称。
  • Path:
    PE在文件系统上的位置。当您希望找到正确的PE以开始使用命令行工具进行查询时,这非常有用。
  • Optimized:这是针对托管代码的。它告诉您,当从MSIL转到目标平台本机指令集时,JIT编译器是否使用了优化。
  • User Code:在调试器选项中,它也被称为“只是我的代码”。如果您刚刚打开了我的代码,并且dll已优化或没有符号,我们将把它视为“非用户”代码。这意味着调用堆栈窗口中的所有帧都将折叠到一个[非用户代码]帧之后。默认情况下,这个特性是关闭的C++,以及VB和C用户配置文件。您可以在Tools/options/Debugging/Enable-Just-My-Code下切换它。“JMC”的全部分支比简单地隐藏调用堆栈帧要大得多。它包括遍历非用户代码,一直到代码开始运行的地方(并且执行得很快)。Mike Stall在博客中提到了其中一些。
  • Symbol Status:这将告诉您是否加载了符号。它还可以告诉您它们是否被剥离(实际上只是一个本地dll问题),或者devenv是否只是加载了导出。如果您正在执行崩溃转储调试,它还可以判断是否已找到二进制文件。右键单击上下文菜单,您可以加载符号,跳转到“符号设置…”对话框,还可以查看我们搜索符号的位置。
  • Symbol File:pdb文件所在的位置。上下文菜单项“符号加载信息…”可以告诉您调试器查找符号的位置。
  • Order:加载顺序
  • Version: dll的版本信息。这对于将相关组件组合在一起非常有用,并且通常会在有人的版本不好时向您显示。查看我们在visual studio版本字符串中保留的信息量。
  • Timestamp: 这是真正的交易-链接器(或.Net编译器)生成并将PE发送到世界上时的内部时间戳。
    看看这个:
    C:\Program Files\Internet Explorer>dir iexplore.exe
     Volume in drive C has no label.
     Volume Serial Number is 2CAB-3E87

     Directory of C:\Program Files\Internet Explorer

    08/04/2004  05:00 AM            93,184 IEXPLORE.EXE
    文件系统的时间戳是什么。如果你想知道它什么时候落在你的硬盘上就好了。对于确定它比过程中的所有相关组件都晚2天构建来说,没有那么有用。
    C:\Program Files\Internet Explorer>dumpbin /headers IEXPLORE.EXE | findstr time
            41107B81 time date stamp Tue Aug 03 23:00:33 2004
    这是你真正想要的。好...这是我真正想要的。这就是你在这个专栏里看到的。在VS之前的版本中,托管代码的时间戳是不太有用的文件系统时间戳。

  • Address:
    进程内存中PE的加载地址范围。最左边是一列没有标题的图标。如果图标看起来像一个小页面,则此地址是首选加载地址。如果它有一个带有红色感叹号的页面,那么这个PE在加载时被重新定位。如果你拥有这个PE,找到一个它自己的地方,它可能加载得更快。
  • Process:
    进程名:其中之一(本机或托管)。注意:如果您正在进行混合模式调试,那么许多dll实际上会在“模块”窗口中有行。一个用于“本机”dll加载。。。在某种程度上,一切都是土生土长的。一个用于托管dll加载。您可以知道它们是同一个dll,因为地址范围将匹配。基名可能不匹配,因为有时NGen的dll显示时,名称中添加了“ni”扩展名。(例如Microsoft.Build.utilities.ni.dll。)

通过单击并拖动列,可以更改“模块”窗口中列的顺序。还可以通过拖动边框来调整列的大小。另一个巧妙的技巧是通过标题栏上的上下文菜单将窗口设置为选项卡式文档。你可以在devenv的任何一个窗口中完成这项工作。

关于MBR的反汇编,我想花点时间向您展示一下快速而肮脏的方法。
首先,使用低级扇区编辑器(如Microsoft资源工具包实用程序DiskProbe)将MBR转储到文件中。保存文件后,启动一个程序(如记事本)并使用Windbg(Windows调试器)附加到它。
接下来,找到一个有效但未占用的内存范围(默认堆是一个很好的候选者)。内存范围至少需要512(0x200)字节。例如,我在这里找到了我的:

000120a0 00000000 00000000 00000000 00000000000120b000000000 00000000 00000000 00000000000120c000000000 00000000 00000000 00000000000120d000000000 00000000 00000000 00000000000120e000000000 00000000 00000000 00000000000120f000000000 00000000 00000000 00000000

00012100 00000000 00000000 00000000 00000000

00012110 00000000 00000000 00000000 00000000

00012120 00000000 00000000 00000000 00000000...