이전 설명은 아래 링크에서 보실 수 있습니다.
API Hooking - 메모장 WriteFile() 후킹 (1)
API Hooking - 메모장 WriteFile() 후킹 (2)
코드 설명
앞서 설명드린 DebugLoop() 함수에서는 세가지 Debug event 를 처리합니다. (위의 코드 그림 참고)
- EXIT_PROCESS_DEBUG_EVENT
- EXCEPTION_DEBUG_EVENT
하나씩 살펴보겠습니다.
- EXIT_PROCESS_DEBUG_EVENT
Debuggee 프로세스가 종료될 때 발생하는 이벤트입니다.
위 소스 코드에서는 이 이벤트가 발생하면 Debugger 도 같이 종료하도록 하였습니다.
- CREATE_PROCESS_DEBUG_EVENT -> OnCreateProcessDebugEvent()
CREATE_PROCESS_DEBUG_EVENT 이벤트 핸들러인 OnCreateProcessDebugEvent()를 살펴보겠습니다. 이 함수는 Debuggee 의 프로세스가 시작(혹은 Attach)될 때 호출됩니다.
{
// WriteFile() API 주소구하기
g_pfWriteFile = GetProcAddress(GetModuleHandle("kernel32.dll"),
"WriteFile");
// 첫번째 byte 를 0xCC (INT3)로 변경
// (orginal byte 는 백업 – g_chOrgByte)
g_cpdi = pde->u.CreateProcessInfo; // CREATE_PROCESS_DEBUG_INFO
&g_chOrgByte, sizeof(BYTE), NULL);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
}
먼저 WriteFile() API 의 시작 주소를 구합니다.
주목할 점은 Debuggee 프로세스의 메모리 주소가 아니라 Debugger 프로세스의 메모리 주소를 얻어서 사용한다는 것입니다. Windows OS 에서 System DLL 인 경우 모든 프로세스에서 동일한 주소(가상 메모리)에 로딩 되므로 이렇게 해도 문제없습니다. (참고 : DLL Injection)
g_cpdi 는 CREATE_PROCESS_DEBUG_INFO 구조체 변수입니다.
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress;
LPVOID lpImageName;
WORD fUnicode;
}CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;
* 출처 : http://msdn.microsoft.com/en-us/library/ms679286(VS.85).aspx
CREATE_PROCESS_DEBUG_INFO 구조체 hProcess 멤버(Debuggee 프로세스 핸들)를 이용하여 WriteFile() API 를 후킹 할 수 있습니다. (Debug Method 가 아니라면 OpenProcess() API 를 통해서 해당 프로세스의 핸들을 얻어야 합니다.)
API 시작 위치에 “BreakPoint 를 설치” 하는 것입니다.
Debuggee 의 프로세스 핸들(Debug 권한을 가짐)을 가지고 있기 때문에 ReadProcessMemory(), WriteProcessmemory() API 를 이용하여 Debuggee 의 프로세스 메모리 공간에 자유롭게 읽기/쓰기 작업을 할 수 있습니다.
위 함수들을 이용해서 Debuggee 에 BreakPoint (INT3 – 0xCC)를 설치할 수 있습니다.
ReadProcessMemory() 를 이용해서 WriteFile() API 의 첫 바이트를 읽어서 g_chOrgByte 변수에 저장합니다. 아래 그림을 보시면 WriteFile() API 의 첫 바이트는 0x6A 입니다.
g_chOrgByte 변수에 첫 바이트를 저장하는 이유는 나중에 후킹을 해제(Unhook) 할 때 필요하기 때문입니다.
그 후 WriteProcessMemory() 를 이용해서 이 값을 0xCC 로 바꿔버립니다. (아래 그림 참조)
0xCC 는 "INT3" 를 뜻하는 OP code 입니다. 즉, BreakPoint 입니다.
CPU 는 INT3 명령을 만나면 프로그램 실행을 멈추고 예외를 발생시킵니다. 만약 해당 프로그램이 디버깅 중이라면 디버거에게 제어를 넘겨서 처리하도록 합니다.
이것이 일반적으로 디버거에서 BreakPoint 를 설치하는 기본 원리입니다.
이제 Debuggee 프로세스에서 WriteFile() API 가 호출되면 Debugger 에게 제어권이 넘어오게 됩니다.
- EXCEPTION_DEBUG_EVENT -> OnExceptionDebugEvent()
이번에는 EXCEPTION_DEBUG_EVENT 이벤트 핸들러인 OnExceptionDebugEvent()를 살펴보겠습니다. 이 함수가 바로 Debuggee 의 INT3 명령을 처리하게 될 함수입니다.
가장 핵심적인 내용이라 설명을 자세히 해보겠습니다.
{
CONTEXT ctx;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
// BreakPoint exception (INT 3) 인 경우
if( EXCEPTION_BREAKPOINT == per->ExceptionCode )
{
// BP 주소가 WriteFile() API 주소인 경우
if( g_pfWriteFile == per->ExceptionAddress )
{
// #1. Unhook
// 0xCC 로 덮어쓴 부분을 original byte 로 되돌림
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
// #2. Thread Context 구하기
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
// #3. WriteFile() 의 param 2, 3 값 구하기
// 함수의 파라미터는 해당 프로세스의 스택에 존재함
// param 2 : ESP + 0x8
// param 3 : ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
// #4. 임시 버퍼 할당
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string : %s\n", lpBuffer);
// #6. 소문자 -> 대문자 변환
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}
printf("\n### converted string : %s\n", lpBuffer);
// #7. 변환된 버퍼를 WriteFile() 버퍼로 복사
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
// #8. 임시 버퍼 해제
free(lpBuffer);
// #9. Thread Context 의 EIP 를 WriteFile() 시작으로 변경
// (현재는 WriteFile() + 1 위치 <– INT3 명령 이후)
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
// #10. Debuggee 프로세스를 진행시킴
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);
// #11. API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
}
코드 양이 좀 많습니다. 하나씩 설명해 보겠습니다.
그 다음 if 문에서 BreakPoint 가 발생한 주소가 kernel32!WriteFile() 시작 주소와 같은지 체크합니다. (WriteFile() 시작 주소는 OnCreateProcessDebugEvent() 에서 미리 얻어 놓았습니다.)
조건이 만족되면 아래 코드가 실행됩니다.
#1. Unhook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
먼저 Unhook 을 하는데요, 이유는 소문자->대문자 작업 이후에 WriteFile() 을 정상적인 상태로 호출 시키기 위해서 입니다. ( API Hooking - 메모장 WriteFile() 후킹 (2) 의 "동작 원리 – Unhook & Hook" 설명을 참고하세요.)
* Unhook 과정이 반드시 필요한 것은 아닙니다. 작업 내용에 따라서 해당 API 호출을 취소할 수 도, 사용자 정의 함수 MyWriteFile() 을 호출할 수 도 있습니다. 상황에 따라서 적절히 변형해서 사용하시기 바랍니다.
#2. Thread Context 구하기
Thread Context 는 제가 블로그에서 처음으로 소개하는 내용인데요, 간단히 설명하면 이렇습니다.
멀티 테스킹(multi-tasking)이라는 개념이 결국은 CPU 자원을 시분할(time-slice) 해서 모든 스레드들을 (우선 순위를 고려하여) 하나씩 골고루 실행해 주는 것이지요.
CPU 가 하나의 스레드를 실행하다가 (일정 시간 후) 다른 스레드를 실행하고자 할 때 기존 스레드에서 작업하던 내용을 잘 백업해 두어야 다음 번 실행할 때 제대로 실행할 수 있을 것입니다.
기존 스레드를 실행 하면서 중요한 (다음 실행에 필요한) 정보라면 바로 CPU 레지스터 값입니다. 이 값이 유지되어야 다음 실행에서 정확히 작업을 이어서 할 수 있습니다. (메모리 정보 – 스택 & 힙은 해당 프로세스의 가상 메모리 공간에 있으므로 따로 보호할 필요가 없지요.)
그 스레드의 CPU 레지스터 정보를 저장하는 구조체가 바로 CONTEXT 구조체 입니다. (스레드 하나당 CONTEXT 구조체 하나입니다.)
CONTEXT 구조체 정의를 보겠습니다.
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
* 출처 : MS VC++ winnt.h
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
위와 같이 GetThreadContext() API 를 호출하면 ctx 구조체 변수에 해당 스레드(g_cpdi.hThread)의 CONTEXT 를 저장합니다. (g_cpdi.hThread 는 Debuggee 의 메인 스레드 핸들입니다.)
HANDLE hThread,
LPCONTEXT lpContext
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms679362(VS.85).aspx
#3. WriteFile() 의 param 2, 3 값 구하기
WriteFile() 호출 시 넘어온 파라미터 중에서 param 2("쓰기버퍼주소"), param 3(버퍼크기) 를 알아내야 합니다.
함수의 파라미터는 스택에 저장되므로 #2 에서 구한 CONTEXT.Esp 멤버를 이용해서 각각의 값을 구합니다.
// param 2 : ESP + 0x8
// param 3 : ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
* 주의! : dwAddrOfBuffer 에 저장되는 "쓰기버퍼" 주소는 Debuggee(notepad.exe) 의 가상메모리 공간의 주소입니다.
* param 2 와 param 3 가 각각 ESP+0x8, ESP+0xC 인 이유는 Stack Frame 을 참고하시기 바랍니다.
#4 ~ #8 소문자 -> 대문자 변환 후 덮어쓰기
"쓰기버퍼" 주소와 크기를 알았기 때문에 이를 Debugger 메모리 공간으로 읽어 들인 후 [소문자 -> 대문자] 변환합니다. 그리고 다시 원래 위치 (Debuggee 의 가상메모리) 에 덮어 써주는 작업입니다.
어렵지 않은 코드이므로 주석을 보시면 쉽게 이해할 수 있을 겁니다.
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
// #5. WriteFile() 의 버퍼를 임시 버퍼에 복사
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string : %s\n", lpBuffer);
// #6. 소문자 -> 대문자 변환
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}
// #7. 변환된 버퍼를 WriteFile() 버퍼로 복사
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
// #8. 임시 버퍼 해제
free(lpBuffer);
#9. Thread Context 의 EIP 를 WriteFile() 시작으로 변경
위의 #2 에서 구한 CONTEXT 에서 Eip 멤버를 WriteFile() 시작 위치로 변경합니다. (EIP 현재 위치는 WriteFile() + 1 입니다. API Hooking - 메모장 WriteFile() 후킹 (2) "# 실행흐름" 설명 참고)
CONTEXT.Eip 멤버를 변경한 후 SetThreadContext() API 를 호출합니다.
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
SetThreadContext() API 입니다.
HANDLE hThread,
const CONTEXT *lpContext
);
* 출처 : http://msdn.microsoft.com/en-us/library/ms680632(VS.85).aspx
#10. Debuggee 프로세스를 진행시킴
모든 준비는 끝났습니다.
이제는 정상적인 WriteFile() API 를 호출해야 할 때입니다.
ContinueDebugEvent() API 를 호출하여 Debuggee 프로세스의 실행을 재개 시킵니다.
위 #9 에서 CONTEXT.Eip 를 WriteFile() 시작으로 되돌렸으므로 깔끔한 WriteFile() 호출이 진행 됩니다.
Sleep(0);
* Sleep(0) 을 해준 이유?
원본 코드 그대로 테스트 해보시고요, Sleep(0) 를 주석 처리한 후 테스트 해보시기 바랍니다. (notepad 에 글을 쓰고 빠르게 반복해서 저장해보세요.)
두 경우 어떤 차이가 있으며, 왜 그런 차이가 발생하는지 생각해 보시기 바랍니다. ^^
이유를 파악하신 분께서는 댓글 남겨주세요~
#11. API Hook
다음 번 후킹을 위하여 다시 API Hook 을 설치합니다.
(이 과정이 생략되면 #1 에서 Unhook 되었기 때문에 WriteFile() API 후킹은 완전히 풀린 상태가 되버립니다.)
&g_chINT3, sizeof(BYTE), NULL);
여기까지 입니다. ^^
이것으로써 DebugLoop() 함수를 상세하게 살펴봤습니다.
실제로 코드를 디버깅 해가면서 각 구조체에 어떤 값이 들어가는지 확인해 보시기 바랍니다. 몇 번만 디버깅 해보시면 저절로 흐름이 파악되실 것입니다.
수고하셨습니다.
* 참고
Windows XP 이상부터는 DebugSetProcessKillOnExit() 를 호출하여 Debugger 가 종료되는(detach) 순간에 Debuggee 가 종료되지 않게 할 수 있습니다.
이때 조심해야 할 점은 Debugger 가 종료되기 전에 unhook 을 해줘야 합니다.
그렇지 않으면 해당 API 시작 부분의 0xCC 가 남아있기 때문에 API 가 호출 시 EXCEPTION_BREAKPOINT 예외가 발생합니다. 이때는 Debugger 가 없기 때문에 Debuggee 프로세스는 종료됩니다.
+---+
지금까지 Debug Method 을 이용한 API Hooking 에 대해서 공부하였으며, 간단한 예제를 통하여 실습을 해보았습니다.
좀 자세히 설명하기 위해 글이 많이 길어졌습니다. 양해 부탁 드립니다.
다음 번에는 DLL Injection & IAT 변경을 이용한 API Hooking 에 대해서 살펴보도록 하겠습니다.
질문 있으시면 댓글 남겨주세요~
ReverseCore
'study' 카테고리의 다른 글
| API Hooking – '스텔스' 프로세스 (1) (18) | 2009/12/13 |
|---|---|
| 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 |
hookdbg.exe
hookdbg.cpp