WinDbg 디버깅2007. 7. 11. 01:00
반응형

조건 브레이크 포인트를 사용하면 디버깅에 도움이 되는 경우가 상당히 많습니다.
하지만 조건문을 어떻게 적어줘야 하는지 잘 몰라서 적극적인 활용을 하지 못하고 있습니다. ^^

이번에 익힌 사용방법을 적어놓고 갈무리하려 합니다. ㅋㅋㅋ

kd> bp b8fb429d ".if @@(pData->dwMyFlag & 0x00010000) {} .else {gc}"

b8fb429d 주소에 브레이크 포인트를 거는데 조건에 따라 걸리게 하고 싶은 상황입니다.

.if {} .else {} 명령을 사용한 조건문입니다.

@@( ) 을 사용하면 괄호 안에서는 C/C++ 소스코드에서 사용하던 표현들을 쓸 수 있습니다.
심볼이 맞아 있으면 위와 같이 소스에서 사용하던 구문 그대로 표현할 수 있습니다.
pData->dwMyFlag 에서 0x00010000 비트가 켜져 있으면 브레이크 하라는 의미입니다.
비트가 켜져 있지 않으면 .else 에 의해서 gc 로 계속 진행합니다.

PS)
조건 브레이크 포인트를 사용할 때 조건이 맞지 않아서 계속 진행하게 할 때는 반드시 gc (Go from Conditional Breakpoint)를 사용해야 합니다. 한 줄씩 step trace 를 하다가 조건 브레이크 포인트를 만났을 경우 gc 를 해야만 다음 라인으로 step trace 가 되기 때문입니다.
.else {g} 와 같이 쓴다면 조건이 맞지 않을 경우 g 를 실행하여 확~~~ 실행되어 버립니다. 한 줄씩 디버깅하면서 step trace 하고 있는 경우에 F10 을 눌렀는데 다음 라인에서 서지 않고 그냥 실행되어 버릴 수 있다는 뜻입니다. 당황스럽겠죠?


http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=1&s_bulu=&s_key=&no=91

반응형
Posted by GreeMate
WinDbg 디버깅2007. 6. 29. 00:51
반응형

WinDbg 사용시에 마우스 오른쪽 버튼을 사용하시나요?
WinDbg에서 마우스 오른쪽 버튼은 매우 간단하고 편리한 기능을 가지고 있습니다.
바로 Copy & Paste 인데요.

WinDbg 를 사용하다보면 생각보다 Copy & Paste 를 해야할 경우가 많습니다.
이 때 일일이 Ctrl+C, Ctrl+V 를 눌러줄 필요없이 마우스 오른쪽 버튼만으로 해결하면 됩니다.
어떻게 하는 것인지 예제를 통해서 알아봅니다.

덤프파일을 열거나 라이브 디버깅중 BSOD 가 발생하거나 항상 나오는 메시지가 있습니다.

Use !analyze -v to get detailed debugging information.

이것을 보고 구지 !analyze -v 를 타이핑하지 않아도 됩니다.
다음과 같이 !analyze -v 부분을 마우스로 드래그하여 반전시킨 다음

Use !analyze -v to get detailed debugging information.

마우스 오른쪽 버튼을 눌러줍니다. 그러면 반전이 풀리면서 클립보드로 복사됩니다.
Ctrl+C 한 것과 같은 효과입니다. 마우스 드래그 한 후에 바로 클릭! 간편하죠.

그리고 바로 마우스 오른쪽 버튼을 다시 한번 눌러줍니다. 그러면 커맨드 라인에
다음과 같이 나타납니다.

kd> !analyze -v

Ctrl+V 한 것과 같은 효과입니다. 그리고 나서 Enter 를 쳐서 명령을 바로 실행합니다.
키보드를 여러번 이용할 것을 마우스 몇번과 키보드 한번으로 해결할 수 있습니다.

커맨드 창에 나오는 메시지 중에 이 기능을 자주 사용하게 되는 또 하나의 예제입니다.

TRAP_FRAME:  f490977c -- (.trap fffffffff490977c)

.trap fffffffff490977c 를 실행해 주면 된다고 설명드렸었죠.
이거 절대로 키보드로 타이핑하지 않습니다. 손가락을 고생시킬 필요가 없겠죠.
자... 이렇게 합니다.

마우스 드래그, (오른쪽)클릭, (오른쪽)클릭, Enter

kd> .trap fffffffff490977c

그러면 명령이 바로 실행됩니다.

WinDbg 노가다 없이 사용합시다. ^^

http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=6&s_bulu=&s_key=&no=29
반응형
Posted by GreeMate
WinDbg 디버깅2007. 6. 29. 00:50
반응형

드라이버를 개발하면서 WinDbg 로 커널 디버깅을 할 때 가장 귀찮은 작업중 하나가
Debugger(개발시스템)에서 드라이버를 빌드한 후에 이것을 Debuggee (테스트시스템)로
복사해줘야 한다는 것입니다.
이것을 해결해 주는 편리한 명령어가 있었으니 바로 .kdfiles 입니다.

.kdfiles (Set Driver Replacement Map)
.kdfiles command 는 맵파일을 읽어서 그 내용에 있는 대로 Debugger 시스템에 있는
드라이버 파일을 Debuggee 시스템에 복사하게 합니다.

[Syntax]
.kdfiles MapFile ( MapFile 을 읽는다 )
.kdfiles -c         ( 현재 설정을 지운다 )
.kdfiles             ( 현재 설정을 보인다 )

파일복사는 커널 디버거가 연결된 통로로 이루어집니다. Serial 디버깅이면 Serial,
1394 디버깅이라면 1394죠. 파일복사이므로 파일이 큰 경우 당연히 1394 권장입니다.

이해가 잘 안가시죠?
예제를 통해 확인해 보겠습니다.

kd> .kdfiles
No KD file associations set

이게 일반적인 상황입니다.
다음과 같이 설정할 수 있습니다.

kd> .kdfiles d:\dbgmap\mymap.ini
KD file assocations loaded from 'd:\dbgmap\mymap.ini'

mymap.ini 파일의 내용은 다음과 같습니다.

# [주석] mydrv.sys 에 대한 맵
map
\Systemroot\system32\mydrv.sys
D:\mytest\mydrv\Build\I386\checked\mydrv.sys

내용은 debuggee 의 \Systemroot\system32\mydrv.sys 는 debugger 시스템의 D:\mytest\
mydrv\Build\I386\checked\mydrv.sys 를 가져와서 사용하도록 해 달라는 의미입니다.
아래와 같이 설정된 내용을 확인할 수 있습니다.

kd> .kdfiles
KD file assocations loaded from 'd:\dbgmap\mymap.ini'
\Systemroot\system32\drivers\mydrv.sys -> D:\mytest\mydrv\Build\I386\checked\mydrv.sys

실제 파일 교체는 드라이버 로드시에 이루어 집니다. 리부팅은 필요없습니다.
테스트시스템의 드라이버를 로드하려는 순간 debugger 측에서 지정된 폴더의 파일을 전송하고
테스트시스템의 드라이버가 전송된 파일로 교체된 후에 드라이버가 로드됩니다.
이 때 다음과 같은 메시지가 뿌려집니다.

KD: Accessing 'D:\mytest\mydrv\Build\I386\checked\mydrv.sys' (\SystemRoot\system32\drivers\mydrv.sys)
  File size 64K........................
MmLoadSystemImage: Pulled \SystemRoot\system32\drivers\mydrv.sys from kd

Windows File Protection (WFP)과 관계없이 교체되구요 부팅하는 시점이더라도 드라이버가
로드되면 교체됩니다.

드라이버 자동 복사가 필요없어지면 다음 명령으로 설정을 지우면 됩니다.

kd> .kdfiles -c

이 명령을 알고 있으면 수동으로 파일을 복사하는 작업이 없어져서 아주 편리합니다.
하지만 가끔 원하지 않게 빌드된 드라이버 파일이 무심코 복사되기도 하지요. ^^

참고로 맵파일 작성시 다음과 같은 포맷도 가능함을 보여드립니다.

# Symbolic Link Example
map
\??\c:\windows\system32\beep.sys
\\myserver\myshare\new_drivers\new_beep.sys

복사할 곳 지정시 심볼릭링크 표현으로 어느 위치던지 지정할 수 있습니다.
원본위치 지정시 네트웍에 있는 위치도 지정할 수 있습니다.

그리고 안타깝게도 .kdfiles 명령어는 타겟시스템이 XP 이후인 경우만 동작합니다.


http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=6&s_bulu=&s_key=&no=25
반응형
Posted by GreeMate
WinDbg 디버깅2007. 6. 29. 00:48
반응형

Verifier의 Special Pool 을 이용할 때 발견할 수 있는 오류를 하나 더 보겠습니다.
BugCheck 0xC1 입니다. 이번엔 바로 !analyze -v 를 해 봅니다.

kd> !analyze -v
****************************************************************************
*                                                                          *
*                     Bugcheck Analysis                                    *
*                                                                          *
****************************************************************************

SPECIAL_POOL_DETECTED_MEMORY_CORRUPTION (c1)
Special pool has detected memory corruption.  Typically the current thread's
stack backtrace will reveal the guilty party.
Arguments:
Arg1: 83d76ff8, address trying to free
Arg2: 83d76ffc, address where bits are corrupted
Arg3: 0081c004, (reserved)
Arg4: 00000024, caller is freeing an address where bytes after the end of the allocation have been overwritten

Debugging Details:
------------------


BUGCHECK_STR:  0xC1_24

SPECIAL_POOL_CORRUPTION_TYPE:  24

DEFAULT_BUCKET_ID:  DRIVER_FAULT

PROCESS_NAME:  System

LAST_CONTROL_TRANSFER:  from 8053425b to 804e5592

STACK_TEXT: 
f88ee700 8053425b 00000003 f88eea5c 00000000 nt!RtlpBreakWithStatusInstruction
f88ee74c 80534d2e 00000003 83d76000 00000004 nt!KiBugCheckDebugBreak+0x19
f88eeb2c 8053531e 000000c1 83d76ff8 83d76ffc nt!KeBugCheck2+0x574
f88eeb4c 806811ae 000000c1 83d76ff8 83d76ffc nt!KeBugCheckEx+0x1b
f88eeb8c 8054d5aa 83d76ff8 81ac7550 e14374d2 nt!MmFreeSpecialPool+0x29b
f88eebcc 8066da60 83d76ff8 00000000 f88eebe8 nt!ExFreePoolWithTag+0x4a
f88eebdc f38e3da3 83d76ff8 f88eebf8 f38e3f23 nt!VerifierFreePool+0x1f
f88eebe8 f38e3f23 83d76ff8 01d76ff8 f88eec20 MyDrv!my_free+0x13 [d:\mytest\mydrv\my_mem.c @ 38]
f88eebf8 f38e1ce0 83d76ff8 83d76ff8 0013a8d9 MyDrv!my_close+0x43 [d:\mytest\mydrv\my_file.c @ 59]
f88eec20 f38e1047 e30c2970 e30c0000 f88eec48 MyDrv!Test_Open+0x130 [d:\mytest\mydrv\my_test.c @ 455]
...

STACK_COMMAND:  kb

FOLLOWUP_IP:
MyDrv!my_free+13 [d:\mytest\mydrv\my_mem.c @ 38]
f38e3da3 5d              pop     ebp

FAULTING_SOURCE_CODE: 
    34:  
    35:  if (pv)
    36:   ExFreePool(pv);
    37:
>   38: }
    39:
    40: void my_copy(LPVOID pDest, LPCVOID pSrc, size_t nLength)
    41: {


SYMBOL_STACK_INDEX:  7

SYMBOL_NAME:  MyDrv!my_free+13

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: MyDrv

IMAGE_NAME:  MyDrv.sys

DEBUG_FLR_IMAGE_TIMESTAMP:  45127eee

FAILURE_BUCKET_ID:  0xC1_24_VRF_MyDrv!my_free+13

BUCKET_ID:  0xC1_24_VRF_MyDrv!my_free+13

Followup: MachineOwner
---------

메시지를 읽어보면 이번에는 특별히 하라는 것은 없습니다.
그래도 메시지는 꼼꼼히 읽어봅시다. 이 BugCheck 의 원인을 정확히 설명해 주고 있습니다.

일단 메모리를 깨뜨려서 발생한 것이라고 설명하고 있습니다. 콜스택을 보면 문제 해결에
도움이 될 거라고 하네요. 콜스택은 조금 있다가 보기로 하구요.

아주 유용한 정보가 보입니다.

Arg4: 00000024, caller is freeing an address where bytes after the end of the allocation have been overwritten

문제가 발생한 원인을 말해주고 있습니다.
할당한 메모리를 넘어서는 부분을 깨뜨리고 메모리를 해제하려고 했다고 하네요.

Special Pool 을 이용하면 이런 문제를 잡을 수 있습니다. 메모리를 깨뜨리는 문제는
매우 까다로운 문제에 속합니다. 원인을 찾지 못하는 경우도 많습니다.
이럴 때 Verifier의 도움을 받으면 효과를 볼 수 있다는 것을 기억하시기 바랍니다.
아니 Verifier 는 개발중에 무조건 켜놓고 사용하는 것이라고 보면 되기 때문에
특별히 기억할 필요도 없겠네요. 그냥 습관적으로 사용하시기 바랍니다.

본론으로 돌아가서 Arg4 는 BugCheck 0xC1 을 발생시키는 원인을 지시하는 값이 들어갑니다.
0x24 는 위와 같은 뜻이구요. 다른 코드들과 그 의미는 WinDbg Help 를 참고하시기 바랍니다.
메모리 사용에 있어서 여러가지 오류 상황들을 잡아낼 수 있다는 것을 아시게 될 겁니다.

Arg1: 83d76ff8, address trying to free
Arg2: 83d76ffc, address where bits are corrupted

Arg1 은 메모리 주소네요. Arg2 는 깨진 곳의 주소입니다.

Verifier 는 어떻게 깨진 곳의 주소를 알고 있는 것일까요?
이제 콜스택을 봅니다.

f88ee700 8053425b 00000003 f88eea5c 00000000 nt!RtlpBreakWithStatusInstruction
f88ee74c 80534d2e 00000003 83d76000 00000004 nt!KiBugCheckDebugBreak+0x19
f88eeb2c 8053531e 000000c1 83d76ff8 83d76ffc nt!KeBugCheck2+0x574
f88eeb4c 806811ae 000000c1 83d76ff8 83d76ffc nt!KeBugCheckEx+0x1b
f88eeb8c 8054d5aa 83d76ff8 81ac7550 e14374d2 nt!MmFreeSpecialPool+0x29b
f88eebcc 8066da60 83d76ff8 00000000 f88eebe8 nt!ExFreePoolWithTag+0x4a
f88eebdc f38e3da3 83d76ff8 f88eebf8 f38e3f23 nt!VerifierFreePool+0x1f
f88eebe8 f38e3f23 83d76ff8 01d76ff8 f88eec20 MyDrv!my_free+0x13 [d:\mytest\mydrv\my_mem.c @ 38]
f88eebf8 f38e1ce0 83d76ff8 83d76ff8 0013a8d9 MyDrv!my_close+0x43 [d:\mytest\mydrv\my_file.c @ 59]
f88eec20 f38e1047 e30c2970 e30c0000 f88eec48 MyDrv!Test_Open+0x130 [d:\mytest\mydrv\my_test.c @ 455]

my_free() 함수에서 호출한 ExFreePool()이 결국 MmFreeSpecialPool() 을 호출하고 여기서
KeBugCheckEx() 를 호출하는 것을 볼 수 있습니다.

즉, Verifier는 메모리를 해제할 때 메모리의 내용을 체크해서 문제가 있는지 확인합니다.
메모리의 내용을 보면

kd> !pool 83d76ff8
Pool page 83d76ff8 region is Special pool
*83d76ff8 size:    4 pagable special pool, Tag is MDRV
  Owning component : Unknown (update pooltag.txt)

kd> db 83d76ff8
83d76ff8  d8 03 00 80 10 d2 8e f3-?? ?? ?? ?? ?? ?? ?? ??  ........????????
83d77008  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
83d77018  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
83d77028  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

size 4 의 메모리였네요. d8 03 00 80 가 할당해서 사용했던 데이터구요.
깨진 주소가 83d76ffc 이므로 10 d2 8e f3 에서 10 에 해당합니다.

깨진것을 판단하는 방법은 지나번에 설명한 Special Pool 의 구조로 설명됩니다.
할당된 메모리 조금 앞을 보면

kd> db 83d76ff8 - 10
83d76fe8  81 81 81 81 81 81 81 81-81 81 81 81 81 81 81 81  ................
83d76ff8  d8 03 00 80 10 d2 8e f3-?? ?? ?? ?? ?? ?? ?? ??  ........????????

81 이라는 값이 차 있는 것을 볼 수 있습니다.

이 페이지 맨 앞을 보면

kd> db 83d76000
83d76000  04 c0 81 00 4d 44 53 56-00 00 00 00 08 4f f1 81  ....MDRV.....O..
83d76010  81 81 81 81 81 81 81 81-81 81 81 81 81 81 81 81  ................
83d76020  81 81 81 81 81 81 81 81-81 81 81 81 81 81 81 81  ................
83d76030  81 81 81 81 81 81 81 81-81 81 81 81 81 81 81 81  ................

맨 앞에 size 4 가 보이고 그 다음 다음에 81 이 보이고 뒤부분이 모두 81 로 채워지
있는 것이 보입니다. 맨 뒤도 사실은 다음과 같이 채워져 있었습니다.

83d76ff8  d8 03 00 80 81 81 81 81-?? ?? ?? ?? ?? ?? ?? ??  ........????????

MmFreeSpecialPool()에서는 81 로 채워진 부분이 온전한지를 검사합니다.
할당해준 메모리 영역만 정확히 사용했는지 검사하는 겁니다.
할당된 영역을 넘어선 뒤부분 검사를 시작하자마자 딱 걸렸네요.
할당된 크기보다 큰 영역을 건드렸으므로 오류라는 뜻입니다.

메시지에서 보여주는 오류 소스코드를 보면

FAULTING_SOURCE_CODE: 
    34:  
    35:  if (pv)
    36:   ExFreePool(pv);
    37:

my_free() 내부의 소스코드인데 메모리 해제 밖에 없으니 원인을 알 수 없습니다.
상위 함수의 내용을 보면

BOOLEAN my_close(PMYHANDLE pMyHandle)
{
 ...

 ZwClose(pMyHandle->Handle);
 my_free(pMyHandle);

 return bRet;
}

pMyHandle 을 해제하라고 넘겨준 것이므로 pMyHandle을 사용하는 곳들을 살펴보면
원인을 찾을 수 있습니다.

생성부터 살펴보니

PMYHANDLE my_open(LPCTSTR pszFile)
{
 PMYHANDLE pMyHandle = my_malloc(sizeof(PMYHANDLE));
 if (!pMyHandle) return 0;

 ...
}

바로 문제가 발견되었네요. 한 눈에 보이시나요?
흔히 하는 실수 중에 하나인데 sizeof() 에 구조체 타입이 아닌 구조체 포인터 타입을
주는 것이 문제였습니다. 포인터 타입이니 4바이트만 할당되겠죠. 구조체는 4바이트
보다 클테니 구조체에 어떤 값을 쓰다가 어디선가 깨뜨리게 됩니다.
이 문제는 다음과 같이 코드를 수정하면 해결됩니다.

 PMYHANDLE pMyHandle = my_malloc(sizeof(MYHANDLE));

메모리를 깨뜨리는 문제에서 이런 경우는 아주 운이 좋은 케이스입니다.
문제의 메모리를 할당하는 곳부터 살펴보기 시작했는데 거기서 바로 문제가 발견된
경우이니까요. 저 부분이 정상이었다면 pMyHandle 을 사용하는 모든 곳을 살펴봐야
하는 상황이 됩니다. 복잡한 코드가 많이 있다면 더더욱 갑갑해 지겠죠.

이것이 BugCheck 0xC1 이 가지는 한계이기도 합니다.
메모리가 깨졌을 때 바로 BugCheck 를 발생시키는 것이 아니라 깨진 메모리가 해제될
때 체크하는 방식이기 때문입니다. 깨뜨린 곳이 어디인지를 찾는 고통의 시간이 항상
필요합니다. 그렇다고 하더라도 이렇게라도 잡아주는 것에 감사해야 합니다. ^^

좀 더 손쉽게 문제의 정확한 위치를 찾는 방법이 있기는 합니다.
덤프파일을 분석하는 상황이었다면 위에서 처럼 소스를 뒤져서 원인을 찾는 것 밖에
방법이 없습니다. 하지만 live debugging 중이었다면 메모리 브레이크 포인트를 활용
할 수 있습니다.

처음에 죽었을 때 깨지는 위치를 확인한 후에 리부팅하고 다시 죽기 전에 깨지는 위치에
메모리 브레이크 포인트를 걸면 되겠지요.

kd> ba w1 83d76ffc
kd> bl
 0 e 83d76ffc w 1 0001 (0001)

이렇게 해 놓으면 누군가가 메모리를 꺠뜨리는 순간... 범인은 바로 잡힙니다.
생각만 해도 통쾌하네요. ^^

이번에는 메모리 깨뜨리는 문제에 대해서 살펴봤습니다.
골치아픈 문제지만 손쉽게 분석해서 해결하시기 바라겠습니다.

http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=6&s_bulu=&s_key=&no=27
반응형
Posted by GreeMate
WinDbg 디버깅2007. 6. 29. 00:47
반응형

드라이버를 개발할 때 Verifier 를 사용하면 매우 유용합니다.
사소 한 버그부터 심각한 오류까지 확실하게 체크해 주기 때문입니다.
Verifier가 오류를 발견했을 때 발생시키는 Bugcheck 중 오늘은 0xD6에 대 해서
살펴보도록 하겠습니다.

*** Fatal System Error: 0x000000d6
(0x82C5D000,0x00000000,0xF48BE08D,0x00000000)

Driver at fault:
*** MyDrv.sys - Address F48BE08D base at F48BA000, DateStamp 4507c937
.
Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
Loading Kernel Symbols
...........................................................................................................................
Loading User Symbols
.............................
Loading unloaded module list
....................
****************************************************************************
* *
* Bugcheck Analysis *
* *
****************************************************************************

Use !analyze -v to get detailed debugging information.

BugCheck D6, {82c5d000, 0, f48be08d, 0}

Probably caused by : MyDrv.sys ( MyDrv+408d )

Followup: MachineOwner
---------

nt!RtlpBreakWithStatusInstruction:
804e5592 cc int 3

기본적인 내용들이 보이지만 역시 하라는 대로 !analyze -v 먼저 해 봅 니다.

kd> !analyze -v
****************************************************************************
* *
* Bugcheck Analysis *
* *
****************************************************************************

DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6)
N bytes of memory was allocated and more than N bytes are being referenced.
This cannot be protected by try-except.
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: 82c5d000, memory referenced
Arg2: 00000000, value 0 = read operation, 1 = write operation
Arg3: f48be08d, if non-zero, the address which referenced memory.
Arg4: 00000000, (reserved)

Debugging Details:
------------------


READ_ADDRESS: 82c5d000 Special pool

FAULTING_IP:
MyDrv!my_strlen+d [d:\mytest\mydrv\my_str.c @ 67]
f48be08d 0fbe11 movsx edx,byte ptr [ecx]

MM_INTERNAL_CODE: 0

IMAGE_NAME: MyDrv.sys

DEBUG_FLR_IMAGE_TIMESTAMP: 4507c937

MODULE_NAME: MyDrv

FAULTING_MODULE: f48ba000 MyDrv

DEFAULT_BUCKET_ID: DRIVER_FAULT

BUGCHECK_STR: 0xD6

PROCESS_NAME: explorer.exe

TRAP_FRAME: f490977c -- (.trap fffffffff490977c)
ErrCode = 00000000
eax=82c5d000 ebx=81b85c00 ecx=82c5d000 edx=ffffff83 esi=81bd5e30 edi=81bdbd38
eip=f48be08d esp=f49097f0 ebp=f49097f4 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010282
MyDrv!my_strlen+0xd:
f48be08d 0fbe11 movsx edx,byte ptr [ecx] ds:0023:82c5d000=??
Resetting default scope

LAST_CONTROL_TRANSFER: from 8053425b to 804e5592

STACK_TEXT:
f49092cc 8053425b 00000003 82c5d000 00000000 nt!RtlpBreakWithStatusInstruction
f4909318 80534d2e 00000003 806f0298 c020b174 nt!KiBugCheckDebugBreak+0x19
f49096f8 8053531e 00000050 82c5d000 00000000 nt!KeBugCheck2+0x574
f4909718 80525f44 00000050 82c5d000 00000000 nt!KeBugCheckEx+0x1b
f4909764 804e3718 00000000 82c5d000 00000000 nt!MmAccessFault+0x6f5
f4909764 f48be08d 00000000 82c5d000 00000000 nt!KiTrap0E+0xcc
f49097f4 f48bce5f 82c5cfe0 f490980c 81bd1740 MyDrv!my_strlen+0xd [d:\mytest\mydrv\my_str.c @ 67]
f4909814 f48bcfa5 82c5cfe0 82c5cf00 f4909890 MyDrv!IsMyFilePath1+0xf [d:\mytest\mydrv\my_path.c @ 949]
f490982c f48bd01d 82c5cfe0 00000000 f4909890 MyDrv!IsMyFilePath+0x15 [d:\mytest\mydrv\my_path.c @ 990]
f490984c f48bb10f 81bd1710 81bd1700 f4909890 MyDrv!CompFilePath+0x4d [d:\mytest\mydrv\my_path.c @ 1013]
f4909864 f46eb2ce 81bd1710 81bd1700 f4909890 MyDrv!MyDrv_CompFilePath+0x2f [d:\mytest\mydrv\my_exp.c @ 142]
...

STACK_COMMAND: kb

FOLLOWUP_IP:
MyDrv!my_strlen+d [d:\mytest\mydrv\my_str.c @ 67]
f48be08d 0fbe11 movsx edx,byte ptr [ecx]

FAULTING_SOURCE_CODE:
63: size_t my_strlen(LPCSTR str)
64: {
65: const char *eos = str;
66:
> 67: while( *eos++ ) ;
68:
69: return( (int)(eos - str - 1) );
70: }
71:


SYMBOL_STACK_INDEX: 6

FOLLOWUP_NAME: MachineOwner

SYMBOL_NAME: MyDrv!my_strlen+d

FAILURE_BUCKET_ID: 0xD6_VRF_MyDrv!my_strlen+d

BUCKET_ID: 0xD6_VRF_MyDrv!my_strlen+d

Followup: MachineOwner
---------

역시 설명을 가만히 읽어보시기 바랍니다.
N 바이트 할당된 메모리인 데 N 바이트 보다 많이 읽으려고 시도했다는 내용입니다.
Verifier 에서 Special Memory Pool 옵션을 켜면 이런 상황을 잡아줍니다.
Verifier 의 도움없이 이런 버그를 잡는 일은 생각만 해도 끔찍합니다. ^^

메시지에서 하라는 대로 KiBugCheckDriver 변수의 내용을 살펴봅니다.

kd> db KiBugCheckDriver
8055be00 1c d2 d0 81 00 00 00 00-00 00 00 00 00 00 00 00 ................
8055be10 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

이런... 포인터군요.

kd> dd KiBugCheckDriver
8055be00 81d0d21c 00000000 00000000 00000000
8055be10 00000000 00000000 00000000 00000000

kd> db 81d0d21c
81d0d21c 14 00 14 00 3c d2 d0 81-00 40 10 0b 01 00 04 00 ....<....@......
81d0d22c ff ff ff ff bf e5 01 00-fe ff ff ff 00 00 00 00 ................
81d0d23c 4d 00 79 00 44 00 72 00-76 00 2e 00 73 00 79 00 M.y.D.r.v...s.y.
81d0d24c 73 00 00 00 00 00 00 00-5c 00 57 00 0e 00 1f 0a s.......\.W.....

메모리의 내용을 보아하니 UNICODE_STRING 구조체인 것 같습니다.
dt 명령어로 구조체 내용을 확인해 봅니다.

kd> dt _UNICODE_STRING 81d0d21c
"MyDrv.sys"
+0x000 Length : 0x14
+0x002 MaximumLength : 0x14
+0x004 Buffer : 0x81d0d23c "MyDrv.sys"

문제의 드라이버 이름이 잘 보이고 있네요.
사실 !analyze -v 메시지 에 MyDrv.sys 라는 이름이 수도 없이 나오기 때문에 구지
이렇게 보지 않아도 됩니다만 그래도 WinDbg가 하라는 것은 한번 해보는 것이 좋습니다.

메시지 중간에 TRAP_FRAME: f490977c -- (.trap fffffffff490977c) 가 있었으므로
이것도 역시 실행해 줍니다.

kd> .trap fffffffff490977c
ErrCode = 00000000
eax=82c5d000 ebx=81b85c00 ecx=82c5d000 edx=ffffff83 esi=81bd5e30 edi=81bdbd38
eip=f48be08d esp=f49097f0 ebp=f49097f4 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010282
MyDrv!my_strlen+0xd:
f48be08d 0fbe11 movsx edx,byte ptr [ecx] ds:0023:82c5d000=??

kv 명령으로 확인해 보면 위에 보였던 콜스택에서 변화가 생겼음을 확인 할 수 있습니다.

kd> kv
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr Args to Child
f49097f4 f48bce5f 82c5cfe0 f490980c 81bd1740 MyDrv!my_strlen+0xd
f4909814 f48bcfa5 82c5cfe0 82c5cf00 f4909890 MyDrv!IsMyFilePath1+0xf ...
f490982c f48bd01d 82c5cfe0 00000000 f4909890 MyDrv!IsMyFilePath+0x15 ...
f490984c f48bb10f 81bd1710 81bd1700 f4909890 MyDrv!CompFilePath+0x4d ...
f4909864 f46eb2ce 81bd1710 81bd1700 f4909890 MyDrv!MyDrv_CompFilePath+0x2f ...
...

이번에도 kn 으로 콜스택을 살펴보고 하나씩 따라 올라가면서 문제를 살 펴봅니다.

kd> kn
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr
00 f49097f4 f48bce5f MyDrv!my_strlen+0xd [d:\mytest\mydrv\my_str.c @ 67]
01 f4909814 f48bcfa5 MyDrv!IsMyFilePath1+0xf [d:\mytest\mydrv\my_path.c @ 949]
02 f490982c f48bd01d MyDrv!IsMyFilePath+0x15 [d:\mytest\mydrv\my_path.c @ 990]
03 f490984c f48bb10f MyDrv!CompFilePath+0x4d [d:\mytest\mydrv\my_path.c @ 1013]
04 f4909864 f46eb2ce MyDrv!MyDrv_CompFilePath+0x2f [d:\mytest\mydrv\my_exp.c @ 142]
...

스택 프레임 00 에서 문제가 발생한 것으로 보입니다.
바로 지역변수 를 확인해 봅니다.

kd> dv
str = 0x82c5cfe0 "\Device\HarddiskVolume1\???"
eos = 0x82c5d000 ""

eos 가 문제가 발생했다는 주소와 일치합니다.

Arguments:
Arg1: 82c5d000, memory referenced
Arg2: 00000000, value 0 = read operation, 1 = write operation

이 주소를 읽다가 죽었다는 의미입니다.
위 메시지에 나와있는 소스 코드를 보면 str 로부터 시작해서 eos까지 읽다가 죽은 것으로 보입니다.
str부터 eos까지 어떤 내용이 들어있는지 봅니다.

kd> db 0x82c5cfe0
82c5cfe0 5c 44 65 76 69 63 65 5c-48 61 72 64 64 69 73 6b \Device\Harddisk
82c5cff0 56 6f 6c 75 6d 65 31 5c-c3 c3 c3 c3 c3 c3 c3 c3 Volume1\........
82c5d000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
82c5d010 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????

문자열인데 NULL 로 끝나지 않는 것이 문제라는 것을 한눈에 알 수 있습 니다.
소스코드의 while( *eos++ )은 NULL 을 만나야 끝나는데 NULL 이 없으니 계속
진행하다가 ?? 에서 죽은 겁니다.

왜 NULL 로 끝나지 않은 문자열이 전달된 것인지 콜스택을 따라가며 알 아 봅니다.

kd> .frame 1
01 f4909814 f48bcfa5 MyDrv!IsMyFilePath1+0xf [d:\mytest\mydrv\my_path.c @ 949]
kd> dv
pszPath = 0x82c5cfe0 "\Device\HarddiskVolume1\???"

상위에서 전달받은 변수네요.

kd> .frame 2
02 f490982c f48bd01d MyDrv!IsMyFilePath+0x15 [d:\mytest\mydrv\my_path.c @ 990]
kd> dv
pszPath = 0x82c5cfe0 "\Device\HarddiskVolume1\???"

역시 상위에서 전달받은 변수군요.

kd> .frame 3
03 f490984c f48bb10f MyDrv!CompFilePath+0x4d [d:\mytest\mydrv\my_path.c @ 1013]
kd> dv
pszPath = 0x81bd1710 "\Device\HarddiskVolume1\"
nPathLen = 0x18
pszPathA = 0x82c5cfe0 "\Device\HarddiskVolume1\???"

여기에서 만들어져서 전달한 변수인 것 같습니다.
사실 이 내용은 kv 명령의 콜스택만 가만히 봐도 파악할 수 있는 사항입니다.

kd> kv
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr Args to Child
f49097f4 f48bce5f 82c5cfe0 f490980c 81bd1740 MyDrv!my_strlen+0xd
f4909814 f48bcfa5 82c5cfe0 82c5cf00 f4909890 MyDrv!IsMyFilePath1+0xf
f490982c f48bd01d 82c5cfe0 00000000 f4909890 MyDrv!IsMyFilePath+0x15
f490984c f48bb10f 81bd1710 81bd1700 f4909890 MyDrv!CompFilePath+0x4d
f4909864 f46eb2ce 81bd1710 81bd1700 f4909890 MyDrv!MyDrv_CompFilePath+0x2f

각라인에서 세번째 보이는 값들이 첫번째 인자이므로 82c5cfe0 는 계속 첫번째 인자로
전달되었음을 알 수 있습니다. 이것을 시작한 녀석이 CompFilePath 라는 것도
보입니다. 이 함수의 내용을 보면

BOOL CompFilePath(IN LPCTSTR pszPath)
{
UINT nPathLen;
LPSTR pszPathA;
...
nPathLen = my_strlen(pszPath);
pszPathA = (LPSTR)my_malloc(nPathLen+4);
if (pszPathA)
{
UnicodeToAnsi(pszPath, pszPathA, nPathLen);
bRet = IsMyFilePath(pszPathA);
}
...
}

이 함수는 전달받았던 pszPath를 pazPathA 로 변환한 후에 하위함수들로 전달합니다.
두개의 내용을 비교해 봅니다.

kd> db 0x81bd1710
81bd1710 5c 00 44 00 65 00 76 00-69 00 63 00 65 00 5c 00 \.D.e.v.i.c.e.\.
81bd1720 48 00 61 00 72 00 64 00-64 00 69 00 73 00 6b 00 H.a.r.d.d.i.s.k.
81bd1730 56 00 6f 00 6c 00 75 00-6d 00 65 00 31 00 5c 00 V.o.l.u.m.e.1.\.
81bd1740 00 00 00 00 43 15 01 00-00 d4 01 00 71 10 01 00 ....C.......q...

kd> db 0x82c5cfe0
82c5cfe0 5c 44 65 76 69 63 65 5c-48 61 72 64 64 69 73 6b \Device\Harddisk
82c5cff0 56 6f 6c 75 6d 65 31 5c-c3 c3 c3 c3 c3 c3 c3 c3 Volume1\........
82c5d000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
82c5d010 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????

유니코드 문자열 일때는 NULL로 끝나는 것이 보이는데 ANSI 로 변환한 후에는 NULL
이 보이지 않습니다. 이렇게 되면 소스코드에서 보이는 변 환함수인 UnicodeToAnsi()
가 문제의 주범이라는 사실이 밝혀집니다. 왜 변환하면서 NULL을 붙이지 않는지
확인해 보면 이 덤프의 원인이 나오 게 됩니다.

덤프는 이렇게 분석이 끝났지만 위에서 보이는 문제의 메모리에서 c3 과 ?? 라는
내용에 대해서 조금 더 이해하고 있으면 좋을 것 같습니다.
!pool 명령으로 메모리를 살펴봅니다.

kd> !pool 82c5cfe0
Pool page 82c5cfe0 region is Special pool
*82c5cfe0 size: 1c pagable special pool, Tag is MDRV
Owning component : Unknown (update pooltag.txt)

Special pool 영역이라고 나옵니다.
Verifier 에서 Special Memory Pool 옵션을 켜야 BugCheck 0xD6가 발생한다고
이미 위에서 언급했습니 다. 이 Special Pool 이란 메모리 할당, 해제, 사용에
대한 버그를 잡기 위해서 특별히 사용되는 메모리 영역을 의미합니다.

Special Pool 은 메모리를 하나 할당할 때마다 최소 2 Page(4KB)를 사용 합니다.
첫번째 Page는 사용자가 사용하는 영역이고 두번째 Page는 Guard Page 입니다.
Invalid 한 메모리 영역을 지정해 놓고 만약 이곳 을 접근하게 되면 Fault 가
발생하도록 설정해 놓은 것입니다. 이것이 바로 ?? 로 나오는 부분의 의미입니다.

이번 예제도 NULL을 못찾은 코드가 계속 진행하여 이 Guard Page 에 접 근하면서
문제가 발견된 것입니다.

Speical Pool 은 사용자가 1c 만큼만 할당해도 실제로는 8KB 의 메모리 가 할당될
만큼 메모리 낭비가 심하기 때문에 Special Pool 로 예약된 영역을 모두 소진하면
Special Pool 에서 할당하지 않고 일반 메모리에 서 할당합니다. 물론 Special
Pool 이 여유가 생기면 다시 Special Pool 할당이 운영됩니다. 따라서 Special
Pool 이 항상 모든 문제를 체크할 수는 없다는 점도 참고하시기 바랍니다.

위에서 설명한 Special Pool 의 구조를 확인해 봅니다.
문제의 메모 리를 조금 당겨서 확인해 보면

kd> db 82c5cfe0 - 10
82c5cfd0 c3 c3 c3 c3 c3 c3 c3 c3-c3 c3 c3 c3 c3 c3 c3 c3 ................
82c5cfe0 5c 44 65 76 69 63 65 5c-48 61 72 64 64 69 73 6b \Device\Harddisk
82c5cff0 56 6f 6c 75 6d 65 31 5c-c3 c3 c3 c3 c3 c3 c3 c3 Volume1\........
82c5c000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????

c3 으로 채워져 있고 Guard Page 가 나오기 전에 우리가 사용한 내용이 저장되어
있습니다. Guard Page 직전의 영역을 사용자에게 할당해서 Guard Page 가 효과를
보기 쉽도록 구성되어 있습니다. 사용자는 항상 첫번째 Page의 뒷부분을 기준으로
할당 받습니다.

이 메모리의 헤더는 첫 페이지의 맨 앞부분입니다.

kd> db 82c5c000
82c5c000 1c c0 c3 00 4d 44 53 56-00 00 00 00 08 4f f1 81 ....MDRV.....O..
82c5c010 c3 c3 c3 c3 c3 c3 c3 c3-c3 c3 c3 c3 c3 c3 c3 c3 ................
82c5c020 c3 c3 c3 c3 c3 c3 c3 c3-c3 c3 c3 c3 c3 c3 c3 c3 ................
82c5c030 c3 c3 c3 c3 c3 c3 c3 c3-c3 c3 c3 c3 c3 c3 c3 c3 ................

첫번째 바이트 1c 가 할당된 메모리 크기입니다.
두번째 바이트 c0 는 뭔지 모르겠네요.
세번째 바이트 c3 는 Page 를 채워놓는 값입니다. 82c5c010 ~ 82c5cfe0 모두 이 값
으로 채워져 있습니다. 이 값은 아마도 Verifier가 메모리가 깨진 것을 확인할 때
사용될 것입니다.
네번재 바이트부터 4바이트는 메모리 태그입니다.
그 뒤는 역시 뭔지 잘 모르겠 네요.
이 내용과 !pool 에서 출력된 결과를 보면 동일하다는 것을 알 수 있을 것입니다.

이렇게 해서 Special Pool 로 발생한 BugCheck와 Special Pool 에 대한 내용을
살펴봤습니다. 아주 유용한 Verifier와 Special Pool 모두 잘 활 용하시기 바랍니다.

http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=6&s_bulu=&s_key=&no=26
반응형
Posted by GreeMate
WinDbg 디버깅2007. 6. 29. 00:45
반응형

덤프파일을 분석하는데 있어서 BugCheck Code는 아주 기본적인 정보일 뿐만아니라
분석을 풀어나가는 실마리가 됩니다. 그래서 누군가 BSOD 가 떴다고 문제를 풀어달라고
할 때 가장 먼저 물어 보게 되는게 "BugCheck Code 가 무엇입니까?" 가 되는 겁니다.

이제부터 여러가지 BugCheck Code 들에 대해서 기본적으로 어떻게 분석을 진행해
나가는지 예제를 통해서 설명하려고 합니다. BugCheck Code 마다 다소 다른점은
있지만 기본적인 맥락은 동일하다는 것을 곧 알게 될 것입니다. 기본적인 분석방법을
익혀서 BSOD 가 발생했을 때 당황하지 않고 스스로 문제의 원인을 찾고 해결할 수
있는 능력을 가지는 것이 중요합니다. WinDbg Help 에도 BugCheck Code 마다의
분석 방법 및 예제가 몇가지 있으므로 이것들을 확인하는 습관을 가지는 것도 좋습니다.

여기서는 기본적으로 개발한 모듈의 심볼(pdb파일)을 가지고 있다는 전제를 합니다.
심볼이 없다면 WinDbg 디버깅이 아주 갑갑해 지겠지요.

첫번째로 BugCheck 0x50 부터 시작합니다.
live debugging 시에 문제가 생기면 다음과 같이 시스템이 정지합니다.


*** Fatal System Error: 0x00000050
                       (0xE2BFD011,0x00000000,0xF2EF9C65,0x00000001)

Driver at fault:
***  mydrv.sys - Address F2EF9C65 base at F2E98000, DateStamp 44ed34b4
.
Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
Loading Kernel Symbols
...................................................................................................................
Loading User Symbols
PEB is paged out (Peb.Ldr = 7ffdf00c).  Type ".hh dbgerr001" for details
Loading unloaded module list
.................
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

Use !analyze -v to get detailed debugging information.

BugCheck 50, {e2bfd011, 0, f2ef9c65, 1}

Probably caused by : mydrv.sys ( mydrv!My_strcmp+15 )

Followup: MachineOwner
---------

nt!RtlpBreakWithStatusInstruction:
804e5592 cc              int     3

여기까지가 기본적으로 뿌려집니다.
안타깝게도 이 상태에서 더 뭘 해야할지 모르고 재부팅을 해 버리는 경우가 많은데요.
이때 문제의 원인을 정확히 파악하고 문제를 해결해야 합니다.

자... 덤프 분석의 초식 1장 나갑니다. "WinDbg 가 하라는 대로 따라하라!"

위에 이미 나온 메시지들을 주의깊게 살펴볼 필요가 있습니다.
WinDbg 가 많은 정보를 알려주고 있습니다. 그중에서 우리에게 하라고 하는 것을 눈여겨 보시기 바랍니다.

바로 이줄 입니다. => Use !analyze -v to get detailed debugging information.
단지 우리는 하라는대로 하면 됩니다. 특히 !analyze -v 는 모든 덤프분석에서 첫번째로 하게 되는 작업이
될테니 앞으로 우리는 자동적으로 이 명령을 내리게 될 것입니다. 하지만 그 이외에도 하라고 하는게 있는지
항상 살펴보시기 바랍니다.

이제 하라는대로 명령을 내려 봅니다.

kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced.  This cannot be protected by try-except,
it must be protected by a Probe.  Typically the address is just plain bad or it
is pointing at freed memory.
Arguments:
Arg1: e2bfd011, memory referenced.
Arg2: 00000000, value 0 = read operation, 1 = write operation.
Arg3: f2ef9c65, If non-zero, the instruction address which referenced the bad memory
 address.
Arg4: 00000001, (reserved)

Debugging Details:
------------------


READ_ADDRESS:  e2bfd011 Paged pool

FAULTING_IP:
mydrv!My_strcmp+15 [d:\mytest\mydrv\my_string.c @ 179]
f2ef9c65 8a02            mov     al,byte ptr [edx]

MM_INTERNAL_CODE:  1

IMAGE_NAME:  mydrv.sys

DEBUG_FLR_IMAGE_TIMESTAMP:  44ed34b4

MODULE_NAME: mydrv

FAULTING_MODULE: f2e98000 mydrv

DEFAULT_BUCKET_ID:  DRIVER_FAULT

BUGCHECK_STR:  0x50

PROCESS_NAME:  wuauclt.exe

TRAP_FRAME:  f4674280 -- (.trap fffffffff4674280)
ErrCode = 00000000
eax=f2ee6f6c ebx=81cec900 ecx=e2bfd011 edx=e2bfd011 esi=81b1d3f8 edi=f4674634
eip=f2ef9c65 esp=f46742f4 ebp=f4674308 iopl=0         nv up ei ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010282
mydrv!My_strcmp+0x15:
f2ef9c65 8a02            mov     al,byte ptr [edx]          ds:0023:e2bfd011=??
Resetting default scope

LAST_CONTROL_TRANSFER:  from 8053425b to 804e5592

STACK_TEXT: 
f4673dd0 8053425b 00000003 e2bfd011 00000000 nt!RtlpBreakWithStatusInstruction
f4673e1c 80534d2e 00000003 806f0298 c038aff4 nt!KiBugCheckDebugBreak+0x19
f46741fc 8053531e 00000050 e2bfd011 00000000 nt!KeBugCheck2+0x574
f467421c 80525f44 00000050 e2bfd011 00000000 nt!KeBugCheckEx+0x1b
f4674268 804e3718 00000000 e2bfd011 00000000 nt!MmAccessFault+0x6f5
f4674268 f2ef9c65 00000000 e2bfd011 00000000 nt!KiTrap0E+0xcc
f4674308 f2ee82d3 e2bfd011 f2ee6f6c 00000000 mydrv!My_strcmp+0x15 [d:\mytest\mydrv\my_string.c @ 179]
f467435c f2ee709a e2909008 f2f1af04 e1196008 mydrv!My_Func3+0x323 [d:\mytest\mydrv\my_func3.c @ 78]
f467439c f2f09c01 e22088d8 e24d5418 00000000 mydrv!My_Func2+0x109 [d:\mytest\mydrv\my_func2.c @ 24]
f46743c0 f2f09b6a e22088d8 e24d5418 00000001 mydrv!My_Func1+0x91 [d:\mytest\mydrv\my_func1.c @ 118]
...
f4674c88 805964bf e1efa000 00080000 81d57020 nt!CcPfPrefetchScenario+0x7b
f4674d04 805831af 81d57020 e254f158 00000000 nt!CcPfBeginAppLaunch+0x158
f4674d50 804fc4da 00000000 7c810665 00000001 nt!PspUserThreadStartup+0xeb
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16


STACK_COMMAND:  kb

FOLLOWUP_IP:
mydrv!My_strcmp+15 [d:\mytest\mydrv\my_string.c @ 179]
f2ef9c65 8a02            mov     al,byte ptr [edx]

FAULTING_SOURCE_CODE: 
   175:
   176: int My_strcmp( const char* cs, const char* ct )
   177: {
   178: #ifdef HAVE_STRCMP
>  179:  return strcmp( cs, ct );
   180: #else
   181:  int res;
   182:
   183:  for( ; ; )
   184:  {


SYMBOL_STACK_INDEX:  6

SYMBOL_NAME:  mydrv!My_strcmp+15

FOLLOWUP_NAME:  MachineOwner

FAILURE_BUCKET_ID:  0x50_VRF_mydrv!My_strcmp+15

BUCKET_ID:  0x50_VRF_mydrv!My_strcmp+15

Followup: MachineOwner
---------

길게도 분석결과가 나왔습니다.
메시지를 처음부터 살펴보면 이 버그체크는 PAGE_FAULT_IN_NONPAGED_AREA 라는 것을 알려주고
이것은 잘못된 시스템 메모리를 참조해서 발생한 것이라고 친절하게 설명해 주고 있습니다.

앞으로 모든 덤프를 분석할 때 반드시 이 설명 문구를 읽으시기 바랍니다. 어떤 문제 때문에 발생한
것이고 심지어는 어떻게 분석하라는 내용까지 있는 경우도 있습니다. 그리고 WinDbg 가 업그레이드
될 때마다 메시지의 내용이 더더욱 심오해 집니다. 더 자세하게 기술되어 분석하기가 쉬워집니다.

이 내용을 읽었는데 여기서 하라는 것이 있으면 그대로 하면 됩니다. 이번 예제에서는 하라는 것은 없네요.
계속해서 전체적인 내용을 살펴봅니다.

친절하게도 콜스택까지 잘 보여주고 있습니다. 운이 좋으면 이 콜스택만 봐도 무슨 문제인지
금방 알 수도 있습니다. 하지만 이번에는 그런 경우는 아닌 것 같습니다. 좀 더 보도록 합시다.

최신 WinDbg 에서는 문제가 발생한 소스코드를 보여주는 기능이 추가되었습니다.
FAULTING_SOURCE_CODE 에서 보이고 있습니다. 역시 운이 좋으면 여기만 봐도 문제가 뭔지 알 수 있겠지요?

strcmp 에서 죽었는데 cs 에서 죽은건지 ct 에서 죽은건지 모르겠네요.
좀 더 분석해 봐야 할 것 같습니다.

메시지 중간에 다음과 같은 라인이 있습니다.
TRAP_FRAME:  f4674280 -- (.trap fffffffff4674280)

앞으로 이와 같은 라인이 보이면 무조건 .trap fffffffff4674280 명령을 실행하기 바랍니다.

kd> .trap fffffffff4674280
ErrCode = 00000000
eax=f2ee6f6c ebx=81cec900 ecx=e2bfd011 edx=e2bfd011 esi=81b1d3f8 edi=f4674634
eip=f2ef9c65 esp=f46742f4 ebp=f4674308 iopl=0         nv up ei ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010282
mydrv!My_strcmp+0x15:
f2ef9c65 8a02            mov     al,byte ptr [edx]          ds:0023:e2bfd011=??

콜스택을 보면서 설명하면 strcmp 에서 문제가 발생했을 때 시스템은 Trap 을 발생시켰고
OS 가 Trap 을 핸들링하면서 결국 BugCheck 를 호출하고 시스템이 죽은 겁니다.
.trap 명령을 수행하기 전에 WinDbg 에 보이는 레지스터 값들을 보면 RtlpBreakWithStatusInstruction
를 호출했을 때의 값들이 보여지고 있습니다. 이것은 strcmp 에서 문제가 발생했을 때의
레지스터 값들이 아니므로 문제를 분석하는데 전혀 도움이 되지 않습니다. strcmp 에서 문제가
발생했을 때의 레지스터 값들은 Trap 이 발생하면서 Trap Frame 에 저장되었고 .trap 명령을 사용하면
trap 이 발생한 시점의 상황으로 레지스터 값들을 돌려줍니다. 친절하게 명령과 파라미터까지
보여주니 그대로 실행해 주면 되는 겁니다.

이렇게 하면 콜스택이 문제가 발생했을 때의 상황으로 정리됩니다.
k 명령을 사용해서 콜스택을 다시 봅니다.

kd> kn
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr 
00 f4674308 f2ee82d3 mydrv!My_strcmp+0x15 [d:\mytest\mydrv\my_string.c @ 179]
01 f467435c f2ee709a mydrv!My_Func3+0x323 [d:\mytest\mydrv\my_func3.c @ 78]
02 f467439c f2f09c01 mydrv!My_Func2+0x109 [d:\mytest\mydrv\my_func2.c @ 24]
03 f46743c0 f2f09b6a mydrv!My_Func1+0x91 [d:\mytest\mydrv\my_func1.c @ 118]
...

n 옵션을 사용한 이유는 맨 앞에 스택 프레임 번호를 보이기 위함입니다. 나중에 k 명령 설명에서
자세하게 다루기로 하구요.

실제 디버깅 할 때는 콜스택 창에 위의 내용이 보이고 있을 것이고 첫번째 줄을 더블클릭하면
소스도 나타나고 지역변수 창에 변수들도 나올 텐데, 저는 지금 command 창에서만 설명해야 하니
여러가지 명령을 동원하게 되었습니다. MS 본사의 디버깅 고수들은 command 창 하나만 띄워놓고
현란한 명령어들로 디버깅하기도 합니다. 그걸 따라하고 싶어서 이러는 건 아닙니다. ^^

여하튼 저는 command 로 지역변수를 보고 싶습니다.
일단 스택 프레임을 첫번째 스택 라인에 맞춰 놓으려 합니다. 스택 프레임 번호가 00 이죠.
.frame 명령을 사용해서 스택 프레임을 변경합니다.

kd> .frame 0
00 f4674308 f2ee82d3 mydrv!My_strcmp+0x15 [d:\mytest\mydrv\my_string.c @ 179]

그리고 나서 지역변수를 보이는 dv 명령을 사용하면 이 함수가 호출되었을 때의 지역변수를 볼 수 있습니다.

kd> dv
             cs = 0xe2bfd011 ""
             ct = 0xf2ee6f6c "GetFileAttributesA"

이제 cs와 ct 가 뭔지 알 수 있습니다.

이렇게 찾고 보면 처음에 BugCheck에서 보여줬던 첫번째 값이 바로 cs 였다는 것을 알게 됩니다.
문제의 잘못된 메모리는 cs 라는 것입니다.

BugCheck 50, {e2bfd011, 0, f2ef9c65, 1}
Arguments:
Arg1: e2bfd011, memory referenced.
Arg2: 00000000, value 0 = read operation, 1 = write operation.

잘못된 메모리 e2bfd011 을 읽다가 죽었다는 뜻이었군요.

이렇게 되면 My_strcmp 함수는 혐의를 벗어나게 됩니다. 잘못된 메모리를 전달받은 죄 밖에 없거든요.
잘못된 메모리를 전달한 녀석이 문제지요. My_strcmp 를 호출한 녀석을 분석해야 합니다.
스택 프레임이 01 이군요.

kd> .frame 1
01 f467435c f2ee709a mydrv!MyFunc3+0x323 [d:\mytest\mydrv\my_func3.c @ 78]

지역변수도 보고 소스코드도 봅니다.

kd> dv
 pNameStrBuffer = 0xe2bf8008
      dwNameStr = 0x4ffd
       pNameStr = 0xe2bfd011 ""
         nFound = 0
              j = 0

*** 소스코드 ***
 pNameStr = (char*)pNameStrBuffer->abyData + dwNameStr;

 nFound = 0;
 for( j = 0; j < nNumOfFunctions; j++ )
 {
  if ( My_strcmp( pNameStr, ppNames[j] ) == 0 )

My_strcmp의 첫번재 파라미터는 pNameStr 입니다. 이게 문제구요.
호출하기 직전에 pNameStr 을 계산하는 것을 볼 수 있습니다. 이 계산이 뭐가 잘못된 것일까요?
결국 abyData + dwNameStr 의 결과가 e2bfd011 이라는 잘못된 메모리라는 뜻입니다.

abyData 를 살펴보기 위해 pNameStrBuffer 의 구조체 타입을 찾아서 살펴봅니다.
dt 명령은 구조체를 확인할 때 사용합니다. 역시 나중에 dt 명령 설명에서 자세히 다루도록 하구요.

kd> dt _MYNAMEBUFFER 0xe2bf8008
   +0x000 dwPos            : 0
   +0x004 ulFp             : 0
   +0x008 nLen             : 0x140000
   +0x00c abyData          : [1024]  "???"

이렇게 보니 문제가 드러납니다.

abyData 는 1024 바이트의 공간만 존재하는데 dwNameStr(0x4ffd) 만큼 더한 뒤에 있는 영역은
이 범위를 벗어나는 잘못된 메모리인 것입니다.

정확한 원인을 밝혀내기 위해서는 dwNameStr 에 왜 0x4ffd 와 같은 터무니 없는 값이
들어갔는지를 알아야 합니다. 그것은 더 상위 함수를 분석해 본다던가 소스코드를 살펴본다던가
해서 추가로 진행하면 될 것입니다.

여기까지 분석한 방법을 응용하면 여러분들도 BugCheck 0x50 을 어렵지 않게 분석할 수 있을 것입니다.

이 첫번째 예제를 설명하면서 제가 강조하고 싶은 부분은 WinDbg 메시지 내용 중에는 분석에 사용할 수
있는 명령어들이 존재하고 있다는 점입니다. 이것들을 눈여겨 보고 적절히 사용하면 분석에 많은 도움이
되므로 십분 활용하시기 바랍니다.


http://www.driveronline.org/bbs/view.asp?tb=tipbbs&GotoPage=6&s_bulu=&s_key=&no=24
반응형
Posted by GreeMate
WinDbg 디버깅2007. 5. 7. 23:54
반응형

BugCheck 0x19 의 또 다른 형태를 살펴보면서 메모리풀 관리에 대한 내용을 조금 더 깊게 확인해 보겠습니다. 좀처럼 풀리지 않는 메모리 깨짐 문제를 삽질하다 보니 커널의 메모리 할당/해제 방식을 살펴보게 되었습니다.
오늘은 커널이 small memory pool 을 어떻게 관리하는지에 대한 내용을 설명하게 될 것입니다.

kd> !analyze -v
****************************************************************************
*                                                                          *
*                        Bugcheck Analysis                                 *
*                                                                          *
****************************************************************************

BAD_POOL_HEADER (19)
The pool is already corrupt at the time of the current request.
This may or may not be due to the caller.
The internal pool links must be walked to figure out a possible cause of
the problem, and then special pool applied to the suspect tags or the driver
verifier to a suspect driver.
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: 8614b6c2, The pool entry we were looking for within the page.
Arg3: 8614b6ca, The next pool entry.
Arg4: 0a010000, (reserved)

Debugging Details:
------------------

BUGCHECK_STR:  0x19_20
POOL_ADDRESS:  8614b6c2 Nonpaged pool
DEFAULT_BUCKET_ID:  DRIVER_FAULT
PROCESS_NAME:  System

LAST_CONTROL_TRANSFER:  from 8054d741 to 8053531e

STACK_TEXT: 
f7a29928 8054d741 00000019 00000020 8614b6c2 nt!KeBugCheckEx+0x1b
f7a29978 8054d0b9 8614b6ca 00000000 f7a29994 nt!ExFreePoolWithTag+0x2be
f7a29988 a9f45c5f 8614b6ca f7a299a0 a9f43530 nt!ExFreePool+0xf
WARNING: Stack unwind information not available. Following frames may be wrong.
f7a29994 a9f43530 85629ae0 f7a299b0 a9f435b7 MyDrv+0xbc5f
f7a299a0 a9f435b7 85960418 85960418 f7a299c8 MyDrv+0x9530
f7a299b0 a9f42c05 85960418 00000000 85960418 MyDrv+0x95b7
f7a299c8 a9f40362 f7a299e0 856a866c 00000000 MyDrv+0x8c05
...

STACK_COMMAND:  kb

FOLLOWUP_IP:
MyDrv+bc5f
a9f45c5f 8b4d08          mov     ecx,dword ptr [ebp+8]

SYMBOL_STACK_INDEX:  3
SYMBOL_NAME:  MyDrv+bc5f
FOLLOWUP_NAME:  MachineOwner
MODULE_NAME: MyDrv
IMAGE_NAME:  MyDrv.sys

DEBUG_FLR_IMAGE_TIMESTAMP:  4501c758
FAILURE_BUCKET_ID:  0x19_20_MyDrv+bc5f
BUCKET_ID:  0x19_20_MyDrv+bc5f

Followup: MachineOwner
---------

지난 번에 봤던 BucCheck 0x19 입니다.
콜스택에서 ExFreePool 을 호출할 때 전달한 메모리 주소는 아래와 같이 8614b6ca 입니다.

f7a29988 a9f45c5f 8614b6ca f7a299a0 a9f43530 nt!ExFreePool+0xf

메시지에서 Arg2 를 보면 문제의 순간에 다루던 메모리 주소를 볼 수 있습니다.
Arg2: 8614b6c2, The pool entry we were looking for within the page.

ExFreePool 에 전달된 메모리 주소에서 8을 뺀 값이죠. ( 8614b6c2 = 8614b6ca - 8 )
8614b6c2 는 8614b6ca 메모리 영역의 헤더 영역입니다.
메모리 할당도 실제로는 여기부터 시작되는거구요.

일단 해제되는 메모리 8614b6ca 를 확인해 봅니다.

kd> !pool 8614b6ca
Pool page 8614b6ca region is Nonpaged pool
    ...                ...
 8614b520 size:   58 previous size:   58  (Allocated)  RBev
 8614b578 size:   f8 previous size:   58  (Allocated)  Driv
 8614b670 size:   18 previous size:   f8  (Free)       HidU
 8614b688 size:   28 previous size:   18  (Free )  NtFs
*8614b6b0 size:   28 previous size:   28  (Allocated) *NtFs
  Pooltag NtFs : StrucSup.c, Binary : ntfs.sys
8614b6d8 is not a valid small pool allocation, checking large pool...
unable to get pool big page table - either wrong symbols or pool tagging is disabled
8614b6d8 is freed (or corrupt) pool
Bad previous allocation size @8614b6d8, last size was 5

***
*** An error (or corruption) in the pool was detected;
*** Attempting to diagnose the problem.
***
*** Use !poolval 8614b000 for more details.
***

Pool page [ 8614b000 ] is __inVALID.

Analyzing linked list...
[ 8614b6b0 --> 8614b730 (size = 0x80 bytes)]: Corrupt region

Scanning for single bit errors...
None found

지난 번과 뭐가 다른 것을 느끼셨나요?
다시 한번 잘 보시기 바랍니다. 뭘까요???

!pool 명령어의 리스트에는 제가 입력한 8614b6ca 의 헤더주소인 8614b6c2 가 존재하지 않습니다.
잘 보시면 8614b6c2 는 8614b6b0 ~ 8614b6d8 사이에 존재하는 메모리 주소인 것을 알 수 있습니다.

*8614b6b0 size:   28 previous size:   28  (Allocated) *NtFs
  Pooltag NtFs : StrucSup.c, Binary : ntfs.sys
8614b6d8 is not a valid small pool allocation, checking large pool...

NTFS 가 사용중인 영역으로 나오는데요...
이게 어떤 상황인지 이해하는 것은 사실 쉽지 않습니다.

지난 번에 문제가 된 메모리 주소는 !pool 에서 나오는 리스트 중의 하나였고 이것은 커널에 의해서
정상적으로 할당된 메모리였습니다. 하지만 이번 경우는 !pool 리스트에 존재하는 메모리
주소가 아니기 때문에 커널에서 정상적으로 할당받은 것인지 아닌지 아리송 합니다. ^^

하지만 일단 ExFreePool 에 해제하라고 전달한 포인터이기 때문에 ExAllocatePool 에서
할당받은 포인터였을 거라고 인정하는게 좋을 것 같습니다.

그.렇.다.면...

어떻게 해서 저런 메모리주소가 ExAllocatePool 에서 나올 수 있었던 걸까요?
잘 보시면 정상적으로 할당된 메모리 주소는 모두 0 아니면 8 로 끝나는 형태를 가집니다.
하지만 문제의 메모리 주소는 8614b6ca 이므로 a로 끝납니다.

이런 일이 어떻게 있을 수 있을까 고민하다가 ExAllocatePoolWithTag와 ExFreePoolWithTag 를
분석하기에 이르렀습니다. -_-;;;

제가 알고 싶은 Small Pool 의 NonPagedPool 할당에 대한 부분만 집중해서 분석하고 나머지는 대충 무시했습니다. (Large Pool 이라는 것도 있죠? 이건 어떻게 동작하는지 아직 잘 모르겠습니다. 누가 좀... ^^)

ExAllocatePoolWithTag 과 ExFreePoolWithTag 의 pseudo 코드는 다음과 같습니다.

ExAllocatePoolWithTag(type, size, tag)
{
  ...
  // size 에 헤더크기를 포함하여 8 byte 단위로 가리킬 수 있는 index를 만든다.
  index = (size + f) / 8

  // ProcessorBlock 주소를 구한다.
  eax = ffdff120h

  // ProcessorBlock 의 FreeNonPagedPoolList(598h옵셋) 에서 index에 해당하는 Head 를 구한다.
  edi = eax + (index * 8) + 598h

  // FreeNonPagedPoolList 에서 하나를 꺼낸다.
  esi = ExInterlockedPopEntrySList(edi , ...)

  // 헤더를 참조하게 한다.
  esi = esi - 8

  // 헤더+4 태그영역에 태그를 복사한다.
  [esi+4] = tag

  // 메모리 주소를 참조하게 한다.
  eax = esi + 8

  // 메모리 주소를 리턴한다.
  return eax
  ...
}

ExFreePoolWithTag(ptr, tag)
{
  ...

  // 헤더+2 에 저장된 index 를 구한다. (index * 8 은 실제 메모리 영역의 크기)
  index = word ptr [ptr-6]
  ...

  // ProcessorBlock 의 FreeNonPagedPoolList(598h옵셋) 에서 index에 해당하는 Head 를 구한다.
  ebx = eax + (index * 8) + 598h

  // FreeNonPagedPoolList 에 넣는다.
  InterlockedPushEntrySList(ebx, ptr)

  // ptr 에는 NextLink 주소가 기록된다.
  ...
}

이 분석을 통해 알게된 사실은 커널이 메모리 할당/해제를 효율적으로 운영하기 위해서 FreeList
를 관리한다는 점이었습니다. 해제되는 메모리는 같은 크기를 가지는 FreeList 에 넣어놓고 할당
요청이 오면 같은 크기의 FreeList 에서 바로 꺼내오는 방식으로 관리하는 것입니다.

이 FreeList 는 ffdff120h + 598h 에 위치하는데 ffdff120 의 의미가 궁금해져서 구글링을 좀 해보니 다음과 같이 KiProcessorBlock 이라는 커널 전역변수에 저장된 값이라는 것을 알게 되었습니다. 대부분 시스템에서 ffdff120h 로 같은 값을 가지고 있었습니다.

kd> dd KiProcessorBlock
8055b320  ffdff120 00000000 00000000 00000000
8055b330  00000000 00000000 00000000 00000000

계속해서 ffdff120h + 598h 에 위치한 FreeNonPagedPoolList 을 좀 더 자세히 살펴봅니다.

kd> dd ffdff120 + 598 + (0*8)
ffdff6b8  00000000 00000000 863b1000 80557000
ffdff6c8  863b1100 80557080 863b1200 80557100
ffdff6d8  863b1300 80557180 863b1400 80557200
ffdff6e8  863b1500 80557280 863b1600 80557300
ffdff6f8  863b1700 80557380 863b1800 80557400
ffdff708  863b1900 80557480 863b1a00 80557500
ffdff718  863b1b00 80557580 863b1c00 80557600
ffdff728  863b1d00 80557680 863b1e00 80557700

계산방식으로 볼때 8 바이트 단위로 배열되어 있는 것으로 보입니다.
여기서 863b1000, 863b1100, 863b1200, 863b1300 등은 index 1, 2, 3, 4 일 때 참조되는 값이고
이것을 Head 로 사용해서 실제 메모리를 링크드 리스트로 연결합니다.

예를 들어 863b1100 을 살펴보면

kd> dd 863b1100 L1
863b1100  855a6380

다음 링크 주소가 있으므로 dl 명령으로 다시 봤습니다.

kd> dl 863b1100
863b1100  855a6380 73500002 01000004 0009c075
855a6380  854ae808 85fea128 0a150002 e56c6946
854ae808  00000000 85e9c128 0a050002 7346744e

두개의 링크가 존재하네요.

어디선가 index 2 (헤더크기를 포함한 메모리크기 9 ~ 16) 에 해당하는 할당을 요청하면 ExAllocatePoolWithTag 는 855a6380 을 바로 꺼내서 리턴해 줍니다.

855a6380 의 헤더를 보면

kd> db 855a6380 - 8
855a6378  01 00 02 02 54 43 49 31-08 e8 4a 85 28 a1 fe 85  ....TCI1..J.(...
855a6388  02 00 15 0a 46 69 6c e5-70 00 00 00 e8 00 00 00  ....Fil.p.......

세번째 바이트인 size(사실은 index) 에 02 가 정확히 적혀 있습니다.
다음 링크인 854ae808 도 헤더를 보면 size 02 가 명확히 보입니다.

kd> db 854ae808 - 8
854ae800  01 00 02 02 54 43 49 31-00 00 00 00 28 c1 e9 85  ....TCI1....(...
854ae810  02 00 05 0a 4e 74 46 73-01 00 00 00 74 0a 7b aa  ....NtFs....t.{.

즉, 같은 크기의 해제된 메모리 블럭 리스트라는 것을 확인한 것입니다.
재미있는 점은 메모리가 Free 되면 ExFreePoolWithTag 안에서

  // FreeNonPagedPoolList 에 넣는다.
  InterlockedPushEntrySList(ebx, ptr)

가 수행되는 바람에 ptr 의 내용은 사용자가 사용하던 데이터가 지워지고 NextLink 주소로
변경된다는 점입니다. 이 내용은 주목할 만한 부분이므로 잠시 기억해 두시기 바랍니다.

여하튼 전체적인 이해를 돕기위해 위 내용을 대강 그림으로 표현하면 다음과 같습니다.

    ProcessorBlock( ffdff120 )
|-------------------------------|
|  0h           ...             |      
|  .            ...             |      
|  .            ...             |    
|  .            ...             |       ListHead  NextLink  NextLink
| 598h  FreeNonPagedPoolList[x] | [0]-> 00000000
|  .            ...             | [1]-> 863b1000->xxxxxxxx->xxxxxxxx ( size 1~8 )
|-------------------------------| [2]-> 863b1100->855a6380->854ae808 ( size 9~16 )
                                  [3]-> 863b1200->xxxxxxxx->xxxxxxxx ( size 17~24 ) 

이 ProcessorBlock 의 내용은 다음과 같이 확인할 수 있습니다.

kd> dt nt!_KPRCB ffdff120
   +0x000 MinorVersion     : 1
   +0x002 MajorVersion     : 1
   +0x004 CurrentThread    : 0x863b5b30 _KTHREAD
   +0x008 NextThread       : (null)
   +0x00c IdleThread       : 0x8055ac20 _KTHREAD
      .      ...
      .      ...
   +0x520 PPLookasideList  : [16] _PP_LOOKASIDE_LIST
   +0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST
   +0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST
      .      ...
      .      ...

제가 FreeNonPagedPoolList 라고 표현했던 필드의 이름이 실제로는 PPNPagedLookasideList 였네요.
이 리스트의 시작도 실제로는 0x5a0 (0x598 + 8) 이구요. 왜 Disassembly 상에서는 598h 를 기준으로 계산하는지는 잘 모르겠습니다. 그리고 그 밑에 PPPagedLookasideList 도 보입니다. PagedPool 도 같은 방식으로 관리된다는 것을 알 수 있습니다.

_PP_LOOKASIDE_LIST 구조체가 어떤 구조를 가지는지 살펴볼까요?

kd> dt _PP_LOOKASIDE_LIST
   +0x000 P                : Ptr32 _GENERAL_LOOKASIDE
   +0x004 L                : Ptr32 _GENERAL_LOOKASIDE

이중에서 P 에 해당하는 영역만 위에서 설명한 거네요.
L 은 어떤 용도인지 궁금하지만 더 이상 알아보지는 않았습니다.

이제야 메모리 할당/해제가 어떻게 이루어 지는지 이해했습니다.
다시 원래 문제로 돌아가서 8614b6ca 라는 주소가 어떻게 할당된 것인지 생각해 봅니다.

흠...
아무리 생각해도 말이 안되네요.

누군가 ExAllocatePoolWithTag 를 호출했을 때 8614b6ca 같은 비정상적인 값이 리턴되려면
FreeNonPagedPoolList 에 이미 8614b6ca 가 들어 있었어야 합니다.
어떻게 이런 일이 가능할까요?

이제부터는 추리소설입니다. ^^
첫번째 가정은 누군가가 ExFreePoolWithTag 를 호출할 때 8614b6ca 를 파라미터로 넘기는 겁니다.
그러면 ExFreePoolWithTag 내부의 코드에서 이것을 FreeNonPagedPoolList 에 넣게 되는거죠.
그 다음 누군가가 ExAllocatePoolWithTag 를 호출했을 때 이것이 꺼내지게 되구요.

이 가정을 검증하기 위해서 디버거로 ExFreePoolWithTag 의 파라미터를 강제로 xxxxxxx2나 xxxxxxxa
와 같은 이상한 값으로 변경해서 호출해 보았습니다. 하지만 안타깝게도 내부에서 헤더체크를
하는 코드에 걸려서 이런저런 BugCheck 를 띄우는 것만 확인되었습니다.

생각보다 헤더체크를 열심히 하더라구요.
헤더영역까지 조작하여 체크를 피하도록 조작한다면 무사히 FreeNonPagedPoolList 에 안착할 수도
있을 것 같았습니다. 하지만 일일이 그것을 맞춰주는 노가다는 하지 않았습니다.

그래서 만약 누군가가 잘못된 포인터 xxxxxxx2 나 xxxxxxxa 을 넣었는데 우연히 체크에 걸리지 않게
헤더의 데이터가 들어있었다면 이것이 FreeNonPagedPoolList 에 들어가면서 나중에 문제를 일으킬
것이라는 가정입니다.

이런 상황이었다면 누군가가 최초에 잘못된 포인터 xxxxxxx2 나 xxxxxxxa 를 ExFreePool 에 넣는 순간을 잡으면 될 것 같습니다.

그래서 생각한 것이 조건 브레이크 포인트 입니다.
ExFreePoolWithTag 에 브레이크 포인트를 걸되 조건은 포인터가 0 또는 8 로 끝나는 경우는 그냥 지나보내고 그렇지 않은 경우는 세우는 겁니다.

kd> bp nt!ExFreePoolWithTag "j (poi(esp+4) & 3) ' ' ; 'gc' "

esp+4 는 첫번째 파라미터인 해제하려는 메모리 주소이고 이것을 3 으로 마스킹하면 0 또는 8 이 아닌 경우에만 0 이 아닌 값이 나옵니다.

이렇게 하면 잡힐 것 같기는 하지만 시스템이 많이 느려지더군요. -_-;;;
문제가 발생할 때까지 너무 오래 기다려야 할 것 같다는...

게다가 곰곰히 생각해 보니 자기가 p = ExAllocatePool() 과 같이 할당받은 메모리 포인터에 p = p+2
같은 행위를 하고 ExFreePool(p) 를 호출하는 막되먹은 코드는 아무래도 존재하기 힘들것 같습니다.
그래서 다른 생각을 해보면...

두번째 가정은 누군가가 스스로 해제한 메모리를 계속 사용하면서 FreeNonPagedPoolList 를 깨먹는 겁니다.
위에서 주목할 만한 부분이라고 기억하자고 한 내용이 있었죠?
우리가 ExFreePoolWithTag 로 해제한 메모리는 이후에 절대로 건드려서는 안됩니다.
왜냐하면 이 메모리는 사라지는 것이 아니라 위에서 본 것처럼 리스트에 들어가서 관리되기 때문입니다.

우리가 사용하던 메모리 주소가 855a6380 였다고 가정하면 ExFreePoolWithTag 를 호출하는 순간 우리가 사용하던 메모리 시작영역(855a6380 부터 4바이트)에 NextLink의 주소가 저장됩니다.
이 링크 정보는 커널의 메모리 관리 구조에서 매우 중요한 부분입니다. 이것이 깨지면 메모리 할당에서 엉뚱한 주소가 나가기 시작하겠죠.

다시 한번 강조합니다. ExFreePool 로 해제한 메모리 주소는 절대로 사용하면 안됩니다.
사용하는 순간 BSOD 를 예약해 놓는 꼴이 됩니다. 시한폭탄이죠.

어느 가정이 맞을까요?
아무래도 프로그래밍 실수라고 하면 두번째 가정이 가능성이 더 높겠네요.
아니면 아무런 규칙성없이 무작위로 깨먹는 녀석에 의해서 우연히 FreeNonPagedPoolList 가 깨지는 것일 수도 있겠죠.

결국 추측만 하고 원인은 찾을 수 없었습니다. OTL

http://www.driveronline.org/bbs/view.asp?tb=tipbbs&no=84

반응형
Posted by GreeMate