做windows产品开发的,永远绕不开一个问题——程序崩溃。如果希望不断提升产品质量,就得不停的收集和分析崩溃日志。但是我们会发现一个问题,我们经常采用的方案无法拦截崩溃。(转载请指明出于breaksoftware的csdn博客)比如会出现如下提示:
这是一个非常不好的体验,至少说这个是对提升软件质量无益的体验。虽然以上框可以通过如下代码禁用掉,但是仍然只是个掩耳盗铃的做法。
SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
我们先看一种标准的Dump生成方案:
#include "CreateDump.h" #include <atlbase.h> #include <atlstr.h> #include <strsafe.h> #include <DbgHelp.h> #pragma comment(lib,"DbgHelp.lib") #define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS (0x00000004) #define MiniDumpWithThreadInfo 0x1000 typedef BOOL (WINAPI *PGetModuleHandleEx)( DWORD dwFlags, LPCTSTR lpModuleName, HMODULE *phModule ); VOID CreateDump(struct _EXCEPTION_POINTERS *pExceptionPointers) { //收集信息 CStringW strBuild; strBuild.Format(L"Build: %s %s", __DATE__, __TIME__); CString strError; HMODULE hModule; WCHAR szModuleName[MAX_PATH] = {0}; PGetModuleHandleEx pFun = (PGetModuleHandleEx)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetModuleHandleExW"); if ( !pFun ) { return; } pFun(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)pExceptionPointers->ExceptionRecord->ExceptionAddress, &hModule); GetModuleFileName(hModule, szModuleName, ARRAYSIZE(szModuleName)); strError.Format(L"%s %d , %d ,%d.", szModuleName,pExceptionPointers->ExceptionRecord->ExceptionCode, pExceptionPointers->ExceptionRecord->ExceptionFlags, pExceptionPointers->ExceptionRecord->ExceptionAddress); //生成 mini crash dump BOOL bMiniDumpSuccessful; WCHAR szPath[MAX_PATH]; WCHAR szFileName[MAX_PATH]; WCHAR* szAppName = L"DumpFile"; WCHAR* szVersion = L"v1.0"; DWORD dwBufferSize = MAX_PATH; HANDLE hDumpFile; SYSTEMTIME stLocalTime; MINIDUMP_EXCEPTION_INFORMATION ExpParam; GetLocalTime( &stLocalTime ); GetTempPath( dwBufferSize, szPath ); StringCchPrintf( szFileName, MAX_PATH, L"%s%s", szPath, szAppName ); CreateDirectory( szFileName, NULL ); StringCchPrintf( szFileName, MAX_PATH, L"%s%s//%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp", szPath, szAppName, szVersion, stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay, stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, GetCurrentProcessId(), GetCurrentThreadId()); hDumpFile = CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); MINIDUMP_USER_STREAM UserStream[2]; MINIDUMP_USER_STREAM_INFORMATION UserInfo; UserInfo.UserStreamCount = 1; UserInfo.UserStreamArray = UserStream; UserStream[0].Type = CommentStreamW; UserStream[0].BufferSize = strBuild.GetLength()*sizeof(WCHAR); UserStream[0].Buffer = strBuild.GetBuffer(); UserStream[1].Type = CommentStreamW; UserStream[1].BufferSize = strError.GetLength()*sizeof(WCHAR); UserStream[1].Buffer = strError.GetBuffer(); ExpParam.ThreadId = GetCurrentThreadId(); ExpParam.ExceptionPointers = pExceptionPointers; ExpParam.ClientPointers = TRUE; MINIDUMP_TYPE MiniDumpWithDataSegs = (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory | MiniDumpWithProcessThreadData | MiniDumpWithThreadInfo); bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL); return; }可以见得,我们生成dump文件必须一个结构体——_EXCEPTION_POINTERS。
这个结构体自然不是我们自己构造的,而是系统给我们的。我们该从哪个接口接收系统给我们的该信息呢?
一般情况下,我们使用SetUnhandledExceptionFilter来设置一个回调函数。当软件即将崩溃时,我们设置的回调函数理论上会被调用。然而,实际并非如此。我们看一个报错的例子。
如果你也见过这个错误,我想你的截取dump方案应该是被绕过了。我专门查了一下该错误,MSDN上有相关例子
#pragma once class A; void fcn( A* ); class A { public: virtual void f() = 0; A() { fcn( this ); } }; class B : A { void f() { } }; void fcn( A* p ) { p->f(); } // The declaration below invokes class B's constructor, which // first calls class A's constructor, which calls fcn. Then // fcn calls A::f, which is a pure virtual function, and // this causes the run-time error. B has not been constructed // at this point, so the B::f cannot be called. You would not // want it to be called because it could depend on something // in B that has not been initialized yet. int PureVirtualFunc() { B b; return 0; }这个例子将协助我们研究如何截取这种无法使用SetUnhandledExceptionFilter截取的dump。
我们构造一个SetUnhandledExceptionFilter可以截获dump的例子
LONG WINAPI DumpCallback(_EXCEPTION_POINTERS* excp) { CreateDump(excp); return EXCEPTION_EXECUTE_HANDLER; } …… SetUnhandledExceptionFilter(DumpCallback); int *p = NULL; *p = 1;我们查看调用堆栈
LONG WINAPI UnhandledExceptionFilter( _In_ struct _EXCEPTION_POINTERS *ExceptionInfo );那么,我们可以猜测,如果我们可以接管该函数,可能可以让我们捕获R6025这样的异常。我使用detours库Hook了这个函数
#include "AutoDump.h" #include <windows.h> #include "../detours/detours.h" #include "CreateDump.h" LONG WINAPI NewUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *ExceptionInfo ){ OutputDebugString(L"NewUnhandledExceptionFilter\n"); CreateDump(ExceptionInfo); return EXCEPTION_EXECUTE_HANDLER; } CAutoDump::CAutoDump(void) { m_lpUnhandledExceptionFilter = NULL; do { SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); m_lpUnhandledExceptionFilter = DetourFindFunction( "KERNEL32.DLL", "UnhandledExceptionFilter" ); if ( NULL == m_lpUnhandledExceptionFilter ) { break; } LONG lRes = NO_ERROR; lRes = DetourTransactionBegin(); if ( NO_ERROR != lRes ) { break; } lRes = DetourAttach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter ); if ( NO_ERROR != lRes ) { break; } lRes = DetourTransactionCommit(); if ( NO_ERROR != lRes ) { break; } } while (0); } CAutoDump::~CAutoDump(void) { if ( m_lpUnhandledExceptionFilter ) { do { LONG lRes = NO_ERROR; lRes = DetourTransactionBegin(); if ( NO_ERROR != lRes ) { break; } lRes = DetourDetach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter ); if ( NO_ERROR != lRes ) { break; } lRes = DetourTransactionCommit(); if ( NO_ERROR != lRes ) { break; } } while (0); } }结果,这种方式,便可以截获R6025这样的CRT错误。
现在,我们开始分析,为什么SetUnhandledExceptionFilter无法截获这些CRT错误。从上面可以分析出,当出现异常时,流程会进入UnhandledExceptionFilter,但是我们设置的回调函数没被调用。那么可以猜测,应该是系统的UnhandledExceptionFilter函数内部走了其他的流程。我查看下UnhandledExceptionFilter函数的逆向结果,此时我不会将其列出来,因为我们要知道其内部是在哪儿调用了我们通过SetUnhandledExceptionFilter设置的回调函数。我们先看下SetUnhandledExceptionFilter的实现,用IDA查看的逆向结果比较杂乱,我就以ReactOS的代码作为例子来讲解,其核心思想是一致的
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(IN LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) { PVOID EncodedPointer, EncodedOldPointer; EncodedPointer = RtlEncodePointer(lpTopLevelExceptionFilter); EncodedOldPointer = InterlockedExchangePointer((PVOID*)&GlobalTopLevelExceptionFilter, EncodedPointer); return RtlDecodePointer(EncodedOldPointer); }从上述代码中,我们可以见到,系统通过原子操作保存了我们设置的回调函数。然后在UnhandledExceptionFilter函数内部,是这样调用我们设置的回调函数的(依然以ReactOs为例)
…… RealFilter = RtlDecodePointer(GlobalTopLevelExceptionFilter); if (RealFilter) { LONG ret = RealFilter(ExceptionInfo); if (ret != EXCEPTION_CONTINUE_SEARCH) return ret; } ……找到这个锚点,我们便可以动态调试,找出回调函数没有被调用的原因。
75BF76D3 mov dword ptr [ebp-20h],6 75BF76DA xor esi,esi 75BF76DC mov dword ptr [ebp-1Ch],esi 75BF76DF mov dword ptr [ebp-24h],esi 75BF76E2 mov dword ptr [ebp-28h],esi 75BF76E5 mov ebx,dword ptr [ebp+8] 75BF76E8 mov eax,dword ptr [ebx] 75BF76EA test byte ptr [eax+4],10h 75BF76EE jne _UnhandledExceptionFilter@4+29h (75BF7934h) 75BF76F4 mov dword ptr [ebp-2Ch],1 75BF76FB cmp dword ptr [eax],0C0000409h 75BF7701 je _UnhandledExceptionFilter@4+3Fh (75BF8146h) 75BF7707 push ebx 75BF7708 call _CheckForReadOnlyResourceFilter@4 (75BF78B9h) 75BF770D cmp eax,0FFFFFFFFh 75BF7710 je _UnhandledExceptionFilter@4+91h (75BF793Bh) 75BF7716 call _BasepIsDebugPortPresent@0 (75BF7831h) 75BF771B test eax,eax 75BF771D jne _UnhandledExceptionFilter@4+29h (75BF7934h) 75BF7723 mov esi,75CA030Ch 75BF7728 push esi 75BF7729 call dword ptr [__imp__RtlAcquireSRWLockExclusive@4 (75BD034Ch)] 75BF772F push dword ptr ds:[75CA0074h] 75BF7735 call dword ptr [__imp__RtlDecodePointer@4 (75BD0670h)] 75BF773B mov edi,eax 75BF773D test edi,edi调试时,需要注意:当运行到75BF771D时,我们要将执行路径指向75BF7723。因为我们是debug状态,要跳过这个检测。然后我们继续执行,会发现75BF7735处执行的结果是0,即我们获取的回调函数执行为空。这样便分析出,为什么SetUnhandledExceptionFilter方法设置的回调没有被执行。但是一个新的问题又被抛了出来——何时这个回调被设置成空了?可以这样设计下:Hook函数NtQueryInformationProcess,使其返回调试端口号一直未0,。然后针对GlobalTopLevelExceptionFilter下硬件断点。或许,这样便可以找到元凶。
最后附上工程。
百度云下载地址:http://pan.baidu.com/s/1qWG14BE 。密码:w5o5