버튼이 동작하지 않습니다.

"이 문서는 https://blogs.msdn.com/ntdebugging blog 의 번역이며 원래의 자료가 통보 없이 변경될 수 있습니다. 이 자료는 법률적 보증이 없으며 의견을 주시기 위해 원래의 blog 를 방문하실 수 있습니다. (https://blogs.msdn.com/ntdebugging/archive/2007/06/15/this-button-doesn-t-do-anything.aspx)"

 

안녕하세요 Matthew 입니다. 오늘은 Tate 가 이전에 작성한 blog의Hang 시나리오 #1 에 대해서 이야기 해보고자 합니다. Debugging 관점에서 볼 때 Application 은 문제점이 발생하였을 때 오류를 발생시켜야 합니다. 하지만 가끔씩 사용자가 한 동작이 (마우스 클릭) 어떤 오류로 인해 응답하지 않을 경우가 있습니다. 이럴 경우 어떤 부분에 문제가 있는지 어떻게 판단할 수 있을까요?

 

일반적으로 Sysinternals 에서 나온 Process Monitor 는 이런 문제를 확인하는데 아주 좋은 툴 입니다. 또한 문제점이 file system 이나 registry 관련된 것이라면 Process Explorer 가 문제점을 파악할 수 있을 것 입니다. 만약 Network 을 통해 문제가 발생하고 있는 것이라면 Network 의 동작을 수집해 보는 것이 좋을 것 입니다.

 

위에서 이야기 한 것과 같은 내용으로 문제점을 설명할 수 없고 Application 의 동작에 대해서도 알 수 없고 소스 코드 마저 없다면 어떻게 할 수 있을까요? 아래 Sample application 을 debugging 하면서 답을 찾아 보도록 하겠습니다.

 

clip_image002

Download the sample application here.

 

우리가 알고 있는 사항 입니다.

1. Button 1 을 누를 경우 다이얼로그 창이 나타납니다.

2. 특정 사용자의 경우 Button 1 을 눌러도 응답이 없습니다.

3. 소스코드는 물론 심볼도 사용할 수 없습니다.

4. 아무도 Button 1 이 동작하는 방식을 모릅니다.

5. Application 의 개발자는 3년째 연락이 되지 않습니다.

 

우리는 Button 1 이 눌렸을 때 어떤 동작이 일어나는지 파악해야 합니다. 모든 Window 는 사용자의 입력을 받기 위해 WindowProc 를 가지고 있습니다. Button 은 "control" 이라고 생각할 수 있습니다. Button 이 눌렸을 때 Application의 Main Window 는 WM_COMMAND 메시지를message 를 전달 받습니다. WM_COMMAND 메시지는 각각 다른 동작을 하기 위한 제어코드를 가지고 있습니다.

 

우리가 필요한 것은

1. Button 1 의 제어코드를 파악

2. Main application window 에서 WindowProc 찾기

3. WM_COMMAND 명령이 Button 1 을 어디서 처리하는지 찾기

4. 에러 코드가 무엇인지 파악

 

시작해 보겠습니다.

 

Button 1 을위한 Control ID 찾기

Spy++ (Visual Studio 에 포함) 가 이 문제를 분석하는데 적합한 Tool 입니다. “Spy” -> “Log Message” 를 클릭 합니다. Finder Tool 을 사용해서 ntdbghang1.exe 의 Main window 를 선택 합니다. (역자주: 조준점 처럼 생긴 것을 Drag 해서 ntdbghang1.exe 의 윈도우 전체를 선택하게 해서 Drop 합니다. )Messages 탭에서 “Clear All” 를 클릭한 후 “WM_COMMAND” 를 선택 합니다. “OK” 를 선택하면 SPY++ 가 로그를 기록하기 시작 합니다. Ntdbghang1.exe 의 Button 1 을 클릭 하면 아래와 같은 메시지가 기록되는 것을 확인할 수 있을 것 입니다. 비교를 위해 Button 2 를 클릭하면 동일한 메시지가 기록되는 것을 확인할 수 있습니다. 이때 화면은 아래와 비슷할 것 입니다.

 

clip_image004

 

이 화면을 통해 Button 1 의 Control ID는 257(0x101) 이고 Button 2 의 Control ID는 258(0x102) 라고 할 수 있습니다. 나중에 이 정보가 필요할 것 입니다.

 

Main application window 를위한 WindowProc 찾기

WindowProc 의 주소를 SPY++ 을 이용해서 찾을 수 있습니다. “SPU” -> “Find Window” 를 클릭한 후 Finder Tool 을 사용해서 ntdbghang1.exe 의 main window 를 선택 한 후 “Show Properties” 를 선택한 후 OK 를 선택 하면 아래와 같은 화면을 볼 수 있습니다.

 

clip_image006

 

Window Proc 에 기록되어 있는 값은 Main window 의 window procedure 주소 값 입니다. 우리가 아는 것과 같이 WindowProc 함수이며 다음과 같습니다.

 

LRESULT CALLBACK WindowProc(

HWND hwnd,

UINT uMsg,

WPARAM wParam,

LPARAM lParam

);

 

wParam 과 lParam 의 값은 uMsg 에 따라 달라 집니다. WM_COMMAND 메시지가 전달되면 wParam 의 낮은 word 값은 우리가 이미 확인한 0x101 값인 Control ID 입니다. 상위 word 값은 Control 통지 code 로 버튼이 클릭되었다는 의미인 BN_CLICKED(내부적으로 0) 입니다.

 

그래서 다음과 같은 값을 가졌다고 생각할 수 있습니다.

uMsg = WM_COMMAND (literally 0x111)

wParam = 0x101

 

WindowProc 를 어셈블리 언어로 보면 다음과 같은 stack 을 가질 것 입니다.

ebp = “old ebp”

ebp+4 = “return address”

ebp+8 = hwnd

ebp+c = uMsg

ebp+10 = wParam

ebp+14 = lParam

 

Button 1 을 위한 WM_COMMAND message 가 어디서 처리되는지 확인

WindowProc 의 주소를 가지고 있다면 WinDbg 를 이용해서 Assembly code 를 확인할 수 있습니다. WinDbg.exe 를 실행한 후 “File -> Attach to a Process “ 를 선택한 후 리스트에서 “ntdbghang1.exe”를 선택 하고 “OK” 를 선택 합니다. Debugger 가 ntdbghang1.exe 를 멈추면 “u <address>” 명령을 사용해서 WinProc 함수를 unassembled 할 수 있습니다. 저의 시스템에서는 “u 01002830” 명령을 사용하였습니다. 뒷 부분을 더 unassembled 하려면 “u” 명령만 반복해서 입력해 주면 됩니다. 관련된 code 를 unassembled 하고 어떤 의미를 가지는지 확인해 보도록 하겠습니다.

 

0:001> u 01002830

ntdbghang1+0x2830:

 

첫 번째 3개의 명령은 함수의 초기명령입니다.

01002830 8bff mov edi,edi

01002832 55 push ebp

01002833 8bec mov ebp,esp

 

uMsg 값을 ecx 로 넣는 명령 입니다.

01002835 8b4d0c mov ecx,dword ptr [ebp+0Ch]

 

Application 이 상태를 확인하는 명령으로 들어 갑니다. 우리는 uMsg = WM_COMMAND 를 확인하는 곳을 중점적으로 볼 것 입니다. (역자주 : 실제 코드는 uMsg == WM_COMMAND 일 것입니다.)

 

if uMsg = WM_CREATE (1) goto 01002893

01002838 49 dec ecx

01002839 7458 je ntdbghang1+0x2893 (01002893)

if uMsg = WM_ DESTROY (2) goto 01002889

0100283b 49 dec ecx

0100283c 744b je ntdbghang1+0x2889 (01002889)

if uMsg = WM_CLOSE (0x10) goto 01002889

0100283e 83e90e sub ecx,0Eh

01002841 743b je ntdbghang1+0x287e (0100287e)

if uMsg = WM_COMMAND (0x111) goto 01002853

01002843 b801010000 mov eax,101h

01002848 2bc8 sub ecx,eax

0100284a 7407 je ntdbghang1+0x2853 (01002853)

 

위의 상태를 확인하는 코드를 보면 uMsg = WM_COMMAND 에서 01002853 으로 실행이 넘어가는 것을 확인할 수 있습니다. 자 그곳을 확인해 보도록 하겠습니다.

 

0:001> u 01002853

ntdbghang1+0x2853:

 

wParam 값을 exd 에 넣습니다.

01002853 8b5510 mov edx,dword ptr [ebp+10h]

 

만약 LOWORD(wParam) == 0x101 이라면 (button 1의 control ID) 0100286f 으로 넘어갑니다.

01002856 0fb7ca movzx ecx,dx

01002859 2bc8 sub ecx,eax

0100285b 7412 je ntdbghang1+0x286f (0100286f)

 

위의 Assembly 코드를 확인해 보면 Button 1 의 Control ID 를 확인하는 것을 볼 수 있고 아래는 우리가 확인하고자 하는 코드 입니다.

 

0:001> u 0100286f

ntdbghang1+0x286f:

 

만약 HIWORD(wparam) != BN_CLICKED (0) 이라면 0100289b 으로 넘어가고, 아니라면 010027f6 함수를 호출합니다.

0100286f c1ea10 shr edx,10h

01002872 6685d2 test dx,dx

01002875 7524 jne ntdbghang1+0x289b (0100289b)

01002877 e87affffff call ntdbghang1+0x27f6 (010027f6)

 

Control id 0x101 이고 BN_CLICKED 라면 010027f6 이 실행되는 것을 확인할 수 있습니다.

무엇이 실패 하였는지 확인

버튼이 클릭되었을 때 어떤 코드(010027f6 의 함수)가 실행되는지 알게 되었습니다. 이제 어떤 코드에서 문제가 발생하였는지 확인해 보고자 합니다. “uf” 명령을 사용해서 전체 함수를 unassembled 할 수 있습니다. 첫 번째 단계에서 로컬 변수의 이름을 확인할 수 있고 이것들이 무엇인지 확인해 볼 것 입니다.

 

0:001> uf 010027f6

ntdbghang1+0x27f6:

Function Prologue

010027f6 8bff mov edi,edi

010027f8 55 push ebp

010027f9 8bec mov ebp,esp

010027fb 51 push ecx

010027fc 56 push esi

 

func1(0x20) 호출 [func1 의 주소는 01002dca 입니다.]

로컬변수 localvar1=0x10 로 설정합니다.

010027fd 6a20 push 20h

010027ff c745fc10000000 mov dword ptr [ebp-4],10h

01002806 e8bf050000 call ntdbghang1+0x2dca (01002dca)

 

Func1 의 결과를 esi 에 저장 합니다.(localvar2 임)

0100280b 8bf0 mov esi,eax

 

func2 호출(localvar2, &localvar1) [func2 address at 0100b484]

0100280d 59 pop ecx

0100280e 8d45fc lea eax,[ebp-4]

01002811 50 push eax

01002812 56 push esi

01002813 e86c8c0000 call ntdbghang1+0xb484 (0100b484)

 

func2 의 결과가 0이면 , 01002821 로 넘어갑니다.

01002818 85c0 test eax,eax

0100281a 7405 je ntdbghang1+0x2821 (01002821)

 

func3() 호출 [func3 의 주소는 0100278d 입니다.]

ntdbghang1+0x281c:

0100281c e86cffffff call ntdbghang1+0x278d (0100278d)

 

func4 호출(localvar2) [func4 주소는01002ce3 입니다.]

ntdbghang1+0x2821:

01002821 56 push esi

01002822 e8bc040000 call ntdbghang1+0x2ce3 (01002ce3)

 

정리 작업 및 함수에서 빠져 나가는 코드 입니다.

01002827 59 pop ecx

01002828 5e pop esi

01002829 c9 leave

0100282a c3 ret

 

아래와 같이 가상의 코드를 만들 수 있습니다.

 

localvar1 = 0x10;

localvar2 = func1(0x20);

if(func2(localvar2, &localvar1)

{

func3();

}

func4(localvar2);

 

함수의 동작은 Func2 의 결과에 따라 달라집니다. Func2 가 어떤 동작을 하는지 살펴 보고자 합니다.

 

0:001> u 0100b484

ntdbghang1+0xb484:

0100b484 ff2568110001 jmp dword ptr [ntdbghang1+0x1168 (01001168)]

0100b48a cc int 3

0100b48b cc int 3

0100b48c cc int 3

0:001> dps 01001168 L1

01001168 70b88cb1 WINSPOOL!GetDefaultPrinterW

 

GetDefaultPrinterW 는 공개된 API 함수로 아래에 MSND에 있는 프로토타입이 있습니다.

BOOL GetDefaultPrinter(

LPTSTR pszBuffer, // printer name buffer

LPDWORD pcchBuffer // size of name buffer

);

 

이 내용은 이전에 우리가 만든 가상코드와 일치 하고 이 함수는 두 개의 파라미터를 사용하고 BOOL 값을 리턴 합니다. 새로 알아낸 내용을 바탕으로 func2 를 GetDefaultPrinter 로 변경하도록 하겠습니다.

 

DWORD cchBuffer = 0x10;

LPWSTR pszBuffer = func1(0x20);

if(GetDefaultPrinterW(pszBuffer, &cchBuffer))

{

func3();

}

func4(pszBuffer);

 

위의 내용을 바탕으로 몇 가지 가정을 할 수 있습니다. Func1 은 무언가를 할당하는 함수(malloc 와 비슷) 이고 Fun4 는 메모리 해제 함수(free 로 생각)로 볼 수 있습니다. Application 이 GetDefaultPrinter (끝이 W 로 끝나는) 함수의 Unicode version 을 사용하고 0x20Byte 를 문자열 Buffer 의 Size로 전달하고 GetDefaultPrinterW 에 0x10 을 전달한다고 볼 수 있습니다. 위의 가정은 Func3 를 제외하고는 맞습니다. Func3 가 무엇을 하는지 확인하기 위해 Unassemble 할 수 있으나 실제로 필요하지 않을 수 있습니다. 가상의 코드에 따르면 GetDefaultPrinterW 에 문제가 있는 것을 확인할 수 있습니다.

 

이제 확인해 보도록 하겠습니다. GetDefaultPrinterW 에 BreadPoint 를 설정하고 Button 을 클릭했을 때 어떤 문제가 있는지 확인해 보도록 하겠습니다.

0:001> bp WINSPOOL!GetDefaultPrinterW

0:001> g

 

<Button 1 클릭>

Breakpoint 0 hit

eax=0006fb84 ebx=00000000 ecx=00000020 edx=00dc0e98 esi=00dc0e70 edi=0006fc0c

eip=70b88cb1 esp=0006fb74 ebp=0006fb88 iopl=0 nv up ei pl nz na po nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202

WINSPOOL!GetDefaultPrinterW:

70b88cb1 8bff mov edi,edi

 

// Call stack 보기

0:000> kb

ChildEBP RetAddr Args to Child

0006fb70 01002818 00dc0e70 0006fb84 00000111 WINSPOOL!GetDefaultPrinterW

WARNING: Stack unwind information not available. Following frames may be wrong.

0006fb88 0100287c 0006fbbc 75d41a10 00320f78 ntdbghang1+0x2818

0006fb90 75d41a10 00320f78 00000111 00000101 ntdbghang1+0x287c

0006fbbc 75d41ae8 01002830 00320f78 00000111 USER32!InternalCallWinProc+0x23

0006fc34 75d4286a 00000000 01002830 00320f78 USER32!UserCallWinProcCheckWow+0x14b

0006fc74 75d42bba 00a90b80 0095ee08 00000101 USER32!SendMessageWorker+0x4b7

 

// return 을 확인해 보면 eax=0 입니다. GetDefaultPrinterW 가 FALSE 를 return 했다는 것을 의미 합니다.

0:000> gu

eax=00000000 ebx=00000000 ecx=76f22033 edx=00e10178 esi=00dc0e70 edi=0006fc0c

eip=01002818 esp=0006fb80 ebp=0006fb88 iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

ntdbghang1+0x2818:

01002818 85c0 test eax,eax

// Check the last error...

// 마지막 에러 확인

0:000> !gle

LastErrorValue: (Win32) 0x7a (122) - The data area passed to a system call is too small.

// 명령 다시 실행

0:000> g

 

Error 는 0x7a = ERROR_INSUFFICIENT_BUFFER 이고 GetDefaultPrinterW 함수로 전달된 Buffer 의 크기가 Default printer 의 이름보다 작다는 것 입니다. 이것이 일부 사용자에게 문제가 발생하는 원인 입니다. 기본 프린터의 이름을 16자 이하로 줄이면 문제는 해결될 것 입니다.

 

정리 하면서 아래 Button1_OnClick 함수의 C 소스코드를 첨부 합니다. (010027f6 주소에 있는 내용)

VOID Button1_OnClick()

{

DWORD cch = 16;

LPTSTR pPrinterName;

pPrinterName = (LPTSTR) malloc(16 * sizeof(TCHAR));

if(GetDefaultPrinter(pPrinterName, &cch))

{

DisplayGoButtonMessage();

}

free(pPrinterName);

return;

}

 

WinDbg 명령중 “wt” 을 사용해서 위의 문제를 확인해볼 수 도 있습니다. 이 글이 여러분에게 도움이 되었기를 바랍니다.