디버그 이벤트의 종류
- **EXCEPTION_DEBUG_EVENT**
- CREATE_THREAD_DEBUG_EVENT
- CREATE_PROCESS_DEBUG_EVENT
- EXIT_THREAD_DEBUG_EVENT
- EXIT_PROCESS_DEBUG_EVENT
- LOAD_DLL_DEBUG_EVENT
- UNLOAD_DLL_DEBUG_EVENT
- OUTPUT_DEBUT_STRING_EVENT
- RIP_EVENT
이 중 디버깅에 관련된 이벤트는 EXCEPTION_DEBUG_EVENT이다. 이와 관련된 이벤트의 종류는 아래와 같다.
- EXCEPTION_ACCESS_VIOLATION
- EXCEPTION_ARRAY_BOUNDS_EXCEEDED
- **EXCEPTION_BREAKPOINT**
- EXCEPTION_DATATYPE_MISALIGNMENT
- EXCEPTION_FLT_DENORMAL_OPERAND
- EXCEPTION_FLT_DIVIDE_BY_ZERO
- EXCEPTION_FLT_INEXACT_RESULT
- EXCEPTION_FLT_INVALID_OPERATION
- EXCEPTION_FLT_OVERFLOW
- EXCEPTION_FLT_STACK_CHECK
- EXCEPTION_FLT_UNDERFLOW
- EXCEPTION_FLT_ILLEGAL_INSTRUCTIOIN
- EXCEPTION_IN_PAGE_ERROR
- EXCEPTION_INT_DIVIDE_BY_ZERO
- EXCEPTION_INT_OVERFLOW
- EXCEPTION_INVALID_DISPOSITION
- EXCEPTION_NONCONTINUABLE_EXCEPTION
- EXCEPTION_PRIV_INSTRUCTION
- EXCEPTION_SINGLE_STEP
- EXCEPTION_STACK_OVERFLOW
각종 예외에서 디버거가 반드시 처리해야 하는 예외는 EXCEPTION_BREAKPOINT이다. BP를 구현하는 방법은 설치하는 코드의 메모리 시작 주소의 1바이트를 CC로 변경하는 것이다.
디버거-디버기 관계를 가진 상태에서 디버기의 API 시작 부분을 0xCC로 바꿔 제어를 디버거로 가져온 후 원하는 작업을 수행한 후 디버기를 다시 실행 상태로 바꾸는 것이다.
notepad에 글을 쓰기 전에, hookdbg.exe를 notepad의 PID를 이용하여 hooking한다.

그리고 글을 test.txt로 저장한다.

그러면 cmd에서 해당 글이 출력됨과 동시에 test.txt는 모두 대문자로 변한다.

DebugLoop()
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
// Debuggee 로부터 event 가 발생할 때까지 기다림
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;
}
// Debuggee 의 실행을 재개시킴
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
디버기로부터 발생하는 이벤트를 받아서 처리한 후 디버기의 실행을 재개하는 역할이다.
아래는 코드 중 디버기로부터 디버그 이벤트가 발생할 때까지 기다리는 함수이다.
BOOL WINAPI WaitForDebugEvent{
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
};
코드에서 WaitForDebufEvent(&de, INFINITE) 라고 되어있는데, de 변수에 해당 이벤트에 대한 정보를 설정한 후 즉시 리턴하는 함수이다. de의 구조체인 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;
디버그 이벤트 9가지 종류 중 하나가 dwDebugEventCode에 세팅되고, 해당 이벤트의 종류에 따라 u(유니온) 멤버가 세팅된다.
아래는 디버기의 실행을 재개하는 함수이다.
BOOL WINAPI ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
};
ContinueDebugEvent() API는 debuggee의 실행을 재개하는 함수다. 해당 API의 마지막 파라미터인 dwContinueStatus는 DBG_CONTINUE(정상적으로 처리) 또는 DBG_EXCEPTION_NOT_HANDLED(처리하지 못함) 중에서 하나의 값을 갖는다.
이렇게 DebugLoop 함수는 이벤트를 기다렸다가 받아서 처리하고 재개하는 함수이다. 처리하는 이벤트는 코드에서 보면 3가지이다.
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
// WriteFile() API 주소구하기 --> 디버기 프로세스의 메모리 주소가 아니라 디버거 프로세스의 메모리 주소를 얻어서 사용
g_pfWriteFile = GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "WriteFile");
//API Hook-WriteFile()
//첫 번째 byte를 0xCC로 변경
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
typedef struct _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;
hProcess 멤버를 이용하여 WriteFile() API를 후킹 할 수 있다. 이 때 ReadProcessMemory를 이용해 WriteFile() API의 첫 바이트를 읽어서 g_ch0rgByte에 저장하는데 이는 후킹을 해제할 때 BP를 없애는 데 필요하기 때문이다.
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
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() 인 경우
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);
// #5. WriteFile() 의 버퍼를 임시 버퍼에 복사
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);
// #6. 소문자 -> 대문자 변환
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}
printf("\n### converted string ###\n%s\n", lpBuffer);
// #7. 변환된 버퍼를 WriteFile() 버퍼로 복사
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
// #8. 임시 버퍼 해제
free(lpBuffer);
// #9. Thread Context 의 EIP 를 WriteFile() 시작으로 변경
// (현재는 WriteFile() + 1 만큼 지나왔음)
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;
}
}
return FALSE;
}
코드 안의 # 주석을 따라가면서 보겠다.
UnHook (API 훅 제거)
BP를 해제하는 과정
Thread Context 구하기
Thread Context = 프로세스의 실제 명령어 코드는 스레드 단위로 실행되는데, 스레드의 CPU 레지스터 정보를 저장하는 구조체가 Context 구조체이다.
GetThreadContext() API를 호출하면 ctx의 구조체 변수에 해당 스레드의 Context를 저장한다.
WriteFile()의 param 2,3 값 구하기
함수의 파라미터는 스택에 저장되므로 Context.esp 멤버를 이용해서 각각의 값을 구한다.
4부터 8까지 대문자 덮어쓰기
~
~
~
~
Thread Context의 EIP를 WriteFile() 시작으로 변경
EIP를 INT3에 의해 1byte 밀렸기 때문에 다시 WriteFile의 시작 주소가 저장된 g_pfWriteFile을 대입한다.
디버거 프로세스를 진행한다.
ContinueDebugEvent를 통해 디버기 프로세스 실행을 재개한다.
API 훅 설치
다음 번 후킹을 위해 파일을 저장할 때 후킹이 되도록 설정한다.