在一年前,发现产品的windows service总是崩溃,但每次用windbg
attach或者adplus
产生dump,总是不能捕获到程序出错时候的栈,而且crash的时候只能看到少数甚至只剩一个线程。后来用windbg单步调试终于找到的罪魁祸首,原来是出错在strcpy_s这个函数。但是为什么直接用windbg attach或者adplus没法获取第一现场呢?当程序比较简单的时候可以用单步调试,但是当程序复杂的时候怎么去最终这种问题呢? 本文主要针对这两个问题来进行研究。
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或者adplus直接获得错误现场的时候,那么可以试一试如下几个方法:
当程序crash之后,在Event Log -> Application Log
中可以看到进程Crash信息如下图:
可以看到进程中最后异常的模块是testforiceking.exe
,异常的代码为0xc000000d
(非法的参数),异常发生的在模块testforiceking.exe
的位置偏移为0x0000108d
。有了这些信息我们可以按照如下步骤去做:
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
在程序中多处被调用的话,这种方法调试起来比较困难,没办法一次性确认到程序的出错地方。
这种方法能够比较迅速的定位问题所在,也是本文所推荐的方法。我们注意到,当调试器附加到进程之后,strcpy_s
出现非法参数后,最后调用了TerminateProcess
,那么可以在Windbg 启动testforiceking.exe
之后在TerminateProcess
处设置断点,并且运行程序(有时候也设置断点在ntdll!ZwTerminateProcess
,kernel32!TerminateProcess
,kernelbase!TerminateProcess
,kernel32!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'