Windbg无法捕获strcpy_s crash时的函数调用栈的研究

问题描述

在一年前,发现产品的windows service总是崩溃,但每次用windbg attach或者adplus产生dump,总是不能捕获到程序出错时候的栈,而且crash的时候只能看到少数甚至只剩一个线程。后来用windbg单步调试终于找到的罪魁祸首,原来是出错在strcpy_s这个函数。但是为什么直接用windbg attach或者adplus没法获取第一现场呢?当程序比较简单的时候可以用单步调试,但是当程序复杂的时候怎么去最终这种问题呢? 本文主要针对这两个问题来进行研究。

strcpy_s

strcpy属于不安全字符串函数,其原型如下。当strSource的字符串长度大于strDestination的缓冲区长度的时候,将会导致缓冲区溢出。当程序中使用这种不安全的字符串拷贝函数,则有可能被黑客利用,导致缓冲区溢出,从而执行黑客的恶意代码(这部分不在这里细讲,以后会针对这类安全问题,写一些博文)。

char *strcpy(  
   char *strDestination,  
   const char *strSource   
);  

在这样的背景下,微软实现了一套安全的字符串操作函数,比如strcpy_s,函数原型如下。

errno_t strcpy_s(  
   char *strDestination,  
   size_t numberOfElements,  
   const char *strSource   
);  

安全函数添加了一个目标缓冲区长度numberOfElements, 当调用strcpy_s时,其会检查numberOfElements长度,如果目标缓冲区长度不够,则会触发非法参数处理(Invalid Parameter Handler Routine)。如果是Debug版本的程序,则会触发断言。如果是Release版本则会调用_invoke_watson,将会向操作系统报告错误信息或者终止程序。 这里提一句,安全函数,并不是保证程序不崩溃,而是这种信条“程序崩溃总比被黑客利用缓冲区溢出漏洞进行攻击来的好一点。。。。”

示例代码

本文将采用如下的示例代码来追踪这个问题,可以看到以下代码因为缓冲区不足,讲在strcpy_s调用后崩溃。

#include  
#include 
#include 
#include 
#include 

DWORD WINAPI Fun(LPVOID lpParamter)
{
    char a[1];
    //Crash Here
    strcpy_s(a, 1, "adsfasdf");
    return 0; 
}
int main()
{
    Sleep(5 * 1000);
    HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
    Sleep(20 * 1000);

    return 0;
}

问题研究

再Visual Studio中调试并且查看了strcpy_s的函数代码,当触发非法参数处理(Invalid Parameter Handler Routine)的时候Release版本则会调用_invoke_watson,其部分代码如下:

    wasDebuggerPresent = IsDebuggerPresent();

    /* Make sure any filter already in place is deleted. */
    SetUnhandledExceptionFilter(NULL);

    ret = UnhandledExceptionFilter(&ExceptionPointers);

    // if no handler found and no debugger previously attached
    // the execution must stop into the debugger hook.
    if (ret == EXCEPTION_CONTINUE_SEARCH && !wasDebuggerPresent) {
        _CRT_DEBUGGER_HOOK(_CRT_DEBUGGER_INVALIDPARAMETER);
    }

    TerminateProcess(GetCurrentProcess(), STATUS_INVALID_PARAMETER);

这个就是本博文问题原因的所在了,注意UnhandledExceptionFilter是直接调用的,当有调试器的时候将直接返回,并不会给windows报告异常错误,于是调用了TerminateProcess,这就导致了windbg attach到进程没法获取错误现场,有时候也只有部分线程的信息了,详细可以参考《The default invalid parameter behavior for the VC8 CRT doesn’t break into the debugger》。
当没有调试器的时候UnhandledExceptionFilter将会向操作系统发送错误,如果是桌面程序,将看到错误信息如下图:
Windbg无法捕获strcpy_s crash时的函数调用栈的研究_第1张图片

调试方法

既然我们现在已经知道了原因,那下次当你没办法用windbg或者adplus直接获得错误现场的时候,那么可以试一试如下几个方法:

Event Log+Windbg

当程序crash之后,在Event Log -> Application Log中可以看到进程Crash信息如下图:
Windbg无法捕获strcpy_s crash时的函数调用栈的研究_第2张图片
可以看到进程中最后异常的模块是testforiceking.exe,异常的代码为0xc000000d(非法的参数),异常发生的在模块testforiceking.exe的位置偏移为0x0000108d。有了这些信息我们可以按照如下步骤去做:

  • 用Windbg启动testforiceking.exe,然后查看对应的异常处的符号信息。如下可以看到其在strcpy_s函数中出现了异常。
0:001> ln testforiceking+0x108d
f:\sp\vctools\crt_bld\self_x86\crt\src\tcscpy_s.inl(18)+0x29
(00401064)   testforiceking!strcpy_s+0x29   |  (004010c9)   testforiceking!_set_osplatform
  • 然后在strcpy_s处设置断点bp testforiceking!strcpy_s,然后每次到strcpy_s后单步调试,查看是否问题所在。

  • 当然这时候也可以去查看程序中调用到strcpy_s的地方,找到可疑的几处进行查看

strcpy_s在程序中多处被调用的话,这种方法调试起来比较困难,没办法一次性确认到程序的出错地方。

Windbg+TerminateProcess断点

这种方法能够比较迅速的定位问题所在,也是本文所推荐的方法。我们注意到,当调试器附加到进程之后,strcpy_s出现非法参数后,最后调用了TerminateProcess,那么可以在Windbg 启动testforiceking.exe之后在TerminateProcess处设置断点,并且运行程序(有时候也设置断点在ntdll!ZwTerminateProcesskernel32!TerminateProcesskernelbase!TerminateProcesskernel32!ExitProcess,或者kernelbase!ExitProcess):

0:000> bp KERNELBASE!TerminateProcess
0:000> g
Breakpoint 0 hit
eax=ffffffff ebx=00000000 ecx=779877e4 edx=002de8b8 esi=00000001 edi=00000000
eip=7790f210 esp=053ffc14 ebp=053ffca4 iopl=0         nv up ei ng nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000286
KERNELBASE!TerminateProcess:
7790f210 8bff            mov     edi,edi

当程序运行到断点TerminateProcess处,查看函数调用栈,就可以找到程序出错的地方了。如下,可以找到函数调用关系为fun->strcpy_s

0:001> kv
ChildEBP RetAddr  Args to Child              
0537fc00 7790f23c ffffffff c000000d 0537fca4 ntdll!ZwTerminateProcess (FPO: [2,0,0])
0537fc10 00401410 ffffffff c000000d 00000022 KERNELBASE!TerminateProcess+0x2c (FPO: [Non-Fpo])
0537ff4c 0040108d 00000000 00000000 00000000 testforiceking!_invoke_watson+0xe6 (FPO: [Non-Fpo]) (CONV: cdecl) [f:\sp\vctools\crt_bld\self_x86\crt\src\invarg.c @ 185]
0537ff70 00401014 0537ff87 00000001 0041218c testforiceking!strcpy_s+0x29 (FPO: [3,0,4]) (CONV: cdecl) [f:\sp\vctools\crt_bld\self_x86\crt\src\tcscpy_s.inl @ 18]
0537ff88 7796336a 00000000 0537ffd4 77e99f72 testforiceking!Fun+0x14 (FPO: [Non-Fpo]) (CONV: stdcall) [d:\vsproject\testforme\testforiceking\test.cpp @ 11]
0537ff94 77e99f72 00000000 7dfe5d2b 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
0537ffd4 77e99f45 00401000 00000000 ffffffff ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
0537ffec 00000000 00401000 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

建议方法

在编程实现的时候,可以在调用strcpy_s之前先判断下源字符串长度是否大于目标字符串缓冲区长度,然后做相应的业务逻辑处理。如果说程序的业务逻辑,能够接受字符串截断,可以使用strncpy_s,其拷贝字符个数设置为_TRUNCATE。 如下MSDN示例:

// crt_truncate.c  
#include   
#include   

int main()  
{  
   char src[] = "1234567890";  
   char dst[5];  
   errno_t err = strncpy_s(dst, _countof(dst), src, _TRUNCATE);  
   if ( err == STRUNCATE )  
      printf( "truncation occurred!\n" );  
   printf( "'%s'\n", dst );  
}  

输出结果为:

truncation occurred!  
'1234'  

你可能感兴趣的:(Windows配置与开发,Windows调试)