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에서 높은 확률로 예외가 발생했을 듯합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]