hookdbg.cpp 의 DebugLoop() 함수에서 호출하는 세가지 Debug event 핸들러에 대해서 설명드리겠습니다.


<hookdbg.cpp 의 DebugLoop() 함수>

이전 설명은 아래 링크에서 보실 수 있습니다.

API Hooking - 메모장 WriteFile() 후킹 (1)
API Hooking - 메모장 WriteFile() 후킹 (2)



코드 설명


앞서 설명드린 DebugLoop() 함수에서는 세가지 Debug event 를 처리합니다. (위의 코드 그림 참고)

- CREATE_PROCESS_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)될 때 호출됩니다.

BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
    // WriteFile() API 주소구하기
    g_pfWriteFile = GetProcAddress(GetModuleHandle("kernel32.dll"),
                                   "WriteFile");

    // API Hook - WriteFile()
    //   첫번째 byte 를 0xCC (INT3)로 변경
    //   (orginal byte 는 백업 – g_chOrgByte)
    g_cpdi = pde->u.CreateProcessInfo; // 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;
}

먼저 WriteFile() API 의 시작 주소를 구합니다.

주목할 점은 Debuggee 프로세스의 메모리 주소가 아니라 Debugger 프로세스의 메모리 주소를 얻어서 사용한다는 것입니다. Windows OS 에서 System DLL 인 경우 모든 프로세스에서 동일한 주소(가상 메모리)에 로딩 되므로 이렇게 해도 문제없습니다. (참고 : DLL Injection)

g_cpdi 는 CREATE_PROCESS_DEBUG_INFO 구조체 변수입니다.

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;

* 출처 : http://msdn.microsoft.com/en-us/library/ms679286(VS.85).aspx

CREATE_PROCESS_DEBUG_INFO 구조체 hProcess 멤버(Debuggee 프로세스 핸들)를 이용하여 WriteFile() API 를 후킹 할 수 있습니다. (Debug Method 가 아니라면 OpenProcess() API 를 통해서 해당 프로세스의 핸들을 얻어야 합니다.)

Debug Method 에서 후킹 방법은 아주 간단합니다.

API 시작 위치에 “BreakPoint 를 설치” 하는 것입니다.

Debuggee 의 프로세스 핸들(Debug 권한을 가짐)을 가지고 있기 때문에 ReadProcessMemory(), WriteProcessmemory() API 를 이용하여 Debuggee 의 프로세스 메모리 공간에 자유롭게 읽기/쓰기 작업을 할 수 있습니다.

위 함수들을 이용해서 Debuggee 에 BreakPoint (INT3 – 0xCC)를 설치할 수 있습니다.

ReadProcessMemory() 를 이용해서 WriteFile() API 의 첫 바이트를 읽어서 g_chOrgByte 변수에 저장합니다. 아래 그림을 보시면 WriteFile() API 의 첫 바이트는 0x6A 입니다.

 
<Fig. 4>

g_chOrgByte 변수에 첫 바이트를 저장하는 이유는 나중에 후킹을 해제(Unhook) 할 때 필요하기 때문입니다.

그 후 WriteProcessMemory() 를 이용해서 이 값을 0xCC 로 바꿔버립니다. (아래 그림 참조)

 
<Fig. 5>

0xCC 는 "INT3" 를 뜻하는 OP code 입니다. 즉, BreakPoint 입니다.

CPU 는 INT3 명령을 만나면 프로그램 실행을 멈추고 예외를 발생시킵니다. 만약 해당 프로그램이 디버깅 중이라면 디버거에게 제어를 넘겨서 처리하도록 합니다.

이것이 일반적으로 디버거에서 BreakPoint 를 설치하는 기본 원리입니다.

이제 Debuggee 프로세스에서 WriteFile() API 가 호출되면 Debugger 에게 제어권이 넘어오게 됩니다.


- EXCEPTION_DEBUG_EVENT -> OnExceptionDebugEvent()

이번에는 EXCEPTION_DEBUG_EVENT 이벤트 핸들러인 OnExceptionDebugEvent()를 살펴보겠습니다. 이 함수가 바로 Debuggee 의 INT3 명령을 처리하게 될 함수입니다.

가장 핵심적인 내용이라 설명을 자세히 해보겠습니다.

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() 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);

            // #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;
            }

            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;

        }
    }
    return FALSE;
}

코드 양이 좀 많습니다. 하나씩 설명해 보겠습니다.

처음 if 문에서 EXCEPTION_BREAKPOINT 예외인지 체크합니다. (이 외에도 약 19개의 EXCEPTION 이 더 존재합니다. 참고 : API Hooking - 메모장 WriteFile() 후킹 (1) <list 2>)

그 다음 if 문에서 BreakPoint 가 발생한 주소가 kernel32!WriteFile() 시작 주소와 같은지 체크합니다. (WriteFile() 시작 주소는 OnCreateProcessDebugEvent() 에서 미리 얻어 놓았습니다.)

조건이 만족되면 아래 코드가 실행됩니다.

#1. Unhook

//   0xCC 로 덮어쓴 부분을 original byte 로 되돌림
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                   &g_chOrgByte, sizeof(BYTE), NULL);

먼저 Unhook 을 하는데요, 이유는 소문자->대문자 작업 이후에 WriteFile() 을 정상적인 상태로 호출 시키기 위해서 입니다. ( API Hooking - 메모장 WriteFile() 후킹 (2) 의 "동작 원리 – Unhook & Hook" 설명을 참고하세요.)

Unhook 방법은 Hook 과 마찬가지로 아주 간단합니다. 원래 바이트(g_chOrgByte) 를 써주면 됩니다.

* Unhook 과정이 반드시 필요한 것은 아닙니다. 작업 내용에 따라서 해당 API 호출을 취소할 수 도, 사용자 정의 함수 MyWriteFile() 을 호출할 수 도 있습니다. 상황에 따라서 적절히 변형해서 사용하시기 바랍니다.

#2. Thread Context 구하기

Thread Context 는 제가 블로그에서 처음으로 소개하는 내용인데요, 간단히 설명하면 이렇습니다.
 
모든 프로그램은 프로세스 단위로 실행됩니다. 그리고 프로세스의 실제 명령어 코드는 스레드 단위로 실행됩니다. Windows OS 는 multi-thread 기반이기 때문에 하나의 프로세스에서 여러 스레드가 동시에 실행될 수 있습니다.

멀티 테스킹(multi-tasking)이라는 개념이 결국은 CPU 자원을 시분할(time-slice) 해서 모든 스레드들을 (우선 순위를 고려하여) 하나씩 골고루 실행해 주는 것이지요.

CPU 가 하나의 스레드를 실행하다가 (일정 시간 후) 다른 스레드를 실행하고자 할 때 기존 스레드에서 작업하던 내용을 잘 백업해 두어야 다음 번 실행할 때 제대로 실행할 수 있을 것입니다.

기존 스레드를 실행 하면서 중요한 (다음 실행에 필요한) 정보라면 바로 CPU 레지스터 값입니다. 이 값이 유지되어야 다음 실행에서 정확히 작업을 이어서 할 수 있습니다. (메모리 정보 – 스택 & 힙은 해당 프로세스의 가상 메모리 공간에 있으므로 따로 보호할 필요가 없지요.)

그 스레드의 CPU 레지스터 정보를 저장하는 구조체가 바로 CONTEXT 구조체 입니다. (스레드 하나당 CONTEXT 구조체 하나입니다.)

CONTEXT 구조체 정의를 보겠습니다.

typedef struct _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
 
아래는 스레드의 CONTEXT 를 구하는 코드입니다.

// Thread Context 구하기
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);

위와 같이 GetThreadContext() API 를 호출하면 ctx 구조체 변수에 해당 스레드(g_cpdi.hThread)의 CONTEXT 를 저장합니다. (g_cpdi.hThread 는 Debuggee 의 메인 스레드 핸들입니다.)

BOOL WINAPI GetThreadContext(
    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 의 가상메모리) 에 덮어 써주는 작업입니다.

어렵지 않은 코드이므로 주석을 보시면 쉽게 이해할 수 있을 겁니다.

// #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 : %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() 시작으로 변경

위의 #2 에서 구한 CONTEXT 에서 Eip 멤버를 WriteFile() 시작 위치로 변경합니다. (EIP 현재 위치는 WriteFile() + 1 입니다. API Hooking - 메모장 WriteFile() 후킹 (2) "# 실행흐름" 설명 참고)

CONTEXT.Eip 멤버를 변경한 후 SetThreadContext() API 를 호출합니다.

//   (현재는 WriteFile() + 1 위치 <– INT3 명령 이후)
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);

SetThreadContext() API 입니다.

BOOL WINAPI SetThreadContext(
    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() 호출이 진행 됩니다.

ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);

* Sleep(0) 을 해준 이유?
원본 코드 그대로 테스트 해보시고요, Sleep(0) 를 주석 처리한 후 테스트 해보시기 바랍니다. (notepad 에 글을 쓰고 빠르게 반복해서 저장해보세요.)

두 경우 어떤 차이가 있으며, 왜 그런 차이가 발생하는지 생각해 보시기 바랍니다. ^^
이유를 파악하신 분께서는 댓글 남겨주세요~

#11. API Hook

다음 번 후킹을 위하여 다시 API Hook 을 설치합니다.
(이 과정이 생략되면 #1 에서 Unhook 되었기 때문에 WriteFile() API 후킹은 완전히 풀린 상태가 되버립니다.)

WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                   &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

 

Trackback Address :: http://www.reversecore.com/trackback/58 관련글 쓰기

  1. 냥냥 2009/11/04 03:10 댓글주소 | 수정 | 삭제 | 댓글

    잘봤습니다~ ㅎㅎ..
    Sleep() 를 해주는 이유는 동기화가 되지 않아기 때문입니다,
    이것은 반복된 상황이 초래될수 있는것을 의미합니다.
    만약 ContinueDebugEvent(); 를 호출후 NOTEPAD.exe 로 스위칭이 일어나게되면 정상적인 동작을 하게되겠지만, 퀀텀 만큼의 시간은 스레드가 소유할수 있는 최소의 시간이기 때문에, ContinueDebugEvent(); 를 호출후에 곧바로 WriteProcessMemory(); 가 호출된 위험이 존재한다는것을 내포하고 있습니다. 결국 위 함수 2개가 연속으로 실행된다는것은, 또다시 INT 3 이 발생한다는것이고 똑같은 반복작업을 수행하게 된다는것을 의미합니다.
    하지만 Sleep(); 를 쓰게된다면 그 스레드는 즉시 블록킹상태에 들어가게되며 ( 비록 0초라 바로 풀려나오겠지만 ) 이 순간 스위칭이 이루어짐으로서 동기화되지않았기 때문에 생기는 위와 같은 문제는 해결할 수 있습니다.

    가 정답이겠죠? > <//
    항상 좋은 자료 감사드립니다~ ㅎ

    • reversecore 2009/11/04 08:45 댓글주소 | 수정 | 삭제

      냥냥님, 안녕하세요.

      설명을 너무 완벽히 해주셨네요. ^^

      냥냥님께서는 개발 경력이 있는 분 같습니다.
      벌써 구사하시는 용어들에서 개발자의 포쓰가 느껴집니다.
      (사실 더 확실한 증거(?)는 댓글이 달린 시간입니다. ^^)

      감사합니다~

  2. 냥냥 2009/11/04 11:35 댓글주소 | 수정 | 삭제 | 댓글

    아는 잔지식을 총동원했을 뿐이에요 ㅠ_ㅠ.. 전 그저 평범한 학생...
    하하... 다음 강좌가 언제나오나 궁금해서 자꾸 들어와서 그래요..ㅎㅎㅎ;;
    혹시 이번 POC참가하나요..?!

  3. 졸작땜시날새는코더 2009/11/04 14:41 댓글주소 | 수정 | 삭제 | 댓글

    한발늦었네여..
    저가일빠인줄알았더니..

  4. 졸작땜시날새는코더 2009/11/04 14:43 댓글주소 | 수정 | 삭제 | 댓글

    덕분에 어느정도 API후킹부분을 마무리할꺼같습니다.
    dll injection 후 api 후킹이라 dll로 만들어야하는데
    잘되지는 모르겠네여 dll을 이번에 처음접하는거라
    항상감사하고 꾸준히 들리겠습니다.
    신종플루가 유행이던데 조심하시고
    좋은일이 가득하시길 바랍니다 감사합니다^^

  5. 늅늅 2009/11/04 16:30 댓글주소 | 수정 | 삭제 | 댓글

    늘 잘 보고 있습니다~!

    옷..POC 참가하시는가봐요?; 회사가 안보내줘서 못가는데..ㅠ.ㅠ..

    늘 친절한 설명 감사합니다 ^^

    • 냥냥 2009/11/04 17:57 댓글주소 | 수정 | 삭제

      전 학생이라... 가격이 좀 괜찮아서..ㅎㅎㅎ
      처음가보는거라... 경험도 쌓고 하려고요 ㅎㅎ

  6. 몽상가 2009/11/05 10:36 댓글주소 | 수정 | 삭제 | 댓글

    SEH 에 대한 문구에서 안티 디버깅에 대한 내용이 나오는군요. :D
    잘보고있습니다. 수고하세요

    • reversecore 2009/11/05 14:35 댓글주소 | 수정 | 삭제

      몽상가님, 안녕하세요.
      SEH 와 Anti-Debugging 둘 다 흥미로운 주제라서 별도로 포스팅할 예정이구요.

      여러 Anti-Debugging 기법 중에 SEH 를 사용하는 기법이 있는데, 한번 보시면 재미있으실 겁니다.

      감사합니다.

  7. 담배값좀내려 2010/05/05 00:03 댓글주소 | 수정 | 삭제 | 댓글

    잘봤습니다^^두어번 계속 보니깐 감이 잡히네요 ㅎ
    근데 kernel32.dll에서 지원하는 WriteFile() api 말고 다른 api함수를 후킹하고 싶다면 어떻게 해야 하나요?
    INT3 말고 다른 주소를 지정해야 하나요?

  8. 담배값좀내려 2010/05/05 15:24 댓글주소 | 수정 | 삭제 | 댓글

    위에 글 수정이 안되길래 이어서 달겠습니다.
    파일을 저장할때는 kernel32.dll의 WriteFile() API를 사용하는 것을 보고 파일을 열때가 갑자기 궁금해져서 OllyDbg를 이용해서 확인을 해보니 OpenFile() API가 마침 있더라구요
    근데 OpenFile()를 breakpoint로 실행을 시켜보니 1. 파일을 저장할때도, 2. 파일열기버튼이나 파일열기 단축키눌러 OpenFile다이얼로그가 나올때도, 3. OpenFile다이얼로그에서파일을 선택할때에도 ReadFile() API가 호출이 되버리네요 ㅡㅡ;;

    notepad로 파일을 열때 파일 내용 중간에 개인문자열을 저장할수 있지 않을까 해서 WriteFile과는 다르게 ReadFile은 종료할때의 주소를 swap하면 되지 않을까 해서 계속 해보고 있는데 이렇게 파일을 열어서 불러올때만이 아닌 다른 경우에도 ReadFile() API가 호출될때에는 어떻게 해야 할까요?

    지금 이거 할때가 아닌데 안되는거 계속 붙들고 있는거 보면 저도 IT인으로서 소질이 있나봅니닼ㅋㅋ

    • reversecore 2010/05/05 22:04 댓글주소 | 수정 | 삭제

      안녕하세요.

      제가 질문하신 내용을 잘 이해하지 못하고 있습니다. @@

      원하시는 작업이 기존에 있는 파일을 notepad 로 열때 그 내용의 중간에 임의의 문자열을 추가하고 싶으시다는 것인지요?

      죄송하지만 원하시는 작업을 간략히 설명해 주실 수 있으신가요?

  9. 담배값좀내려 2010/05/05 22:36 댓글주소 | 수정 | 삭제 | 댓글

    제가 말이 너무 길었나보네요 ㅎ
    운영자분께서는 kernel32.dll ! WriteFile()에 Debug 후킹을 가하셨잖아요?
    간단히 말해서 전 반대로 ReadFile()을 Debug 후킹을 할 수 있지 않을까 해서 시도해보고 있습니다.

    파일을 읽어올때마다 맨위에 이름을 단다던지 맨뒤에 파일명을 저장한다던지 하는 식으로요 ㅎㅎ
    근데 위에서 말한것처럼 OllyDBug를 통해서 확인을 해보니 WriteFile과는 다르게 OpenFile은
    파일을 불러오기 할때만 호출되는게 아니라 저장할때도 호출되고 그러더라구요;;

    • reversecore 2010/05/07 17:52 댓글주소 | 수정 | 삭제

      안녕하세요.

      원하시는 작업은 바로 notepad 에서 파일을 읽어들일때 ReadFile() API 등을 후킹하여 읽기 버퍼의 내용을 적절히 가공하고 싶으신 거 맞으시죠?

      중간에 OpenFile() API 를 써주셔서 제가 잠시 헷갈렸습니다.
      * 참고로 OpenFile() 은 이미 예젅에 CreateFile() 로 대체되었지요. MS 에서 하위 호환을 위해서 남겨둔 것이지요.

      문제는 ReadFile() 을 후킹하고 싶지만 이 API 가 생각보다 자주 호출 된다는 것이구요? 이렇게 많은 호출중에서 내가 원하는 호출을 어떻게 구별하느냐가 문제의 핵심이 되겠지요?

      네, 말씀하신대로 ReadFile() 은 시스템 DLL 에서 자주 사용됩니다. 원하는 내용을 구현하시려면 먼저 스택의 리턴 주소를 보고 이번의 ReadFile() 호출이 어디서 호출되었는지 확인하는 방법도 있구요. -> notepad.exe 의 .text 섹션에서 호출된 것만 관심을 가지면 됩니다.

      더 편한 방법은 kernel32!CreateFileW() 를 먼저 후킹하신 다음에 파라미터를 잘 살펴보시면서 특정 텍스트 파일이름을 읽기 모드로 열 때를 파악하신 후 그다음 ReadFile() 호출을 유효한 호출로써 간주하고 읽기버퍼를 신경쓰시면 되겠네요. 파일 크기에 따라서 ReadFile() 이 여러번 호출될 수 있습니다.

      질문에 비해 설명이 너무 장황하네요. 죄송합니다.
      설명 능력이 부족합니다. 백문이 불여일견이라는 말을 정말 백배 공감합니다.

      다른 궁금한점 있으시면 다시 올려주세요.

      감사합니다.

  10. 담배값좀내려 2010/07/26 22:09 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요^^ 몇가지 질문사항이 있어서 다시 댓글을 달아 봅니다.
    디버깅과 리버싱에 관한 지식이 부족하여 여러 책자나 관리자분의 디버깅쪽
    강좌 등 을 보며 조금씩 공부를 해나가는 중입니다 ^^

    위에서 말씀하신대로 ReadFile을 분석하던 중 ReadFile의 내부적으로 ntdll.dll의 ntReadFile을
    통해 메모리로 파일의 내용을 읽어오더군요. 그런데 Drag&Drop을 통해서 파일을 열었을대에는
    ReadFile이 아예 호출되지도 않더라구요....

    그래서 찾아낸 것이 CreateFile인데 파일을 열때나 Drag&Drop을 통해서 파일을 열때의 전달인자를 보니
    세번째전달인자 = FILE_SHARE_READ | FILE_SHARE_WRITE,
    여섯번째전달인자 = FILE_ATTRIBUTE_NORMAL,
    일곱번째전달인자 = NULL,
    일 때에는 파일을 열때더군요....그런데 그 후로 파일의 내용을 메모리로 읽어들인다음
    메모장을 통해 화면으로 뿌려주는 부분을 찾기가 굉장히 어렵네요 ㅎㅎ;;

    이런 경우엔 파일을 열때에 파일의 내용이 메모리로 올라오는 경우는 대체 언제쯤일까요...
    리버싱경험이 전무하다보니 혼자서 알아내기가 생각보다 쉽지 않네요;;
    혹 ReadFile외에 메모장의 Edit부분에 파일의 내용을 표시할수 있는 TextOut이나 SetWindowText같은 다른 API가 없을까요?
    (TextOut나 SetWindowText는 안되더라구요 ㅎㅎ;)

    • reversecore 2010/07/28 16:39 댓글주소 | 수정 | 삭제

      안녕하세요.

      메모장에서 파일을 열때 CreateFile() -> CreateFileMapping() -> MapViewOfFile() API 를 사용합니다. 즉 메모리 맵 파일 기법을 사용하지요. 그래서 ReadFile() 이 호출되지 않고도 내용을 볼 수 있던 것입니다. 참고하시기 바랍니다.

      감사합니다.

  11. 2011/04/29 15:15 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

    • reversecore 2011/05/03 00:26 댓글주소 | 수정 | 삭제

      안녕하세요.

      그림 한 두개 정도가 아니라 전부를 가져가시는 것은 안되구요.

      그냥 필요하실 때 방문해 주시면 안될까요? ^^

      감사합니다.

  12. lunaticapple 2011/07/06 16:03 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요? 윈도우 프로그래밍을 처음해봅니다.^^ 제가 위방법으로 readfile함수에 대해
    break를 걸어보았습니다. winamp에서 음악을 플레이할때 readfile을 후킹해서 좀 가져올
    정보가 있어서요^^. 현재 OnExceptionDebugEvent 함수부분은 별도로 아무것도 한거 없이
    print문하나찍고 unhook, 프로세스 재개, hook 의 방식으로 되어있습니다. 그런데 이게,,
    print문 두번정도 찍고(readfile 후킹 두번) 더이상 반응을 하지 않습니다. winamp가 죽은것 같기도 한데 실제 프린트를 찍어보면 OnExceptionDebugEvent함수는 계속 탑니다. 그런데 readfile은 더이상 불리지도 않고 winamp는 소리를 내지 않습니다,, .. 어떻게 디버깅을 해야할지
    몰라서,, 혹시 짐작가시는게 있으시면 조언 부탁드리겠습니다.

    • reversecore 2011/07/14 05:53 댓글주소 | 수정 | 삭제

      안녕하세요.

      위 실습 예제 코드는 동작 원리를 소개하기 위한 목적이기 때문에 코드 가독성을 위해서 예외처리가 빠져 있습니다.

      의심되는 부분이 몇 군데 있기는 한데 비슷한 환경에서 디버깅을 해봐야 정확한 원인을 파악할 수 있을 것 같습니다.

      제가 직접 해보고 다시 댓글 달아드릴께요.

      감사합니다.

  13. LeapOfFaith 2011/07/11 16:43 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요
    조용히 맨날 보면서 따라하다가 막히는 부분이 있어서 여쭤봅니다.
    <fig.4>와 <fig.5>부분인데요.
    notepad에 hookdbg.exe가 붙어있는 상태에서 올리디버거로 notepad의 저기 저 7C7E0E27부분이 바뀌는 것을 확인하려고 했습니다만, 올리디버거가 notepad에 붙질 못하네요.
    아마 hookdbg.exe가 디버거로 이미 notepad에 붙어있어서 올리가 못붙는거 같은데..
    (마찬가지로 올리를 notepad에 먼저 붙인 후, hookdbg.exe를 notepad에 붙이려 시도하면 hookdbg.exe는 DebugActiveProcess<notepad의 PID> failed!!!
    Error Code = 87
    을 출력하고 붙질 못합니다.
    검색해보니 GetLastError() 리턴 값 87은 ERROR_INVALID_PARAMETER라는 군요.)
    어떻게 <fig.4>와 <fig.5>처럼 올리로 지켜보셨는지 궁금합니다..ㅠㅠ

    • reversecore 2011/07/13 21:23 댓글주소 | 수정 | 삭제

      안녕하세요.

      말씀하신 것처럼 하나의 프로세스를 동시에 2개의 디버거로 디버깅을 할 수 없습니다.

      문의하신 <fig. 4> 와 <fig. 5> 는 제가 동작원리를 보여드리기 위해서 OllyDbg 로 notepad 를 디버깅해서 캡쳐한 것입니다. 관련된 설명이 부족해서 착오가 있으셨던것 같습니다.

      감사합니다.

  14. polaris 2011/11/10 00:25 댓글주소 | 수정 | 삭제 | 댓글

    아주 재미나게 잘 보앗습니다. 정말 이런 정보를 공유해 주셔서 감사합니다.
    그런데 한가지 문제가 있어서 이렇게 글을 올립니다.
    Windows7에서 실험해봣는데요 XP에서 할때하고 좀 다른게 있더라구요...
    XP에서는 아무런 문제없이 제대로 되던데 Windows7에서는 Debugger를 실행시킬때 Notepad가 잠시 멈추고 보관할때도 파일보관대화창이 제때에 반응하지 안더니 확인단추를 누르면 아주 응답이 없어집니다.
    Windows7에서는 무엇이 좀 다른거 같은데 여기에 대해서 어떻게 생각하시는지...
    조언부탁드립니다.

  15. 익명 2012/01/21 19:21 댓글주소 | 수정 | 삭제 | 댓글

    thread context에 대해 윗글 링크 따라가 보니 다음과 같이 적혀있더군요
    Protected Processes
    Protected processes enhance support for Digital Rights Management. The system restricts access to protected processes and the threads of protected processes.

    Windows Server 2003 and Windows XP/2000: Protected processes were added starting with Windows Vista.
    The following specific access rights are not allowed from a process to the threads of a protected process:

    THREAD_ALL_ACCESS
    THREAD_DIRECT_IMPERSONATION
    THREAD_GET_CONTEXT
    THREAD_IMPERSONATE
    THREAD_QUERY_INFORMATION
    THREAD_SET_CONTEXT
    THREAD_SET_INFORMATION
    THREAD_SET_TOKEN
    THREAD_TERMINATE
    The THREAD_QUERY_LIMITED_INFORMATION right was introduced to provide access to a subset of the information available through THREAD_QUERY_INFORMATION.

    출처: http://msdn.microsoft.com/en-us/library/ms686769(v=vs.85).aspx



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

두 번째 파라미터(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;
}


main() 함수의 코드는 간단합니다.

프로그램 실행 파라미터로 API 후킹 하려는 프로세스의 PID 를 받습니다.

그 후 DebugActiveProcess() API 를 통해서 실행중인 프로세스에 attach 하여 디버깅을 시작합니다. (위에서 입력한 PID 를 파라미터로 넘겨줍니다.)

BOOL WINAPI DebugActiveProcess(
    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;

    // 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);
    }
}

DebugLoop() 함수는 마치 윈도우 프로시저 함수(WndProc)와 유사하게 동작합니다.
Debuggee 로부터 발생하는 event 를 받아서 처리한 후 Debuggee 의 실행을 재개 시키는 역할입니다.

역시 간단한 코드이므로 주석을 보시면 쉽게 이해하실 수 있으실 것입니다.

그 중에서 몇 가지 중요 API 들을 알아보겠습니다.

WaitForDebugEvent() API 는 이름 그대로 Debuggee 로부터 Debug event 가 발생할 때까지 기다리는 함수입니다. (WaitForSingleObject() API 와 비슷하게 동작합니다.)

BOOL WINAPI WaitForDebugEvent(
    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

이전 포스트에서 Debug event 는 9 가지 종류가 있다고 설명 드렸습니다.
(참고 : API Hooking - 메모장 WriteFile() 후킹 (1) <list 1>)

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

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

 

Trackback Address :: http://www.reversecore.com/trackback/57 관련글 쓰기

  1. alex 2009/11/12 13:20 댓글주소 | 수정 | 삭제 | 댓글

    좋은 글 잘 보고 갑니다!

  2. reversingk 2011/04/27 17:08 댓글주소 | 수정 | 삭제 | 댓글

    항상 감사 합니다.
    코어님 덕에 리버싱 공부하는 것이 즐겁습니다 ^^



앞서 소개해 드린 각 기법들 중에서 Debug 기법에 대한 설명입니다.

실습으로 메모장의 kernel32!WriteFile() API 를 후킹하여 기존과는 다른 동작을 하도록 만들어 보겠습니다.


<실습 예제 – WriteFile() API hooking>

API Hooking 의 기본 설명은 아래 글을 참고하세요.

API Hooking - 리버싱의 '꽃'
API Hooking - Tech Map



Debug Technique




<Fig. 1>

Debug 방식의 API 후킹을 설명 드리겠습니다. (위의 Tech Map 에서 빨간색 표시 부분을 참고하세요)

이 방식의 장점은 후킹을 위해서 '디버깅'을 사용하므로 좀 더 interactive 한 후킹을 수행할 수 있습니다. 즉, 간단한 GUI 를 제공하여 후킹 대상 프로그램의 실행을 제어하고, 메모리를 자유롭게 사용할 수 있습니다.

하지만 먼저 Debugger 구조에 대한 이해가 필요합니다.



Debugger 설명


# 용어

간단한 용어 정리부터 하겠습니다.

Debugger – debugging tool
Debuggee – application to debug

# 디버거 기능

Debugger 의 기능은 Debuggee 가 올바르게 실행되는지 확인하고 (예상치 못한) 프로그램의 오류를 발견하는 것입니다.

Debugger 는 Debuggee 의 명령어 instruction 을 하나씩 실행 가능하며, 레지스터와 메모리에 대한 모든 접근 권한을 가집니다.

# 디버거 동작 원리

일단 Debugger 프로세스로 등록되면 OS 는 Debuggee 에게 debug event가 발생할 때 Debuggee 의 실행을 멈추고 해당 event 를 Debugger 에게 통보합니다. Debugger 는 해당 event 에 대해 적절한 처리를 한 후 Debuggee 의 실행을 재개합니다.

* 일반적인 예외(Exception)도 Debug event 에 해당합니다.
* 만약 해당 프로세스가 디버깅 중이 아니었다면 debug event 는 자체 예외처리 아니면 OS 의 예외처리 루틴에서 처리됩니다.
* Debugger 는 debug event 중에서 처리할 수 없거나 관심 없는 event 들은 OS 가 처리하도록 만들어 줍니다.

아래 그림은 위 설명을 도식화 한 것입니다.


<Fig. 2>

# Debug event

Debug event 입니다.

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_DEBUG_STRING_EVENT  
RIP_EVENT

* 출처 : http://msdn.microsoft.com/en-us/library/ms679302(VS.85).aspx

<List 1>

위 Debug event 중에서 Debugging 관련된 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_ILLEGAL_INSTRUCTION
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

* 출처 : http://msdn.microsoft.com/en-us/library/aa363082(VS.85).aspx

<List 2>

각종 예외(Exception) 중에서 Debugger 가 반드시 처리해야 하는 예외는 바로 EXCEPTION_BREAKPOINT 예외입니다.

BreakPoint 는 어셈블리 명령어 "INT3" 이며, IA-32 Op code 는 0xCC 입니다. 코드 디버깅 중에 INT3 명령어를 만나면 Debugger 에게 EXCEPTION_BREAKPOINT 예외 이벤트가 날아갑니다.

Debugger 에서 BreakPoint 를 구현하는 방법은 간단합니다.

BreakPoint 를 설치하기 원하는 코드의 메모리 시작 주소에서 1 byte 를 0xCC 로 바꿔 치는 것입니다. 디버깅을 계속 진행하고 싶을 때는 다시 원래 값으로 복원시키고 실행해줍니다.

Debug 방식의 API Hooking 은 이와 같은 BreakPoint 의 특성을 이용하는 것입니다.



API Hooking - Debug Method


Debug 기법을 통한 API Hooking 에 대해 좀 더 자세히 설명 드리겠습니다.

기본적인 아이디어는 Debugger-Debuggee 관계를 가진 상태에서 Debuggee 의 API 시작 부분을 0xCC 로 바꿔 제어를 Debugger 로 가져온 상태에서 원하는 작업을 수행한 후 Debuggee 를 다시 실행상태로 바꾸는 것입니다.

작업 흐름은 아래와 같습니다.

- 후킹을 원하는 프로세스에 ‘attach’ 하여 Debuggee 로 만듦
- Hook : API 시작 주소의 첫 바이트를 0xCC 로 변경
- 해당 API 가 호출되면 제어는 Debugger 에게 넘어옴
- 원하는 작업을 수행(파라미터, 리턴 값 조작 등)
- Unhook : Debuggee 의 0xCC 를 원래대로 복원시킴 (<– API 의 정상 실행을 위해)
- 해당 API 실행 (0xCC 가 빠진 정상적인 상태)
- Hook : 다시 0xCC 로 바꿈 (<– 지속적인 후킹을 위해)
- Debuggee 에게 제어를 되돌려줌


위 방식은 가장 간단한 경우를 소개한 것입니다.

이걸 기준으로 다양하게 변형할 수 있습니다. 가령 original API 를 호출 하지 않을 수 도 있고, 사용자가 제공한 custom API 를 호출 할 수 도 있고, 한번만 후킹 할 수도 있고, 여러 번 후킹 할 수도 있습니다.

작업 목적에 따라서 알맞게 변형해서 사용하시면 됩니다.



Notepad.exe 의 WriteFile() API Hooking


지금 까지 공부한 내용을 바탕으로 실제 코드를 보면서 실습을 해보도록 하겠습니다.

작업할 내용은 Notepad.exe 의 WriteFile() API 후킹입니다.
입력된 파라미터를 조작하여 소문자로 입력된 내용을 전부 대문자로 바꿔 보겠습니다.

즉, Notepad 에서 입력된 모든 소문자는 파일로 저장되는 순간에 대문자로 변경되어 저장됩니다.

Notepad.exe 를 실행시킨 후 PID 를 알아냅니다.


<Fig. 3>

첨부된 후킹 프로그램(hookdbg.exe)을 실행합니다.

hookdbg.exe 는 콘솔 기반 프로그램이며 실행 파라미터로 후킹 할 프로세스의 PID 를 넘겨 받습니다.



<Fig. 4>

위 그림과 같이 hookdbg.exe 를 실행하면 PID 1688 에 해당하는 notepad 프로세스의 WriteFile() API 후킹이 시작됩니다.

그리고 notepad 에 아무 글자나 입력해 보세요.


<Fig. 5>

입력을 마치셨으면 저장을 해주세요.


<Fig. 6>

저장을 마치면 notepad 화면에는 아무런 변화가 일어나지 않습니다. (WriteFile() API 만 후킹 했다는 사실을 기억해 주세요.)

notepad 를 종료해 주시고, hookdbg 프로그램을 봐주세요.


<Fig. 7>

위 그림을 보시면 “original string” 에는 제가 입력한 문자열이 나타나고, “converted string” 에는 변경된(소문자 -> 대문자) 문자열이 나타납니다.

이것은 hookdbg.exe 프로그램 내부에서 후킹의 진행과정을 표시하기 위해서 출력하는 문자열입니다.

실제로 대문자로 저장되었는지 파일을 열어서 확인합니다.


<Fig. 8>

정확히 모든 소문자가 대문자로 변경되어서 저장되었습니다.

이 예제는 아주 간단한 기능을 가지고 있지만 Debug Method 에 대한 기본적인 개념을 잘 설명해줄 수 있습니다.

다음 포스트에서 hookdbg.exe 의 실제 코드를 상세히 살펴보도록 하겠습니다.

API Hooking - 메모장 WriteFile() 후킹 (2)


+---+

ReverseCore

 

Trackback Address :: http://www.reversecore.com/trackback/56 관련글 쓰기

  1. 늅늅 2009/10/28 13:58 댓글주소 | 수정 | 삭제 | 댓글

    언제나 친절하게 설명 되어있는 강좌 잘 보고 있습니다 ^^

  2. 냥냥 2009/10/28 23:59 댓글주소 | 수정 | 삭제 | 댓글

    와... 정말 많은걸 보고 배워가요 ^^*
    좋은글 앞으로도.. 많이 많이 very 많이 많이 부탁드릴께요..ㅎㅎ
    댓글중에 회사원이라고 들었는데요... 하는 일이 무엇인가요..?
    전 학생이라... IT업계의 일에는 무엇이 있는지 궁금해서요... 프라이버시라면... 말 안해주셔도되요~
    히힛~..~ 양질의 좋은 자료 부탁드리겠습니다~ ㅎㅎ.. 근데 퍼가도 되는건가요..?!

    • ReverseCore 2009/10/29 06:14 댓글주소 | 수정 | 삭제

      냥냥님, 안녕하세요.

      도움이 되셨다니 기쁘네요~ ^^

      그냥 평범한 IT 관련 회사원이구요, 개발과 리버싱에 많은 관심을 가지고 있지요.

      퍼가지는 마시구요, 링크만 해주세요~
      (미리 물어봐 주셔서 감사합니다.)

  3. 이승철 2009/10/29 11:07 댓글주소 | 수정 | 삭제 | 댓글

    언제나 잘 보고 있습니다. 감사합니다.

  4. 김형근 2009/10/29 18:05 댓글주소 | 수정 | 삭제 | 댓글

    좋은글 감사합니다.
    궁금한것이 더 많아지네여 ^^
    좋은글 앞으로도 부탁드립니다.

  5. 몽상가 2009/10/30 03:19 댓글주소 | 수정 | 삭제 | 댓글

    저도 요즘 공부 차원에서 다시 보고 있습니다. ^^
    목표는 모 프로그램의 크랙입니다. 물론 나쁜 용도는 아니고, 최소한 내가 의도한 정도까지? 지만, 욕심이 크다보니 할 수 있는데 까지 해보는게 목표입니다.

    근데 일단 가장 기본적인 디버거를 붙이면 강제 종료되는데, 아마 이게 다른 쓰레드에서 디버그가 붙으면 강제종료를 하는 스레드가 있나 봅니다. 이럴땐 어떤 방식으로 해야되는가 궁금합니다.
    아예 실행하고 붙이는게 아니라 실행전에 수정을 해야되는건가요?

    강좌가 계속 나와서 이런 방식까지 나왔으면 좋겠습니다.
    제 블로그 링크도 해주시고 감사합니다. RSS 로 꾸준히 보고있어요 :D

    • reversecore 2009/10/30 07:19 댓글주소 | 수정 | 삭제

      몽상가님, 안녕하세요.

      디버깅 당했다고 판단되면 스스로 종료 <- 안티 디버깅 기법을 사용한 듯 하군요.

      계획에는 일반적인 안티디버깅에 대한 강좌를 하고, 특별한 안티기법은 파일을 분석하는 도중에 따로 정리해보려고 합니다.

      말씀하신 내용도 참고할께요~ (attach 할 수 없는 프로세스)

      감사합니다.

  6. 졸작땜시날새는코더 2009/10/30 13:51 댓글주소 | 수정 | 삭제 | 댓글

    글잘보았습니다 참고많이되었구요
    혹시
    특정 이벤트처리가아니라
    그 해당 프로그램이 사용하는 API함수들을 모두 체크할수있는 방법이없을까요
    변조하는게 아닌 그저 무슨 API함수를 사용했다 정도로만요
    대충 이미지는 나오는데 어떻게해야할지 막막합니다 ㅠㅠ

    • ReverseCore 2009/10/31 12:30 댓글주소 | 수정 | 삭제

      안녕하세요.

      프로그램이 사용하는 모든 API 함수를 확인하고 싶으시다는 말씀이시죠?

      IDAPro, OllyDbg 에서 그런 기능을 지원하는데요, 그 원리는 코드의 CALL, JMP 명령어을 전부 파싱하는 것입니다.

      혹시 이런류의 프로그램이나 OllyDbg 플러그인등이 존재할 수도 있겠네요.

      참고로 예전에 제가 했던 비슷한(?) 방법으로는 관심있는 API 를 전부(약 120여개) 후킹해서 각 파라미터/리턴값을 확인하고 호출 순서등을 확인한 적이 있었습니다.

  7. 냥냥 2009/10/31 03:16 댓글주소 | 수정 | 삭제 | 댓글

    NotePad에 관한 질문드리겠습니다 ㅠ
    제가 더미dll ( Attatch 시 빈 메세지 박스만띄우는 ) 을 만든후 Notepad.exe에 dll injection을 통해서 구현해보려고 했습니다.
    방식은 Notepad를 디버거로서 연다음 Notepad.exe의 메모리공간을 할당받은후 LoadLabrary()를 실행해서 더미dll을 Attatch시키게하는 명령어 코드를 복사한뒤 EIP수정후 실행하게한후, 실행후 다시 EIP를 원래의 EIP주소로 변경하도록 만들었습니다,
    근데... 제대로 동작을안하고 Notepad.exe가 그냥 죽어버리는군요...
    코드가 잘못됐는지 확인하려고... 아무런 보안기능이 없는 전에 만든 툴로 대상을 변경후 실행하니 아무런 이상없이 실행이 잘됐습니다 ( 빈 메세지박스를 확인 )
    Notepad.exe에서는 제대로 실행되지 않는 이유가 무엇을까요..?
    운영체제에서 특별한 알고리즘을 가지고 보호하는건가요..? ( 부팅시 원도우 기본파일들인 calc,notepad 같은 프로그램은 수정되었을시 복원하는 기능은 있다고 들었지만... )

    • ReverseCore 2009/10/31 12:35 댓글주소 | 수정 | 삭제

      안녕하세요

      어떤 OS 를 쓰시나요?

      전 주로 XP 를 쓰는데, DLL Injection 은 아무런 문제가 없습니다.

      다른 프로그램에서는 잘 동작하는데 notepad 만 동작하지 않는다고 하셨는데요, 저도 잘 이해가지 않는군요.

      참고로 DLL Injection 을 위해서 반드시 notepad 를 디버깅 할 필요는 없습니다. 그리고 DLL Injection 의 코드를 확인 해보시는 것이 좋을것 같습니다.

      가능하시면 저에게 보내주시기 바랍니다. 이곳에 올리셔도 되구요.

    • 나그네 2009/11/03 05:34 댓글주소 | 수정 | 삭제

      WFP(Windows File Protection) 때문에 그런거 아닐까요? system 폴더에 있는 notepad.exe 를 다른 폴더로 옮겨서하면 괜찮을 것 같은데요.ㅋ

    • reversecore 2009/11/03 22:25 댓글주소 | 수정 | 삭제

      나그네님, 소중한 답변 감사드립니다. ^^

  8. 냥냥 2009/10/31 16:09 댓글주소 | 수정 | 삭제 | 댓글

    메일 주소가 어떻게 되시나요..? ;;
    마땅히 보낼만한...ㅠㅠ

  9. vice 2009/12/16 11:15 댓글주소 | 수정 | 삭제 | 댓글

    잘 보구 갑니다. 간단하구 쉽게 설명 잘 해주시네요 ^^

  10. thav 2009/12/21 13:23 댓글주소 | 수정 | 삭제 | 댓글

    잘봤습니다. 근데. 님께서 첨부해주신 샘플 소스 구동하다가 notepad 저장할때 정지됩니다.
    혹시 이런 현상 보신적 있나요?
    OS 는 윈도우 XP 입니다.

    • ReverseCore 2009/12/22 11:30 댓글주소 | 수정 | 삭제

      thav님, 안녕하세요.
      notepad 저장시에 프로그램이 멈춘다는 말씀이시죠?

      제 환경(XP SP3)에서는 그런 일이 없었지만, 충분히 그럴 가능성은 있습니다.

      * 참고로 제 테스트 PC 는 OS 만 설치된 클린 PC 환경입니다.

      제가 여러가지 상황에서 충분히 테스트하지 않아서 그럴 수 있고요. thav님 PC 에 실행중인 프로세스들에 의해서 그렇게 될 수 도 있습니다. (충돌!)
      (또한 실습 예제 파일들은 코드의 간결성을 위해서 에러처리가 거의 되어있지 않습니다.)

      (가능하시다면) 다른 PC 에서도 테스트 해보시기 바랍니다.

      감사합니다.