Debug 방식의 API Hooking 실습 예제 코드(hookdbg.cpp)의 동작 원리를 살펴 보고 코드를 자세히 분석해보겠습니다.
<hookdbg.cpp 의 DebugLoop() 함수>
작업 목표 : "notepad 에서 파일 저장할 때 모든 소문자를 대문자로 변경함"
* 참고
API Hooking - 메모장 WriteFile() 후킹 (1)
동작 원리
이해를 돕기 위해 먼저 동작 원리를 설명 드리겠습니다.
notepad 에서 뭔가를 파일에 저장하려면 kernel32!WriteFile() API 를 사용할 거라고 가정합니다.
(일단 가정이 맞는지 확인을 해봐야겠군요.)
# 스택(Stack)
WriteFile() API 정의를 봐주세요.
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
* 출처 : http://msdn.microsoft.com/en-us/library/aa365747(VS.85).aspx
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
* 출처 : http://msdn.microsoft.com/en-us/library/aa365747(VS.85).aspx
두 번째 파라미터(lpBuffer)가 “쓰기 버퍼” 이고, 세 번째 파라미터(nNuberOfBytesToWrite)가 “써야 할 크기” 입니다. 함수의 파라미터는 스택에 역순으로 저장된다는 사실을 기억해주세요. (-> 참고 : Stack Frame)
OllyDbg 를 이용해 실제로 notepad 를 디버깅 하면서 확인해 보겠습니다.
<Fig. 1>
위 그림처럼 OllyDbg 를 이용해서 notepad 를 열어서 kernel32!WriteFile() API 에 BreakPoint 를 설치한 후 실행[F9] 시킵니다.
그리고 테스트로 아래와 같이 적당한 문자열을 입력한 후 적당한 파일 이름으로 저장합니다.
<Fig. 2>
예상대로 BreakPoint 를 설치한 kernel32!WriteFile() 에 멈춥니다.
이 상태에서 스택을 살펴보겠습니다.
<Fig. 3>
현재 스택(ESP : 7FA7C)에는 리턴 주소(01004C30)가 있고, ESP+8 (7FA84) 에 "쓰기버퍼" 주소(0E7310)가 저장되어 있습니다. 바로 그 "쓰기버퍼" 주소(0E7310)로 가면 notepad 에서 저장하려고 하는 문자열("ReverseCore") 이 보입니다.
따라서 WriteFile() API 를 후킹해서 "쓰기버퍼"를 제가 원하는 문자열로 덮어쓰면 목표 달성입니다.
# 실행흐름
이제 Debuggee 의 프로세스 메모리 어느 부분을 수정해야 하는지 알았습니다.
그 다음은 WriteFile() 을 정상적으로 실행시켜서 제가 수정한 문자열이 파일에 저장되도록 하면 됩니다.
지금 우린 Debug Method 를 사용해서 API 후킹을 하고 있습니다.
이전 포스트에서 소개한 hookdbg.exe 를 이용하여 WriteFile() API 시작 주소에 BP (INT3) 를 설치하면, Debuggee(notepad.exe) 에서 파일을 저장 할 때 Debugger(hookdbg.exe) 에게 EXCEPTION_BREAKPOINT 이벤트가 올 것입니다.
그렇다면 그 순간 Debuggee(notepad.exe) 의 EIP 값은 얼마일까요?
얼핏 생각하면 WriteFile() API 시작주소(7C7E0E27) 라고 생각하기 쉽습니다.
하지만 실제 EIP 는 WriteFile() API 시작주소(7C7E0E27) + 1 = 7C7E0E28 입니다.
그 이유는 이렇습니다.
먼저 BP 를 WriteFile() API 시작 주소에 설치하였지요?
Debuggee(notepad.exe) 내부에서 WriteFile() 이 호출되면 시작주소인 7C7E0E27 에 있는 INT3 (0xCC) 명령어를 만나게 됩니다.
이 명령어(BreakPoint – INT3)를 실행 하면 EIP 는 INT3 명령어 길이(1 byte)만큼 늘어나게 됩니다.
그 후에 제어가 Debugger(hookdbg.exe) 로 넘어오게 되는 것입니다. (Debugger - Debuggee 관계에서 Debuggee 에서 발생한 EXCEPTION_BREAKPOINT 예외는 Debugger 에서 처리하도록 되어 있기 때문입니다.)
따라서 "쓰기 버퍼" 에 있는 내용을 수정해서 덮어쓴 다음에 EIP 를 WriteFile() API 시작 주소로 되돌려서 실행 시켜야 합니다.
# Unhook & Hook
또 하나의 문제는 단순히 실행 흐름을 WriteFile() 시작 주소로 되돌리기만 해서는 똑 같은 INT3 명령을 만나게 되기 때문에 무한루프(EXCEPTION_BREAKPOINT 발생)에 빠지게 됩니다.
이러한 무한루프에 빠지지 않으려면 WriteFile() API 시작 주소에 설치한 BP 를 제거해야 합니다.
즉, 0xCC 를 원래 original byte 인 0x6A 로 변경해줘야 합니다. (original byte 는 API 후킹 전에 미리 저장해 둡니다.)
이것을 Unhook 이라고 합니다. API 후킹을 풀어버리는 것이지요.
"쓰기버퍼" 를 덮어쓰고 WriteFile() API 코드를 정상으로 되돌린 후 EIP 값을 WriteFile() API 로 변경하면 드디어 변경된 문자열이 파일에 저장됩니다. 이것이 hookdbg.cpp 의 동작 원리입니다.
후킹이 1 회성이면 여기서 끝이고요, 지속적인 후킹을 원하시면 다시 BP 를 설치합니다.
설명만 읽어서는 잘 이해되지 않을 수 있습니다.
아래 소스 코드(hookdbg.cpp)를 보면서 설명 드리겠습니다.
* 참고
OllyDbg 같이 범용적인 디버거의 경우는 <Fig. 3> 에서 보듯이 EIP 값이 BP 설치 주소와 같고, INT3(0xCC) 명령어가 보이지 않습니다. 이것은 편리한 사용자 인터페이스를 위하여 OllyDbg 에서 제공하는 기능입니다.
즉, INT3(0xCC) 를 덮어 쓴 후 이 명령어를 실행하게 되면 EIP 가 1 증가합니다. 그때 OllyDbg 에서 0xCC 를 원래 byte 로 복원하고 EIP 도 보정해 주는 것이지요. (결과적으로 구현 알고리즘은 위 설명과 동일합니다.)
코드 설명
첨부된 hookdbg.cpp 의 코드를 살펴보도록 하겠습니다.
# main()
#include "windows.h"
#include "stdio.h"
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
int main(int argc, char* argv[])
{
DWORD dwPID;
if( argc != 2 )
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}
// Attach Process
dwPID = atoi(argv[1]);
if( !DebugActiveProcess(dwPID) )
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());
return 1;
}
// 디버거 루프
DebugLoop();
return 0;
}
#include "stdio.h"
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
int main(int argc, char* argv[])
{
DWORD dwPID;
if( argc != 2 )
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}
// Attach Process
dwPID = atoi(argv[1]);
if( !DebugActiveProcess(dwPID) )
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());
return 1;
}
// 디버거 루프
DebugLoop();
return 0;
}
main() 함수의 코드는 간단합니다.
프로그램 실행 파라미터로 API 후킹 하려는 프로세스의 PID 를 받습니다.
그 후 DebugActiveProcess() API 를 통해서 실행중인 프로세스에 attach 하여 디버깅을 시작합니다. (위에서 입력한 PID 를 파라미터로 넘겨줍니다.)
BOOL WINAPI DebugActiveProcess(
DWORD dwProcessId
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms679295(VS.85).aspx
DWORD dwProcessId
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms679295(VS.85).aspx
그 후 DebugLoop() 함수로 들어가서 Debuggee 로부터 오는 Debug event 를 처리합니다.
* 또 다른 디버깅 시작 방법은 CreateProcess() API 를 사용하여 아예 해당 프로세스를 Debug 모드로 실행시키는 방법이 있습니다. 이와 관련된 설명은 MSDN 을 참고하세요.
# DebugLoop()
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
// Debuggee 로부터 event 가 발생할 때까지 기다림
while( WaitForDebugEvent(&de, INFINITE) )
{
dwContinueStatus = DBG_CONTINUE;
while( WaitForDebugEvent(&de, INFINITE) )
{
dwContinueStatus = DBG_CONTINUE;
// Debuggee 프로세스 생성 혹은 attach 이벤트
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
OnCreateProcessDebugEvent(&de);
}
// 예외 이벤트
else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
{
if( OnExceptionDebugEvent(&de) )
continue;
}
// Debuggee 프로세스 종료 이벤트
else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
// Debuggee 종료 -> Debugger 종료
break;
}
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
OnCreateProcessDebugEvent(&de);
}
// 예외 이벤트
else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
{
if( OnExceptionDebugEvent(&de) )
continue;
}
// Debuggee 프로세스 종료 이벤트
else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
// Debuggee 종료 -> Debugger 종료
break;
}
// Debuggee 의 실행을 재개시킴
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
DebugLoop() 함수는 마치 윈도우 프로시저 함수(WndProc)와 유사하게 동작합니다.
Debuggee 로부터 발생하는 event 를 받아서 처리한 후 Debuggee 의 실행을 재개 시키는 역할입니다.
역시 간단한 코드이므로 주석을 보시면 쉽게 이해하실 수 있으실 것입니다.
그 중에서 몇 가지 중요 API 들을 알아보겠습니다.
WaitForDebugEvent() API 는 이름 그대로 Debuggee 로부터 Debug event 가 발생할 때까지 기다리는 함수입니다. (WaitForSingleObject() API 와 비슷하게 동작합니다.)
Debug event 가 발생하면 WaitForDebugEvent() API 는 첫 번째 파라미터인 de 변수(DEBUG_EVENT 구조체 객체)에 해당 event 에 대한 정보를 설정한 후 즉시 리턴합니다.
BOOL WINAPI WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms681423(VS.85).aspx
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms681423(VS.85).aspx
Debug event 가 발생하면 WaitForDebugEvent() API 는 첫 번째 파라미터인 de 변수(DEBUG_EVENT 구조체 객체)에 해당 event 에 대한 정보를 설정한 후 즉시 리턴합니다.
DEBUG_EVENT 구조체 정의는 아래와 같습니다.
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
* 출처 : http://msdn.microsoft.com/en-us/library/ms679308(VS.85).aspx
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
* 출처 : http://msdn.microsoft.com/en-us/library/ms679308(VS.85).aspx
DEBUG_EVENT.dwDebugEventCode 멤버에 9가지 event 종류 중 하나가 세팅되며, 해당 event 종류에 따라 적절한 DEBUG_EVENT.u (유니온) 멤버가 세팅 됩니다. (DEBUG_EVENT.u 유니온 멤버 역시 event 종류 개수에 맞춰서 내부에 9 개의 구조체로 구성되어 있습니다.)
예) Exception event 인 경우 => dwDebugEventCode 멤버가 EXCEPTION_DEBUG_EVENT 로 세팅 되고, u.Exception 구조체가 세팅됩니다.
ContinueDebugEvent() API 는 Debuggee 의 실행을 재개 시키는 함수입니다.
BOOL WINAPI ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms679285(VS.85).aspx
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms679285(VS.85).aspx
ContinueDebugEvent() API 의 마지막 파라미터인 dwContinueStatus 는 DBG_CONTINUE 또는 DBG_EXCEPTION_NOT_HANDLED 중에서 하나의 값을 가질 수 있습니다.
정상적으로 처리된 경우 DBG_CONTINUE 로 세팅 하고, 처리하지 못했거나 어플리케이션의 SEH(Structured Exception Handler) 에서 처리하길 원할 때는 DBG_EXCEPTION_NOT_HANDLED 로 세팅합니다.
* SEH(Structured Exception Handler) 는 Windows 에서 제공하는 예외 처리 메커니즘입니다. 이를 이용한 예외 처리와 안티 디버깅 기법들에 대해서는 향후 따로 다루어 보도록 하겠습니다.
위의 DebugLoop() 에서는 3 가지의 Debug event 만 처리합니다. (CREATE_PROCESS_DEBUG_EVENT, EXIT_PROCESS_DEBUG_EVENT, EXCEPTION_DEBUG_EVENT)
다음 포스트에서 위 Debug event 에 대한 상세한 코드 설명이 이어집니다.
API Hooking - 메모장 WriteFile() 후킹 (3)
+---+
ReverseCore
'study' 카테고리의 다른 글
| API Hooking - 계산기, 한글을 배우다. (4) (10) | 2009/11/27 |
|---|---|
| API Hooking – 계산기, 한글을 배우다. (3) (35) | 2009/11/20 |
| API Hooking - 계산기, 한글을 배우다. (2) (14) | 2009/11/13 |
| API Hooking - 계산기, 한글을 배우다. (1) (30) | 2009/11/10 |
| API Hooking - 메모장 WriteFile() 후킹 (3) (26) | 2009/11/04 |
| API Hooking - 메모장 WriteFile() 후킹 (2) (3) | 2009/11/03 |
| API Hooking - 메모장 WriteFile() 후킹 (1) (22) | 2009/10/08 |
| API Hooking - Tech Map (10) | 2009/09/29 |
| API Hooking - 리버싱의 '꽃' (22) | 2009/09/22 |
| DLL Ejection - 침투시킨 DLL 빼내기 (20) | 2009/08/14 |
| DLL Injection - 다른 프로세스에 침투하기 (4) (27) | 2009/07/30 |
hookdbg.exe
hookdbg.cpp
