Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일
 

windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례

이번 글은 이전에 쓴 ODAC 문제의,

windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12062

후기입니다. ^^

일단, 해당 고객사에서 ODAC을 Managed Driver로 바꿨는데 그래도 (비록 횟수는 줄었지만) 비정상 종료가 발생한다고 합니다. 오호~~~, 그렇다면 ODAC 내에서의 버그가 아니고 다른 구성 요소가 Heap을 깨 먹었다는 이야기가 되는데요. 어쨌든 고객사 측에서 비정상 종료 시의 덤프를 3개 떠서 보내왔고 다행히 그 안에 원래의 버그를 발생시킨 콜 스택이 있었습니다.

어디... 분석을 들어가 볼까요? ^^




첫 번째 덤프는 다음의 call stack에서 crash가 발생했지만,

// .NET Call Stack
[[HelperMethodFrame_PROTECTOBJ] (System.String.ReplaceInternal)] System.String.ReplaceInternal(System.String, System.String) 
System.String.Replace(System.String, System.String)+f 
TestApp.MyUtil.ConvertDate(System.String)+227 
TestApp.MyEditor.ConvertText(Int32, System.String, System.String, System.Collections.Hashtable ByRef)+5b7 
TestApp.MyEditor.Page_Load(System.Object, System.EventArgs)+160e 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)+8a5 
...[생략]...
System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)+13 
DomainNeutralILStubClass.IL_STUB_ReversePInvoke(Int64, Int64, Int64, Int32)+52 
[[ContextTransitionFrame]] 

// Full Call Stack
ntdll!RtlReportCriticalFailure+97 
ntdll!RtlpHeapHandleError+12 
ntdll!RtlpLogHeapFailure+96 
ntdll!RtlFreeHeap+823 
clr!EEHeapFreeInProcessHeap+45 
clr!CQuickMemoryBase<512,128>::Destroy+41 
clr!COMString::ReplaceString+3e4 
System.String.Replace(System.String, System.String)+f 
TestApp.MyUtil.ConvertDate(System.String)+227 
TestApp.MyEditor.ConvertText(Int32, System.String, System.String, System.Collections.Hashtable ByRef)+5b7 
TestApp.MyEditor.Page_Load(System.Object, System.EventArgs)+160e 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
...[생략]...
clr!Thread::intermediateThreadProc+8b 
kernel32!BaseThreadInitThunk+14 
ntdll!RtlUserThreadStart+21 

이것도 또 다른 희생자일 뿐 문제의 원인과는 상관없습니다. (만약 고객사가 이번의 덤프 하나만 보내줬다면 원인을 찾을 수 없었을 것입니다.) 그래도 RtlReportCriticalFailure로부터 힙이 깨졌음을 확인할 수는 있습니다.

0:044> .exr -1
ExceptionAddress: 00007ff8678882d3 (ntdll!RtlReportCriticalFailure+0x0000000000000097)
   ExceptionCode: c0000374
  ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 00007ff8678df6b0

0:044> dq /c1 00007ff8678df6b0 L4
00007ff8`678df6b0  000006e0`00000002
00007ff8`678df6b8  00000000`00000004
00007ff8`678df6c0  00000190`4efb0000
00007ff8`678df6c8  00000194`9ef50580

0:044> dq /c1 00000194`9ef50580 L4
00000194`9ef50580  00000000`00000000
00000194`9ef50588  90000000`00000000
00000194`9ef50590  00000000`00000000
00000194`9ef50598  00000000`00000000




두 번째 덤프에서 다행히 힙을 깬 범인을 접하게 되었습니다.

// .NET Call Stack
DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)+6e 
[[InlinedCallFrame] (Microsoft.Win32.Win32Native.LocalFree)] Microsoft.Win32.Win32Native.LocalFree(IntPtr) 
System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)+22 
TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)+2cf 
TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)+342 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)+8a5 
...[생략]...
System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)+425 
System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)+13 
DomainNeutralILStubClass.IL_STUB_ReversePInvoke(Int64, Int64, Int64, Int32)+52 
[[ContextTransitionFrame]] 

// Full Call Stack
ntdll!RtlReportCriticalFailure+97 
ntdll!RtlpHeapHandleError+12 
ntdll!RtlpLogHeapFailure+96 
ntdll!RtlFreeHeap+823 
KERNELBASE!LocalFree+2e 
DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)+6e 
System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)+22 
TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)+2cf 
TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)+342 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
...[생략]...
clr!Thread::intermediateThreadProc+8b 
kernel32!BaseThreadInitThunk+14 
ntdll!RtlUserThreadStart+21 

물론 이것 역시 또 다른 희생자일 수 있으므로 저것만 봐서는 확신할 수 없습니다. 단지 call stack에서 사용자가 작성한 TestApp.Thumbnail.GetThumbnailImage 메서드에서 명시적으로 Marshal.FreeHGlobal을 호출했다는 사실에서, 대부분의 버그는 사용자 코드에서 발생하기 때문에 잠정적인 후보로 채택할 수 있습니다.




그리고 마침내, 세 번째 덤프를 보고 확신이 좀 들었습니다.

// .NET Call Stack
DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)+6e 
[[InlinedCallFrame] (Microsoft.Win32.Win32Native.LocalFree)] Microsoft.Win32.Win32Native.LocalFree(IntPtr) 
System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)+22 
TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)+2cf 
TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)+342 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
...[생략]...
System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)+13 
DomainNeutralILStubClass.IL_STUB_ReversePInvoke(Int64, Int64, Int64, Int32)+52 
[[ContextTransitionFrame]] 

// Full Call Stack
ntdll!RtlReportCriticalFailure+97 
ntdll!RtlpHeapHandleError+12 
ntdll!RtlpLogHeapFailure+96 
ntdll!RtlFreeHeap+823 
KERNELBASE!LocalFree+2e 
DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)+6e 
System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)+22 
TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)+2cf 
TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)+342 
System.Web.UI.Control.OnLoad(System.EventArgs)+68 
System.Web.UI.Control.LoadRecursive()+70 
System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)+8a5 
...[생략]...
clr!Thread::intermediateThreadProc+8b 
kernel32!BaseThreadInitThunk+14 
ntdll!RtlUserThreadStart+21 

자.. 그럼 도대체 TestApp.Thumbnail.GetThumbnailImage 메서드에서는 어떤 일이 벌어지고 있었던 걸까요? 우선, 해당 메서드를 역어셈블하면 다음과 같은 식의 코드를 볼 수 있습니다.

public Image GetThumbnailImage(string filePath, int sizepic)
{
    // ...[생략]...
    intPtr2 = Marshal.AllocHGlobal(1024);
    try
    {
        int num3 = 0;
        Thumbnail.IExtractImage extractImage2 = extractImage;
        IntPtr pszPathBuffer = intPtr2;
        num = 0;
        extractImage2.GetLocation(pszPathBuffer, 1024, ref num, ref size, 32, ref num3);
        extractImage.Extract(ref hbitmap);
    }
    catch (Exception ex2)
    {
    }
    if (!hbitmap.Equals(IntPtr.Zero))
    {
        result = Image.FromHbitmap(hbitmap);
    }
    if (intPtr2 != IntPtr.Zero)
    {
        Marshal.FreeHGlobal(intPtr2); // crash가 발생한 코드
    }
    // ...[생략]...
}

그렇다면 문제가 발생한 시점의 GetThumbnailImage에 전달된 filePath의 값을 추적해 보겠습니다. 이를 위해 우선 !clrstack 명령어를 통해 해당 메서드가 수행된 frame의 "Child SP"와 "IP" 값을 알아내고,

0:040> !clrstack
OS Thread Id: 0x20cc (40)
        Child SP               IP Call Site
0000007e357be4d8 00007ff8678882d3 [InlinedCallFrame: 0000007e357be4d8] Microsoft.Win32.Win32Native.LocalFree(IntPtr)
0000007e357be4d8 00007ff7f7a6958e [InlinedCallFrame: 0000007e357be4d8] Microsoft.Win32.Win32Native.LocalFree(IntPtr)
0000007e357be4b0 00007ff7f7a6958e DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr)
0000007e357be560 00007ff7f854d502 System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr)
0000007e357be5a0 00007ff7f8be80ff TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)
0000007e357be690 00007ff7f8bdaf82 TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)
0000007e357be750 00007ff7f84dc768 System.Web.UI.Control.OnLoad(System.EventArgs)
0000007e357be790 00007ff7f84dc5b0 System.Web.UI.Control.LoadRecursive()
...[생략]...
0000007e357bf840 00007ff7f838abd2 DomainNeutralILStubClass.IL_STUB_ReversePInvoke(Int64, Int64, Int64, Int32)
0000007e357bfa18 00007ff856ea2473 [ContextTransitionFrame: 0000007e357bfa18] 

IP 값을 대상으로 코드를 덤프합니다.

0:040> !u 00007ff7f8be80ff
Normal JIT generated code
TestApp.Thumbnail.GetThumbnailImage(System.String, Int32)
Begin 00007ff7f8be7e30, size 36e
00007ff7`f8be7e30 55              push    rbp
00007ff7`f8be7e31 4156            push    r14
00007ff7`f8be7e33 57              push    rdi
00007ff7`f8be7e34 56              push    rsi
00007ff7`f8be7e35 53              push    rbx
00007ff7`f8be7e36 4881ecc0000000  sub     rsp,0C0h
00007ff7`f8be7e3d 488dac24e0000000 lea     rbp,[rsp+0E0h]
00007ff7`f8be7e45 488bf1          mov     rsi,rcx
00007ff7`f8be7e48 488dbd68ffffff  lea     rdi,[rbp-98h]
00007ff7`f8be7e4f b91e000000      mov     ecx,1Eh
00007ff7`f8be7e54 33c0            xor     eax,eax
00007ff7`f8be7e56 f3ab            rep stos dword ptr [rdi]
00007ff7`f8be7e58 488bce          mov     rcx,rsi
00007ff7`f8be7e5b 4889a558ffffff  mov     qword ptr [rbp-0A8h],rsp
00007ff7`f8be7e62 488bf9          mov     rdi,rcx
00007ff7`f8be7e65 488bf2          mov     rsi,rdx // filePath
00007ff7`f8be7e68 418bd8          mov     ebx,r8d
00007ff7`f8be7e6b 488bce          mov     rcx,rsi // filePath
00007ff7`f8be7e6e e89dff2bff      call    00007ff7`f7ea7e10 (System.String.IsNullOrWhiteSpace(System.String), mdToken: 00000000060004cb)
00007ff7`f8be7e73 84c0            test    al,al
00007ff7`f8be7e75 750c            jne     00007ff7`f8be7e83
00007ff7`f8be7e77 488bce          mov     rcx,rsi // filePath
00007ff7`f8be7e7a e8f1bbe5fe      call    00007ff7`f7a43a70 (System.IO.File.Exists(System.String), mdToken: 00000000060017a7)
...[생략]...
00007ff7`f8be7ec6 e845362dff      call    00007ff7`f7ebb510 (System.Guid..ctor(System.String), mdToken: 0000000006000ed1)
00007ff7`f8be7ecb 488bce          mov     rcx,rsi  // filePath
00007ff7`f8be7ece e85dfb43ff      call    00007ff7`f8027a30 (System.IO.Path.GetDirectoryName(System.String), mdToken: 0000000006001900)
00007ff7`f8be7ed3 4c8bf0          mov     r14,rax
00007ff7`f8be7ed6 488bce          mov     rcx,rsi  // filePath
00007ff7`f8be7ed9 e8a21a0dff      call    00007ff7`f7cb9980 (System.IO.Path.GetFileName(System.String), mdToken: 0000000006001916)
00007ff7`f8be7ede 488bf0          mov     rsi,rax  // rsi == GetFileName(filePath)
...[생략]...
00007ff7`f8be8011 488b5590        mov     rdx,qword ptr [rbp-70h]
00007ff7`f8be8015 48b9384ab5f8f77f0000 mov rcx,7FF7F8B54A38h (MT: TestApp.Thumbnail+IExtractImage)
00007ff7`f8be801f e8acbe2b5e      call    clr!JIT_ChkCastInterface (00007ff8`56ea3ed0)
00007ff7`f8be8024 488bf0          mov     rsi,rax // extractImage = (Thumbnail.IExtractImage)objectValue;
                                                  // rsi == extractImage
...[생략]...
00007ff7`f8be80ed e8eeb3e5fe      call    00007ff7`f7a434e0 (System.IntPtr.op_Inequality(IntPtr, IntPtr), mdToken: 0000000006000f99)
00007ff7`f8be80f2 84c0            test    al,al
00007ff7`f8be80f4 7409            je      00007ff7`f8be80ff
00007ff7`f8be80f6 488b4db8        mov     rcx,qword ptr [rbp-48h]
00007ff7`f8be80fa e8e15396ff      call    00007ff7`f854d4e0 (System.Runtime.InteropServices.Marshal.FreeHGlobal(IntPtr), mdToken: 000000000600600b) // crash 발생한 코드
>>> 00007ff7`f8be80ff 488b4db8        mov     rcx,qword ptr [rbp-48h]
00007ff7`f8be8103 33d2            xor     edx,edx
...[생략]...
00007ff7`f8be8199 5f              pop     rdi
00007ff7`f8be819a 415e            pop     r14
00007ff7`f8be819c 5d              pop     rbp
00007ff7`f8be819d c3              ret

하지만, 보는 바와 같이 GetThumbnailImage에 전달된 filePath인자는 rdx에 전달되어 rsi에만 보관되고 스택 상에는 백업되지 않고 있습니다. 게다가 문제가 발생하는 FreeHGlobal 시점에는 rsi가 Thumbnail.IExtractImage 인스턴스의 값을 갖게 되므로 일단 이 frame에서는 filePath 인자 값을 알 방법이 없습니다.

그렇다면, 이제 GetThumbnailImage를 호출한 측에서 구할 수 있는지 희망을 가져보겠습니다. ^^

0000007e357be690 00007ff7f8bdaf82 TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)

Reflection을 통해 .NET 코드를 먼저 보면,

protected override void Page_Load(object sender, EventArgs e)
{
    base.Page_Load(RuntimeHelpers.GetObjectValue(sender), e);
    // ...[생략]...
    Hashtable hashtable = GetEnvInfo();
    // ...[생략]...
    string text = ConfigurationManager.AppSettings["FileDir"] + Conversions.ToString(hashtable["PATH"]);
    // ...[생략]...
    Thumbnail thumbnail = new Thumbnail();
    Image image = null;
    try
    {
        image = thumbnail.GetThumbnailImage(text, 260);
    }
    catch (Exception ex)
    {
        image = null;
    }
    // ...[생략]...
}

RuntimeHelpers.GetObjectValue 코드가 있는 것을 보니 애당초 소스 코드가 VB.NET으로 만들어졌음을 짐작게 합니다. 그리고 GetThumbnailImage에 전달한 첫 번째 인자의 값이 "ConfigurationManager.AppSettings["FileDir"] + Conversions.ToString(hashtable["PATH"])"라는 것을 알 수 있고, 이쯤 해서 IP 값의 코드를 덤프해 함께 보면,

0:040> !u 00007ff7f8bdaf82
Normal JIT generated code
TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)
Begin 00007ff7f8bdac40, size 523
00007ff7`f8bdac40 55              push    rbp
00007ff7`f8bdac41 57              push    rdi
00007ff7`f8bdac42 56              push    rsi
00007ff7`f8bdac43 53              push    rbx
00007ff7`f8bdac44 4881ec98000000  sub     rsp,98h
00007ff7`f8bdac4b c5f877          vzeroupper
00007ff7`f8bdac4e 488dac24b0000000 lea     rbp,[rsp+0B0h]
00007ff7`f8bdac56 488bf1          mov     rsi,rcx
00007ff7`f8bdac59 488d7dc8        lea     rdi,[rbp-38h]
00007ff7`f8bdac5d b908000000      mov     ecx,8
00007ff7`f8bdac62 33c0            xor     eax,eax
00007ff7`f8bdac64 f3ab            rep stos dword ptr [rdi]
00007ff7`f8bdac66 488bce          mov     rcx,rsi
00007ff7`f8bdac69 4889a570ffffff  mov     qword ptr [rbp-90h],rsp
00007ff7`f8bdac70 48894d10        mov     qword ptr [rbp+10h],rcx
00007ff7`f8bdac74 498bf0          mov     rsi,r8
00007ff7`f8bdac77 488bca          mov     rcx,rdx
00007ff7`f8bdac7a e8e1c8a75e      call    clr!ObjectNative::GetObjectValue (00007ff8`57657560)
00007ff7`f8bdac7f 488bd0          mov     rdx,rax
00007ff7`f8bdac82 488b4d10        mov     rcx,qword ptr [rbp+10h]
00007ff7`f8bdac86 4c8bc6          mov     r8,rsi
00007ff7`f8bdac89 e8726e69ff      call    00007ff7`f8271b00 (TestApp.BasePage.Page_Load(System.Object, System.EventArgs), mdToken: 00000000060001af)
    // ...[생략]...
00007ff7`f8bdad7f ff5028          call    qword ptr [rax+28h] (TestApp.GetEnvInfo(), mdToken: 0000000006000231)
00007ff7`f8bdad82 48894588        mov     qword ptr [rbp-78h],rax
00007ff7`f8bdad86 488b4d88        mov     rcx,qword ptr [rbp-78h]
    // ...[생략]...
00007ff7`f8bdae42 e8e9e690ff      call    00007ff7`f84e9530 (System.Configuration.ConfigurationManager.get_AppSettings(), mdToken: 0000000006000234)
00007ff7`f8bdae47 488bc8          mov     rcx,rax  // rcx == ConfigurationManager.AppSettings
00007ff7`f8bdae4a 48ba08aa436c5c020000 mov rdx,25C6C43AA08h 
00007ff7`f8bdae54 488b12          mov     rdx,qword ptr [rdx] // rdx == "FileDir"
00007ff7`f8bdae57 3909            cmp     dword ptr [rcx],ecx
00007ff7`f8bdae59 e8528511ff      call    00007ff7`f7cf33b0 (System.Collections.Specialized.NameValueCollection.get_Item(System.String), mdToken: 0000000006002354)
00007ff7`f8bdae5e 488bf0          mov     rsi,rax
00007ff7`f8bdae61 48bad89a436c5c020000 mov rdx,25C6C439AD8h
00007ff7`f8bdae6b 488b12          mov     rdx,qword ptr [rdx] // rdx == "PATH"
00007ff7`f8bdae6e 488b4d88        mov     rcx,qword ptr [rbp-78h] // rcx == hashtable
00007ff7`f8bdae72 488b4588        mov     rax,qword ptr [rbp-78h] // 
00007ff7`f8bdae76 488b00          mov     rax,qword ptr [rax]
00007ff7`f8bdae79 488b4048        mov     rax,qword ptr [rax+48h]
00007ff7`f8bdae7d ff5020          call    qword ptr [rax+20h]
00007ff7`f8bdae80 488bc8          mov     rcx,rax
00007ff7`f8bdae83 e8e80b90ff      call    00007ff7`f84dba70 (Microsoft.VisualBasic.CompilerServices.Conversions.ToString(System.Object), mdToken: 00000000060003bf)
00007ff7`f8bdae88 488bd0          mov     rdx,rax
00007ff7`f8bdae8b 488bce          mov     rcx,rsi
00007ff7`f8bdae8e e83dd0e4fe      call    00007ff7`f7a27ed0 (System.String.Concat(System.String, System.String), mdToken: 0000000006000555)
00007ff7`f8bdae93 488bf0          mov     rsi,rax // string text = ConfigurationManager.AppSettings["FileDir"] + Conversions.ToString(hashtable["PATH"]);
...[생략]...
00007ff7`f8bdaf66 488bf8          mov     rdi,rax
00007ff7`f8bdaf69 488bcf          mov     rcx,rdi
00007ff7`f8bdaf6c e8e77bdeff      call    00007ff7`f89c2b58 (TestApp.Thumbnail..ctor(), mdToken: 000000000600039e)
00007ff7`f8bdaf71 488bcf          mov     rcx,rdi
00007ff7`f8bdaf74 488bd6          mov     rdx,rsi // rsi == filePath
00007ff7`f8bdaf77 41b804010000    mov     r8d,104h
00007ff7`f8bdaf7d e8de7bdeff      call    00007ff7`f89c2b60 (TestApp.Thumbnail.GetThumbnailImage(System.String, Int32), mdToken: 000000000600039f)
>>> 00007ff7`f8bdaf82 48894580        mov     qword ptr [rbp-80h],rax
00007ff7`f8bdaf86 488b4d80        mov     rcx,qword ptr [rbp-80h]
00007ff7`f8bdaf8a e801a992ff      call    00007ff7`f8505890 (Microsoft.VisualBasic.Information.IsNothing(System.Object), mdToken: 0000000006000130)
...[생략]...
00007ff7`f8bdb15f 5e              pop     rsi
00007ff7`f8bdb160 5f              pop     rdi
00007ff7`f8bdb161 5d              pop     rbp
00007ff7`f8bdb162 c3              ret

코드 분석을 GetThumbnailImage가 호출된 곳부터 위로 올라가면서 분석을 진행하면 됩니다. 우선, 아래의 코드를 통해 rsi로부터 filePath가 rdx에 대입되어 GetThumbnailImage에 넘어온 것을 확인할 수 있습니다.

00007ff7`f8bdaf74 488bd6          mov     rdx,rsi // rsi == filePath
00007ff7`f8bdaf77 41b804010000    mov     r8d,104h
00007ff7`f8bdaf7d e8de7bdeff      call    00007ff7`f89c2b60 (TestApp.Thumbnail.GetThumbnailImage(System.String, Int32), mdToken: 000000000600039f)

이어서 rsi에 마지막으로 값이 대입된 곳을 보면,

00007ff7`f8bdae61 48bad89a436c5c020000 mov rdx,25C6C439AD8h
00007ff7`f8bdae6b 488b12          mov     rdx,qword ptr [rdx] // rdx == "PATH"
00007ff7`f8bdae6e 488b4d88        mov     rcx,qword ptr [rbp-78h] // rcx == hashtable
00007ff7`f8bdae72 488b4588        mov     rax,qword ptr [rbp-78h]
00007ff7`f8bdae76 488b00          mov     rax,qword ptr [rax]
00007ff7`f8bdae79 488b4048        mov     rax,qword ptr [rax+48h]
00007ff7`f8bdae7d ff5020          call    qword ptr [rax+20h] // System.Collections.Hashtable 타입의 indexer 메서드 호출
00007ff7`f8bdae80 488bc8          mov     rcx,rax
00007ff7`f8bdae83 e8e80b90ff      call    00007ff7`f84dba70 (Microsoft.VisualBasic.CompilerServices.Conversions.ToString(System.Object), mdToken: 00000000060003bf)
00007ff7`f8bdae88 488bd0          mov     rdx,rax
00007ff7`f8bdae8b 488bce          mov     rcx,rsi
00007ff7`f8bdae8e e83dd0e4fe      call    00007ff7`f7a27ed0 (System.String.Concat(System.String, System.String), mdToken: 0000000006000555)
00007ff7`f8bdae93 488bf0          mov     rsi,rax // string text = ConfigurationManager.AppSettings["FileDir"] + Conversions.ToString(hashtable["PATH"]);

결국 마지막으로 "mov rsi, rax"에 의해 "ConfigurationManager.AppSettings["FileDir"] + Conversions.ToString(hashtable["PATH"])" 값이 rsi에 들어갔으며, 좀 더 위를 보면 운이 좋게도 hashtable 인스턴스의 값이 "[rbp - 78h]" 스택 위치에 들어있습니다.

이 시점의 rbp 값을 얻기 위해서는 함수의 prologue 코드를 봐야 하는데,

00007ff7`f8bdac40 55              push    rbp
00007ff7`f8bdac41 57              push    rdi
00007ff7`f8bdac42 56              push    rsi
00007ff7`f8bdac43 53              push    rbx
00007ff7`f8bdac44 4881ec98000000  sub     rsp,98h
00007ff7`f8bdac4b c5f877          vzeroupper
00007ff7`f8bdac4e 488dac24b0000000 lea     rbp,[rsp+0B0h]

저 시점의 "rsp" 값은 "!callstack"에서 확인한 "Child SP" 값이므로,

0000007e357be690 00007ff7f8bdaf82 TestApp.FileInfoApi.Page_Load(System.Object, System.EventArgs)

따라서 다음과 같이 hashtable 인스턴스를 알아낼 수 있습니다.

0:040> dq 0000007e357be690+0xb0-0x78 L1
0000007e`357be6c8  0000025a`7a9c60e8

0:040> !do 0000025a`7a9c60e8
Name:        System.Collections.Hashtable
MethodTable: 00007ff7f7b4a710
EEClass:     00007ff7f7b70c28
Size:        80(0x50) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff7f7c13b38  400182e        8 ...ashtable+bucket[]  0 instance 0000025a7a9c77b8 buckets
00007ff7f79efe60  400182f       30         System.Int32  1 instance               44 count
00007ff7f79efe60  4001830       34         System.Int32  1 instance               22 occupancy
00007ff7f79efe60  4001831       38         System.Int32  1 instance               64 loadsize
00007ff7f7aa1d60  4001832       3c        System.Single  1 instance 0.720000 loadFactor
00007ff7f79efe60  4001833       40         System.Int32  1 instance               48 version
00007ff7f79ecd90  4001834       44       System.Boolean  1 instance                0 isWriterInProgress
00007ff7f79eab78  4001835       10 ...tions.ICollection  0 instance 0000000000000000 keys
00007ff7f79eab78  4001836       18 ...tions.ICollection  0 instance 0000000000000000 values
00007ff7f7ac1a30  4001837       20 ...IEqualityComparer  0 instance 0000000000000000 _keycomparer
00007ff7f79e6800  4001838       28        System.Object  0 instance 0000000000000000 _syncRoot

System.Collections.Hashtable+bucket[] 타입의 buckets 속성을 다음의 글을 통해 덤프해 보면,

windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
; https://www.sysnet.pe.kr/2/0/12084

hashtable["PATH"]에는 "myfile.xlsm" 값이 들어 있음을 확인할 수 있습니다. 그럼, ConfigurationManager.AppSettings["FileDir"]에는 어떤 값이 들어 있을까요? 아쉽게도 이 값은 스택에 보관된 적이 없어 조회가 안 됩니다. 단지, 운이 좋다면 이미 해제/할당을 반복한 스택 공간에서 저 값을 발견할 수도 있으므로 "!dso" 명령을 내려 보면,

0:040> !dso
OS Thread Id: 0x20cc (40)
RSP/REG          Object           Name
rbx              0000025a7a9aaa38 TestApp.FileExt
rsi              0000025a7aa07808 System.String    c:\temp\myfile.xlsm
rdi              0000025a7aa0a3e0 TestApp.Thumbnail
0000007E357BE438 0000025a7aa0a6c0 System.Object
0000007E357BE528 0000025a7aa0a3e0 TestApp.Thumbnail
...[생략]...
0000007E357BE5A0 0000025a7aa0a6d8 System.__ComObject
0000007E357BE5A8 0000025a7aa0a618 System.String    myfile.xlsm
...[생략]...
0000007E357BE668 0000025a7aa07808 System.String    c:\temp\myfile.xlsm
0000007E357BE670 0000025a7aa0a3e0 TestApp.Thumbnail
0000007E357BE690 0000025a7a9aaa38 TestApp.FileExt
...[생략]...

"myfile.xlsm"이 hashtable["PATH"]였으므로 "c:\temp\myfile.xlsm" 문자열을 통해 ConfigurationManager.AppSettings["FileDir"] == "c:\temp\" 였음을 유추할 수 있습니다. (물론, 100% 확신해서는 안 됩니다.)

windbg에서의 분석은 일단 여기서 끝이 납니다.




자, 그럼 도대체 myfile.xlsm으로 GetThumbnailImage 함수가 호출될 때 어떤 일이 벌어지는 걸까요? 재현을 위해 웹상에서 예제 파일을 하나 구하고,

https://www.exceltip.com/wp-content/uploads/2015/06/Download-Sample-File-xlsm.xlsm

제 컴퓨터에는 Office가 설치되어 있으므로 이 값을 입력으로 해보면 현상이 나올 듯한데요. 실제로 GetLocation 메서드 수행 시,

public Image GetThumbnailImage(string filePath, int sizepic)
{
    // ...[생략]...
    intPtr2 = Marshal.AllocHGlobal(1024);

    // ...[생략]...
    extractImage2.GetLocation(pszPathBuffer, 1024, ref num, ref size, 32, ref num3);
/*
GetLocation 호출 시 예외 발생
    Critical error detected c0000374
    ConsoleApp1.exe has triggered a breakpoint.
*/
    extractImage.Extract(ref hbitmap);
    // ...[생략]...
}

예외가 발생하는 것을 볼 수 있습니다. 그리고 IExtractImage 인터페이스의 GetLocation 함수 원형을 보면 예외의 원인이 나옵니다.

IExtractImage::GetLocation method
; https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-iextractimage-getlocation

HRESULT GetLocation(LPWSTR pszPathBuffer, DWORD cch, DWORD *pdwPriority, const SIZE *prgSize, DWORD dwRecClrDepth, DWORD *pdwFlags);

첫 번째 인자의 타입이 "LPWSTR"이므로 GetLocation 함수는 입력된 버퍼에 대해 "글자 하나" 당 2바이트로 취급하게 되는데 "intPtr2 = Marshal.AllocHGlobal(1024)" 코드에서 1024 바이트를 할당해 두었지만 "GetLocation(pszPathBuffer, 1024, ...)"와 같이 pszPathBuffer의 크기가 1024개라고 지정했으므로 내부적으로는 2048 바이트로 취급하게 되는 것입니다.

이로 인해 버퍼 오버플로우가 발생했고 운이 나쁘게도 하필 ODAC에서,

windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12062

누명을 썼던 것입니다. ^^; 문제를 수정하는 방법은 간단합니다.

[버퍼를 그에 맞게 늘려주거나]
intPtr2 = Marshal.AllocHGlobal(1024 * sizeof(char)); // C++의 경우 1024 * sizeof(wchar_t)

[절반만 전달하거나]
extractImage2.GetLocation(pszPathBuffer, 1024 / sizeof(char), ref num, ref size, 32, ref num3);

휴~~~ 이것으로 기나긴 분석이 끝났습니다. ^^




그런데, 도대체 저 이상한 VB.NET 코드는 어떻게 탄생한 것일까요? 제 생각에는 고객사 측의 개발자가 직접 만들었을 것 같지는 않고 아마도 인터넷을 참고했을 거라는 생각에 유사하게 검색을 했더니 다음의 글이 나옵니다.

Load Images in form Like the opendialog does vb.net
; https://www.experts-exchange.com/questions/27755159/Load-Images-in-form-Like-the-opendialog-does-vb-net.html

Dim flags = IEIFLAG.ORIGSIZE ' Or IEIFLAG.QUALITY
Dim bmp As IntPtr
Dim thePath = Marshal.AllocHGlobal(MAX_PATH)

'Interop will throw an exception if one of these calls fail.
Try
    extract.GetLocation(thePath, MAX_PATH, 0, size, colorDepth, flags)
    extract.Extract(bmp)
Catch ex As Exception
End Try

하필 저 코드를 참조한 개발자도 어지간히 운이 없었던 것입니다. 왜냐하면 유사한 제목의 다른 코드들은 대부분 이렇게 작성되어 있는데,

Int32 GetLocation([MarshalAs(UnmanagedType.LPWStr)] out StringBuilder pszPathBuffer, int cch, ...);

StringBuilder location = new StringBuilder(260, 260);
extractImage.GetLocation(out location, location.Capacity, ...);

LPWStr로 마샬링되는 StringBuilder는 문자 당 2바이트를 점유하게 되므로 힙을 망가뜨리는 일이 없습니다.




자, 그럼 마지막 의문입니다. 왜 해당 웹 응용 프로그램은 GetThumbnailImage 메서드를 호출할 때마다 "무조건" 비정상 종료하지 않고 버텼을까요?

그건 IExtractImage::GetLocation을 구현하는 것이 또 다른 응용 프로그램 개발자가 임의 재량으로 구현할 수 있는 여지가 있기 때문입니다. 예를 들어, 자신만의 확장자를 정의해 다루는 응용 프로그램을 개발하는 경우, 개발자는 그 확장자에 대한 Thumbnail을 제공하도록 "IExtractImage" COM 객체를 구현한 전용 모듈을 제공할 수 있습니다. 그렇다면 당연히 IExtractImage::GetLocation 함수의 내부 구현은 개발자마다 다를 수 있다는 결론을 얻을 수 있습니다.

아마도 다음과 같이 다양하게 구현되었을 것입니다.

// 1번 유형: 미처 구현을 안 했을 수도 있고,
HRESULT IExtractImage::GetLocation(LPWSTR pszPathBuffer, DWORD cch, DWORD *pdwPriority, const SIZE *prgSize, DWORD dwRecClrDepth, DWORD *pdwFlags)
{
    return E_NOTIMPL;
}

// 2번 유형: 일단 전체 버퍼를 초기화하고 시작했을 수도 있고,
HRESULT IExtractImage::GetLocation(LPWSTR pszPathBuffer, DWORD cch, DWORD *pdwPriority, const SIZE *prgSize, DWORD dwRecClrDepth, DWORD *pdwFlags)
{
    memset(pszPathBuffer, 0, cch * sizeof(wchar_t));
    // ...[생략]...
}

// 3번 유형: 단일 null 초기화만 했을 수도 있고,
HRESULT IExtractImage::GetLocation(LPWSTR pszPathBuffer, DWORD cch, DWORD *pdwPriority, const SIZE *prgSize, DWORD dwRecClrDepth, DWORD *pdwFlags)
{
    *pszPathBuffer = L'\0';
    // ...[생략]...
}

// 4번 유형: 프로그램에서 필요한 만큼의 버퍼만 사용했을 수도 있고,
HRESULT IExtractImage::GetLocation(LPWSTR pszPathBuffer, DWORD cch, DWORD *pdwPriority, const SIZE *prgSize, DWORD dwRecClrDepth, DWORD *pdwFlags)
{
    wchar_t *pszFilePath = GetFilePath();
    wcscpy(pszPathBuffer, pszFilePath); 
    // ...[생략]...
}

그리고 고객사의 덤프가 발생했던 시점의 환경에서 사용한 "xlsm 확장자"의 경우에는 IExtractImage::GetLocation의 구현체가 (테스트해 보면) 2번 유형에 해당한다는 것을 알 수 있습니다.




한 가지 아쉬운 점이 있다면, 이럴 거면 gflags.exe의 heap 관련 옵션을 실습해 볼 걸 그랬습니다. ^^

GFlags and PageHeap
; https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-and-pageheap

아마도 저걸로 했으면 ODAC 사용 당시에도 IExtractImage::GetLocation에서 높은 확률로 예외가 발생했을 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 12/20/2019 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12131정성태1/27/2020602개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법
12130정성태1/26/2020360VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/20201135.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [1]
12128정성태1/26/2020413오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/28/2020440.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/2020399.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/2020315VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/24/2020747VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/2020486웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법
12122정성태1/22/2020530.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/24/2020306VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/20/2020463.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/2020566디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/2020520개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 [1]
12117정성태1/15/2020529디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
12116정성태1/15/2020428디버깅 기술: 158. Visual Studio로 디버깅 시 sos.dll 확장 명령어를 (비롯한 windbg의 다양한 기능을) 수행하는 방법
12115정성태1/14/2020431디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/2020615디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/2020842오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib' [1]
12112정성태1/12/2020368오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태3/23/2020870디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [1]
12110정성태6/23/2020574디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례파일 다운로드1
12109정성태1/10/2020474오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/2020376오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/2020488.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/2020541VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...