Tutorial 29: Win32 Debug API Part 2
win32 디버그 API의 계속되는 설명이다. 이 번장에서는 디버그 대상 프로그램을 편집하는 방법에 대해 설명한다.
메인 소스 1 | 메인 소스 2 | 디버그 대상 소스 |
실행 결과 1 | 실행 결과 2 | 실행 결과 3 |
|
이전장에서 디버그 대상 프로그램의 실행방법과 디버그 이벤트에 대한 처리 방법을 설명했다. 보다 강력하게 사용하기 위해서는 디버그 대상 프로그램을 수정할 수 있어야한다. 이럴 경우를 대비해서 API가 몇개 준비되어 있다.
- ReadProcessMemory
이 함수로 해당 프로세스의 메모리를 읽어낼 수 있다. 프로토타입은 다음과 같다.
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
- hProcess
메모리를 읽고 싶은 프로세스의 핸들- lpBaseAddress
읽기 작업을 수행할 프로세스의 기본주소로서, 예를 들어, 40100 h에서 4바이트 읽고 싶다면, 이 변수를 401000h로 한다.- lpBuffer
읽어온 데이터를 저장하기 위한 버퍼- nSize
읽어들일 바이트수- lpNumberOfBytesRead
실제로 읽혀진 바이트수를 저장하는 버퍼. 대부분의 경우 NULL을 지정하면 좋다- WriteProcessMemory
ReadProcessMemory함수와 비슷한 함수로, 대상 프로세스의 메모리에 쓰기작업을 할 수 있는 함수로서 인수는 ReadProcessMemory 함수와 동일하다.
다음의 2개의 API 함수는 설명이 조금 필요하다. 윈도우즈와 같은 멀티태스킹 OS에서는, 동시에 여러개의 프로그램을 실행할 수 있다. 그 방법은 몇가지 존재하지만, Windows에서는 각 thread에 일정한 시간(타임 슬라이스)을 할당해서, 해당시간내에서만 thread에 대한 처리를 수행한다. 일정 시간이 경과하면 우선도가 높은 thread를 선택하고, 그 thread를 일정시간동안 처리한다.
Windows는, 처리하는 thread를 바꾸는 순간, 처리하고 있던 thread의 레지스터의 값을 저장해두고,다시 해당 thread를 재개할 경우에는 이전에 저장해둔 환경을 복구해서 처리를 재개한다. 저장해 둔 레지스터의 값을 총칭해서 문맥(Context)이라 부른다.
주제로 다시 돌아와서, 디버그 이벤트가 발생했을 때, Windows는 디버그 대상 프로그램을 정지시키고, 문맥을 저장한다. 디버그 대상 프로그램은 정지상태이므로, 값이 바뀔 것이 없다. 추가로 이런값을 구할려면, GetThreadContext 함수를 호출하고, 내용을 변경하고 싶다면 SetThreadContext함수를 호출하면 된다.
이들 2개의 API는 매우 강력해서, 가상 디바이스 드라이버와 같은 작업에서 사용된다.왜냐하면, 디버그 대상 프로그램이 정지상태일때도 문맥의 값이 존재하고 이를 변경 할 수도 있기 때문에, 주의해서 사용해야 한다. 예를 들어, 정지상태의 프로그램의 EIP레지스터를 변경한 후, 프로그램을 재개하면 프로그램은 Down된다. 이런 일은 보통 환경에서는 자주 일어나지 않는다.
GetThreadContext proto hThread:DWORD, lpContext:DWORD
- hThread
문맥을 구한 thread 핸들
- lpContext
CONTEXT구조체의 주소로서, 함수가 성공적으로 수행한 값이 저장되어 있다.
SetThreadContext함수의 인수도 같다. 그럼 CONTEXT 구조체에 대해 알아보자.
- CONTEXT STRUCT
ContextFlags dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_DEBUG_REGISTERS일 경우 ;----------------------------------------------------------------------------------------------------------- iDr0 dd ? iDr1 dd ? iDr2 dd ? iDr3 dd ? iDr6 dd ? iDr7 dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_FLOATING_POINT일 경우 ;----------------------------------------------------------------------------------------------------------- FloatSave FLOATING_SAVE_AREA <> ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_SEGMENTS일 경우 ;----------------------------------------------------------------------------------------------------------- regGs dd ? regFs dd ? regEs dd ? regDs dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_INTEGER일 경우 ;----------------------------------------------------------------------------------------------------------- regEdi dd ? regEsi dd ? regEbx dd ? regEdx dd ? regEcx dd ? regEax dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_CONTROL일 경우 ;----------------------------------------------------------------------------------------------------------- regEbp dd ? regEip dd ? regCs dd ? regFlag dd ? regEsp dd ? regSs dd ? ;---------------------------------------------------------------------------------------------------------- ; ContextFlags가 CONTEXT_EXTENDED_REGISTERS일 경우 ;----------------------------------------------------------------------------------------------------------- ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(? ) CONTEXT ENDS보면 알 수 있듯이, 이런 구조체는 실제 레지스터들의 그룹이다. 어떤 레지스터 그룹이 필요한지는 ContextFlags멤버의 값으로 결정하게 된다.
예를 들면, 모든 레지스터를 읽고 쓰기 작업을 하고 싶다면,ContextFlags에 CONTEXT_CONTROL을 지정해야만 한다.CONTEXT구조체를 사용할 경우 주의 사항이 한가지 있다.구조체를 DWORD 경계로 정렬해야하는 것이다. 그렇지 않으면, WindowsNT에서는 오작동을 초래하게 된다.
그래서 다음과 같이, 이 구조체를 선언하는 바로 위의 행에서 "align dword" 와 같이 해줘야 한다.
align dword MyContext CONTEXT <>
|
처음으로 DebugActiveProcess함수의 사용 방법에 대한 간단한 소개이다. 먼저, win.exe 라는 타겟 실행 파일을 실행한다. 이 실행 파일은 윈도우가 나타나기전에 무한루프에 빠지게 되어있다.
실행 후, 디버그 프로그램을 실행해서, win.exe프로그램을 추가(Attatch) 한다. 그리고 무한루프에서 탈출할 수 있도록 win.exe를 수정해서, 윈도우를 표시한다.
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib .data AppName db "Win32 Debug Example no.2",0 ClassName db "SimpleWinClass",0 SearchFail db "Cannot find the target process",0 TargetPatched db "Target patched!",0 buffer dw 9090h .data? DBEvent DEBUG_EVENT <> ProcessId dd ? ThreadId dd ? align dword context CONTEXT <> .code start: invoke FindWindow, addr ClassName, NULL .if eax!=NULL invoke GetWindowThreadProcessId, eax, addr ProcessId mov ThreadId, eax invoke DebugActiveProcess, ProcessId .while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId,DBG_CONTINUE .continue .endif .endif invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId,DBG_EXCEPTION_NOT_HANDLED .endw .else invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif invoke ExitProcess, 0 end start
|
invoke FindWindow, addr ClassName, NULL이 프로그램은, DebugActiveProcess함수를 호출해서 디버그 대상 프로그램을 추가(Attatch) 하고 있지만, 이 때 프로세스 ID가 필요하게 된다. 그 프로세스 ID는 GetWindowThreadProcessId함수를 호출해서 얻게되지만, 예제에서는 윈도우 핸들도 필요하게 된다. 따라서 먼저 윈도우 핸들도 구해야 한다.
윈도우 클래스명을 지정해서, FindWindow함수를 호출하면, 윈도우 핸들을 구할 수 있다. NULL이면, 그 클래스명의 윈도우가 현재 없다는 것을 의미한다.
.if eax != NULL invoke GetWindowThreadProcessId, eax, addr ProcessId mov ThreadId, eax invoke DebugActiveProcess, ProcessId프로세스 ID를 얻었다면, DebugActiveProcess함수를 호출해서, 디버그 이벤트를 기다리기 위한 디버그 루프에 들어간다.
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr contextCREATE_PROCESS_DEBUG_INFO구조체를 얻은 경우에는 디버그 대상 프로그램이 정지상태에 있기 때문에, 프로그램에 여러가지 처리를 할 수 있다. 예제에서는, 디버그 대상 프로그램의 루프명령어를( 0EBh 0FEh ) 아무일도 하지 않는 명령어인 NOPs(90h 90h)로 변경해 보자.
먼저 명령코드의 주소를 알아낼 필요가 있다. 디버그 대상 프로그램은, 추가 했을 때 이미 루프에 들어가 있으므로, EIP 레지스터는 항상 유효한 명령어의 주소를 보관하고 있기때문에, EIP 레지스터의 값을 구하면 된다. 따라서, GetThreadContext함수를 호출하면 된다. CONTEXT구조체의 "control" 레지스터 멤버를 얻기위해, ContextFlags멤버에CONTEXT_CONTROL을 설정 한 후, GetThreadContext함수를 호출한다.
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip , addr buffer, 2, NULL겨우 EIP 레지스터의 값을 얻을수 있게 됬으므로, WriteProcessMemory함수를 호출해서 "jmp $" 명령어를 NOP 명령으로 변경해보자. 이로 인해, 디버그 대상 프로그램이 무한루프에서 탈출하게 된다. 그 후, 유저에게 메세지를 출력하고, ContinueDebugEvent함수를 호출해서 디버그 대상 프로그램을 재개한다.
다음의 코드는 조금 다른 방법으로 무한루프로부터 탈출시키고 있다.
....... ....... .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context add context.regEip, 2 invoke SetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION ....... .......이 샘플에서는, "jmp $"명령을 NOP 명령으로 변경하는 것이 아니라, EIP 레지스터의 값을 GetThreadContext함수를 통해서 구하고, regEip에 2를 더해서 코드를"스킵(Skip)"시키고 있다.
결과적으로, 디버그 대상 프로그램의 처리가 재개될 경우, "jmp $"명령을 지나게 되므로, 루프를 탈출하게 된다.Get/SetThreadContext 함수의 위력은 어느정도 인가? 예제에서는 EIP 레지스터만 변경했지만, 다른 레지스터 또한 변경할 수 있고, 물론 디버그 대상 프로그램에 변경을 반영시킬 수도 있다. 게다가, 디버그 대상 프로그램에 breakpoint를 설정하기 위한 int 3h 명령어도 추가하는 것이 가능해 진다.