分析两种Dump(崩溃日志)文件生成的方法及比较

        做windows产品开发的,永远绕不开一个问题——程序崩溃。如果希望不断提升产品质量,就得不停的收集和分析崩溃日志。但是我们会发现一个问题,我们经常采用的方案无法拦截崩溃。(转载请指明出于breaksoftware的csdn博客)比如会出现如下提示:

分析两种Dump(崩溃日志)文件生成的方法及比较_第1张图片分析两种Dump(崩溃日志)文件生成的方法及比较_第2张图片

        这是一个非常不好的体验,至少说这个是对提升软件质量无益的体验。虽然以上框可以通过如下代码禁用掉,但是仍然只是个掩耳盗铃的做法。

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(崩溃日志)文件生成的方法及比较_第3张图片

        如果你也见过这个错误,我想你的截取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;
        我们查看调用堆栈
分析两种Dump(崩溃日志)文件生成的方法及比较_第4张图片
        可以见得,在调用我们回调函数之前,调用了系统的UnhandledExceptionFilter函数,这个函数的入参也是_EXCEPTION_POINTERS指针。

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

你可能感兴趣的:(安全,反汇编)