Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

x86 메모리 덤프 분석 시 닷넷 메서드의 호출 인자 값 확인

x64 관련해서는 기록해 둔 것이 있는데,

x64 콜 스택 인자 추적과 windbg의 Child-SP, RetAddr, Args to Child 값 확인
; https://www.sysnet.pe.kr/2/0/10832

windbg - x64 역어셈블 코드에서 닷넷 메서드 호출의 인자를 확인하는 방법
; https://www.sysnet.pe.kr/2/0/11348

windbg - x64 SOS 확장의 !clrstack 명령어가 출력하는 Child SP 값의 의미
; https://www.sysnet.pe.kr/2/0/11349

x86이 없군요. ^^

예를 들면서 해볼까요? ^^ C# 프로그램을 작성하다 보면 EventWaitHandle 타입으로 WaitOne을 호출하며 대기하는 경우가 있습니다.

using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            ExitOrSleep(1000 * 50);
        }

        private static void ExitOrSleep(int timeout)
        {
            EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset);
            ewh.WaitOne(timeout, false);
        }
    }
}

바로 이 WaitOne을 호출했을 때 메모리 덤프를 뜬 것을 분석 시 timeout으로 주어진 시간을 찾는다고 가정해 봅니다.

다음은 WaitOne 호출에 따른 콜 스택을 보여줍니다.

0:000> kv
 # ChildEBP RetAddr  Args to Child              
00 001beb64 77061293 00000001 001bef10 00000000 ntdll!NtWaitForMultipleObjects+0xc (FPO: [5,0,0])
01 001becf8 72a0ff96 00000001 001bef10 00000001 KERNELBASE!WaitForMultipleObjectsEx+0x103 (FPO: [SEH])
02 001bed48 72a0fcd8 00000001 0000c350 00000001 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x3c (FPO: [Non-Fpo])
03 001bedd4 72a0fdc9 00000001 001bef10 00000001 clr!Thread::DoAppropriateWaitWorker+0x237 (FPO: [5,25,0])
04 001bee40 72a1011f 00000001 001bef10 00000001 clr!Thread::DoAppropriateWait+0x64 (FPO: [Non-Fpo])
05 001bef48 70df9911 00000000 00000000 045e247c clr!WaitHandleNative::CorWaitOneNative+0x163 (FPO: [Non-Fpo])
06 001bef5c 70df98d8 00000000 0000c350 00000000 mscorlib_ni+0x429911
07 001bef78 02990474 00000000 00000000 001bef94 mscorlib_ni+0x4298d8
WARNING: Frame IP not in any known module. Following frames may be wrong.
08 001bef88 7296eb16 0275e020 001befe8 72976e84 0x2990474
09 001bef94 72976e84 001bf024 001befd8 72b07020 clr!CallDescrWorkerInternal+0x34
0a 001befe8 729782f4 00000000 045e2440 00000000 clr!CallDescrWorkerWithHandler+0x6b (FPO: [Non-Fpo])
0b 001bf050 72a2d34c 001bf144 57a56079 02704d0c clr!MethodDescCallSite::CallTargetWorker+0x16a (FPO: [Non-Fpo])
0c 001bf17c 72a2d2b0 001bf1a0 00000000 57a56095 clr!RunMain+0x1ad (FPO: [Non-Fpo])
0d 001bf3f0 72a2da26 00000000 57a56505 00020000 clr!Assembly::ExecuteMainMethod+0x124 (FPO: [1,149,0])
0e 001bf8e8 72a2dac9 57a56845 00000000 00000000 clr!SystemDomain::ExecuteMainMethod+0x631 (FPO: [0,311,0])
0f 001bf940 72a2d426 57a56885 00000000 72a50320 clr!ExecuteEXE+0x4c (FPO: [Non-Fpo])
10 001bf980 72a5033c 57a568b9 00000000 72a50320 clr!_CorExeMainInternal+0xdc (FPO: [Non-Fpo])
11 001bf9bc 737dd91b a2f79cdd 739e4df0 737dd8a0 clr!_CorExeMain+0x4d (FPO: [Non-Fpo])
12 001bf9fc 739de879 739e4df0 737d0000 2bf7ae78 mscoreei!_CorExeMain+0x10e (FPO: [0,10,4])
13 001bfa10 739e4df8 739e4df0 77188654 003a9000 mscoree!ShellShim__CorExeMain+0xa9 (FPO: [Non-Fpo])
14 001bfa18 77188654 003a9000 77188630 dc4d9cdf mscoree!_CorExeMain_Exported+0x8 (FPO: [0,0,4])
15 001bfa2c 77594a77 003a9000 b6694afe 00000000 kernel32!BaseThreadInitThunk+0x24 (FPO: [Non-Fpo])
16 001bfa74 77594a47 ffffffff 775b9eee 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
17 001bfa84 00000000 739e4df0 003a9000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

x86의 경우에는 WIN32 API가 따르는 기본 호출 규약인 __stdcall이 ecx, edx 등을 통한 레지스터를 이용하지 않기 때문에 스택에 값이 잘 남게 됩니다. 이 때문에 kv 명령어로 "Args to Child" 항목을 출력하면 넘겨진 인자 값을 쉽게 확인하는 수 있는 경우가 많습니다. 가령 이번 예제에서 50,000 (0xc350) 값을 WaitOne에 전달했는데 위에서 보는 바와 같이 2개의 호출 스택에서 그 값을 직접 확인할 수 있습니다. (실제로 x86 덤프에서 WaitOne에 전달한 시간을 알고 싶다면 clr!WaitForMultipleObjectsEx_SO_TOLERANT 함수의 "Args to Child"에 나타난 2번째 인자를 확인하면 거의 잘 맞을 것입니다.)

이런 찍는 방법 말고 ^^ 정식으로 한번 파고들어 보겠습니다. 우선, Native 호출 스택은 낯설을 테니 닷넷 호출 스택으로 출발해 보겠습니다.

0:000> !clrstack
OS Thread Id: 0x92ec (0)
Child SP       IP Call Site
001bee74 7759ed3c [HelperMethodFrame_1OBJ: 001bee74] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
001bef58 70df9911 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
001bef70 70df98d8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
001bef84 02990474 ConsoleApp1.Program.Main(System.String[]) [E:\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
001bf0f8 7296eb16 [GCFrame: 001bf0f8] 

Main 함수에서 WaitOne을 호출하고 있는데 이때 Main 함수의 IP 주솟값이 02990474입니다. 이것은 WaitOne 호출이 반환되면 시작할 IP 주소를 의미하는데 따라서 WaitOne을 호출할 당시의 명령어를 확인하고 싶다면 다음과 같은 식으로 할 수 있습니다.

0:000> uf 02990474 -20
02990454 0470            add     al,70h
02990456 8bf0            mov     esi,eax
02990458 6a01            push    1
0299045a 8bce            mov     ecx,esi
0299045c 33d2            xor     edx,edx
0299045e e8c9ec356e      call    mscorlib_ni+0x31f12c (70cef12c)
02990463 6a00            push    0
02990465 8bce            mov     ecx,esi
02990467 ba50c30000      mov     edx,0C350h
0299046c 8b01            mov     eax,dword ptr [ecx]
0299046e 8b402c          mov     eax,dword ptr [eax+2Ch]
02990471 ff5004          call    dword ptr [eax+4]
02990474 5e              pop     esi
02990475 5d              pop     ebp
02990476 c3              ret

이를 기반으로 WaitOne의 signature와 함께,

public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);

인자 값을 다음과 같이 유추할 수 있습니다.

mov ecx, esi = EventWaitHandle 인스턴스의 주솟값
mov edx, 0C350h = millisecondsTimeout
push 0 = exitContext

그런데 벌써 시간 값이 0x0c350 상수로 나왔습니다. 그럼 여기서 끝낼 수도 있겠지만 대개의 경우 변숫값으로도 전달하는 경우도 많기 때문에 그런 상황을 가정하고 좀 더 파헤쳐 보겠습니다. 일단, 이번 함수 호출에서는 스택 상으로 인자 값을 확인할 방법이 없습니다. 왜냐하면 millisecondsTimeout 값이 volatile 레지스터인 edx로 전달되기 때문에 현재 CPU 상태에서 edx의 값이 다른 값으로 덮어써졌을 확률이 높습니다.

다음 함수로 넘어가 이번엔 WaitOne에서 InternalWaitOne 함수를 호출하는 상황을 보겠습니다.

0:000> uf 70df98d8 -20
mscorlib_ni+0x4298b8:
70df98b8 0f8c429ac000    jl      mscorlib_ni+0x1033300 (71a03300)  Branch

mscorlib_ni+0x4298be:
70df98be 8b7108          mov     esi,dword ptr [ecx+8]
70df98c1 8bc2            mov     eax,edx
70df98c3 c1f81f          sar     eax,1Fh
70df98c6 50              push    eax
70df98c7 52              push    edx
70df98c8 0fb65110        movzx   edx,byte ptr [ecx+10h]
70df98cc 0fb64508        movzx   eax,byte ptr [ebp+8]
70df98d0 50              push    eax
70df98d1 8bce            mov     ecx,esi
70df98d3 e818000000      call    mscorlib_ni+0x4298f0 (70df98f0)
70df98d8 25ff000000      and     eax,0FFh
70df98dd 59              pop     ecx
70df98de 5e              pop     esi
70df98df 5d              pop     ebp
70df98e0 c20400          ret     4
...[생략]...

현재 "call mscorlib_ni+0x4298f0 (70df98f0)" 호출을 한 상태이고 해당 호출이 완료되면 70df98d8의 "and eax, 0FFh"를 실행할 예정입니다. 즉, 70df98f0 주소의 함수는 InternalWaitOne입니다. .NET Reflector로 InternalWaitOne의 signature를 보면,

internal static bool InternalWaitOne(SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext)

4개의 인자를 전달받고 있습니다. 그런데... 이상하군요. 위의 코드에 보면 push eax, push edx,..., push eax로 총 3번의 push와 함께 ecx, edx에 값을 설정하고 있습니다. 그렇다면 5개의 인자여야 하는데... 음... 여기서 해석이 안되는군요. (혹시 아시는 분은 덧글 부탁드립니다.) 하지만 일단 4개라 가정하고 어셈블리 코드에서 전달되는 인자의 값을 대략 다음과 같이 유추할 수 있습니다.

push eax 
    ; hasThreadAffinity

push edx
    ; exitContext

push eax
    ; ?????

mov ecx, esi
    ; waitableSafeHandle

movzx edx, byte ptr [ecx + 10h]
    ; millisecondsTimeout

우리가 알고 싶은 [ecx + 10h]는 이번에도 스택에 저장되어 있지 않으므로 값을 알아낼 수 없습니다. (ecx + 10h과 같은 연산인 걸로 봐서 아마도 ecx가 스택의 어느 지점을 가리키는 것이 아닌가 생각되지만 이를 알아내기 위해서는 70df98d8 단계까지의 코드를 해석해야 하므로 분석이 더뎌질 수 있습니다.) 따라서 이번에도 다시 InternalWaitOne으로 넘어갑니다.

0:000> uf 70df9911 -21
mscorlib_ni+0x4298f0:
70df98f0 55              push    ebp
70df98f1 8bec            mov     ebp,esp
70df98f3 56              push    esi
70df98f4 90              nop
70df98f5 85c9            test    ecx,ecx
70df98f7 0f84439ac000    je      mscorlib_ni+0x1033340 (71a03340)  Branch

mscorlib_ni+0x4298fd:
70df98fd 81e2ff000000    and     edx,0FFh
70df9903 52              push    edx
70df9904 0fb64508        movzx   eax,byte ptr [ebp+8]
70df9908 50              push    eax
70df9909 8b550c          mov     edx,dword ptr [ebp+0Ch]
70df990c e83f16efff      call    mscorlib_ni+0x31af50 (70ceaf50)
70df9911 8bf0            mov     esi,eax
70df9913 b901000000      mov     ecx,1
70df9918 ba9b000000      mov     edx,9Bh
70df991d e81607efff      call    mscorlib_ni+0x31a038 (70cea038)
70df9922 80b8380d000000  cmp     byte ptr [eax+0D38h],0
70df9929 0f853f9ac000    jne     mscorlib_ni+0x103336e (71a0336e)  Branch

InternalWaitOne에서 호출하는 System.Threading.WaitHandle.WaitOneNative의 signature는 InternalWaitOne과 동일합니다.

[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical]
private static extern int WaitOneNative(SafeHandle waitableSafeHandle, uint millisecondsTimeout, bool hasThreadAffinity, bool exitContext);

인자 전달을 위한 어셈블리 코드 해석은 이렇게 되고,

ecx
    ; InternalWaitOne에서 넘긴 ecx를 그대로 전달

mov edx, dword ptr [ebp + 0ch]
    ; millisecondsTimeout

and     edx,0FFh
push edx
    ; hasThreadAffinity

movzx   eax,byte ptr [ebp+8]
push eax
    ; exitContext

드디어 나왔습니다. 이번에는 ebp + 0ch에 millisecondsTimeout 값이 담겨 있습니다. 따라서 InternalWaitOne 호출 당시의 ebp 값을 알아내면 됩니다. 우리가 살펴본 InternalWaitOne에 대해 !clrstack에서 살펴보면 IP 주솟값을 얻을 수 있었는데요.

0:000> !clrstack
OS Thread Id: 0x92ec (0)
Child SP       IP Call Site
001bee74 7759ed3c [HelperMethodFrame_1OBJ: 001bee74] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
001bef58 70df9911 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
001bef70 70df98d8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
001bef84 02990474 ConsoleApp1.Program.Main(System.String[]) [E:\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
001bf0f8 7296eb16 [GCFrame: 001bf0f8] 

전에 설명한 대로 70df9911 값은 InternalWaitOne이 WaitOneNative를 호출하고 돌아왔을 때 실행을 계속할 주소였습니다. 이를 염두에 두고 kv 호출 스택을 보면,

0:000> kv
 # ChildEBP RetAddr  Args to Child              
00 001beb64 77061293 00000001 001bef10 00000000 ntdll!NtWaitForMultipleObjects+0xc (FPO: [5,0,0])
01 001becf8 72a0ff96 00000001 001bef10 00000001 KERNELBASE!WaitForMultipleObjectsEx+0x103 (FPO: [SEH])
02 001bed48 72a0fcd8 00000001 0000c350 00000001 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x3c (FPO: [Non-Fpo])
03 001bedd4 72a0fdc9 00000001 001bef10 00000001 clr!Thread::DoAppropriateWaitWorker+0x237 (FPO: [5,25,0])
04 001bee40 72a1011f 00000001 001bef10 00000001 clr!Thread::DoAppropriateWait+0x64 (FPO: [Non-Fpo])
05 001bef48 70df9911 00000000 00000000 045e247c clr!WaitHandleNative::CorWaitOneNative+0x163 (FPO: [Non-Fpo])
06 001bef5c 70df98d8 00000000 0000c350 00000000 mscorlib_ni+0x429911
07 001bef78 02990474 00000000 00000000 001bef94 mscorlib_ni+0x4298d8
WARNING: Frame IP not in any known module. Following frames may be wrong.
...[생략]...
16 001bfa74 77594a47 ffffffff 775b9eee 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
17 001bfa84 00000000 739e4df0 003a9000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

70df9911 주솟값을 RetAddr로 표기한 WaitHandleNative::CorWaitOneNative 호출을 확인할 수 있습니다. 그 함수의 RetAddr라고 되어 있으니 그렇다면 그 아래의 "mscorlib_ni+0x429911" 호출이 InternalWaitOne에 해당합니다. (.NET Managed 함수 호출은 k 명령어에서 저런 식으로 이름이 보입니다.)

따라서 "mscorlib_ni+0x429911"의 ChildEBP로 나온 001bef5c 값이 InternalWaitOne 함수가 호출될 당시의 EBP 값이 됩니다. 그리하여 [ebp + 0ch]의 값은,

mov edx, dword ptr [ebp + 0ch]

다음과 같이 구할 수 있습니다.

0:000> ? 001bef5c + c
Evaluate expression: 1830760 = 001bef68

0:000> dd 001bef68
001bef68  0000c350 00000000 70e798a4 045e244c
001bef78  001bef88 02990474 00000000 00000000
001bef88  001bef94 7296eb16 0275e020 001befe8
001bef98  72976e84 001bf024 001befd8 72b07020
001befa8  001bf0f8 72976e3d 57a57eed 001bf14c
001befb8  001bf0b8 001bf06c 729d432a 001bf024
001befc8  00000000 57a57eed 001befa0 001bf0b8
001befd8  001bf16c 72b05370 2529ff9d 00000001

원하던 바로 그 값(0000c350)입니다.




위에서 EBP 레지스터의 값을 k 명령어의 ChildEBP를 통해서 구하긴 했지만 돌이켜 보면 !clrstack의 "Child SP"로도 구할 수 있습니다.

0:000> !clrstack
OS Thread Id: 0x92ec (0)
Child SP       IP Call Site
001bee74 7759ed3c [HelperMethodFrame_1OBJ: 001bee74] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
001bef58 70df9911 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
001bef70 70df98d8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
001bef84 02990474 ConsoleApp1.Program.Main(System.String[]) [E:\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
001bf0f8 7296eb16 [GCFrame: 001bf0f8] 

InternalWaitOne의 Child SP가 001bef58인 것은 부모 함수(WaitOne)가 InternalWaitOne을 call한 시점의 SP를 의미합니다. 따라서 call 이후 prologue로 "push ebp", "mov ebp, esp"로 구성되는 EBP 프레임 레지스터의 값은 Child SP 값의 +4가 됩니다. 즉, 001bef58 + 4 = 001bef5c로 결국 ChildEBP의 값과 같게 됩니다.

(그런데 좀 이상하군요. x64에서는 Child SP의 의미가 prologue가 모두 반영된 코드인데 x86에서는 프레임 스택 구성 전의 값을 가리키고 있다는 것이.)




사실 .NET 메서드들은 __clrcall 호출 규약을 따르기 때문에 ecx, edx의 활용으로 인자 추적이 살짝 어렵습니다. 따라서 쉽게 찾으려면 .NET 메서드에서 Win32 API 등의 __stdcall 호출 규약을 따르는 시점부터 찾는 것이 좋습니다. 그 외에, primitive 타입이 아닌 객체라면 !dso 명령어를 통해 출력되는 결과로부터 유추하는 것이 더 빠릅니다.




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







[최초 등록일: ]
[최종 수정일: 1/25/2018]

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/20233736Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233936Windows: 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/20233996오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233942개발 환경 구성: 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/20234199오류 유형: 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/20234289스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...