이번 덤프 역시 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
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가 잘못된 Object를 ObQueryNameString 에 전달했을 가능성도 낮다.
혹시 분석 초반에 봤던 오브젝트 헤더의 이상한 값 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 |
이번 덤프의 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: ffff0000, memory referenced.
Arg2: 00000000, value 0 = read operation, 1 = write operation.
Arg3: 82ad19fe, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 00000000, (reserved)
Debugging Details:
------------------
DUMP_CLASS: 1
DUMP_QUALIFIER: 401
BUILD_VERSION_STRING: 7601.17514.x86fre.win7sp1_rtm.101119-1850
SYSTEM_MANUFACTURER: VMware, Inc.
VIRTUAL_MACHINE: VMware
SYSTEM_PRODUCT_NAME: VMware Virtual Platform
SYSTEM_VERSION: None
BIOS_VENDOR: Phoenix Technologies LTD
BIOS_VERSION: 6.00
BIOS_DATE: 07/31/2013
BASEBOARD_MANUFACTURER: Intel Corporation
BASEBOARD_PRODUCT: 440BX Desktop Reference Platform
BASEBOARD_VERSION: None
DUMP_TYPE: 1
BUGCHECK_P1: ffffffffffff0000
BUGCHECK_P2: 0
BUGCHECK_P3: ffffffff82ad19fe
BUGCHECK_P4: 0
READ_ADDRESS: ffff0000
FAULTING_IP:
nt!strstr+1e
82ad19fe 8a07 mov al,byte ptr [edi]
MM_INTERNAL_CODE: 0
CPU_COUNT: 1
CPU_MHZ: e07
CPU_VENDOR: GenuineIntel
CPU_FAMILY: 6
CPU_MODEL: 3c
CPU_STEPPING: 3
CPU_MICROCODE: 6,3c,3,0 (F,M,S,R) SIG: 19'00000000 (cache) 19'00000000 (init)
DEFAULT_BUCKET_ID: WIN7_DRIVER_FAULT
BUGCHECK_STR: 0x50
PROCESS_NAME: MyApp.exe
CURRENT_IRQL: 2
ANALYSIS_SESSION_HOST: PAUL-PC
ANALYSIS_SESSION_TIME: 11-29-2017 10:35:11.0546
ANALYSIS_VERSION: 10.0.10575.567 amd64fre
TRAP_FRAME: 89812c30 -- (.trap 0xffffffff89812c30)
ErrCode = 00000000
eax=ffff0000 ebx=00000000 ecx=9213a600 edx=ffff4d5c esi=ffff0000 edi=ffff0000
eip=82ad19fe esp=89812ca4 ebp=89813bfc iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010206
nt!strstr+0x1e:
82ad19fe 8a07 mov al,byte ptr [edi] ds:0023:ffff0000=??
Resetting default scope
LAST_CONTROL_TRANSFER: from 82a933d8 to 82ae041b
STACK_TEXT:
89812c18 82a933d8 00000000 ffff0000 00000000 nt!MmAccessFault+0x106
89812c18 82ad19fe 00000000 ffff0000 00000000 nt!KiTrap0E+0xdc
89813bfc 82a89593 84525860 843f0858 843f0858 nt!strstr+0x1e
89813c14 82c7d99f 84489e18 843f0858 843f08c8 nt!IofCallDriver+0x63
89813c34 82c80b71 84525860 84489e18 00000000 nt!IopSynchronousServiceTail+0x1f8
89813cd0 82cc73f4 84525860 843f0858 00000000 nt!IopXxxControlFile+0x6aa
89813d04 82a901ea 0000015c 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
89813d04 76fd70b4 0000015c 00000000 00000000 nt!KiFastCallEntry+0x12a
0012f7f0 00000000 00000000 00000000 00000000 0x76fd70b4
STACK_COMMAND: kb
THREAD_SHA1_HASH_MOD_FUNC: 1d162d18111a222172b462becb3845e53e690213
THREAD_SHA1_HASH_MOD_FUNC_OFFSET: d3c548c893cd75b9806e6f8940169c9ae8e41dba
THREAD_SHA1_HASH_MOD: cb5f414824c2521bcc505eaa03e92fa10922dad8
FOLLOWUP_IP:
nt!strstr+1e
82ad19fe 8a07 mov al,byte ptr [edi]
FAULT_INSTR_CODE: c683078a
SYMBOL_STACK_INDEX: 2
SYMBOL_NAME: nt!strstr+1e
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: nt
IMAGE_NAME: ntkrpamp.exe
DEBUG_FLR_IMAGE_TIMESTAMP: 4ce78a09
IMAGE_VERSION: 6.1.7601.17514
FAILURE_BUCKET_ID: 0x50_nt!strstr+1e
BUCKET_ID: 0x50_nt!strstr+1e
PRIMARY_PROBLEM_CLASS: 0x50_nt!strstr+1e
TARGET_TIME: 2017-11-28T10:00:12.000Z
OSBUILD: 7601
OSSERVICEPACK: 1000
SERVICEPACK_NUMBER: 0
OS_REVISION: 0
SUITE_MASK: 272
PRODUCT_TYPE: 1
OSPLATFORM_TYPE: x86
OSNAME: Windows 7
OSEDITION: Windows 7 WinNt (Service Pack 1) TerminalServer SingleUserTS
OS_LOCALE:
USER_LCID: 0
OSBUILD_TIMESTAMP: 2010-11-20 17:42:49
BUILDDATESTAMP_STR: 101119-1850
BUILDLAB_STR: win7sp1_rtm
BUILDOSVER_STR: 6.1.7601.17514.x86fre.win7sp1_rtm.101119-1850
ANALYSIS_SESSION_ELAPSED_TIME: 83a
ANALYSIS_SOURCE: KM
FAILURE_ID_HASH_STRING: km:0x50_nt!strstr+1e
FAILURE_ID_HASH: {955d5f89-fa0e-8221-ae1a-1f21f03e2986}
Followup: MachineOwner
---------
가장 먼저 .trap 명령을 사용해서 문제가 발생한 부분으로 컨텍스트를 맞춰야한다.
kd> .trap 0xffffffff89812c30
ErrCode = 00000000
eax=ffff0000 ebx=00000000 ecx=9213a600 edx=ffff4d5c esi=ffff0000 edi=ffff0000
eip=82ad19fe esp=89812ca4 ebp=89813bfc iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010206
nt!strstr+0x1e:
82ad19fe 8a07 mov al,byte ptr [edi] ds:0023:ffff0000=??
nt!strstr+0x1e 명령을 수행하다 ffff0000 주소가 유효하지 않아 문제가 발생했다.
kv 명령으로 파라미터를 포함한 콜 스택을 확인해보자.
kd> kv
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr Args to Child
00 89813bfc 82a89593 84525860 843f0858 843f0858 nt!strstr+0x1e
01 89813c14 82c7d99f 84489e18 843f0858 843f08c8 nt!IofCallDriver+0x63
02 89813c34 82c80b71 84525860 84489e18 00000000 nt!IopSynchronousServiceTail+0x1f8
03 89813cd0 82cc73f4 84525860 843f0858 00000000 nt!IopXxxControlFile+0x6aa
04 89813d04 82a901ea 0000015c 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
05 89813d04 76fd70b4 0000015c 00000000 00000000 nt!KiFastCallEntry+0x12a (FPO: [0,3] TrapFrame @ 89813d34)
06 0012f7f0 00000000 00000000 00000000 00000000
0x76fd70b4
콜 스택을 보면 IofCallDriver 함수 내부에서 호출된 strstr 함수를 수행하다 문제가 발생한 것으로 보인다.
얼핏 봐서는 이상한 부분이 전혀 없지만 실은 굉장히 이상한 상황이다. IofCallDriver 함수 내부에는 strstr 함수를 부르는 곳이 없기 때문이다.
이게 도대체 무슨 소리인가? 부르지도 않은 함수가 콜 스택에 표시된다니 정말 말도 안 되는 상황이다. 사실 여부를 확인하기 위해 IofCallDriver 함수의 디스어셈블리 코드를 살펴 보자.
kd> u nt!IofCallDriver L30
nt!IofCallDriver:
82a8952f 8bff mov edi,edi
82a89531 55 push ebp
82a89532 8bec mov ebp,esp
82a89534 51 push ecx
82a89535 a15ccabb82 mov eax,dword ptr [nt!pIofCallDriver (82bbca5c)]
82a8953a 56 push esi
82a8953b 8bf1 mov esi,ecx
82a8953d 33c9 xor ecx,ecx
82a8953f 3bc1 cmp eax,ecx
82a89541 7409 je nt!IofCallDriver+0x1d (82a8954c)
82a89543 ff7504 push dword ptr [ebp+4]
82a89546 8bce mov ecx,esi
82a89548 ffd0 call eax
82a8954a eb47 jmp nt!IofCallDriver+0x63 (82a89593)
82a8954c fe4a23 dec byte ptr [edx+23h]
82a8954f 384a23 cmp byte ptr [edx+23h],cl
82a89552 7f0c jg nt!IofCallDriver+0x30 (82a89560)
82a89554 51 push ecx
82a89555 51 push ecx
82a89556 51 push ecx
82a89557 52 push edx
82a89558 6a35 push 35h
82a8955a e8a3790a00 call nt!KeBugCheckEx (82b30f02)
82a8955f cc int 3
82a89560 8b4260 mov eax,dword ptr [edx+60h]
82a89563 83e824 sub eax,24h
82a89566 894260 mov dword ptr [edx+60h],eax
82a89569 8a08 mov cl,byte ptr [eax]
82a8956b 897014 mov dword ptr [eax+14h],esi
82a8956e 80f916 cmp cl,16h
82a89571 7514 jne nt!IofCallDriver+0x57 (82a89587)
82a89573 8a4001 mov al,byte ptr [eax+1]
82a89576 3c02 cmp al,2
82a89578 7404 je nt!IofCallDriver+0x4e (82a8957e)
82a8957a 3c03 cmp al,3
82a8957c 7509 jne nt!IofCallDriver+0x57 (82a89587)
82a8957e 8bf2 mov esi,edx
82a89580 e8b6f2fdff call nt!IopPoHandleIrp (82a6883b)
82a89585 eb0c jmp nt!IofCallDriver+0x63 (82a89593)
82a89587 8b4608 mov eax,dword ptr [esi+8]
82a8958a 52 push edx
82a8958b 0fb6c9 movzx ecx,cl
82a8958e 56 push esi
82a8958f ff548838 call dword ptr [eax+ecx*4+38h]
82a89593 5e pop esi
82a89594 59 pop ecx
82a89595 5d pop ebp
82a89596
c3 ret
내 눈에는 strstr 함수를 호출하는 부분이 보이지 않는다. call dword ptr [eax+ecx*4+38h] 부분이 조금 의심스럽긴하다. 디스어셈블리 코드가 어려우니 간단한 의사 코드(Pseudo code)로 바꾼 코드를 보자.
NTSTATUS
IofCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
PIO_STACK_LOCATION pIoStackLocation;
PDRIVER_OBJECT pDriverObject;
NTSTATUS ntStatus;
/* 조건에 따른 pIofCalldriver 함수 호출 */
Irp->CurrentLocation--;
// NO_MORE_IRP_STACK_LOCATIONS(0x35) 에러 처리
if (Irp->CurrentLocation <= 0)
{
KiBugCheckEx(NO_MORE_IRP_STACK_LOCATIONS, Irp, 0, 0, 0);
}
pIoStackLocation = IoGetNextIrpStackLocation( Irp );
Irp->Tail.Overlay.CurrentStackLocation = pIoStackLocation;
pIoStackLocation->DeviceObject = DeviceObject;
pDriverObject = DeviceObject->DriverObject;
// IRP 요청에 따른 드라이버의 디스패치 루틴 함수 호출
ntStatus = pDriverObject->MajorFunction[pIoStackLocation->MajorFunction](DeviceObject, Irp);
/* 조건에 따른 IopPoHandleIrp 함수 호출 */
return ntStatus;
}
어떤가? 어디에도 strstr 함수는 보이지 않는다. 나도 처음에는 굉장히 당황스럽고 혼란스러웠다.
다행히 예전에 IofCallDriver 함수를 분석해 본 경험이 있어 이상하다고 생각할 수 있었다.
이런 상황이라면 스택 정보에 문제가 있어 WinDbg가 정상적인 콜 스택을 보여주지 못하는 것은 아닌지 의심해볼 필요가 있다.
스택 정보를 담고 있는 곳은 esp(64비트는 rsp)다. dps esp 명령을 사용하면 콜 스택을 보는 k 명령으로 보여주지 않는 원본 스택 내용을 출력할 수 있다.
이 방법을 통해 스택에서 진짜 함수 호출의 흔적을 찾을 수 있다.
kd> dps esp
89812ca4 84525860
89812ca8 00000000
89812cac 84489e18
89812cb0 9213a4d1 MyDrv+0x44d1 // 2) 의심 부분
89812cb4 ffff0000
89812cb8 9213a600 MyDrv+0x4600 // 1) 의심 부분
89812cbc 00000060
89812cc0 ffff0000
89812cc4 00000000
89812cc8 00000000
89812ccc 0000000a
89812cd0 00000000
89812cd4 88173470
89812cd8 00000000
89812cdc 00000000
89812ce0 89812ca0
89812ce4 8454ba60
89812ce8 8d7e19ea
89812cec 9e50224a
89812cf0 88e1bb88
89812cf4 00000006
89812cf8 87148cde Ntfs!NtfsReadMftRecord+0x236
89812cfc 88e1ba98
89812d00 92d7c6a0
89812d04 8d7e1800
89812d08 00080008
89812d0c 89812d3c
89812d10 87163f6d Ntfs!NtfsFileIsEqual+0x56
89812d14 89812d2c
89812d18 89812d34
89812d1c 00000001
89812d20
896d4000
놀랍게도 콜 스택에 보이지 않던 MyDrv모듈의 함수 호출 흔적이 확인된다.
함수가 호출되면 esp에 함수 수행 후 복귀할 리턴 주소인 함수 호출 명령 다음 위치가 저장된다는 사실을 떠올려보자.
esp에 MyDrv 관련된 주소 두 곳이 보이는데, ub 명령으로 역 디스어셈블링했을 때 strstr 함수를 호출하는 부분이 바로 리턴 주소다.
1)번 위치인 MyDrv+4600 먼저 확인해보자.
kd> ub MyDrv+0x4600
MyDrv+0x45f8:
9213a5f8 cc int 3
9213a5f9 cc int 3
9213a5fa cc int 3
9213a5fb cc int 3
9213a5fc cc int 3
9213a5fd cc int 3
9213a5fe cc int 3
9213a5ff
cc int 3
strstr 함수 호출 부분이 확인되지 않으니 찾는 부분이 아니다.
다음 2)번 위치인 MyDrv+44d1을 확인해보자.
kd> ub MyDrv+44d1
MyDrv+0x44ab:
9213a4ab c7411c00000000 mov dword ptr [ecx+1Ch],0
9213a4b2 eb57 jmp MyDrv+0x450b (9213a50b)
9213a4b4 e857cbffff call MyDrv+0x1010 (92137010)
9213a4b9 8985c4f0ffff mov dword ptr [ebp-0F3Ch],eax
9213a4bf 6800a61392 push offset MyDrv+0x4600 (9213a600)
9213a4c4 8b95c4f0ffff mov edx,dword ptr [ebp-0F3Ch]
9213a4ca 52 push edx
9213a4cb ff1558801392 call
dword ptr [MyDrv+0x2058 (92138058)]
의미있는 정보가 나온다. 마지막 시점에 MyDrv+0x2058(92138058)가 가리키는 주소로 함수를 호출한다.
dword ptr 명령으로 92138058 주소를 참조하고 있으니 poi 명령으로 함수 호출한 부분을 확인해보자.
kd> u poi(92138058)
nt!strstr:
82ad19e0 8b4c2408 mov ecx,dword ptr [esp+8]
82ad19e4 57 push edi
82ad19e5 53 push ebx
82ad19e6 56 push esi
82ad19e7 8a11 mov dl,byte ptr [ecx]
82ad19e9 8b7c2410 mov edi,dword ptr [esp+10h]
82ad19ed 84d2 test dl,dl
82ad19ef
746f je
nt!strstr+0x80 (82ad1a60)
빙고! strstr 함수가 나온다. 스택의 2)번 위치인 MyDrv+0x44d1에서 strstr 함수를 호출했음이 밝혀졌다.
이제 문제 발생 원인에 초점을 맞춰보자. MyDrv 모듈에서 strstr 함수 호출시 전달한 파라미터에 문제가 있었을 가능성이 높다.
strstr 함수는 원본 문자열에 검색 문자열이 있을 경우 일치하는 원본 문자열의 첫 번째 위치를 리턴해주는데 함수 원형은 다음과 같다.
PTSTR StrStr(
_In_ PTSTR pszFirst,
_In_ PCTSTR pszSrch
함수 원형은 확인했으니 다시 dps esp 명령으로 스택 정보를 확인해보자.
kd> dps esp L10
89812ca4 84525860
89812ca8 00000000
89812cac 84489e18
89812cb0 9213a4d1 MyDrv+0x44d1 // esp+c : 리턴 어드레스
89812cb4 ffff0000 // esp+10 : 첫 번째 파라미터
89812cb8 9213a600 MyDrv+0x4600 // esp+14 : 두 번째 파라미터
89812cbc 00000060
89812cc0 ffff0000
89812cc4 00000000
89812cc8 00000000
89812ccc 0000000a
89812cd0 00000000
89812cd4 88173470
89812cd8 00000000
89812cdc 00000000
89812ce0
89812ca0
원본 문자열인 pszFirst 는 ffff0000이고, 검색할 문자열인 pszSrch는 9213a600이다.
문제가 되는 함수가 문자열 함수여서 da 명령으로 파라미터 문자열을 출력해 보았다.
kd> da ffff0000
ffff0000 "????????????????????????????????"
ffff0020 "????????????????????????????????"
ffff0040 "????????????????????????????????"
ffff0060 "????????????????????????????????"
ffff0080 "????????????????????????????????"
ffff00a0 "????????????????????????????????"
ffff00c0 "????????????????????????????????"
ffff00e0 "????????????????????????????????"
ffff0100 "????????????????????????????????"
ffff0120 "????????????????????????????????"
ffff0140 "????????????????????????????????"
ffff0160 "????????????????????????????????"
kd> da 9213a600
9213a600 "\Microsoft\Windows\Burn\"
원본 문자열인 첫 번재 파라미터의 주소는 페이지 아웃되어 ??로 표시된다. 접근할 수 없는 영역이라는 의미다. 검색 문자열인 두 번째 파라미터에는 정상적인 경로 문자열이 확인된다.
잘못된 주소인 ffff0000에서 "\Microsoft\Windows\Burn\" 문자열이 있는지 찾으려고 시도하다 문제가 발생한 것이 원인이다.
그렇다면, 원본 문자열 주소인 ffff0000는 어디서 온 것일까? ffff0000은 strstr 함수에 넘긴 첫 번째 파라미터니 strstr 함수를 호출한 MyDrv+44d1 부분을 다시 살펴보자.
kd> ub MyDrv+44d1
MyDrv+0x44ab:
9213a4ab c7411c00000000 mov dword ptr [ecx+1Ch],0
9213a4b2 eb57 jmp MyDrv+0x450b (9213a50b)
9213a4b4 e857cbffff call MyDrv+0x1010 (92137010) // 1) 내부 함수 호출
9213a4b9 8985c4f0ffff mov dword ptr [ebp-0F3Ch],eax // 2) eax를 ebp-f3c에 저장
9213a4bf 6800a61392 push offset MyDrv+0x4600 (9213a600) // 3) MyDrv+0x4600을 두 번째 파라미터로 push
9213a4c4 8b95c4f0ffff mov edx,dword ptr [ebp-0F3Ch] // 4) ebp-f3c을 edx에 저장
9213a4ca 52 push edx // 5) edx를 첫 번째 파라미터로 push
9213a4cb ff1558801392 call
dword ptr [MyDrv+0x2058 (92138058)] //
6) strstr 함수 호출
2)번을 보면 eax 값이 ebp-f3c에 저장되고, 4)번에서 다시 edx로 옮겨진다. 그리고 5)번을 보면 edx가 첫 번째 파라미터로 넘겨졌음을 알 수 있다.
즉 eax 가 첫 번째 파라미터이므로 2)번 앞 쪽에서 eax를 설정한 곳을 찾아봐야 한다.
1)번에 함수가 하나 호출되는데 그 안에서 eax가 설정되었을 가능성이 높다. 보통 eax는 함수 호출 완료 후 리턴 값을 저장하는 용도로 사용되기 때문이다.
1)번에서 호출된 함수 주소인 92137010을 살펴보자.
kd> u 92137010 L6
MyDrv+0x1010:
92137010 8bff mov edi,edi
92137012 55 push ebp
92137013 8bec mov ebp,esp
92137015 b80000ffff mov eax,0FFFF0000h // ffff0000을 eax에 저장
9213701a 5d pop ebp
9213701b c3 ret
이런! 이 함수에서 ffff0000 값을 리턴하고 있다. 이 값이 결국 strstr 함수의 첫 번째 파라미터로 전달되어 문제가 발생한 것이다.
이제 ffff0000을 리턴한 문제의 함수를 확인해서 정상적인 값을 리턴하도록 변경하면 문제가 해결될 것이다.
나는 앞서 원인 분석에 집중하기 위해 의도적으로 숨겨진 콜 스택이 발생한 이유에 대해서는 언급하지 않았다. 이제 문제 발생 원인에 대한 분석이 끝났으니 그 얘기를 마저 해보겠다.
콜 스택을 확인하는 k 명령으로 MyDrv의 콜 스택이 제대로 확인되지 않았던 이유는 strstr 함수가 컴파일된 방식에 원인이 있다.
다음은 strstr 함수와 memcpy 함수의 앞 부분을 비교한 내용이다.
<nt!strstr 함수 프롤로그>
kd> u nt!strstr
nt!strstr:
82ac59e0 8b4c2408 mov ecx,dword ptr [esp+8]
82ac59e4 57 push edi
82ac59e5 53 push ebx
82ac59e6 56 push esi
82ac59e7 8a11 mov dl,byte ptr [ecx]
82ac59e9 8b7c2410 mov edi,dword ptr [esp+10h]
82ac59ed 84d2 test dl,dl
82ac59ef 746f je nt!strstr+0x80 (82ac5a60)
<nt!memcpy 함수 프롤로그>
kd> u nt!memcpy
nt!memcpy:
82a7f7c0 55 push ebp
82a7f7c1 8bec mov ebp,esp
82a7f7c3 57 push edi
82a7f7c4 56 push esi
82a7f7c5 8b750c mov esi,dword ptr [ebp+0Ch]
82a7f7c8 8b4d10 mov ecx,dword ptr [ebp+10h]
82a7f7cb 8b7d08 mov edi,dword ptr [ebp+8]
82a7f7ce 8bc1 mov eax,ecx
어떤 차이가 보이는가? 32비트 환경에서 함수 호출을 하게 되면 기본적으로 스택의 베이스 포인터인 ebp를 기준으로 스택을 구성하게 된다.
memcpy 함수처럼 "push ebp, mov ebp, esp" 로 원본 ebp를 스택에 저장하고 현재 esp를 기준으로 ebp를 재설정하는 패턴이 함수 시작시 수행된다.
k 명령도 이 ebp 를 기준으로 콜 스택을 보여준다. 하지만 strstr 함수의 경우 재미있게도 ebp를 사용하지 않고 64비트 환경처럼 esp 를 기준으로 함수가 동작하게 컴파일되어 있다.
다음은 CompareString 함수 안에서 strstr 함수를 호출하는 테스트 코드를 만들어 콜 스택을 확인한 결과다.
1. CompareString 함수에서 strstr 함수 호출 전 콜 스택
kd> kv
# ChildEBP RetAddr Args to Child
00 92237bc8 913da27b 00000388 00000388 82b7d944 MyDrv!CompareString+0x37 (FPO: [Non-Fpo]) (CONV: stdcall)
01 92237d28 82a841ea 00000388 01f4f900 771670b4 MyDrv!TestMain+0x4b (FPO: [Non-Fpo]) (CONV: stdcall)
02 92237d28 771670b4 00000388 01f4f900 771670b4 nt!KiFastCallEntry+0x12a (FPO: [0,3] TrapFrame @ 92237d34)
kd> ub 913d9d3e
MyDrv!CompareString+0x26
913d9d27 7507 jne MyDrv!CompareString+0x2f (913d9d30)
913d9d29 32c0 xor al,al
913d9d2b e9b3000000 jmp MyDrv!CompareString+0xe2 (913d9de3)
913d9d30 8b55d0 mov edx,dword ptr [ebp-30h]
913d9d33 52 push edx
913d9d34 8b45dc mov eax,dword ptr [ebp-24h]
913d9d37 50 push eax
913d9d38 ff15f4103e91 call
dword ptr [MyDrv!_imp__strstr (913e10f4)] // 현재 strstr 함수를
호출하기 직전 상태
2. CompareString 함수에서 strstr 함수 호출 직후 콜 스택
kd> kv
# ChildEBP RetAddr Args to Child
00 92237bc8 913da27b 00000388 00000388 82b7d944 nt!strstr
01 92237d28 82a841ea 00000388 01f4f900 771670b4 MyDrv!TestMain+0x4b (FPO: [Non-Fpo]) (CONV: stdcall)
02 92237d28 771670b4 00000388 01f4f900 771670b4
nt!KiFastCallEntry+0x12a (FPO: [0,3] TrapFrame @ 92237d34)
예상대로 strstr 함수 내부에서 ebp를 설정하는 부분이 없으므로 CompareString 함수에 대한 콜 스택이 표시되지 않고 사라졌다.
이처럼 함수에 따라 예상과는 다른 코드 패턴으로 컴파일될 수 있다는 사실을 기억하자.
이 외에도 스택 오버플로우나 jmp를 이용한 코드 후킹 기법 등에 의해 스택이나 함수 프롤로그가 망가지면 ebp 기준으로 해석하는 k 명령은 콜 스택을 제대로 표시하지 못할 수 있다.
뭔가 콜 스택이 이상하다고 느껴지면 주저없이 dps esp(64비트는 rsp) 명령으로 스택을 확인하거나 k=address 명령으로 콜 스택을 재구성해보자. 의외의 단서를 얻을 수 있을 것이다.
'Dump Analysis' 카테고리의 다른 글
[0xC5] 풀 헤더 손상 (0) | 2018.07.16 |
---|---|
[0x1A] 페이지 손상 (0) | 2018.07.12 |
[0x50] 해제된 핸들 (0) | 2018.07.09 |
[0x50] UNICODE_STRING (0) | 2018.07.05 |
필터매니저(fltmgr.sys) 버그? DRAINING ZOMBIED 와 DOE_UNLOAD_PENDING (0) | 2015.01.22 |
이번 덤프는 문자열 처리 중 유효하지 않은 메모리 영역에 접근하여 발생한 BSOD다.
0x50은 BugCheck 코드명을 의미하며 BSOD 유형을 분류한 것이라고 생각하면 된다. 이 코드는 주로 접근할 수 없는 메모리 주소 영역을 접근했을 때 발생한다.
분석이 크게 어렵지는 않으니 가볍게 읽을 수 있을 것이다.
항상 분석의 시작은 !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.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: a0f6c000, memory referenced.
Arg2: 00000000, value 0 = read operation, 1 = write operation.
Arg3: 92140037, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 00000000, (reserved)
Debugging Details:
------------------
DUMP_CLASS: 1
DUMP_QUALIFIER: 401
BUILD_VERSION_STRING: 7601.17514.x86fre.win7sp1_rtm.101119-1850
SYSTEM_MANUFACTURER: VMware, Inc.
VIRTUAL_MACHINE: VMware
SYSTEM_PRODUCT_NAME: VMware Virtual Platform
SYSTEM_VERSION: None
BIOS_VENDOR: Phoenix Technologies LTD
BIOS_VERSION: 6.00
BIOS_DATE: 07/31/2013
BASEBOARD_MANUFACTURER: Intel Corporation
BASEBOARD_PRODUCT: 440BX Desktop Reference Platform
BASEBOARD_VERSION: None
DUMP_TYPE: 1
BUGCHECK_P1: ffffffffa0f6c000
BUGCHECK_P2: 0
BUGCHECK_P3: ffffffff92140037
BUGCHECK_P4: 0
READ_ADDRESS: a0f6c000 Paged pool
FAULTING_IP:
MyDrv+1037
92140037 0fb708 movzx ecx,word ptr [eax]
MM_INTERNAL_CODE: 0
IMAGE_NAME: MyDrv.sys
DEBUG_FLR_IMAGE_TIMESTAMP: 5a1cdd5a
MODULE_NAME: MyDrv
FAULTING_MODULE: 9213f000 MyDrv
CPU_COUNT: 1
CPU_MHZ: e07
CPU_VENDOR: GenuineIntel
CPU_FAMILY: 6
CPU_MODEL: 3c
CPU_STEPPING: 3
CPU_MICROCODE: 6,3c,3,0 (F,M,S,R) SIG: 19'00000000 (cache) 19'00000000 (init)
DEFAULT_BUCKET_ID: WIN7_DRIVER_FAULT
BUGCHECK_STR: 0x50
PROCESS_NAME: explorer.exe
CURRENT_IRQL: 2
ANALYSIS_SESSION_HOST: PAUL-PC
ANALYSIS_SESSION_TIME: 11-28-2017 14:40:07.0617
ANALYSIS_VERSION: 10.0.10575.567 amd64fre
TRAP_FRAME: 8b4a8a90 -- (.trap 0xffffffff8b4a8a90)
ErrCode = 00000000
eax=a0f6c000 ebx=a0e78680 ecx=0000006c edx=a0f6c000 esi=846c5838 edi=8b4a8b5c
eip=92140037 esp=8b4a8b04 ebp=8b4a8b0c iopl=0 nv up ei pl nz ac po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010212
MyDrv+0x1037:
92140037 0fb708 movzx ecx,word ptr [eax] ds:0023:a0f6c000=????
Resetting default scope
LAST_CONTROL_TRANSFER: from 82a783d8 to 82ac541b
STACK_TEXT:
8b4a8a78 82a783d8 00000000 a0f6c000 00000000 nt!MmAccessFault+0x106
8b4a8a78 92140037 00000000 a0f6c000 00000000 nt!KiTrap0E+0xdc
8b4a8b0c 82ca639c 84653218 00000564 8b4a8b5c MyDrv+0x1037
8b4a8b34 82c8e27f 84653218 00000564 82b79b84 nt!PsCallImageNotifyRoutines+0x62
8b4a8be8 82c7ed4a 84664d80 85eb6030 8b4a8ce4 nt!MiMapViewOfImageSection+0x670
8b4a8c58 82c7ee3a 85eb6030 8b4a8ce4 00000000 nt!MiMapViewOfSection+0x22e
8b4a8c88 82c7f599 9fcdb1e8 85eb6030 8b4a8ce4 nt!MmMapViewOfSection+0x2a
8b4a8d04 82a751ea 00001458 ffffffff 0cccee94 nt!NtMapViewOfSection+0x204
8b4a8d04 771d70b4 00001458 ffffffff 0cccee94 nt!KiFastCallEntry+0x12a
0cccefa0 00000000 00000000 00000000 00000000 0x771d70b4
STACK_COMMAND: kb
THREAD_SHA1_HASH_MOD_FUNC: 53ccc111e8062d929cc0fad19df6ef4e0edf76dd
THREAD_SHA1_HASH_MOD_FUNC_OFFSET: 23675873345d32020fde61dc27c04558a8fd2339
THREAD_SHA1_HASH_MOD: 4e5c1c39aac5cd40526a2d8509134f551a034639
FOLLOWUP_IP:
MyDrv+1037
92140037 0fb708 movzx ecx,word ptr [eax]
FAULT_INSTR_CODE: 8508b70f
SYMBOL_STACK_INDEX: 2
SYMBOL_NAME: MyDrv+1037
FOLLOWUP_NAME: MachineOwner
FAILURE_BUCKET_ID: 0x50_MyDrv+1037
BUCKET_ID: 0x50_MyDrv+1037
PRIMARY_PROBLEM_CLASS: 0x50_MyDrv+1037
TARGET_TIME: 2017-11-28T05:34:28.000Z
OSBUILD: 7601
OSSERVICEPACK: 1000
SERVICEPACK_NUMBER: 0
OS_REVISION: 0
SUITE_MASK: 272
PRODUCT_TYPE: 1
OSPLATFORM_TYPE: x86
OSNAME: Windows 7
OSEDITION: Windows 7 WinNt (Service Pack 1) TerminalServer SingleUserTS
OS_LOCALE:
USER_LCID: 0
OSBUILD_TIMESTAMP: 2010-11-20 17:42:49
BUILDDATESTAMP_STR: 101119-1850
BUILDLAB_STR: win7sp1_rtm
BUILDOSVER_STR: 6.1.7601.17514.x86fre.win7sp1_rtm.101119-1850
ANALYSIS_SESSION_ELAPSED_TIME: 766
ANALYSIS_SOURCE: KM
FAILURE_ID_HASH_STRING: km:0x50_mydrv+1037
FAILURE_ID_HASH: {348be9c7-8048-f84c-6228-bb7942eb0452}
Followup: MachineOwner
---------
.trap 명령을 사용해서 문제가 발생한 부분으로 컨텍스트 정보를 설정해보자.
kd> .trap 0xffffffff8b4a8a90
ErrCode = 00000000
eax=a0f6c000 ebx=a0e78680 ecx=0000006c edx=a0f6c000 esi=846c5838 edi=8b4a8b5c
eip=92140037 esp=8b4a8b04 ebp=8b4a8b0c iopl=0 nv up ei pl nz ac po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010212
MyDrv+0x1037:
92140037 0fb708 movzx ecx,word ptr [eax] ds:0023:a0f6c000=????
eax가 가리키는 a0f6c000 메모리 영역이 접근할 수 없는 영역이라 BSOD가 발생했다.
kv 명령어로 파라미터를 포함한 콜 스택을 확인해보자.
kd> kv
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr Args to Child
00 8b4a8b0c 82ca639c 84653218 00000564 8b4a8b5c MyDrv+0x1037
01 8b4a8b34 82c8e27f 84653218 00000564 82b79b84 nt!PsCallImageNotifyRoutines+0x62
02 8b4a8be8 82c7ed4a 84664d80 85eb6030 8b4a8ce4 nt!MiMapViewOfImageSection+0x670
03 8b4a8c58 82c7ee3a 85eb6030 8b4a8ce4 00000000 nt!MiMapViewOfSection+0x22e
04 8b4a8c88 82c7f599 9fcdb1e8 85eb6030 8b4a8ce4 nt!MmMapViewOfSection+0x2a
05 8b4a8d04 82a751ea 00001458 ffffffff 0cccee94 nt!NtMapViewOfSection+0x204
06 8b4a8d04 771d70b4 00001458 ffffffff 0cccee94 nt!KiFastCallEntry+0x12a (FPO: [0,3] TrapFrame @ 8b4a8d34)
07 0cccefa0 00000000 00000000 00000000 00000000 0x771d70b4
nt 커널에서 콜백 함수로 호출해주는 MyDrv 모듈의 MyDrv+0x1037 함수에서 문제가 발생했다.
이 콜백은 PsSetLoadImageNotifyRoutine을 통해 미리 등록하면 exe나 dll 등의 파일이 로드될 때 해당 모듈의 정보를 받을 수 있는 함수다.
MyDrv 모듈 심볼은 없지만 콜 스택에서 PsSetLoadImageNotifyRoutine 함수 다음에 호출된 함수이므로 해당 콜백임을 알 수 있다.
나는 이 콜백을 편의상 LoadImageNotifyRoutine이라고 부른다.
다행히 BSOD가 발생한 위치도 명확하고 콜 스택도 단순한 편이라 마지막 MyDrv+0x1037 부분만 분석하면 답을 찾을 수 있겠다..
다음은 BSOD가 발생한 MyDrv+0x1037 부분이 포함되도록 함수 시작부터 디스어셈블링한 내용이다.
kd> u 92140010 L19
MyDrv+0x1010:
92140010 8bff mov edi,edi
92140012 55 push ebp
92140013 8bec mov ebp,esp
92140015 83ec08 sub esp,8
92140018 0fb60520201492 movzx eax,byte ptr [MyDrv+0x3020 (92142020)]
9214001f 83f801 cmp eax,1
92140022 752e jne MyDrv+0x1052 (92140052)
92140024 8b4d08 mov ecx,dword ptr [ebp+8] // 1) 첫 번째 파라미터 ecx에 저장(ecx = ebp + 8)
92140027 8b5104 mov edx,dword ptr [ecx+4] // 2) 첫 번째 파라미터의 +4 위치 edx에 저장(edx = ecx + 4)
9214002a 8955fc mov dword ptr [ebp-4],edx
9214002d c745f800000000 mov dword ptr [ebp-8],0
92140034 8b45fc mov eax,dword ptr [ebp-4] // 3) 루프 시작
92140037 0fb708 movzx ecx,word ptr [eax] // 4) 2바이트 읽음, BSOD 발생 위치
9214003a 85c9 test ecx,ecx // 5) 값이 NULL 이면 루프 종료
9214003c 7414 je MyDrv+0x1052 (92140052)
9214003e 8b55fc mov edx,dword ptr [ebp-4]
92140041 83c202 add edx,2 // 6) 다음 2바이트로 이동
92140044 8955fc mov dword ptr [ebp-4],edx
92140047 8b45f8 mov eax,dword ptr [ebp-8]
9214004a 83c001 add eax,1
9214004d 8945f8 mov dword ptr [ebp-8],eax
92140050 ebe2 jmp MyDrv+0x1034 (92140034) // 7) NULL 이 아니면 3)번으로 이동해서 다음 2바이트 읽기
92140052 8be5 mov esp,ebp
92140054 5d pop ebp
92140055 c20c00 ret 0Ch
MyDrv 모듈에서 nt 커널에서 전달 받은 첫 번째 파라미터로 뭔가 반복적인 처리를 하다 문제가 발생했다.
전달된 파라미터가 이상했거나 잘못된 방식으로 파라미터를 처리했을 가능성이 의심된다.
첫 번째 파라미터의 정체를 알기 위해 LoadImageNotifyRoutine 콜백(MyDrv+0x1037)의 함수 원형을 MSDN에서 찾아보자.
우선 콜백을 등록하는 PsSetLoadImageNotifyRoutine 함수를 먼저 살펴보자.
NTSTATUS PsSetLoadImageNotifyRoutine(
_In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
PLOAD_IMAGE_NOTIFY_ROUTINE 함수 포인터를 전달해주면 나중에 DLL 등의 파일이 커널로 로드될 때 이 함수 포인터가 콜백으로 불리는 방식이다.
이제 PLOAD_IMAGE_NOTIFY_ROUTINE을 살펴보자.
PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine;
void SetLoadImageNotifyRoutine(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
);
아하, 첫 번째 파라미터는 커널과 드라이버에서 자주 사용하는 문자열 형식인 UNICODE_STRING 구조체의 포인터다. MSDN을 읽어보면 로드되는 이미지의 전체 경로를 UNICODE_STRING 형태로 알려준다.
이제 파라미터의 정체를 알았으니 dt 명령으로 UNICODE_STRING 구조체의 내용을 살펴보자.
kd> dt _UNICODE_STRING
nt!_UNICODE_STRING
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x004 Buffer : Ptr32
Uint2B
앞 4바이트는 문자열의 크기와 문자열 버퍼의 최대 크기로 바이트 단위다. 다음 4바이트는 문자열 버퍼의 포인터다.
구조체를 보니 앞서 함수 앞 부분에서 수행했던 연산들의 의미를 알 것 같다. 2)번 이후의 연산들은 결국 UNICODE_STRING 구조체의 +4 위치에 있는 Buffer를 가지고 와서 NULL을 만날 때까지 2바이트 씩 읽는 동작이다.
2바이트 씩 읽은 이유는 UNICODE_STRING의 Buffer가 WCHAR의 포인터인 PWSTR 자료형이기 때문이다.
앞서 1)번에서 ebp+8이 첫 번째 파라미터인 것을 확인했으니 동일하게 dt 명령으로 확인해보자.
kd> dt nt!_UNICODE_STRING poi(ebp+8)
"\Windows\assembly\GAC_MSIL\Microsoft.Web.Delegation.resources\7.1.0.0_it_31bf3856ad364e35\Microsoft.Web.Delegation.resources.dll"
+0x000 Length : 0x100
+0x002 MaximumLength : 0x100
+0x004 Buffer : 0xa0f6bf00 "\Windows\assembly\GAC_MSIL\Microsoft.Web.Delegation.resources\7.1.0.0_it_31bf3856ad364e35\Microsoft.Web.Delegation.resources.dll"
ebp+8 위치의 값이 UNICODE_STRING 포인터이므로 입력된 주소의 값을 가져오는 poi 명령을 사용했다.
poi(x)는 x라는 메모리 주소의 내용을 읽는 구문이다.
C 언어에서 포인터의 값을 읽을 때
value = *x;
로 표현하는데
value = poi(x)
라고 생각하면 쉽다.
Buffer 필드에 "\Windows\assembly\GAC_MSIL\Microsoft.Web.Delegation.resources\7.1.0.0_it_31bf3856ad364e35\Microsoft.Web.Delegation.resources.dll" 문자열이 보이고, Length, MaximumLength는 0x100이다.
문자열을 직접 세보니 128자였다. Length가 0x100이고 바이트 크기니 0x100의 10진수인 256을 WCHAR 크기인 2로 나누면 정확하게 128이 나온다.
MaximumLength는 Buffer인 a0f6bf00의 최대 크기로 접근 가능한 영역을 의미한다. Length와 크거나 같으면 되므로 정상이다.
입력된 파라미터에는 이상한 부분이 보이지 않는데 왜 문제가 발생했을까?
MyDrv 모듈에서는 Buffer인 a0f6bf00 값을 직접 접근해서 NULL을 만날 때까지 2바이트 씩 증가하는 루프를 수행했다. 아무래도 이 동작에 문제가 있을 것 같다.
db 명령으로 a0f6bf00 내용을 확인해보자.
kd> db a0f6bf00 L110
a0f6bf00 5c 00 57 00 69 00 6e 00-64 00 6f 00 77 00 73 00 \.W.i.n.d.o.w.s.
a0f6bf10 5c 00 61 00 73 00 73 00-65 00 6d 00 62 00 6c 00 \.a.s.s.e.m.b.l.
a0f6bf20 79 00 5c 00 47 00 41 00-43 00 5f 00 4d 00 53 00 y.\.G.A.C._.M.S.
a0f6bf30 49 00 4c 00 5c 00 4d 00-69 00 63 00 72 00 6f 00 I.L.\.M.i.c.r.o.
a0f6bf40 73 00 6f 00 66 00 74 00-2e 00 57 00 65 00 62 00 s.o.f.t...W.e.b.
a0f6bf50 2e 00 44 00 65 00 6c 00-65 00 67 00 61 00 74 00 ..D.e.l.e.g.a.t.
a0f6bf60 69 00 6f 00 6e 00 2e 00-72 00 65 00 73 00 6f 00 i.o.n...r.e.s.o.
a0f6bf70 75 00 72 00 63 00 65 00-73 00 5c 00 37 00 2e 00 u.r.c.e.s.\.7...
a0f6bf80 31 00 2e 00 30 00 2e 00-30 00 5f 00 69 00 74 00 1...0...0._.i.t.
a0f6bf90 5f 00 33 00 31 00 62 00-66 00 33 00 38 00 35 00 _.3.1.b.f.3.8.5.
a0f6bfa0 36 00 61 00 64 00 33 00-36 00 34 00 65 00 33 00 6.a.d.3.6.4.e.3.
a0f6bfb0 35 00 5c 00 4d 00 69 00-63 00 72 00 6f 00 73 00 5.\.M.i.c.r.o.s.
a0f6bfc0 6f 00 66 00 74 00 2e 00-57 00 65 00 62 00 2e 00 o.f.t...W.e.b...
a0f6bfd0 44 00 65 00 6c 00 65 00-67 00 61 00 74 00 69 00 D.e.l.e.g.a.t.i.
a0f6bfe0 6f 00 6e 00 2e 00 72 00-65 00 73 00 6f 00 75 00 o.n...r.e.s.o.u.
a0f6bff0 72 00 63 00 65 00 73 00-2e 00 64 00 6c 00 6c 00 r.c.e.s...d.l.l.
이런! a0f6bf00 위치에 접근할 때 BSOD가 발생했는데 메모리 내용을 보니 문자열 끝에 NULL(0x00) 값이 존재하지 않는다. a0f6c000부터는 페이지 아웃되어 접근할 수 없는 영역이다.
문자열 버퍼인 a0f6bf00은 MaximumLength 크기인 a0f6bfff까지만 접근 가능한데, 이 영역을 넘어 a0f6c000까지 접근하다 BSOD가 발생한 것이다.
MyDrv에서 UNICODE_STRING의 특징을 잘 모르고 사용한 것이 문제의 원인이다.
MSDN을 찾아보면 만약 Buffer 필드의 문자열이 NULL 문자로 끝날 경우 Length 필드의 길이는 NULL 문자를 포함하지 않는다고 적혀있다. UNICODE_STRING의 Buffer 문자열이 NULL 로 끝나지 않을 수 있음을 암시하고 있는 것이다.
Remarks : If the string is null-terminated, Length does not include the trailing null character.
MyDrv는 아마 다음과 같은 함수를 구현해서 사용하다 BSOD를 발생시켰을 것이다.
다음에 소개하는 GetLength, TestFunc1, TestFunc2 함수는 이해를 돕기 위해 내가 작성해 본 의사 코드(Pseudo Code)다. GetLength 함수는 문자열의 길이를 구하는 함수고, TestFunc1, TestFunc2 함수는 GetLength 함수를 사용하는 함수를 의미한다.
ULONG
GetLength(PWSTR pImagePath)
{
ULONG ulLength = 0;
while (*pImagePath)
{
pImagePath ++;
ulLength ++;
}
return ulLength;
}
VOID
TestFunc1(PWSTR pImagePath)
{
ulCount = GetLength(pImagePath);
}
VOID
TestFunc2(PUNICODE_STRING pImagePath)
{
ulCount = GetLength(pImagePath->Buffer);
GetLength 함수는 단순하게 문자열 길이를 구하는 함수인데 입력된 문자열에서 NULL 값을 만날 때까지 포인터를 하나씩 증가시키며 문자열 길이를 센다. 얼핏 보기에는 문제가 없어 보이지만 이 함수에는 심각한 문제가 숨어 있다. 바로 문자열이 항상 NULL로 끝난다고 가정하는 것이다.
문제를 단순화하기 위해 TestFunc1, TestFunc2 함수에 전달되는 pImagePath 파라미터는 항상 유효하다고 가정해보자.
TestFunc1 함수는 언제나 잘 동작할 것이다. 하지만 TestFunc2 함수는 BSOD가 발생할 가능성이 있다.
왜냐하면 UNICODE_STRING의 Buffer 문자열은 NULL로 끝나지 않을 수 있기 때문이다!
절대로 UNICODE_STRING의 Buffer 를 pusImagePath->Buffer 형태로 직접 사용하면 안된다.
문자열 길이를 구하기 위함이라면 단순히 UNICODE_STRING의 Length를 사용하면 된다. Buffer의 값을 사용할 때는 반드시 Length 필드를 확인해서 유효한 메모리 영역만 접근해야 한다.
과거 코드 리뷰 중 다음과 같은 코드를 본 기억이 난다.
SomeFunc(…)
{
PWSTR pszImagePath;
// 절대로 안 된다!
pszImagePath = pusImagePath->Buffer;
이런 코드를 본다면 머릿속에서 경고음이 마구 울려야 한다.
다시 한 번 말하지만 절대로 UNICODE_STRING의 문자열 버퍼가 NULL로 끝날 것이라 가정하지 마라. 그리고 UNICODE_STRING은 가급적 가공하지 말고 그대로 사용하도록 하자.
RtlUnicodeStringxxx로 시작하는 API를 사용하는 편이 좋고, 만약 Buffer 필드를 직접 사용해야 한다면 반드시 Length 필드를 함께 확인하는 습관을 들이자.
안타깝게도 개발을 하다보면 이런 주의 사항을 알고도 잊어 버리는 경우가 종종 있다.
보통 문자열을 다룰 때 많은 버그가 발생하게 되는데 UNICODE_STRING을 다룰 때는 좀 더 긴장하는 편이 좋다.
가장 좋은 방법은 문자열을 다룰 때 RtlStringCbxxx, RtlStringCchxxx 계열의 안전한 문자열 API를 사용하는 것이다.
이런 API가 사용된 코드는 문자열 관련 버그나 버퍼 오버플로우 같은 문제가 쉽게 발생하지 않는다.
'Dump Analysis' 카테고리의 다른 글
[0xC5] 풀 헤더 손상 (0) | 2018.07.16 |
---|---|
[0x1A] 페이지 손상 (0) | 2018.07.12 |
[0x50] 해제된 핸들 (0) | 2018.07.09 |
[0x50] 숨겨진 콜 스택 (0) | 2018.07.07 |
필터매니저(fltmgr.sys) 버그? DRAINING ZOMBIED 와 DOE_UNLOAD_PENDING (0) | 2015.01.22 |