본문 바로가기

High Level Technique/Reversing

API Hooking - Debug Technique

API Hooking - Debug Technique


Debug 기법을 이용한 실습을 해보도록 하겠습니다.


핵심원리에 나와있는 notedpad.exe WriteFile()을 해볼텐데, Windows 10 x64 notepad.exe를 이용하여 해보도록 하겠습니다.



먼저 디버거 동작원리와 디버그 이벤트에 대해서 알아보도록 하겠습니다.



디버거 동작원리


프로세스가 디버거에 붙어서 실행이 되면 OS에서는 프로세스에서 디버그 이벤트가 발생할 때 해당 프로세스 실행을 정지시키고 디버거에게 전달합니다.

그러면 디버거는 해당 이벤트에 대해서 처리를 한 후에 프로세스 실행을 다시 시작합니다.


일반적인 예외도 디버그 이벤트에 해당하고, 해당 프로세스가 디버깅 중이 아니라면 디버그 이벤트는 예외처리되거나 OS의 예외 처리 루틴에서 처리됩니다.

또한 디버거는 디버그 이벤트 중에서 처리 할 수 없거나 관심 없는 이벤트들은 OS가 처리하도록 만들어 줍니다.



디버그 이벤트https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms679308(v=vs.85).aspx




위 디버그 이벤트 중에서 디버깅에 관련된 이벤트는 EXCEPTION_DEBUG_EVENT 입니다.


이와 관련된 예외는 아래와 같습니다. https://msdn.microsoft.com/en-us/library/windows/desktop/aa363082(v=vs.85).aspx




위와 같이 여러 예외 중에서 디버거가 반드시 처리해야 하는 예외가 EXCEPTION_BREAKPOINT 입니다. BreakPoint는 어셈블리어로 INT3 이며 IA-32 명령어로 0xCC 입니다.

코드 디버깅 중에 INT3 명령어를 만나면 실행이 중지되고, 디버거에게 EXCEPTION_BREAKPOINT 예외를 보냅니다. 이때 디버거에서 작업을 할 수 있습니다.


디버거에서 BreakPoint를 구현하는 방법은 메모리 시작 주소의 1바이트를 0xCC로 변경하고 다시 디버깅을 시작하고 싶으면 바꿨던 값을 원래 값으로 복원시켜주고 실행하면 됩니다.




이제 notepad.exe를 이용하여 WriteFile()을 후킹해보도록 하겠습니다. 저장될 때 소문자로 입력된 내용을 전부 대문자로 바꾸도록 해보겠습니다.



#include <Windows.h>
#include <stdio.h>
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
// WriteFile() Address 구하기
// 해당 프로세스의 주소를 구하는 것이 아니라 디버거에서느 주소를 사용.
// Windows는 System DLL 이면 프로세스에서 동일한 주소에 로딩되므로 문제 없음.
g_pfWriteFile = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "WriteFile");
// API Hook - WriteFile() 첫번째 바이트를 0xCC(INT3)으로 변경 원래 첫 바이트는 g_ChOrgByte에 저장.
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;
}
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)
{
//BreakPoint가 발생한 주소가 kernel32의 WriteFile() 주소와 같은지 체크
if (g_pfWriteFile == per->ExceptionAddress)
{
// Unhook 0xCC(INT3)으로 덮은 부분을 원래 Byte로 되돌림.
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
// Thread Context 구하기
// Thread Context는 프로그램은 프로세스 단위로 실행이 되는데, 실제 명령어 코드는 Thread 단위로 실행된다.
// Windows는 Multi Thread 기반이여서 하나의 프로세스에서 여러 Thread가 실행 될 수 있다.
// CPU 자원을 시분할 하여 각 Thread에 실행을 한다.
// CPU에서 하나의 Thread를 실행시키다가 다른 Thread를 실행시키고자 하는 경우 이전에 실행한 Thread 내용을 저장해둬야 한다.
// 저장된 정보중 중요한 것이 레지스터 값인데, 이 값이 유지되어야 다음 실행에서 정확히 작업을 이어서 할 수 있다.
// 그래서 이 정보를 저장하는 구조체가 Thread Context 이다.
ctx.ContextFlags = CONTEXT_CONTROL;
// ctx 구조체 변수에 해당 Thread(g_cpdi.hThread)의 Context를 저장한다.
// g_cpdi.hThread는 프로세스의 Main Thread Handle
GetThreadContext(g_cpdi.hThread, &ctx);
// WriteFile() parameter 2, 3 구하기, 스택에 존재하므로 ESP + 0x8, ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Rsp + 0x8),&dwAddrOfBuffer, sizeof(DWORD), NULL); // 쓰기 버퍼 주소
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Rsp + 0xC),&dwNumOfBytesToWrite, sizeof(DWORD), NULL); // 버퍼 크기
// 임시 버퍼 할당
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);
// WriteFile() 의 버퍼를 임시 버퍼에 복사
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);
// 소문자 -> 대문자 변환
for (i = 0; i < dwNumOfBytesToWrite; i++)
{
if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)
lpBuffer[i] -= 0x20;
}
printf("\n### converted string ###\n%s\n", lpBuffer);
// 변환된 버퍼를 WriteFile() 버퍼로 복사
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,lpBuffer, dwNumOfBytesToWrite, NULL);
// 임시 버퍼 해제
free(lpBuffer);
// Tread Context의 EIP를 WriteFile()로 변경 현재 위치는 WriteFile() + 1 위치임 (INT3 명령 이후).
ctx.Rip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
// 프로세스를 진행시킴
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0); // WriteFile()를 호출하는 도중에 0xCC로 변경하는 것을 방지.
// API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
// 프로세스에서 Event가 발생할 때까지 기다림
// DEBUG_EVENT 구조체에 해당 이벤트에 대한 정보를 설정한 후 리턴. 디버그 이벤트는 9가지.
// DEBUG_EVENT.dwDebugEventCode에 9가지 중 하나로 세팅. 해당 이벤트 종류에 따라 DEBUG_EVENT.u 멤버가 세팅.
while (WaitForDebugEvent(&de, INFINITE))
{
dwContinueStatus = DBG_CONTINUE;
// 프로세스 생성 혹은 Attach Event
if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)
{
OnCreateProcessDebugEvent(&de);
}
// Exception Event
else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)
{
if (OnExceptionDebugEvent(&de))
{
continue;
}
}
// 프로세스 종료 이벤트
else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)
{
// 프로세스 종료 또는 디버거 종료
break;
}
// 프로세스 다시 실행
// dwContinueStatus는 DBG_CONTINUE 또는 DBG_EXCEPTION_NOT_HANDLE 둘 중 하나의 값을 갖는다.
// 정상적인 경우 DEG_CONTINUE 처리하지 못하거나 SEH(Structured Exception Handler)에서 처리하길 원할 때 DBG_eXCEPTION_NOT_HANDLE로 세팅.
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
int main(int argc, char* argv[])
{
DWORD dwPID;
if (argc != 2)
{
printf("사용법: HookDBG PID\n");
return 0;
}
// Attach Process
dwPID = atoi(argv[1]);
if (!DebugActiveProcess(dwPID))
{
printf("DebugActiveProcess(%d) failed!!!\n", dwPID);
printf("Error Code = %d \n", GetLastError());
}
// Debug Loop
DebugLoop();
return 0;
}









위 소스코드를 가지고 Windows 10에서 실행한 결과 저장할 때에 메모장이 정지가 되서 Hooking이 되지 않았습니다.


값은 정확히 가져오는 듯 하다. 0xFF가 첫 바이트 인데, 왜 안되는 것일까?


디버깅을 해본 결과 BreakPoint가 발생한 주소와 kernel32의 WriteFile() 주소가 달라서 진행되지 않았다.




왜 다르게 나타나는 것일까?

















'High Level Technique > Reversing' 카테고리의 다른 글

API Hooking - API Code Patch, Global Hooking  (0) 2016.07.25
API Hooking - IAT Hooking  (3) 2016.07.25
API Hooking  (0) 2016.07.19
Code Injection  (0) 2016.07.15
DLL Load Using PE Patch  (0) 2016.07.14