Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 3개 있습니다.)

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://learn.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://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-and-pageheap

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/9/2024]

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13294정성태3/22/20234122.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234191오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234305Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234712.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234219.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233416Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233515Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233683Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234146Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233737Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233937Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233478오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233807Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233716Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234453개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20233997오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233945개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234576개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234306.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234660.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234252.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233953.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234202오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234142오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
13270정성태2/24/20233754.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234290스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...