BLOG main image
분류 전체보기 (17)
Life (2)
Dump Analysis (9)
Reversing (1)
Windows (1)
Book (2)
Reference (2)
Visitors up to today!
Today hit, Yesterday hit
daisy rss
tistory 티스토리 가입하기!
'동기화'에 해당되는 글 1건
2018. 7. 9. 23:43

이번 덤프 역시 BugCheck 0x50다. 앞서 덤프와 같이 0x50 은 대부분 다른 모듈이 메모리를 손상시켰거나 정말 유효하지 않은 메모리나 해제된 메모리를 접근할 때 주로 발생한다. 원인에 따라 분석이 불가능한 경우도 많다.

이번에는 동기화와 관련된 주제이므로 약간 어려울 수 있다. 심호흡 한 번하고 차분하게 시작해보자.

kd> !analyze -v

*******************************************************************************

*      *

*      Bugcheck Analysis   *

*      *

*******************************************************************************

PAGE_FAULT_IN_NONPAGED_AREA (50)

Invalid system memory was referenced.  This cannot be protected by try-except.

Typically the address is just plain bad or it is pointing at freed memory.

Arguments:

Arg1: bad0b154, memory referenced.

Arg2: 00000000, value 0 = read operation, 1 = write operation.

Arg3: 8058c7b4, If non-zero, the instruction address which referenced the bad memory

       address.

Arg4: 00000002, (reserved)

Debugging Details:

------------------

DUMP_CLASS: 1

DUMP_QUALIFIER: 401

BUILD_VERSION_STRING:  2600.xpsp_sp3_gdr.120504-1619

SYSTEM_MANUFACTURER:  SAMSUNG ELECTRONICS CO., LTD.

SYSTEM_PRODUCT_NAME:  400B4B/400B5B/200B4B/200B5B

SYSTEM_SKU:  To be filled by O.E.M.

SYSTEM_VERSION:  04VC

BIOS_VENDOR:  American Megatrends Inc.

BIOS_VERSION:  04VC.M014.20110811.LDG

BIOS_DATE:  08/11/2011

BASEBOARD_MANUFACTURER:  SAMSUNG ELECTRONICS CO., LTD.

BASEBOARD_PRODUCT:  400B4B/400B5B/200B4B/200B5B

BASEBOARD_VERSION:  04VC

DUMP_TYPE:  1

BUGCHECK_P1: ffffffffbad0b154

BUGCHECK_P2: 0

BUGCHECK_P3: ffffffff8058c7b4

BUGCHECK_P4: 2

READ_ADDRESS:  bad0b154

 

FAULTING_IP:

nt!ObQueryNameString+9b

8058c7b4 8b88a4000000    mov     ecx,dword ptr [eax+0A4h]

MM_INTERNAL_CODE:  2

CPU_COUNT: 4

CPU_MHZ: 9be

CPU_VENDOR:  GenuineIntel

CPU_FAMILY: 6

CPU_MODEL: 2a

CPU_STEPPING: 7

CPU_MICROCODE: 6,2a,7,0 (F,M,S,R)  SIG: 1A'00000000 (cache) 1A'00000000 (init)

DEFAULT_BUCKET_ID:  DRIVER_FAULT

BUGCHECK_STR:  0x50

PROCESS_NAME:  EXCEL.EXE

ANALYSIS_SESSION_HOST:  PAUL-PC

ANALYSIS_SESSION_TIME:  11-01-2017 11:28:27.0997

ANALYSIS_VERSION: 10.0.10575.567 amd64fre

 

TRAP_FRAME:  a3dd1a04 -- (.trap 0xffffffffa3dd1a04)

ErrCode = 00000000

eax=bad0b0b0 ebx=00000000 ecx=00000000 edx=868edd44 esi=00000000 edi=868edd48

eip=8058c7b4 esp=a3dd1a78 ebp=a3dd1b2c iopl=0         nv up ei pl zr na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246

nt!ObQueryNameString+0x9b:

8058c7b4 8b88a4000000    mov     ecx,dword ptr [eax+0A4h] ds:0023:bad0b154=????????

Resetting default scope

 

LAST_CONTROL_TRANSFER:  from 8052b198 to 805396c2

 

STACK_TEXT: 

a3dd199c 8052b198 00000050 bad0b154 00000000 nt!KeBugCheckEx+0x1b

a3dd19ec 804e2956 00000000 bad0b154 00000000 nt!MmAccessFault+0x77e

a3dd19ec 8058c7b4 00000000 bad0b154 00000000 nt!KiTrap0E+0xd0

a3dd1b2c f76487ed 868edd60 86410000 00007fff nt!ObQueryNameString+0x9b

a3dd1b7c f7649841 868edd60 00000000 8639f018 BadDrv+0x17ed

a3dd1bc0 a6728f9c 000009b0 00000000 a3dd1be0 BadDrv+0x2841

a3dd1c00 a672900b 000009b0 a3dd1d64 a50a9740 HookDrv+0x6f9c

a3dd1d58 804df99f 000009b0 00000000 7c93e514 HookDrv+0x700b

a3dd1d58 7c93e514 000009b0 00000000 7c93e514 nt!KiFastCallEntry+0xfc

00000000 00000000 00000000 00000000 00000000 0x7c93e514

 

STACK_COMMAND:  kb

THREAD_SHA1_HASH_MOD_FUNC:  662e78c74f8e63f10fbd8043327f172f7b75bbdc

THREAD_SHA1_HASH_MOD_FUNC_OFFSET:  c46449496b9268570a50343287cb531d6153a414

THREAD_SHA1_HASH_MOD:  5ecdf531ba7867e0fd5f6dd6366341b3e5123821

 

FOLLOWUP_IP:

BadDrv+17ed

f76487ed 85c0            test    eax,eax

 

FAULT_INSTR_CODE:  4d7cc085

SYMBOL_STACK_INDEX:  4

SYMBOL_NAME:  BadDrv+17ed

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: BadDrv

IMAGE_NAME:  BadDrv.sys

DEBUG_FLR_IMAGE_TIMESTAMP:  4bb921b6

FAILURE_BUCKET_ID:  0x50_BADMEMREF_BadDrv+17ed

BUCKET_ID:  0x50_BADMEMREF_BadDrv+17ed

PRIMARY_PROBLEM_CLASS:  0x50_BADMEMREF_BadDrv+17ed

TARGET_TIME:  2012-07-13T08:54:52.000Z

OSBUILD:  2600

OSSERVICEPACK:  3000

SERVICEPACK_NUMBER: 3

OS_REVISION: 0

SUITE_MASK:  272

PRODUCT_TYPE:  1

OSPLATFORM_TYPE:  x86

OSNAME:  Windows XP

OSEDITION:  Windows XP WinNt (Service Pack 3) TerminalServer SingleUserTS

OS_LOCALE: 

USER_LCID:  0

OSBUILD_TIMESTAMP:  2012-05-04 22:16:02

BUILDOSVER_STR:  5.1.2600.xpsp_sp3_gdr.120504-1619

ANALYSIS_SESSION_ELAPSED_TIME: 8b2

ANALYSIS_SOURCE:  KM

FAILURE_ID_HASH_STRING:  km:0x50_badmemref_BadDrv+17ed

FAILURE_ID_HASH:  {aea5e7cf-9e2c-84a6-3525-f1fe4a8eaf29}

Followup:     MachineOwner

---------


BugCode 의 파라미터 정보를 보면 다음과 같다.

Arg1: bad0b154, memory referenced.

Arg2: 00000000, value 0 = read operation, 1 = write operation.

Arg3: 8058c7b4, If non-zero, the instruction address which referenced the bad memory

       address.


Arg1을 보면 bad0b154 메모리를 참조하는 중에 문제가 발생했는데, Arg2 를 보면 읽기 동작 중 문제가 발생했다고 한다. Arg3에는 문제가 발생한 코드 위치인 8058c7b4 가 담겨 있다.

물론 이것만으로는 더 알 수 없으니 실제 문제가 발생한 부분을 자세히 확인해야 한다.

항상 분석의 시작은 실제 문제가 발생한 부분부터 임을 명심하자.

.trap 명령으로 문제가 발생한 부분을 확인해보자

kd> .trap 0xffffffffa3dd1a04

ErrCode = 00000000

eax=bad0b0b0 ebx=00000000 ecx=00000000 edx=868edd44 esi=00000000 edi=868edd48

eip=8058c7b4 esp=a3dd1a78 ebp=a3dd1b2c iopl=0         nv up ei pl zr na pe nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246

nt!ObQueryNameString+0x9b:

8058c7b4 8b88a4000000    mov     ecx,dword ptr [eax+0A4h] ds:0023:bad0b154=????????


nt!ObQueryNameString 함수 수행 중에 mov ecx,dword ptr [eax+0A4h] 코드에서 유효하지 않은 메모리인 bad0b154를 접근해서 문제가 발생했다.

kv 명령어로 파라미터를 포함한 콜 스택을 확인해보자.

kd> kv

  *** Stack trace for last set context - .thread/.cxr resets it

 # ChildEBP RetAddr  Args to Child             

00 a3dd1b2c f76487ed 868edd60 86410000 00007fff nt!ObQueryNameString+0x9b (FPO: [Non-Fpo])

01 a3dd1b7c f7649841 868edd60 00000000 8639f018 BadDrv+0x17ed

02 a3dd1bc0 a6728f9c 000009b0 00000000 a3dd1be0 BadDrv+0x2841

03 a3dd1c00 a672900b 000009b0 a3dd1d64 a50a9740 HookDrv+0x6f9c

04 a3dd1d58 804df99f 000009b0 00000000 7c93e514 HookDrv+0x700b

05 a3dd1d58 7c93e514 000009b0 00000000 7c93e514 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ a3dd1d64)

06 00000000 00000000 00000000 00000000 00000000 0x7c93e514


BadDrv에서 nt 커널의 ObQueryNameString 함수를 호출하다 문제가 발생했다. 보통은 BadDrv에서 해당 함수 호출시 잘못된 파라미터를 넘겨주었을 가능성이 큰 상황이다.

NTSTATUS ObQueryNameString(

    _In_      PVOID                    Object,

    _Out_opt_ POBJECT_NAME_INFORMATION ObjectNameInfo,

    _In_      ULONG                    Length,

    _Out_     PULONG                   ReturnLength

);


MSDN에서 검색해본 함수 원형이다. 이 함수는 입력 받은 오브젝트를 이용해서 오브젝트 이름을 얻는 함수다(예를 들어 특정 파일 오브젝트를 전달하면 해당 파일 오브젝트가 가리키는 파일 경로 문자열을 얻는다). 첫 번째 파라미터인 오브젝트가 의심스럽지만 아직 확신할 수 있는 단계는 아니다.

BadDrv에서 ObQueryNameString 함수 호출시 전달한 오브젝트는 868edd60 이다. kv 명령의 Args to Child 첫 번째 값을 봐도 되고 함수 호출 규약을 이해한다면 ebp 레지스터의 +8 한 값을 봐도 알 수 있다.

kd> dd ebp+8 L1

a3dd1b34  868edd60


문제가 발생한 지점이 nt 커널 내부인 ObQueryNameString 함수이므로 이제 해당 함수 안으로 들어가야한다. 미안하지만 더 집중해서 살펴보자.

kd> u nt!ObQueryNameString L25

nt!ObQueryNameString:

8058c731 6898000000      push    98h

8058c736 68a8015080      push    offset nt!ObWatchHandles+0xe4 (805001a8)

8058c73b e86387f5ff      call    nt!_SEH_prolog (804e4ea3)

8058c740 c745d4010000c0  mov     dword ptr [ebp-2Ch],0C0000001h

8058c747 8365c800        and     dword ptr [ebp-38h],0

8058c74b 8365d800        and     dword ptr [ebp-28h],0

8058c74f c645de01        mov     byte ptr [ebp-22h],1

8058c753 c645df00        mov     byte ptr [ebp-21h],0

8058c757 8b7d08          mov     edi,dword ptr [ebp+8] // 4) edi=ebp+8

8058c75a 83c7e8          add     edi,0FFFFFFE8h  // 3) edi=edi-0n24

8058c75d 897dc0          mov     dword ptr [ebp-40h],edi

8058c760 8a470c          mov     al,byte ptr [edi+0Ch]

8058c763 84c0            test    al,al

8058c765 0f8486040000    je      nt!ObQueryNameString+0x36 (8058cbf1)

8058c76b 0fb6c0          movzx   eax,al

8058c76e 8bdf            mov     ebx,edi

8058c770 2bd8            sub     ebx,eax

8058c772 85db            test    ebx,ebx

8058c774 0f843c040000    je      nt!ObQueryNameString+0x93 (8058cbb6)

8058c77a 8d530c          lea     edx,[ebx+0Ch]

8058c77d 8b0a            mov     ecx,dword ptr [edx]

8058c77f 85c9            test    ecx,ecx

8058c781 0f841f390000    je      nt!ObQueryNameString+0x63 (805900a6)

8058c787 8d4101          lea     eax,[ecx+1]

8058c78a 8bf0            mov     esi,eax

8058c78c 8bc1            mov     eax,ecx

8058c78e f00fb132        lock    cmpxchg dword ptr [edx],esi

8058c792 3bc1            cmp     eax,ecx

8058c794 0f852c6d0700    jne     nt!ObQueryNameString+0x5d (806034c6)

8058c79a b001            mov     al,1

8058c79c 84c0            test    al,al

8058c79e 0f8412040000    je      nt!ObQueryNameString+0x93 (8058cbb6)

8058c7a4 f6420380        test    byte ptr [edx+3],80h

8058c7a8 0f85276d0700    jne     nt!ObQueryNameString+0x73 (806034d5)

8058c7ae 895de0          mov     dword ptr [ebp-20h],ebx

8058c7b1 8b4708          mov     eax,dword ptr [edi+8] // 2) edi=868edd48

8058c7b4 8b88a4000000    mov     ecx,dword ptr [eax+0A4h] // 1) eax=bad0b0b0


1)이 문제가 발생한 곳으로 어디에서 eax bad0b0b0 값을 채웠는지 함수 시작 지점부터 찾아봐야 한다.

2)에 edi + 8 주소의 값을 eax 에 넣는 코드가 있으므로 edi 값을 찾아보자. 다행히 edi 를 덮어쓰는 코드는 없으므로 r 명령으로 확인 가능하다.

kd> r edi

Last set context:

edi=868edd48


2)에서 mov eax,dowrd ptr [edi+8] 명령을 통해 868edd48 + 8 위치의 값을 eax 에 설정했다.

kd> dd 868edd48+8 L1

868edd50  bad0b0b0


값을 참조하는 dword ptr 명령이 사용됐음을 유의하자. 주소가 아닌 값을 참조하는 명령이므로 868edd50 이 아닌 bad0b0b0을 참조한다. 즉 이 값이 eax에 설정된다.

3) 여기서 원래 edi 값에 ffffffe8을 더한 값(add)으로 edi가 설정됐다. ? 명령으로 알기 쉽게 10진수로 변환해보자.

kd> ? ffffffe8

Evaluate expression: -24 = ffffffe8


아하, edi 10진수 -24 를 더한 것이니 결국 edi = edi – 18(0n24)이다.

여기서 18 10진수 24 16진수 값을 의미한다.

[참고]

WinDbg 에서는 16진수 표현 방식이 기본값이므로 앞에 0x를 붙이지 않고 표기했다. 기본값은 n 명령어로 변경 가능한데 이 경우 16진수 앞에 붙이는 접두사인 0x 는 생략 가능하다. 다른 진수 사용시에는 명시적으로 진수에 맞는 접두사와 함께 사용해야 한다. 자주 혼동되는 부분이기 때문에 유의하자.

- 16진수 : OO / 0xOO (기본값)

- 10진수 0nOO

- 8진수 0tOO

- 2진수 0y00


4) edi ebp + 8 값을 가져와 설정한다. 앞서 ebp + 8(첫 번째 파마리터) 868edd60 였으므로 이를 토대로 재구성해보자. 

4) edi = 868edd60 (ebp+8)

3) edi = 868edd60 - 18

2) eax = poi(868edd48+8)

1) ecx = poi(bad0b0b0+a4)


2) 1) 에서 사용된 poi는 해당 주소의 값을 얻는 명령이다.

<poi 사용 유무에 따른 결과>

kd> dd 868edd48+8 L1

868edd50  bad0b0b0

 

kd> dd poi(868edd48+8) L1

bad0b0b0  ????????


1) 에서 bad0b0b0 + a4 를 하면 바로 문제가 발생했던 bad0b154 !

kd> dd bad0b154 L1

bad0b154  ????????


역시나 bad0b154는 유효하지 않은 메모리 값으로 여기를 접근했으므로 문제가 발생한 것이다.

지금까지 확인된 내용을 쉽게 정리해보면 첫 번째 파라미터 값인 868edd60으로 여러 연산(-18, +8, +a4)을 수행하다 bad0b154 값에 접근한 것이 문제의 원인이다. 그런데 왜 이런 알 수 없는 연산을 한 것일까? 감이 좋은 사람이라면 전달된 값이 어떤 구조체이고 해당 구조체의 필드를 참조하는 연산을 수행한 것임을 눈치 챘을 수도 있다. 보통 저런 숫자를 빼거나 더하는 연산은 구조체를 참조하는 코드인 경우가 많다.

여기까지 잘 따라왔다면 정말 훌륭하다. 자부심을 가져도 좋다. 이제부터는 약간의 윈도우 커널 내부 지식을 필요로 하지만 크게 어렵지는 않다.

앞서 ObQueryNameString 함수 원형을 통해 868edd60 값이 커널의 오브젝트 포인터라는 것은 이미 알고 있다. 윈도우는 프로세스, 스레드, 파일 등 대부분의 자원을 커널 오브젝트라는 것으로 관리하는데 이들 오브젝트에는 공통적으로 오브젝트 헤더가 존재한다. 오브젝트 헤더 구조체는 dt _OBJECT_HEADER 명령으로 확인 가능하다.

kd> dt _OBJECT_HEADER 868edd60-18

nt!_OBJECT_HEADER

   +0x000 PointerCount     : 0

   +0x004 HandleCount      : 0

   +0x004 NextToFree       : (null)

   +0x008 Type             : 0xbad0b0b0 _OBJECT_TYPE

   +0x00c NameInfoOffset   : 0x10 ''

   +0x00d HandleInfoOffset : 0 ''

   +0x00e QuotaInfoOffset  : 0 ''

   +0x00f Flags            : 0xa0 ''

   +0x010 ObjectCreateInfo : (null)

   +0x010 QuotaBlockCharged : (null)

   +0x014 SecurityDescriptor : (null)

   +0x018 Body             : _QUAD


오브젝트 헤더의 +18 한 필드명이 Body 이다. 예상한 대로 868edd60 는 어떤 오브젝트이고 첫 번째 연산인 -18은 오브젝트 헤더 위치를 구하기 위한 것이었다. 어떤 구조체인지 안 이상 다음은 어렵지 않다. 두 번째 연산인 +8 Type 필드를 가져오기 위한 연산이었고 이를 통해 0xbad0b0b0 값을 얻은 것이다. Body 에는 어떤 오브젝트 포인터든 올 수 있기 때문에 커널은 이 Type 필드를 통해 오브젝트 유형을 판단한다.

WinDbg가 친절하게도 Type 값은 _OBJECT_TYPE 구조체라고 알려준다. Type 값인 0xbad0b0b0 dt _OBJECT_TYPE 명령으로 확인하면 마지막 연산인 +a4의 의미를 알 수 있다.

kd> dt _OBJECT_TYPE 0xbad0b0b0

nt!_OBJECT_TYPE

   +0x000 Mutex            : _ERESOURCE

   +0x038 TypeList         : _LIST_ENTRY

   +0x040 Name             : _UNICODE_STRING

   +0x048 DefaultObject    : ????

   +0x04c Index            : ??

   +0x050 TotalNumberOfObjects : ??

   +0x054 TotalNumberOfHandles : ??

   +0x058 HighWaterNumberOfObjects : ??

   +0x05c HighWaterNumberOfHandles : ??

   +0x060 TypeInfo         : _OBJECT_TYPE_INITIALIZER

   +0x0ac Key              : ??

   +0x0b0 ObjectLocks      : [4] _ERESOURCE


0xbad0b0b0 + a4 위치는 TypeInfo 0x060 Key 0x0ac의 사이다. TypeInfo 내부의 어떤 필드일 것이다. 하지만 Type 값인 bad0b0b0 은 유효한 메모리가 아니어서 구조체 내부 값들이 ?? 로 표시된다. 여기서는 TypeInfo 구조체의 어떤 필드였는지만 알면 되니 문제되지는 않는다.

kd> dt _OBJECT_TYPE_INITIALIZER

nt!_OBJECT_TYPE_INITIALIZER

   +0x000 Length           : Uint2B

   +0x002 UseDefaultObject : UChar

   +0x003 CaseInsensitive  : UChar

   +0x004 InvalidAttributes : Uint4B

   +0x008 GenericMapping   : _GENERIC_MAPPING

   +0x018 ValidAccessMask  : Uint4B

   +0x01c SecurityRequired : UChar

   +0x01d MaintainHandleCount : UChar

   +0x01e MaintainTypeList : UChar

   +0x020 PoolType         : _POOL_TYPE

   +0x024 DefaultPagedPoolCharge : Uint4B

   +0x028 DefaultNonPagedPoolCharge : Uint4B

   +0x02c DumpProcedure    : Ptr32     void

   +0x030 OpenProcedure    : Ptr32     long

   +0x034 CloseProcedure   : Ptr32     void

   +0x038 DeleteProcedure  : Ptr32     void

   +0x03c ParseProcedure   : Ptr32     long

   +0x040 SecurityProcedure : Ptr32     long

   +0x044 QueryNameProcedure : Ptr32     long

   +0x048 OkayToCloseProcedure : Ptr32     unsigned char


a4에서 TypeInfo 시작 위치인 60을 빼면 44가 나온다. 44 _OBJECT_TYPE_INITIALIZER QueryNameProcedure에 해당한다. 이제 문제가 발생한 상황이 명확해지는 것 같다.

4) 오브젝트를 가져 옴                 // edi = 868edd60 (ebp+8)

3) 오브젝트 헤더를 구함               // edi = 868edd60 - 18

2) 헤더에서 오브젝트 타입을 구함    // eax = poi(868edd48+8)

1) 오브젝트 타입의 TypeInfo에서 QueryNameProcedure를 참조  // ecx = poi(bad0b0b0+a4)


문제가 발생한 원인이 밝혀졌다. ObQueryNameString 함수에서 입력 받은 오브젝트로 QueryNameProcedure 함수 포인터를 구하려다 2) 시점에 오브젝트 타입 값이 유효하지 않은 값이라 1) 에서 문제가 발생했다.

이쯤에서 잠시 쉬는 것이 좋겠다. 장시간 분석은 정신 건강에 매우 해롭다.

거의 분석이 끝났지만 왜 이런 상황이 발생했는지는 아직 밝혀지지 않았다.

! 휴식이 끝났으면 다음 3가지 시나리오를 살펴 보자.

1. 외부에서 이미 잘못된 값을 BadDrv 에 전달했고 BadDrv 는 그대로 ObQueryNameString 함수에 전달했다.

2. 외부에서 정상적인 값을 BadDrv 에 전달했지만 BadDrv 에서 잘못된 값으로 바꿔 ObQueryNameString 함수에 전달했다.

3. 외부에서 정상적인 값을 BadDrv 에 전달했고 BadDrv 도 그대로 ObQueryNameString 함수에 전달했지만 중간에 잘못된 값으로 바뀌었다.

어떤 시나리오가 마음에 드는가? 분석할 때 가장 경계해야 하는 부분은 결론을 정해 놓고 분석에 임하는 것이다. 그럴 경우 자신이 보고 싶은 부분만 보기 때문에 진실을 놓칠 우려가 있다. 그렇기에 나는 이 3가지 시나리오를 모두 검증해볼 것이다.

kd> kv

  *** Stack trace for last set context - .thread/.cxr resets it

 # ChildEBP RetAddr  Args to Child             

00 a3dd1b2c f76487ed 868edd60 86410000 00007fff nt!ObQueryNameString+0x9b (FPO: [Non-Fpo])  // 4) nt 커널 영역

01 a3dd1b7c f7649841 868edd60 00000000 8639f018 BadDrv+0x17ed      // 3) BadDrv 영역

02 a3dd1bc0 a6728f9c 000009b0 00000000 a3dd1be0 BadDrv+0x2841      // 2) BadDrv 영역

03 a3dd1c00 a672900b 000009b0 a3dd1d64 a50a9740 HookDrv+0x6f9c     // 1) 여기부터 아래로 외부 영역

04 a3dd1d58 804df99f 000009b0 00000000 7c93e514 HookDrv+0x700b

05 a3dd1d58 7c93e514 000009b0 00000000 7c93e514 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ a3dd1d64)

06 00000000 00000000 00000000 00000000 00000000 0x7c93e514



3)번 콜 스택의 첫 번째 파라미터인 868edd60 는 앞서 분석에서 확인된 오브젝트다. 외부 영역인 1)번과 2)번까지 보이는 9b0 값이 3)번에서 오브젝트로 바뀌었으므로 9b0 값을 먼저 확인해야 한다.

3)번의 BadDrv+0x17ed 위치는 ObQueryNameString 함수를 호출한 위치다. BadDrv+0x17ed 함수의 시작 부분을 확인하려면 2)번에서 마지막 함수를 부른 위치를 확인하면 된다.

ub 명령을 통해 2)번 위치에서 함수 호출하는 부분을 살펴보자.

kd> ub BadDrv+0x2841

BadDrv+0x2825:

f7649825 b89a0000c0      mov     eax,0C000009Ah

f764982a e9f3000000      jmp     BadDrv+0x2922 (f7649922)

f764982f 8d7e18          lea     edi,[esi+18h]

f7649832 66c7070100      mov     word ptr [edi],1

f7649837 57              push    edi

f7649838 53              push    ebx

f7649839 ff7508          push    dword ptr [ebp+8]

f764983c e8a7eeffff      call    BadDrv+0x16e8 (f76486e8)


BadDrv+0x16e8 BadDrv+0x17ed 주소가 수행된 함수의 시작 위치다. 시작 위치부터 ObQueryNameString 함수가 호출된 부분까지 확인해보자.

kd> u BadDrv+0x16e8 L4d

BadDrv+0x16e8:

f76486e8 6a18            push    18h

f76486ea 68e8d164f7      push    offset BadDrv+0x61e8 (f764d1e8)

f76486ef e8cc3a0000      call    BadDrv+0x51c0 (f764c1c0)

f76486f4 33ff            xor     edi,edi

f76486f6 33f6            xor     esi,esi

f76486f8 397d08          cmp     dword ptr [ebp+8],edi

f76486fb 0f844f010000    je      BadDrv+0x1850 (f7648850)

f7648701 7d0e            jge     BadDrv+0x1711 (f7648711)

f7648703 ff155cd064f7    call    dword ptr [BadDrv+0x605c (f764d05c)]

f7648709 84c0            test    al,al

f764870b 0f853f010000    jne     BadDrv+0x1850 (f7648850)

f7648711 57              push    edi

f7648712 8d45e0          lea     eax,[ebp-20h]

f7648715 50              push    eax

f7648716 57              push    edi

f7648717 57              push    edi

f7648718 57              push    edi

f7648719 ff7508          push    dword ptr [ebp+8] // 1) ebp+8 = Handle

f764871c ff1558d064f7    call    dword ptr [BadDrv+0x6058 (f764d058)]     // 2) ObReferenceObjectByHandle 함수 호출

f7648722 85c0            test    eax,eax

f7648724 0f8526010000    jne     BadDrv+0x1850 (f7648850)

f764872a 8b4de0          mov     ecx,dword ptr [ebp-20h]

f764872d 8bd9            mov     ebx,ecx

f764872f 895d08          mov     dword ptr [ebp+8],ebx // 3) ebp+8 = Object 변경

f7648732 3bcf            cmp     ecx,edi

f7648734 0f8416010000    je      BadDrv+0x1850 (f7648850)

f764873a ff1564d064f7    call    dword ptr [BadDrv+0x6064 (f764d064)]      // 4) ObfDereferenceObject 함수 호출

f7648740 ff1538d064f7    call    dword ptr [BadDrv+0x6038 (f764d038)]

f7648746 c1eb02          shr     ebx,2

f7648749 83e37f          and     ebx,7Fh

f764874c 8bc3            mov     eax,ebx

f764874e 6bc038          imul    eax,eax,38h

f7648751 8db880ef64f7    lea     edi,BadDrv+0x7f80 (f764ef80)[eax]

f7648757 6a01            push    1

f7648759 57              push    edi

f764875a ff1534d064f7    call    dword ptr [BadDrv+0x6034 (f764d034)]

f7648760 8b049d000c65f7  mov     eax,dword ptr BadDrv+0x9c00 (f7650c00)[ebx*4]

f7648767 eb0a            jmp     BadDrv+0x1773 (f7648773)

f7648769 8b4d08          mov     ecx,dword ptr [ebp+8]

f764876c 3908            cmp     dword ptr [eax],ecx

f764876e 7407            je      BadDrv+0x1777 (f7648777)

f7648770 8b4004          mov     eax,dword ptr [eax+4]

f7648773 85c0            test    eax,eax

f7648775 75f2            jne     BadDrv+0x1769 (f7648769)

f7648777 85c0            test    eax,eax

f7648779 742a            je      BadDrv+0x17a5 (f76487a5)

f764877b 8b7008          mov     esi,dword ptr [eax+8]

f764877e 8d0c36          lea     ecx,[esi+esi]

f7648781 51              push    ecx

f7648782 83c00e          add     eax,0Eh

f7648785 50              push    eax

f7648786 8b5d10          mov     ebx,dword ptr [ebp+10h]

f7648789 53              push    ebx

f764878a e89b390000      call    BadDrv+0x512a (f764c12a)

f764878f 83c40c          add     esp,0Ch

f7648792 8bcf            mov     ecx,edi

f7648794 ff152cd064f7    call    dword ptr [BadDrv+0x602c (f764d02c)]

f764879a ff1528d064f7    call    dword ptr [BadDrv+0x6028 (f764d028)]

f76487a0 e995000000      jmp     BadDrv+0x183a (f764883a)

f76487a5 8bcf            mov     ecx,edi

f76487a7 ff152cd064f7    call    dword ptr [BadDrv+0x602c (f764d02c)]

f76487ad ff1528d064f7    call    dword ptr [BadDrv+0x6028 (f764d028)]

f76487b3 ff35e0ee64f7    push    dword ptr [BadDrv+0x7ee0 (f764eee0)]

f76487b9 ff35e4ee64f7    push    dword ptr [BadDrv+0x7ee4 (f764eee4)]

f76487bf 6a00            push    0

f76487c1 ff150cd064f7    call    dword ptr [BadDrv+0x600c (f764d00c)]

f76487c7 8bf8            mov     edi,eax

f76487c9 85ff            test    edi,edi

f76487cb 0f8431020000    je      BadDrv+0x1a02 (f7648a02)

f76487d1 662137          and     word ptr [edi],si

f76487d4 66c74702feff    mov     word ptr [edi+2],0FFFEh

f76487da 8d45e4          lea     eax,[ebp-1Ch]

f76487dd 50              push    eax

f76487de 68ff7f0000      push    7FFFh

f76487e3 57              push    edi

f76487e4 ff7508          push    dword ptr [ebp+8]  // 5) ebp+8 = Object

f76487e7 ff1560d064f7    call    dword ptr [BadDrv+0x6060 (f764d060)]      // 6) nt!ObQueryNameString 함수 호출


1)번에서 첫 번째 파라미터인 ebp+8 push 하고 2)번에서 ObReferenceObjectByHandle 함수를 호출하고 있다. ObReferenceObjectByHandle 함수의 원형은 다음과 같다.

NTSTATUS ObReferenceObjectByHandle(

    _In_      HANDLE                     Handle,

    _In_      ACCESS_MASK                DesiredAccess,

    _In_opt_  POBJECT_TYPE               ObjectType,

    _In_      KPROCESSOR_MODE            AccessMode,

    _Out_     PVOID                      *Object,

    _Out_opt_ POBJECT_HANDLE_INFORMATION HandleInformation

    );


첫 번째 파라미터는 Handle 이다. 9b0 값은 Handle 임을 알 수 있다.

3)번에서는 ebp + 8 값을 ObReferenceObjectByHandle 함수를 호출해서 리턴 받은 Object로 변경한다. 5), 6)번에서는 이 Object로 문제 발생 함수인 ObQueryNameString을 호출한다.

ObReferenceObjectByHandle 함수는 Handle을 가지고 Handle이 가리키는 커널 오브젝트를 얻는 함수다. 참조에 성공하면 대상 커널 오브젝트의 참조 카운트는 1 증가하게 된다.

어쨌든 9b0 값을 !handle 명령으로 확인해보자.

kd> !handle 9b0

Failed to get VAD root

PROCESS 89863020  SessionId: 0  Cid: 12b0    Peb: 7ffdf000  ParentCid: 0e6c

    DirBase: 23041000  ObjectTable: e10f9978  HandleCount: 623.

    Image: EXCEL.EXE

Handle table at e10f9978 with 623 entries in use

09b0: free handle, Entry address e1225360, Next Entry 000009d0


역시 9b0은 소유자인 EXCEL.EXE 프로세스의 핸들 테이블에서 해제된 핸들(free handle)이라고 나온다. 해제되어 사용할 수 없는 핸들이라는 의미다. 여기서 1번 시나리오 대로 외부에서 잘못된 핸들을 BadDrv에 전달했군! 이라고 생각했다면 성급한 판단이다.

ObReferenceObjectByHandle 함수 호출이 성공해서 Object 868edd60 값을 얻었음을 잊지 말자. 함수 호출이 성공했다는 것은 당시에는 유효한 핸들이었다는 의미다. 만약 해제되거나 유효하지 않은 핸들이었다면 ObReferenceObjectByHandle 함수 호출시 STASTUS_INVALID_HANDLE 오류가 리턴되었을 것이고 Object 또한 NULL 이 반환되었을 것이다.

2)번에서 ObReferenceObjectByHandle 함수 호출이 성공했으므로 1번 시나리오는 가능성이 없다.

4)번에서는 ObfDereferenceObject 함수로 ObReferenceObjectByHandle 를 통해 증가시킨 참조 카운트를 다시 원래대로 내려주고 있다.

이후 6) ObQueryNameString 함수 호출 부분까지는 Object 값을 변경하는 행위가 보이지 않는다.

“BadDrv는 외부에서 정상적인 Handle 값인 9b0을 받았고 이를 통해 Object 값인 868edd60를 얻어 ObQueryNameString 함수에 전달했다고 판단하는 데 무리가 없어 보인다. 2번 시나리오인 BadDrv가 잘못된 ObjectObQueryNameString 에 전달했을 가능성도 낮다.

혹시 분석 초반에 봤던 오브젝트 헤더의 이상한 값 0xbad0b0b0을 기억하는가?

kd> dt _OBJECT_HEADER 868edd60-18

nt!_OBJECT_HEADER

       ... ...

   +0x008 Type             : 0xbad0b0b0 _OBJECT_TYPE

       ... ...

   +0x018 Body             : _QUAD


Type 에 저장되어 있는 0xbad0b0b0 값은 실은 Object 가 명확하게 해제될 때 커널 내부적으로 기록하는 상태 값이다.

이 부분을 설명하기 위해 현재 분석하고 있는 덤프가 아닌 가상 머신에서 라이브 디버깅을 통해 커널이 0xbad0b0b0 값을 설정하는 순간을 확인해봤다. 분석 중인 덤프에서 확인한 내용이 아니므로 혼동하지 말자.

<라이브 디버깅 환경에서 확인한 0xbad0b0b0 값을 설정하는 부분>

kd> kc

 #

00 nt!ObpFreeObject+0x16c

01 nt!ObpRemoveObjectRoutine+0xe8

02 nt!ObfDereferenceObject+0x4c

03 nt!ObpCloseHandleTableEntry+0x155

04 nt!ObpCloseHandle+0x87

05 nt!NtClose+0x1d

06 nt!KiFastCallEntry+0xfc

 

kd> u nt!ObpFreeObject+a1

nt!ObpFreeObject+0x12f:

8056f68f 3bc3            cmp     eax,ebx

8056f691 5f              pop     edi

8056f692 0f8562680000    jne     nt!ObpFreeObject+0x134 (80575efa)

8056f698 8b45f4          mov     eax,dword ptr [ebp-0Ch]

8056f69b 3bc3            cmp     eax,ebx

8056f69d 0f850f0c0000    jne     nt!ObpFreeObject+0x14e (805702b2)

8056f6a3 8b45f0          mov     eax,dword ptr [ebp-10h]

8056f6a6 c74608b0b0d0ba  mov     dword ptr [esi+8],0BAD0B0B0h


ObFreeObject 라는 함수에서 Object가 해제될 시 오브젝트 헤더의 Type(+8) 위치에 정확하게 bad0b0b0이라는 값을 기록해주고 있다.

따라서 bad0b0b0 값은 해당 Object가 해제되었다는 명백한 증거다.

 

다시 예제 덤프로 돌아와보자. !handle 정보와 오브젝트 헤더 정보를 통해 Handle 이 해제된 것은 명확하다. 하지만 해제 시점이 애매하다.

이제부터 설명은 덤프에서 확인이 어려운 부분도 포함되어 있다. 나는 3번 시나리오인정상적인 Object 값을 ObQueryNameString 함수에 전달했지만 중간에 해제됐을 가능성이 가장 높다고 본다. 여기에는 동기화 문제가 숨어 있다.


1) 외부(EXCEL) Thread 1에서 정상적인 Handle을 BadDrv에 전달

2) BadDrv에서 전달된 Handle로 ObReferenceObjectByHandle로 Object 획득(참조 카운트 +1)

3) BadDrv에서 ObfDereferenceObject로 Object 참조 카운트 복원(참조 카운트 -1)

  a) 외부(EXCEL) Thread 2에서 Handle 해제?

4) BadDrv에서 Object를 이용하여 nt 커널의 ObQueryNameString 함수 호출

  b) 외부(EXCEL) Thread 2에서 Handle 해제?

5) ObQueryNameString 함수 내부에서 잘못된 메모리 접근


아마 3)~5)번 사이 a) b) 시점에 Handle 해제가 발생했을 것이다. 이렇게 특정한 이유는 3)번에서 ObfDereferenceObject Object의 참조 카운트를 돌려놨기 때문이다. 이로 인해 해당 Handle은 언제든 해제될 수 있는 상태가 되었다.

커널에서 Object를 가지고 작업을 수행할 때는 흔히 ObReferenceObjectByHandle 함수를 통해 참조 카운트를 증가시키고 수행한다. 참조 카운트가 증가된 상태에서는 오브젝트가 해제되지 않기 때문이다. 작업이 완료되면 DereferenceObject 류 함수를 통해 참조 카운트를 감소시켜 해제 가능한 상태로 돌려 놓는다. 만약 참조 카운트를 증가시키지 않고 작업할 경우 중간에 작업 중인 오브젝트가 해제되어 문제가 발생할 수 있다.

이렇게 Handle Object로 안전하지 않게 참조할 경우 다른 Thread에 의해 해당 Handle 이 해제되면, 골치 아픈 동기화 문제가 발생할 수 있다. 이런 동기화 문제는 이번처럼 덤프 상에서는 결과만 보이므로 제대로 원인이 확인되지 않는 경우가 많다. 하지만 실제로 심심치 않게 발생한다.

드라이버 개발 시 뭔가 Object로 참조해서 사용할 경우 참조 카운트 해제는 Object 사용이 다 끝난 후에 하는 것이 원칙이다.

만약 ObfDereferenceObject를 ObQueryNameString 다음에 했다면 Object 의 참조 카운트가 증가된 상태라 설령 다른 Thread 에서 중간에 Handle 을 해제하더라도 Object가 해제되지 않는다. Object ObfDereferenceObject가 호출될 때 비로서 참조 카운트가 내려가면서 해제 가능한 상태가 된다. 그러므로 ObQueryNameString에서 문제도 발생하지 않는다.

현업에서 많은 덤프를 분석하다 보면 이렇게 겉으로만 봐서는 논리적으로 설명이 안 되는 상황이 있는데, 동기화 이슈 관점에서 보면 의외로 답을 찾는 경우가 많았다. 역시나 덤프 내면을 볼 수 있는 상상력이 조금은 필요한 것 같다.



'Dump Analysis' 카테고리의 다른 글

[0xC5] 풀 헤더 손상  (0) 2018.07.16
[0x1A] 페이지 손상  (0) 2018.07.12
[0x50] 숨겨진 콜 스택  (0) 2018.07.07
[0x50] UNICODE_STRING  (0) 2018.07.05
필터매니저(fltmgr.sys) 버그? DRAINING ZOMBIED 와 DOE_UNLOAD_PENDING  (0) 2015.01.22
prev"" #1 next