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

2020/06/19 08:49
阅读数 212

    做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;
        我们查看调用堆栈

        可以见得,在调用我们回调函数之前,调用了系统的UnhandledExceptionFilter函数,这个函数的入参也是_EXCEPTION_POINTERS指针。
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
————————————————
版权声明:本文为CSDN博主「breaksoftware」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/breaksoftware/article/details/23134445

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部