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 에서도 테스트 해보시기 바랍니다.

      감사합니다.


UPX 실행 압축된 notepad_upx.exe 를 디버깅 함으로써 실행 압축에 대한 개념을 이해합니다. 코드를 하나하나 tracing 하면서 결국 원본 notepad.exe 코드를 찾아내는 것이 목표입니다. 마지막에는 UPX 실행 압축 파일을 디버거에서 간단히 통과하는 팁을 설명하겠습니다.


 

notepad.exe 의 EP code

먼저 원본 notepad.exe 의 EP code 를 보겠습니다.

<Fig. 1>

010073B2 주소에서 GetModuleHandleA() API 를 호출해서 notepad.exe 프로세스의 ImageBase 를 구합니다.
그리고 010073B4 와 010073C0 주소에서 각각 MZ 와 PE 시그니처를 비교하는군요.

원본 notepad.exe 의 EP 코드를 눈에 잘 익혀두시기 바랍니다.

 

notepad_upx.exe 의 디버깅

notepad_upx.exe 를 OllyDbg 로 열어보면 아래와 같은 경고 메시지 박스가 나타납니다.

 

<Fig. 2>

디버거가 해당 파일이 압축되었다고 판단하였습니다.
상관 없으니 Y/N 아무거나 선택해서 넘어가면 아래 그림과 같이 UPX stub 코드가 나타납니다.

 

<Fig. 3>

EP 주소는 01015330 으로서 두 번째 섹션의 끝부분입니다.
실제로 압축된 notepad 원본 코드는 EP 주소 위쪽으로 존재합니다.

코드 시작 부분을 살펴보겠습니다.

01015330    60              PUSHAD                
01015331    BE 00100101     MOV ESI, 01011000
01015336    8DBE 0000FFFF   LEA EDI, DWORD PTR DS:[ESI+FFFF0000]

PUSHAD 명령으로 EAX ~ EDI 레지스터 값을 스택에 저장하고,
ESI 와 EDI 레지스터를 각각 2nd 섹션 시작과 1st 섹션 시작 주소로 세팅합니다.
ESI 와 EDI 는 각각 source 와 destination 버퍼를 가리킨다는 것은 다 알고 계실 것입니다.

이후에 나타나는 코드는 <Fig. 3> 에서 처럼 수많은 조건 분기명령어들로 이루어진 코드가 나타납니다.
이 코드가 바로 압축 해제 코드입니다. (ESI 에서 값을 읽어서 EDI 에 써주는 코드)

우리 목표는 이 코드를 전부 tracing 해서 결국 <Fig. 1> 과 같이 원본 notepad 의 EP 코드를 찾아내는 것입니다.

* 리버싱에서는 원본 EP 를 OEP(Original Entry Point) 라고 표현합니다.

* 실제 리버싱에서는 이러한 실행 압축 코드를 일일이 tracing 하지 않고 자동화 스크립트, 노하우 등을 통하여 OEP 로 바로 갑니다. 하지만 실행압축 리버싱을 처음 공부하는 사람들의 입장에서는 하나하나 코드를 tracing 하는 것이 올바른 방법입니다.

이제부터 저와 같이 Tracing 을 시작해 보겠습니다.
일단 아래 동영상을 보시죠.

<Video. 1>

조금 trace (F7) 하다 보면 짧은 루프가 나타납니다.
해당 루프를 아래 그림에서 더 자세히 살펴보겠습니다.

<Fig. 4>

루프 회전 수 ECX = 36Bh 이고, 그 내용은 EDX (01001000) 에서 한 바이트를 읽어 EDI (01001001) 에 쓰는 것입니다.

01001000 주소는 첫 번째 섹션 (“UPX0”) 주소이며, 메모리 상에서만 존재하는 섹션입니다.
(내용은 전부 NULL 입니다.)

* 참고 : 실행 압축 (Run-Time Packer) 에 관하여

실행 압축된 파일을 디버깅 할 때 이러한 루프를 만났다면 그 루프를 빠져 나와야 합니다.

010153E6 주소에 BP 를 설치 후 실행 명령(F9) 으로 해당 루프를 탈출합니다.

조금 더 진행하다 보면 다시 아래와 같은 루프를 만나게 됩니다.

<Video. 2>

이것이 본격적인 디코딩(decoding) 루프입니다.
ESI 가 가리키는 두 번째 섹션(“UPX1”) 의 주소에서 차례대로 값을 읽어서 적절한 연산을 거쳐 압축을 해제하여 EDI 가 가리키는 첫 번째 섹션(“UPX0”) 의 주소에 값을 써주게 됩니다.

이 과정에서 사용되는 명령어들은 아래와 같습니다.
AL/EAX 에는 압축이 풀린 데이타가 있고, EDI 는 첫 번째 섹션의 주소를 가리키고 있습니다.

0101534B    8807    MOV BYTE PTR DS:[EDI],AL
...
010153E0    8807    MOV BYTE PTR DS:[EDI],AL
...
010153F1    8907    MOV DWORD PTR DS:[EDI],EAX

이 두 번째 루프를 탈출하기 위해서는 아래 그림과 같이 01015402 주소에 BP 를 설치하면 됩니다.

 

<Fig. 5>

01015402 주소까지 디버깅을 하셨다면 위 그림처럼 첫 번째 섹션(“UPX0”) 영역에 코드가 쓰여진걸 확인 할 수 있습니다. (원래는 0 으로 채워져 있던 영역입니다.)

조금 더 내려가면 아래와 같은 루프를 만나게 됩니다.

 

<Fig. 6>

이것은 원본 코드의 CALL/JMP 명령어(op code : E8/E9)의 destination 주소를 복원시켜주는 코드입니다.
이 루프도 01015436 주소에 BP 를 설치하여 탈출할 수 있습니다.

이제 거의 막바지입니다. 
IAT 세팅만 하면 UPX 압축 해제 코드는 끝납니다.
조금만 더 내려가 보겠습니다.

 

<Fig. 7>

위 그림에서 파란색으로 표시된 부분이 바로 IAT 를 세팅하는 루프입니다.

01015436 주소에서 EDI = 01014000 으로 세팅되며, 이곳은 두 번째 섹션(“UPX1”) 영역입니다.
이곳에는 원본 notepad.exe 에서 사용되는 API 이름 문자열이 저장되어 있습니다.
이 문자열들을 가지고 01015467 주소의 GetProcAddress() 를 호출하여 API 시작 주소를 얻은 후 EBX 레지스터가 가리키는 원본 notepad.exe 의 IAT 영역에 API 주소를 입력합니다.

이 과정을 API 이름 문자열이 끝날 때까지 반복하면 원본 notepad.exe 의 IAT 를 완벽히 복원하게 됩니다.

원본 notepad.exe 의 압축을 모두 해제하였으므로, OEP 로 제어를 돌려줘야겠지요?
아래 그림이 바로 OEP 로 jump 하는 코드입니다.

 

<Fig. 8>

010154AD 주소의 POPAD 명령은 바로 UPX 코드 중 가장 첫 번째 명령인 PUSHAD 에 대응되는 명령으로써 이제 레지스터를 원래대로 복원시키는 명령입니다. (<Fig. 3> 참고)

최종적으로 010154BB 주소의 JMP 명령어로 OEP 로 갑니다.
JMP 하는 주소 0100739D 는 원본 notepad.exe 의 EP 주소입니다. (각자 확인해보세요.)

 

UPX 의 OEP 를 빨리 찾는 방법

위 설명대로 tracing 을 잘 하셨나요?
리버싱 초보자라면 꼭 한번 해보시기 바랍니다.
다른 packer 로 압축된 파일을 디버깅 할 때 큰 도움이 될 것입니다.

매번 위와 같은 방법(루프 탈출)으로 OEP 로 가는 것은 힘들겠지요?

실전 리버싱에서는 간단히 OEP 로 들어갑니다.
그 방법들을 설명 드리겠습니다. (UPX 기준입니다.)

#1. POPAD 명령어 이후의 JMP 명령어에 BP 설치

UPX 는 POPAD 명령어 이후에 나오는 JMP 명령어가 바로 OEP 로 가는 명령어 입니다.
이곳에 BP 를 설치하고 실행하면 바로 OEP 로 갈 수 있습니다.

#2. Hardware BP 설치

이 방법도 UPX 특징인 PUSHAD/POPAD 명령어의 특성을 이용하는 것입니다.

<Fig. 3> 에서 01015330 주소의 PUSHAD 명령을 실행한 후 스택을 보면 아래와 같습니다.

<Fig. 9>

EAX 에서 EDI 까지 스택에 차곡차곡 저장되어 있습니다.

OllyDbg의 Dump 윈도우에 저 스택 주소 0006FFA4 를 표시한 후 마우스 우측 메뉴를 이용하여 Hardware BP 를 설치합니다.

<Fig. 10>

Hardware BP 란 CPU 에서 지원하는 BP 이며 4개까지 설치 가능합니다.
일반적인 BP 와 다른 Hardware BP 의 특징은 BP 설치된 명령어가 실행 된 이후에 제어가 멈추게 됩니다.

이 상태에서 실행하게 되면 압축이 해제되고 POPAD 가 호출되는 순간에 Hardware BP 가 설치된 0006FFA4 주소를 access 하게 되고, 그때 제어가 멈춥니다. 바로 밑에 OEP 로 가는 JMP 명령어가 있지요.

+—+

이상으로 UPX 실행압축 파일의 디버깅 설명을 마치도록 하겠습니다.

초보자 분들께서는 위 설명대로 직접 디버깅 하여 루프를 하나하나 탈출해서 OEP 로 가보시길 권해드립니다.

여러분들의 디버깅 실력이 부쩍 향상됨을 느낄 수 있으실 겁니다.

 

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

  1. Tracked from www.reversecore.com 2009/06/20 02:06 삭제

    Subject: 실행 압축 (Run-Time Packer) 에 관하여

    실행 압축(Run-Time Packer)은 소프트웨어 역공학 (Reverse Code Engineering)의 단골 주제입니다. 이를 잘 이해하기 위해서는 PE 파일 포멧과 운영체제의 기본 적인 사항들(프로세스, 메모리, DLL, etc)에 대해 잘 알고 있어야 하며, 또한 압축/해제(Compress/Decompress) 알고리즘에 대한 기본 적인 내용에 대해서 알아야 합니다. 이미 많은 부분은 제 블로그에서 다뤘던 내용들입니다. 실행 압축에 대한..
  1. 1phee 2009/11/18 18:08 댓글주소 | 수정 | 삭제 | 댓글

    아.. 언팩은 하나도 몰랐는데.. 요새는 시간이 없어서 크랙미를 못풀지만 예전에는 언팩의 언자도 몰라서 크랙미 풀때마다 패킹안된거만 찾아다녔던 기억이 나네요..ㅎㅎ 어쨌든 이런식으로 UPX 언팩에 대해서 자세하게 설명하는 글은 처음 보네요! 언팩에 대해서 전혀 개념도 없다가 이글보고 대략 감 잡았습니다. UPX가 쉬운 패킹이라 그런지 이해가 잘 되네요. 잘 보았습니다. ^^

    • reversecore 2009/11/20 00:16 댓글주소 | 수정 | 삭제

      1phee님, 안녕하세요.

      Unpack 의 기본원리는 거의 비슷합니다.
      압축풀고 IAT 만들고 OEP 로 가는거죠.

      여기에 각종 옵션이 끼어들면서 복잡해지는 것입니다.

      다른 패커에도 도전해 보세요~

  2. 시간의흔적 2010/03/31 10:52 댓글주소 | 수정 | 삭제 | 댓글

    너무너무 글 잘 보고 갑니다.. 어렵지만 열심히 볼께요 ^^

    • reversecore 2010/04/01 10:11 댓글주소 | 수정 | 삭제

      시간의흔적님, 안녕하세요.

      네~ 공부하시다가 의문 사항 있으시면 질문 남겨주세요~ ^^

      감사합니다.

  3. 안정현 2010/06/23 04:19 댓글주소 | 수정 | 삭제 | 댓글

    지금 나이지리아 전 하는데 그냥 안보고 블로그보는중 ㅡ.,ㅡ;; ㅋㅋㅋ 지금현재는 제삶의 낙인듯 ㅋㅋㅋㅋㅋ 미친듯이해서 달려왔는데
    hook.exe 같이 소스코드 짜놓아서 주석달아 설명해주신것들있잖아여~
    c를 공부하긴했는데 소스파일 들이 주석들봐도 해석이안돼더라구요..
    해석을 할수있을려면 시스템프로그래밍을 공부따로해야하나요 ㅋ

    api를 몰라서그런가..
    암튼 잘읽고있습니당~
    중독성이강하네용 ㅋㅋ 한페이지만더읽고자야지 하다가 밤샐기세 ㄷㄷㄷㄷ ㅋㅋㅋ

    • reversecore 2010/06/23 21:21 댓글주소 | 수정 | 삭제

      월드컵보다 제 블로그를 선택해 주시다뇨... @@~
      리버싱 공부에 대한 열정과 각오가 남다르신 것 같습니다.

      C 언어 예제 소스가 잘 이해안가셨나요?

      사실 Win32 API 프로그래밍 경험이 없으시면 좀 어려울 수 있습니다. 그럴때 질문올려주세요~ ^^

      감사합니다.

  4. newbie 2010/07/15 19:08 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 :)
    게시글 잘 보고 있습니다.
    궁금증이 생겨서 질문드리고 싶은데
    언패킹부분중에 E8, E9 찾아서 주소값 수정해주는 부분이 있는데
    이건 왜 그렇게 해주는거죠? 그냥 통째로 원본 소스를 풀어서 UPX0부분에 넣었다면 고쳐줄 내용이 하나도 없지 않나요..또 EAX로 주소를 받아서 쉬프트연산등을 해주는거 같은데 그건 왜그런걸까요? 패킹할때 주소를 거꾸로 저장해놓은건지...


    그리고 원본 파일의 IAT부분은 압축할때 코드랑 같이 된거 아닌가요? 보니까 IAT부분을 기존의 위치가 아닌 UPX1 위치에다 만들어 주는거 같던데.. 그냥 기존 IAT부분에 채워주지않고 왜 새로 만들어주는건지 궁금하네요

    마지막으로 stolen byte가 왜 생기는지 알고 계시면 알려주세요 ㅜㅡ

    너무 두서없이 질문만했네요
    다시한번 좋은 블로그 만들어주셔서 감사합니다

    • reversecore 2010/07/15 23:38 댓글주소 | 수정 | 삭제

      안녕하세요.

      애석하게도 저 또한 UPX 에서 왜 E8, E9 를 보정해 주는지 이유를 잘 모르겠습니다. 모든 패커에서 나타나는 일반적인 특징이라고 볼 수 도 없고 말이죠. 복잡한 연산은 UPX 프로그래머가 그렇게 만든 것이구요. 4 byte 를 디코딩 해서 원래 값으로 복원시키는 명령입니다. 어떻게 연산하던 자유니까요.

      IAT 는 원본 IAT 영역에 정상적으로 풀어주고 있습니다. 새롭게 GetProcAddress() 를 호출해서 만드는 이유는 압축된 PC 의 kernel32.dll 주소와 실행되는 PC 의 kernel32.dll 의 주소가 다를 수 있기 때문입니다.

      stolen byte 는 아마 protector 에서 사용되는 그 stolen byte 말씀이시죠?

      쉽게 덤프 뜨지 못하도록 그런겁니다.
      그 상태로 덤프 떠버리면 stolen byte 때문에 정상 실행이 안됩니다. packer 로 정상 실행 된 경우만 stolen byte 에 해당되는 (혹은 유사한) 코드를 실행하여 stack, register 를 맞춰준 후 그 이후 코드를 실행하기 때문에 잘 실행되는 것이죠.

      괜히 설명이 복잡한데요... protector 류를 한번 분석해 보시면 금방 파악하실듯... ^^

      * 굉장히 깊이 있게 분석을 하셨나 봅니다. ^^ 자주 들러 주시고요... E8/E9 의 이유를 알게 되시면 저에게도 공유해 주세요~

      감사합니다.

  5. newbie 2010/07/18 20:04 댓글주소 | 수정 | 삭제 | 댓글

    친절한 답변 감사드립니다 :)
    시간이 많이 걸리겠지만 포스팅하신거 다 보고 싶네요
    또 궁금한거 있으면 여쭤볼께요
    좋은시간보내세요~

  6. newbie 2010/07/19 19:19 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요 또 이렇게 질문드리게되네요
    앞으로 질문으로 도배를 할것 같은 느낌이...
    다름이아니고 언패킹과정에서 덤프를뜨고 IAT를 다시설정해주지않으면 초기화에러가나는데
    도대체 이게왜 생기는걸까요 ?
    사실 OEP만 찾으면 패커에 의해 IAT는 복구가 된 상태고 실행하는데 문제가 없잖아요
    (PE헤더에는 제대로 바껴있지 않지만..)
    로더가 어떤 원리로 PE헤더에 있는 IAT가 패커가 사용한 다른 IAT인지 알고 에러를 내는걸까요?
    코드하나하나보면서 API가 나올때마다 IAT랑 대조해볼것 같지는 않은데..
    어차피 그냥 적힌대로 잘못된 IAT지만 그걸 채우고 실행만 하면 정상 실행이 될것 같은데..
    많은 자료를 찾아봤지만 단지 IAT가 복구되지 않아서 에러를 낸다는 설명들만 있는데 왜 그런지에
    대한 설명은 없네요 혹시 알고 계시면 알려주세요
    감사합니다!

    • reversecore 2010/07/19 23:12 댓글주소 | 수정 | 삭제

      예를 들어 UPX 파일을 실행시킨 후 덤프하면 IAT 에는 정확한 API 주소가 들어있지요. 하지만 INT 가 깨져있습니다.

      PE 로더는 INT 에서 API 이름 문자열을 얻어서 LoadLibrary()/GetProcAddress() 조합을 사용하여 실제 API 주소를 얻어서 IAT 에 적어주는데요. 이 과정에서 실행 에러가 발생하는 것입니다.

      간혹 어떤 패커들은 IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk (INT) 를 0 으로 세팅하기도 합니다.

      그래도 잘 실행되는 이유는 IMAGE_IMPORT_DESCRIPTOR.FirstThunk (IAT) 를 따라가면 이름 문자열이 나오고 이를 바탕으로 함수 주소를 구할 수 있기 때문이지요.

      근데 UPX 덤프 파일은 OriginalFirstThunk, FirstThunk 어느것을 따라가도 이름 문자열이 나오지 않습니다. FirstThunk 에 가보면 실제 함수 주소들만 있지요. 그건 PE 로더가 원하는게 아니거든요.

      그래서 실행 에러가 발생하는 것입니다.

      잘 이해안가시면 다시 질문 올려주세요~

      감사합니다.

  7. newbie 2010/07/20 18:14 댓글주소 | 수정 | 삭제 | 댓글

    답변 정말 감사합니다
    궁금증이 해결되었습니다 :)
    디버거로만 패킹된 파일과 덤프된 파일의 PE헤더를 확인하다보니까 두개가 같아서 착각을 했네요..
    패킹된 파일은 이미 로더가 처리를 해서 그렇게 보였던 것인데...

    리버싱관련 공부하면 궁금증이 생겨도 친절이 알려주는 분이 없어서 답답했는데 정말 근사한 오아시스를 발견했네요 :) 좋은 시간 보내시길 바랍니다~

  8. vio 2011/03/25 16:32 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요
    올려주신 글 덕분에 정말 하루하루 리버싱실력이 느는것 같습니다. 감사드립니다
    질문이 한가지 있는데요
    리버싱을 하다보면 모르는 어셈명령어들때문에 뭔지 모를때가 많습니다
    이번 upx 파일 트레이싱을 하는데도 처음보는 명령어들이 너무많습니다.
    어셈블리 명령어 잘 정리된 사이트를 아시나요?
    인텔 매뉴얼?을 언듯 어디서 들은거같은데 못찾겠더군요
    모르는 명령어를 찾아볼 수 있는 사이트를 알려주시면 감사하겠습니다

  9. 2011/05/04 08:40 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      그런 파일도 리버싱의 원리가 다르지 않습니다.
      그냥 EP 부터 하시면 되구요.

      대부분의 프로텍터는 스레드가 많이 실행됩니다.
      물론 디코딩 스레드가 대부분 이지요.

      감사합니다.

  10. 2011/05/04 08:41 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

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

      안녕하세요.

      음, 질문 하시는 내용이 본문 내용과 동떨어진 내용들이군요?

      질문이 잘 이해되지 않습니다. ^^~

      감사합니다.

  11. 아장아장 2011/05/12 23:34 댓글주소 | 수정 | 삭제 | 댓글

    이미 알고있었다고 생각했었는데 다시 보니 새로게 느껴져요.
    째인 설명 인상에 남았습니다.

    한가지 의문은 c++에서 클라스함수호출시 어떤 호출규약을 쓰고 이때 탄창을 어떻게 리용하는지 상세히 알고싶은데요...
    시간나는대로 이 내용을 주재로 하는 강좌 내와주시든가 댓글 보내주시면 고맙겠습니다.

    열심히 강좌 찾겠습니다.

    • reversecore 2011/05/13 20:10 댓글주소 | 수정 | 삭제

      안녕하세요.

      C++ 에서는 C 와 마찬가지로 cdecl 방식을 사용합니다.

      탄창이요? 스택(stack)을 말씀하시는 거죠?
      cdecl 에서는 Caller 가 stack 을 정리합니다.

      향후 C++ 클래스 멤버 함수 호출 관련한 내용을 블로그에 올릴 예정입니다. ^^

      감사합니다.

  12. 2011/06/29 18:54 댓글주소 | 수정 | 삭제 | 댓글

    안녕하세요! 여쭤볼게 있습니다 ㅠ.ㅠ
    선생님 말씀대로 루프들 다 빠져나와서 OEP를 찾았는데요
    찾고나서 dump를 뜨면 실행압축 해제한 실행파일이 생성되는건가요?
    LordPE라는 tool을 써야 된다고 하는 사람들도 있던데...
    해제된 실행파일은 어떻게 얻을수 있을까요??
    그리고... 디버깅을 안하고 notepad_upx.exe 를 olldbg로 돌렸을때
    밑으로 쭉~ 내려서 000000많은 부분에서 바로 위에있는 JMP 쪽이
    바로OEP라고 생각해도 되는건가요~?

  13. TeamKhan 2011/09/16 19:59 댓글주소 | 수정 | 삭제 | 댓글

    어떻게 ESI EDI 가 가르키는 주소가 몇번째 섹션에 해당하는 주소인지 어떻게 파악하는지 모르겟습니다.
    그리고 어떤 어셈블리어 코드가 IAT 를 복구해주는 부분의 코드라는걸
    알수있는지 또 이 부분이 어떻게 디코등 루프부분의 코드라는걸 알수있으셧는지
    참으로 궁금합니다.

    • reversecore 2011/09/19 20:10 댓글주소 | 수정 | 삭제

      안녕하세요.

      메모리 복사 명령에서 ESI 는 Source를, EDI 는 Destination 을 각각 의미합니다. 따라서 ESI/EDI 가 가리키는 주소를 본 후 PEView 로 파일을 확인하면 어느 섹션을 가리키는지 알 수 있지요. OllyDbg 의 "Memory Map" 을 확인해도 어느 섹션인지 알 수 있습니다.)

      IAT 복구 하는 것은 GetProcAddress() 호출하면서 API 함수 주소를 반복적으로 얻는 것을 보고 알 수 있었습니다.

      그리고 저는 몇 번의 Unpack 경험이 있기 때문에 처음 하시는 분들보다는 좀 더 수월하게 (예측해 가면서) 진행할 수 있었던 것 같습니다.

      답변이 되었는지요?

      감사합니다.

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

    010073B2 에서 MZ 를 비교하신다고 했는데 010073B4 를 잘못 적으신게 아닌가 합니다.