做windows产品开发的,永远绕不开一个问题——程序崩溃。如果希望不断提升产品质量,就得不停的收集和分析崩溃日志。但是我们会发现一个问题,我们经常采用的方案无法拦截崩溃。(转载请指明出于breaksoftware的csdn博客)比如会出现如下提示:
这是一个非常不好的体验,至少说这个是对提升软件质量无益的体验。虽然以上框可以通过如下代码禁用掉,但是仍然只是个掩耳盗铃的做法。
SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
我们先看一种标准的Dump生成方案:
#include "CreateDump.h"
#include
#include
#include
#include
#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
#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