2010.8.3 12:56
다음의 내용은 Quad Dimensions에서 Debugging 교육 자료로 만든 내용입니다.
본부장
가져 가실때는 이 정보 남겨 주시는것이 예의 겠지요 ^^
1. Stack Overrun
(code sample A-1) 예제처럼 stack memory 공간이 침범된 경우를 운이 좋게 실행되는 중에 발견할 수 있는 경우는 매우 곤란하지만 아래처럼 stack overrun 상황으로 바로 예외처리가 되는 경우도 있다.
아래의 코드를 보자.
char szTmp2[10];
sprintf(szTmp2,"%s","123456789abcdefghi");
위의 경우는 ebp 저장 영역을 침범하기 때문에 시스템에서 바로 오류를 확인 가능하다. 아래는 AV에서 발생된 로그 내용이다.
<avrf:message>First chance access violation for current stack trace.</avrf:message>
<avrf:parameter1>6968 - Invalid address causing the exception.</avrf:parameter1>
<avrf:parameter2>6968 - Code address executing the invalid access.</avrf:parameter2>
eip 값이 터무니 없는 6968이라는 값으로 보인다.
Windbg로 확인해 보면 아래와 같은 내용을 확인을 할 수 있다.
0:000> r (register를 출력하는 명령어)
eax=00000013 ebx=7ffdc000 ecx=00426050 edx=7c96860c esi=00000000 edi=00000000
eip=00006968 esp=0013ff74 ebp=67666564 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010216
00006968 ?? ???
eip의 내용을 dump 해 보면 아래와 같다
0:000> dd eip
00006968 ???????? ???????? ???????? ????????
00006978 ???????? ???????? ???????? ????????
00006988 ???????? ???????? ???????? ????????
대개의 경우 위와 레지스터 또는 메모리를 덤프 했을 때 ? 등으로 보이면 메모리가 손상되었다고 보면 된다.
정상적일 경우 아래처럼 보인다.
0:000> dd eip
004123de 5de58bcc 909090c3 90909090 90909090
004123ee ec839090 24448d0c 15d46800 98680042
위와 같은 오류가 발생했을 때는 다음과 같이 추적을 한다.
1) Callstack 확인을 위해 kb
0:000> kb
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
0013ff70 0013ff80 00416cb3 0041fdc6 0013ffc0 0x6968 --a)
*** WARNING: Unable to verify checksum for Timetest.exe
0013ff74 00416cb3 0041fdc6 0013ffc0 00418b20 0x13ff80 --b)
0013ff80--c) 00418b20--d) 00000001 0375afa0 0375cf40 Timetest!main+0x1b [G:\byun\test_code\100716_umdh_test\Timetest.cpp @ 169]
0013ffc0 7c82f23b 00000000 00000000 7ffd5000 Timetest!mainCRTStartup+0xb4
0013fff0 00000000 00418a6c 00000000 78746341 kernel32!BaseProcessStart+0x23
--a)와 --b)를 보면 정보가 깨진 것을 알 수 있고, 마지막 정상 수행된 뒤에 돌아갈 주소가 Timetest!main+0x1b 까지 인 것을 알 수 있다.
★ --c)는 해당 함수의 ebp를 --d)는 복귀할 eip 주소를 이야기 한다.
2) 마지막 정상함수 Timetest!main 확인
Timetest!main+0x1b 와 Timetest!main를 해 보면 최종적으로 진행했던 함수가 Timetest!ErrorTest 인것을 알 수 있다.
( uf Timetest!main+0x1b 와 uf Timetest!main 를 해서 각각의 역어셈블된 내용을 비교해 볼 것 )
3) uf Timetest!ErrorTest 를 해서 해당 함수 내용 확인
0:000> uf Timetest!ErrorTest
Timetest!ErrorTest [G:\byun\test_code\100716_umdh_test\Timetest.cpp @ 142]:
142 00416c8e 55 push ebp
142 00416c8f 8bec mov ebp,esp
148 00416c91 e8fefeffff call Timetest!errTestStackOverrun (00416b94)
152 00416c96 5d pop ebp
152 00416c97 c3 ret
ErrorTest 함수는 errTestStackOverrun 함수를 호출 한다.
첫 번째 줄의 번호는 소스코드 상의 line 번호이다. ( PDB 파일이 있어야 볼 수 있다. )
0:000> uf Timetest!errTestStackOverrun
Timetest!errTestStackOverrun [G:\byun\test_code\100716_umdh_test\Timetest.cpp @ 85]:
85 00416b94 55 push ebp
85 00416b95 8bec mov ebp,esp
85 00416b97 83ec0c sub esp,0Ch
88 00416b9a 68905f4200 push offset Timetest!`string'+0x78 (00425f90) --b)
88 00416b9f 68a45f4200 push offset Timetest!`string'+0x8c (00425fa4) --c)
88 00416ba4 8d45f4 lea eax,[ebp-0Ch] --a)
88 00416ba7 50 push eax
88 00416ba8 e87f0c0000 call Timetest!sprintf (0041782c)
88 00416bad 83c40c add esp,0Ch
89 00416bb0 8d4df4 lea ecx,[ebp-0Ch]
89 00416bb3 51 push ecx
89 00416bb4 68a85f4200 push offset Timetest!`string'+0x90 (00425fa8)
89 00416bb9 e87d1e0000 call Timetest!printf (00418a3b)
89 00416bbe 83c408 add esp,8
92 00416bc1 8be5 mov esp,ebp
92 00416bc3 5d pop ebp
92 00416bc4 c3 ret
4) 코드 분석 ( 데이터 사이즈 ? 내용 등 확인 )
여기까지 진행한 상태로 경험 많은 디버거라면 call stack이 깨졌고 , eip가 깨졌으며 메모리가 손상되었다면 stack 또는 heap에 문제가 생겼을 것이고 , 확률상 call stack이 깨진 경우라면 stack 손상일 가능성이라고 유추해 볼 수 있다.
--a),--b),--c)를 보고 sprintf 에 인자는 3개이고 --b),--c)는 문자열 이라고 확인 가능 하다. 또한 --a)를 보고 해당 내용은 버퍼 공간인 것을 알 수 있다.
0Ch면 10바이트 공간을 확보 했음을 알 수 있다.
--b), --c)의 메모리 내용은 00425f90 , 00425fa4 주소에서 확인해 볼 수 있다.
0:000> da 00425f90
00425f90 "123456789abcdefghi"
0:000> da 00425fa4
00425fa4 "%s"
sprintf() 함수의 기능을 알고 있기 때문에 다음과 같이 코드를 생각해 볼 수 있다.
sprintf(변수,”%s”,” 123456789abcdefghi"”);
그런데 변수의 사이즈는 10 바이트 인데 18 바이트의 문자열을 복사 한 것을 알 수 있다.
다른 방법으로는 bm 명령어를 이용해서 Timetest!errTestStackOverrun 함수에 break point를 지정하고 , t 명령어를 이용해서 trace 해가면서 함수의 내용들을 체크해서 알아 내는 방법도 있다.
★ 위 역어셈블 내용은 컴파일러와 OS에 따라 다른 내용들을 보여주는 것에 주의.
예를 들어 window 7에서 VS 2008로 컴파일 하면 코드의 내용이 아래처럼 바뀐다.
0:000> uf Timetest!errTestStackOverrun
Timetest!errTestStackOverrun [d:\byun\test_code\100716_umdh_test\timetest.cpp @ 85]:
85 0041a640 55 push ebp
85 0041a641 8bec mov ebp,esp
85 0041a643 83ec10 sub esp,10h (sub 하는 사이즈가 다르다)
85 0041a646 a140314400 mov eax,dword ptr [Timetest!__security_cookie
85 0041a64b 33c5 xor eax,ebp
85 0041a64d 8945fc mov dword ptr [ebp-4],eax
88 0041a651 6898d74300 push offset Timetest!std::basic_string
<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::npos+0x5c (0043d798)
88 0041a656 68acd74300 push offset Timetest!std::basic_string
<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::npos+0x70 (0043d7ac)
88 0041a65b 8d45f0 lea eax,[ebp-10h]
88 0041a65e 50 push eax
88 0041a65f e8b7110000 call Timetest!sprintf (0041b81b)
…. 중간 생략 …
92 0041a677 8b4dfc mov ecx,dword ptr [ebp-4]
92 0041a67a 33cd xor ecx,ebp
92 0041a67c e8ef020000 call Timetest!__security_check_cookie (0041a970)
92 0041a681 8be5 mov esp,ebp
92 0041a683 5d pop ebp
92 0041a684 c3 ret
위에서 특이한 점은 sub 시키는 사이즈가 다르다는 것과 __security_cookie의 존재이다. (sub의 사이즈가 4바이트 커진 이유는 위 cookie의 존재 때문)
여기서 보안 cookie는 2000년대 이후 계속 문제가 되고 있듯이 stack overrun을 통한 바이러스나 해커의 공격이 있기 때문에 스택을 보호하기 위해서 넣은 일종의 checksum 코드 이다.
위처럼 ebp-4에 checksum 값을 넣고, __security_check_cookie에서 이 값이 변경되었는지 검사를 한 뒤 문제가 있다면 시스템에 오류를 표시하고 종료를 하게 된다.
- VS 2003 이후 /GS 컴파일 옵션이 추가 되었다.
C/C++ -> 코드생성 -> 버퍼 보안 검사가 예로 되어 있으면 /GS 옵션 사용
위 코드 외에 이 문서의 모든 역어셈블 된 코드는 2003 server에서 VS 6.0으로 컴파일 했을 때의 코드이다.
위의 경우처럼 EIP 또는 EBP가 stack 파괴로 손상된 경우는 실제 디버깅이 안 되는 경우가 많이 있다. 때문에 MS에서는 Prefast 라는 툴을 배포한다. Windows Driver Kit (WDK)에 들어 있으니 관심 있는 사람은 받아서 해 보기 바란다.
이 툴을 사용하면 stack 파괴로 발생된 문제에 대한 report를 확인 할 수 있다.
댓글 없음:
댓글 쓰기