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