2012. 6. 8. 16:45

Application Verifier를 이용하여 Heap Corruption(buffer overrun) 찾기

개발 과정에서 아래와 같은 유사한 실수가 자주 있습니다.

LPTSTR buf = new TCHAR[10];
...
::GetSystemDirectory(buf, MAX_PATH);
...
즉, 동적 할당한 크기(10글자)보다 더 크게(260글자) 메모리(Heap)에 쓰는 경우입니다.
이런 경우, 오류는 즉각적으로 보고되지 않으며 엉뚱한 위치에서 Crash가 발생하는 경우가 대다수 입니다.
그래서 정확한 오류 위치를 찾기가 생각보단 쉽지 않습니다.
그리고, 이런 경우 buffer overrun으로 인해 프로그램의 취약점에 포함되어, 해커의 공격대상이 되기도 합니다.

아래와 같은 샘플을 가지고 진행하겠습니다.
#include "stdafx.h"
#include <WINDOWS.H>
#include <CONIO.H>
#include <TIME.H>
#include <STDLIB.H>

void heap_corrupt(IN LPTSTR lpszBuf)
{
	_tprintf(TEXT("try to corrupt\r\n"));

	::GetSystemDirectory(lpszBuf, MAX_PATH);

	_tprintf(TEXT("corruptted\r\n"));
}

int _tmain(int argc, _TCHAR* argv[])
{
	LPTSTR buf = NULL;

	buf = new TCHAR[10];

	heap_corrupt(buf);

	int i = 0;

	srand(time(NULL));

	_tprintf(TEXT("start\r\n"));

	for (i=0; i<100; i++)
	{
		LPBYTE p = new BYTE[rand() % 100 + 100];
		LPBYTE p2 = new BYTE[rand() % 100 + 100];
		_tprintf(TEXT("[%d] alloc\r\n"), i);
		delete [] p;
		delete [] p2;
		_tprintf(TEXT("[%d] free\r\n"), i);
	}
	
	_tprintf(TEXT("all done\r\n"));

	getche();

	return 0;
}
즉, GetSystemDirectory는 보통 C:\Windows\system32를 리턴하는데, 10글자만 할당하여 오류가 발생하는 코드입니다. 그리고, 보통 Heap Corruption 발생 이후에, Heap 관련 API를 사용하게 되면 오류가 잘 발생하기 때문에, 동적할당 2개를 연속 수행하고 해제하여 일반적인 프로그램 행위 상태를 만들었습니다.

위는 Source Code, Binary, 그리고 디버깅을 위한 pdb 파일이 포함되어 있습니다.

우선, Heap.exe를 실행하게 되면,


와 같이 오류가 발생하거나 혹은 경우에 따라 오류가 발생하지 않기도 합니다.
즉, 항상 문제가 재현되지 않기 때문에 개발자는 당황스럽기도 합니다.

개발자 입장에서는 Crash가 발생했기 때문에, windbg를 설치하여 해결하려고 할 것입니다.
일단,
"C:\Program Files\Debugging Tools for Windows (x86)\windbg.exe" -I
를 실행하여 WinDbg를 포스트모텀 디버거로 등록합니다. (Crash 발생시 windbg 실행)

그다음 Heap.exe를 실행하고, 만일 오류가 발생하게 된다면 windbg가 실행됩니다.
(만일 오류가 발생하지 않는 경우는 아래 내용은 해당되지 않습니다.)

windbg->File->Symbol File Path에


와 같이 하고, C:\corrupt에 heap.zip의 pbd 파일을 넣습니다.
그리고, command에,
0:000> .reload -i
*********************************************************************
* Symbols can not be loaded because symbol path is not initialized. *
*                                                                   *
* The Symbol Path can be set by:                                    *
*   using the _NT_SYMBOL_PATH environment variable.                 *
*   using the -y <symbol_path> argument when starting the debugger. *
*   using .sympath and .sympath+                                    *
*********************************************************************
와 같이 실행합니다.

그리고 통상적인 작업을 하면,
0:000> !analyze -v
...
STACK_TEXT: 
WARNING: Stack unwind information not available. Following frames may be wrong.
0012ff2c 00402bf1 00390000 00000000 00000068 ntdll!wcsncpy+0x99f
0012ff4c 00401259 00000068 00310037 00000000 Heap!malloc+0x79 [f:\dd\vctools\crt_bld\self_x86\crt\src\malloc.c @ 163]
0012ff64 00401065 00000068 00310037 00350034 Heap!operator new+0x1f [f:\dd\vctools\crt_bld\self_x86\crt\src\new.cpp @ 59]
0012ff78 00401401 00000001 00393028 00393060 Heap!wmain+0x65 [e:\temp\work\heap\heap.cpp @ 35]
0012ffc0 7c817067 00310037 00350034 7ffd4000 Heap!__tmainCRTStartup+0xfa [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 266]
0012fff0 00000000 00401458 00000000 78746341 kernel32!RegisterWaitForInputIdle+0x49
...
와 같은 결과가 나옵니다.
윈도우 Web Symbol을 맞추는 일도 필요해 보입니다. 그래서,
앞선 symbol 설정을,
C:\corrupt;SRV*C:\websymbol*http://msdl.microsoft.com/download/symbols
와 같이 하여 다시 .reload -i 하면 다음과 같이 됩니다.

0:000> !analyze -v
...
STACK_TEXT: 
0012ff2c 00402bf1 00390000 00000000 00000068 ntdll!RtlAllocateHeap+0x653
0012ff4c 00401259 00000068 00310037 00000000 Heap!malloc+0x79 [f:\dd\vctools\crt_bld\self_x86\crt\src\malloc.c @ 163]
0012ff64 00401065 00000068 00310037 00350034 Heap!operator new+0x1f [f:\dd\vctools\crt_bld\self_x86\crt\src\new.cpp @ 59]
0012ff78 00401401 00000001 00393028 00393060 Heap!wmain+0x65 [e:\temp\work\heap\heap.cpp @ 35]
0012ffc0 7c817067 00310037 00350034 7ffd4000 Heap!__tmainCRTStartup+0xfa [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 266]
0012fff0 00000000 00401458 00000000 78746341 kernel32!BaseProcessStart+0x23
...
MODULE_NAME: heap_corruption

IMAGE_NAME:  heap_corruption
...

즉, Heap Corruption 오류는 알려주지만, 어디가 문제인지를 알려주지 않습니다.

그래서 Application Verifier를 설치하고 진행하겠습니다. 자세한건 도움말이나 구굴링으로 확인하세요.
Application Verifier->File->Add Application
하고, Heap만 체크한뒤 Save를 합니다.

그리고, 다시 Heap.exe를 실행해 봅니다.
그럼 Application Verifier에 의해 강제 오류가 발생하게 되어, 무조건 WinDbg가 실행하게 됩니다. (포스트모텀으로 연결했다는 가정하에)

그럼 위 과정 처럼 .reload -i를 한 뒤, 통상적인 분석을 해보면 다음과 같이 보고됩니다.

0:000> !analyze -v
...
STACK_TEXT: 
0012ff48 7c831e08 0180efe8 7f6f2190 00000026 ntdll!memmove+0x33
0012ff60 00401025 0180efe8 00000104 00310037 kernel32!GetSystemDirectoryW+0x36
0012ff78 00401401 00000001 017ccfd0 017cef88 Heap!wmain+0x25 [e:\temp\work\heap\heap.cpp @ 25]
0012ffc0 7c817067 00310037 00350034 7ffdc000 Heap!__tmainCRTStartup+0xfa [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 266]
0012fff0 00000000 00401458 00000000 78746341 kernel32!BaseProcessStart+0x23

FAULTING_SOURCE_CODE: 
No source found for 'e:\temp\work\heap\heap.cpp'


SYMBOL_STACK_INDEX:  2

SYMBOL_NAME:  Heap!wmain+25

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: Heap

IMAGE_NAME:  Heap.exe

DEBUG_FLR_IMAGE_TIMESTAMP:  4fd1a3ac

STACK_COMMAND:  ~0s ; kb

FAILURE_BUCKET_ID:  STRING_DEREFERENCE_c0000005_Heap.exe!wmain

BUCKET_ID:  APPLICATION_FAULT_STRING_DEREFERENCE_INVALID_POINTER_WRITE_Heap!wmain+25

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

heap.cpp의 25줄, 그리고 GetSystemDirectory를 호출하여 발생한 원인 지점을 대략 알려주게 됩니다.

즉, Application Verifier로 인해 Heap을 Corrupt하는 위치를 알 수 있게 되었습니다.

경험삼아, 아래와 같은 경우 Application Verifier를 이용하면 효과를 볼 수 있습니다.

  • Crash가 발생할 때도 있고, 안할 때도 있다.
  • 문제의 제현 자체가 제약이 많다 (예를 들어, 특정 PC에서만 재현된다던지)
  • Crash가 발생하나, !analyze -v한 위치가 매번 변경된다.