wenmo8 发布的文章

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 兼容性表

Windbg调试器的!htrace扩展对于调试泄漏处理非常方便。
该过程基本上可归结为以下简单步骤:

  1. 启用跟踪
  2. 拍张快照
  3. 情景分析
  4. 显示差异

在第四步!htrace将在最后一个快照之后显示所有额外打开的句柄,以及调用堆栈(如果可用)。这大大有助于调试哪些句柄是泄漏的,以及由谁来处理。与其他任何资源泄漏检测工具一样,也会有误报。您需要了解什么是真正的泄漏,什么只是暂时的分配。

我看到了一些关于Rotor(和CLR)中使用的异常处理机制的问题。下面是关于Rotor异常处理的另一个注意事项列表。目的是帮助Rotor开发人员调试和理解CLR中的异常。

异常生成和抛出

此步骤在很大程度上取决于异常的类型以及从何处引发异常:

jitted代码中的软件异常---使用C#中throw new MyException(…)引发的异常

  • 异常对象的创建方式与任何其他托管对象的创建方式相同
  • vm\jitiface.cpp中调用JIT_Throw
  • HelperFrame被推送以标记从jitted到EE代码的转换。这是可靠的stack walk工作所必需的——安全性和垃圾收集是可靠stackwalk的主要用户。
  • vm\excep.cpp中调用了RaiseTheException
  • 异常对象存储在当前线程对象中
  • EXCEPTION_COMPLUS异常是使用Win32的RaiseException API引发的

执行引擎代码中的软件异常---在clr\src\vm目录中使用COMPlusThrow(…)引发异常

  • 如果尚未提供给调用,则异常对象的创建方式与任何其他托管对象的创建方式相同
  • vm\excep.cpp:RaiseTheException被调用,故事的其余部分与第一个案例相同。
  • 注意:不需要推送任何转换框架
  • 注意:在托管异常堆栈跟踪中不包含C++ stacttrace。

jitted代码中的硬件异常-如访问空对象、除以零等。

  • 将调用jitted代码的异常处理程序。大多数时候它是vm\i386\excepx86.cpp:COMPlusFrameHandler。
  • 异常被标识为直接来自vm\i386\excepx86.cpp:CPFH_HandleManagedFault中的jitted代码。
  • 推送FaultingExceptionFrame以标记从jitted到EE代码的特殊转换。
  • 使用vm\i386\excepx86.cpp:LinkFrameAndRow和关联的asm帮助程序重新引发异常。
    对于Win32/i386 SEH,通过将IP修改为指向NakedThrowHelper的异常过滤器返回EXCEPTION_CONTINUE_EXECUTION来完成重新触发。
    对于PAL_PORTABLE_SEH,通过简单调用LaunchNakedThrowHelper来完成重新触发。
  • 第二次在异常过滤器中,为硬件异常创建正确类型的异常对象。硬件异常到BCL异常类型的映射在vm\excep.cpp:MapWin32FaultToCOMPlusException中完成。
  • 注意:硬件异常重试是一个实现细节,它应该允许可继续的异常(异常继续执行)无缝工作。CLI目前不支持可延续的异常,但将来可能会有所改变。此外,可能存在一些互操作场景,其中异常继续执行非常重要。
  • 注意:对于PAL_PORTABLE_SEH,硬件异常的重新触发是不同的,因为PAL不支持异常继续执行。稍后为调试器支持添加了异常继续执行。

执行引擎代码中的硬件异常

执行引擎代码中的硬件异常通常不应该发生。如果他们这样做了,通常意味着事情真的出了问题,需要大量的运气才能从这种情况中恢复过来。不过,这条规则有几个例外:

  • 写入屏障帮助程序(vm\i386\jithelp.asm:JIT_write barrier and friends)中的访问冲突被转换为调用程序中的访问冲突。转换在vm\i386\excepx86.cpp:CPFH_AdjustContextForWriteBarrier中完成。这允许JIT在调用写回载波时优化空检查。FJIT当前不使用此功能。
  • 尽管存在一些防止堆栈溢出异常(vm\stackprobe.h)的屏蔽,但它们可能发生在执行引擎代码中,这通常非常糟糕。这是为.NET v1设计的,CLR团队正在为Whidbey开发更好的解决方案。便携式堆栈溢出处理不在转子-我们只是死时堆栈溢出发生。可移植堆栈溢出处理的设计和实现留给读者练习
  • 元数据代码用于访问监视(#define ZAP_MONITOR),以允许按配置文件引导的元数据布局优化。检测是通过使所有元数据访问A/V、记录它们并通过异常继续执行从它们恢复来完成的。(是的,这个解决方案的性能很差。)元数据工具不是作为Rotor的一部分提供的,但是必须记住它以避免代码损坏。

FCalls中的软件异常--使用FCThrow(…)及其类似调用引发的异常

Fcall闻起来像jitted代码,但实际上不是,因此Fcall中的异常处理是特殊的。

  • 如果未推送helper框架,则使用FCThrow helpers从fcall抛出异常。(如果推送helperframe,则它是使用complushrow在执行引擎代码中抛出软件异常的简单方法)
  • FCThrow helper使用特殊帧ATTR_CAPTURE_DEPTH_2推动辅助帧。因此,只能直接从FCALL调用FCThrow。不能从从FCALL调用的函数调用它。
  • 一旦helper框架被推送,complushrow就会像执行引擎代码中的任何其他软件异常一样实际抛出异常。
  • 注意:FCThrow只是HELPER_METHOD_FRAME_BEGIN(…);complusthrow(…);HELPER_METHOD_FRAME_END()的快捷方式。如果您看到了长代码模式,那么在尝试使用FCThrow对其进行优化时要小心—它可能是有原因的。fcall依赖于一个非常脆弱的x86解释器。这个翻译有时需要帮助.

可以看到,所有异常都转换为一个看起来像常规托管异常的统一案例。从托管代码引发的每个异常(无论是硬件异常还是软件异常)都会下放到Win32 RaiseException层。这样做是为了保持一致性。它被认为可以简化互操作性的实现。

处理异常

异常的处理遵循两次机会的Win32模型。
第一次,stackwalker为堆栈上的每个方法调用回调。一旦某个过滤器返回它处理异常,异常展开的第二次传递就开始了。
第二次,堆栈已展开。展开代码的结构在本机Win32 i386异常处理和PAL可移植异常处理之间有所不同。

异常和垃圾收集器

请注意,对RaiseException的调用是由EE代码发出的,并且此代码以协作模式运行。因此,在异常分派期间不会阻止垃圾收集。

异常和帧

帧的链接列表(Frame-vm\frames.h类的后代)与每个线程对象关联。这些是可靠的堆栈遍历基础设施的关键部分。执行引擎代码正在使用一种特殊的机制来释放异常时的链。COMPLUS-TRY/COMPLUS-CATCH宏(vm\exceptmacros.h)通过在异常时调用unwind frame chain(vm\excepp.cpp)来展开帧链。此外,这些宏还释放嵌套的异常信息(vm\excep.cpp:UnwindExInfo)并还原GC模式,这非常方便。
请注意,UnwindFrameChain正在访问已展开的堆栈空间。这是可行的,但它取决于堆栈展开的微妙的低级实现细节。由于Win32/i386 SEH和PAL_PORTABLE_SEH之间堆栈展开的低级实现细节不同,因此COMPLUS_TRY/COMPLUS_CATCH宏对于这两个宏是不同的。它还解释了为什么有必要在执行引擎中使用COMPLUS-TRY/COMPLUS-catch捕获异常,而不使用常规的PAL-TRY/PAL-catch除外。
CLR开发人员没有选择通过为类框架创建C++析构函数来解开帧链,这将是一个非常简单的解决方案。这可能是一个纯粹的历史遗留物;它们可能有一些性能童话;或者对象析构函数与结构化异常处理的混合,而MSC不支持的结构异常处理会在编写代码时造成太多的不适。

异常与C++对象展开

在异常解除时,PAL_PORTABLE_SEH 不调用堆栈上的对象的C++析构函数。这是可以的,因为执行引擎代码似乎没有利用当前的C++特性。嗯,我们还没有找到它依赖于被调用的C++的地方。这可能会因Whidbey而改变

异常和安全

由于存在安全隐患,因此从异常处理中正确执行堆栈遍历非常重要。对于过滤器来说,这是特别棘手的,因为在调用过滤器时,真正的堆栈是不展开的。在抛出异常到最后调用异常之间,可以调用来自堆栈上源代码的任意用户代码。

 

 

 

 

Rotor

《根据《CLR Exception---E0434352》和《CLR Exception---E0434F4D》这两篇随笔,我们会发现,这两个异常太相似了,除了代码值不一样,其他几乎都一样。在windbg里调试dmp时,也会看到都叫它们CLR Exception。那他们有什么区别呢?这个问题值得研究研究。

我查了很多资料都没查明白。但是黄天不负有心人。在我搞其他问题时,让我窥得一线道道儿来。

首先上个实际的例子,在VS2013里新建一个c#控制台工程ConsoleApplication1,代码如下:

static void Main(string[] args)
{
throw newSystem.Exception();return;
}