Programming/Win32 API2008. 12. 1. 15:33

다음은 데브피아의 성상훈님의 강좌 입니다.


Win32 Global API Hook - 제1강 Win32 API 후킹의 기본
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(강의의 진행상 존칭을 생략합니다.)


자신이 만약 어느정도의 레벨을 가진 윈도우즈 프로그래머라면 이런 생각을 한번쯤 해보았을 것이다.

"만약 Windows API를 후킹할 수 있다면 재미있는 것을 많이 해볼수 있을텐데..."

그리고 의욕과 시간이 있었다면 아마 도전해본 사람도 꽤 있었을것이다.

그러나 실제로 이것을 성공한 사람은 그리 많지 않았을것으로 안다. 여러분이 만약 어플리케이션 레벨에서만 프로그래밍했다면 이것 은 불가능해 보였을지도 모른다. 시스템 레벨 프로그래머라면 이것이 제법 까다롭고 다루기 힘든 주제라는것을 알았을것이다.

자 여기서 필자는 많은 사람들이 궁금해하는 이 비밀스런 작업을 하나하나 풀어가려고 한다. 그리
고 의외로 간단한 곳에 해답이 있었음을 이 강좌가 끝날때쯤 알게될 것이다.

자, 서론을 접고 본론으로 들어가자. 우리가 하려는 일은 다음과 같다.

Win32 API를 후킹해서 내가 원하는 작업을 수행하거나, 작업을 흐름을 원하는대로 제어할 수 있다.

단 지 이것이다. 무슨 설명이 더 필요한가? 실제로 우리가 하 려는 것은 이것이 전부이다. 예를 들어서 쉽게 말하자면 CreateProcess() 라는 API를 후킹하면 내가 원하지않는 프로그램의 실행을 막을수도 있고, 윈속함수인 send()나 recv()를 후킹하면 나가고 들어오는 패킷을 훔쳐보거나 조작할 수 있다. 느낌이 팍 오지 않는가? 느낌이 오지않는 사람은 아마 도 아직 이 강좌를 들을만한 수준이 아니거나 해커(순수한 의미의)의 기질이 없는 사람일 수도 있다.

일단 오늘은 첫날이니 그동안 많은 사람들이 제시했던 Windows API 후킹방법에 대해서 먼저 얘기해보자.

1. exe 파일헤더의 import descriptor table을 변경하는 방법

가 장 쉬운 방법이 되겠다. 물론 쉬운만큼 문제점이 적지 않 다. 일단 Win32 응용프로그램은 PE (PortableExecutable)이라는 형식으로 바이너리화 되어있다. 실행가능한 바이너리 파일구조에는 POSIX, COFF, PE 형식등등이 있는데, 윈도는 PE형식을 사용한다. 따라서 PE형식을 알면 윈도실행파일구조를 분석할 수 있겠다. 실제 윈도실행 파일은 크게 헤더, 리소스, 임포트테이블, 익스포트테이블, 데이터, 코드 등등의 영역으로 나누어지는데 여기서 임포트테이블이 이름 그대로 임포트된 라이브러리의 함수에 관한 정보가 담겨졌있다. 따라서 정적으로 링크된 모든 함수는 이곳에서 볼수 있게된다. 그 러면 이부분의 임포트된 함수의 주소들을 내가 만든 후킹함수주소로 바꿔치기 해주면 간단할 것이다. 아래에 리스트된 코드를 보자.

// 포인터 변환 및 연산 매크로
#ifndef MakePtr
#define MakePtr(cast, ptr, addValue) (cast)((DWORD)(ptr)+(DWORD)(addValue))
#endif // MakePtr

PIMAGE_IMPORT_DESCRIPTOR GetImportDescriptor(HMODULE hMod,        // 모듈
                                       
     LPCTSTR pszModName)// 모듈이름
{
    TRACE("[FIND IMPORT DESCRIPTOR] \n");

    // 매개변수 유효성 검사
    ASSERT(!IsBadReadPtr(hMod, sizeof(IMAGE_DOS_HEADER)));
    ASSERT(NULL != pszModName);

    // DOS 헤더
    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)hMod;
    if(IMAGE_DOS_SIGNATURE/*0x5A4D*/ == pDosHdr->e_magic)
    {
        // NT 헤더
        PIMAGE_NT_HEADERS pNtHdr = MakePtr(PIMAGE_NT_HEADERS,
            pDosHdr, pDosHdr->e_lfanew);
        if(!IsBadReadPtr(pNtHdr, sizeof(IMAGE_NT_HEADERS))
            && IMAGE_NT_SIGNATURE/*0x00004550*/ == pNtHdr->Signature)
        {
            // image descriptor
            PIMAGE_IMPORT_DESCRIPTOR pImpDesc = 
                MakePtr(PIMAGE_IMPORT_DESCRIPTOR,
                    pDosHdr,
                    pNtHdr->OptionalHeader.
                    DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].
                    VirtualAddress);
            if(NULL != pImpDesc)
            {
                while(NULL != pImpDesc->Name)
                {
                    PSTR pszName = MakePtr(PSTR, pDosHdr, (DWORD)pImpDesc-
>Name);
                    TRACE(" %s ", pszName);

                    if(stricmp(pszName, pszModName) == 0)
                    {
                        // 찾았다 !!
                        TRACE(": Found It !! \n");

                        return pImpDesc;
                    }

                    TRACE("\n");
                    pImpDesc++;
                }
            }
        }
    }

    return NULL;
}

PROC WINAPI HookImportFunction(HMODULE hMod,// Hooking 할 모듈
                     PSTR pszModName,        // Hooking 함수가 위치한 모듈이름
                     PSTR pszFuncName,        // Hooking 할 함수
                     PROC pfnNewProc)        // Hooking 함수
{
    // 매개변수 유효성 검사
    ASSERT(!IsBadReadPtr(hMod, sizeof(IMAGE_DOS_HEADER)));
    ASSERT(NULL != pszModName);
    ASSERT(NULL != pszFuncName);
    ASSERT(NULL != pfnNewProc);
    ASSERT(!IsBadCodePtr(pfnNewProc));

    // 반환값 (원래 함수)
    PROC pfnOrgProc = NULL;

    // Win9x이고 2GB 이상의 시스템 DLL 이라면
    // 조용히 사라져야 한다.
    if(1 != __GetOsType() && 0x80000000 < (DWORD)hMod)
    {
        return NULL;
    }

    // import descriptor 를 얻는다.
    PIMAGE_IMPORT_DESCRIPTOR pImpDesc = GetImportDescriptor(hMod, pszModName);
    if(NULL == pImpDesc)
        return NULL;

    // 원본 thunk
    PIMAGE_THUNK_DATA pOrgThunk = MakePtr(PIMAGE_THUNK_DATA,
        hMod, pImpDesc->OriginalFirstThunk);

    // 실제 thunk (for Hooking)
    PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA,
        hMod, pImpDesc->FirstThunk);

    // 루프를 돌면서 Hooking 할 함수를 찾는다.
    TRACE("[FIND IMPORT FUNCTION] : %s \n", pszModName);
    while(NULL != pOrgThunk->u1.Function)
    {
        // 이름으로 import된 함수만 검색
        if(IMAGE_ORDINAL_FLAG !=
            (pOrgThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG))
        {
            // import된 함수 이름
            PIMAGE_IMPORT_BY_NAME pByName =
                MakePtr(PIMAGE_IMPORT_BY_NAME,
                hMod, pOrgThunk->u1.AddressOfData);

            // 이름이 NULL로 시작되면 넘어간다.
            if(0 == pByName->Name[0])
                continue;

            TRACE(" %s ", (PSTR)pByName->Name);

            if(pszFuncName[0] == pByName->Name[0]
                && 0 == stricmp(pszFuncName, (PSTR)pByName->Name))
            {
                // 찾았다 !!
                TRACE(": Found It !! \n");

                MEMORY_BASIC_INFORMATION mbi;
                VirtualQuery(pRealThunk, &mbi, sizeof(mbi));

                // 가상 메모리의 보호속성 변경
                if(!VirtualProtect(mbi.BaseAddress, mbi.RegionSize,
                    PAGE_READWRITE, &mbi.Protect))
                {
                    // VirtualProtect() fail !!
                    ASSERT(0);
                    return NULL;
                }

                // 원래 함수(반환값) 저장
                pfnOrgProc = (PROC)pRealThunk->u1.Function;

                // 새로운 함수로 덮어쓴다.
                TRACE("** old function : 0x%08X \n", (DWORD)pRealThunk-
>u1.Function);
                pRealThunk->u1.Function =
                    (DWORD)pfnNewProc;
                TRACE("** new function : 0x%08X \n", (DWORD)pRealThunk-
>u1.Function);

                // 가상 메모리의 보호속성 원래대로 되돌림
                DWORD dwTmp;
                VERIFY(VirtualProtect(mbi.BaseAddress, mbi.RegionSize,
                    mbi.Protect, &dwTmp));

                // 세상을 다 가져라 !!
                TRACE("** Hook OK !! What a wonderful world !! \n");
                return pfnOrgProc;
            }

            TRACE("\n");
        }

        pOrgThunk++;
        pRealThunk++;
    }

    return NULL;
}

GetImportDescriptor()함수는 모듈의 임포트디스크립터를 얻어내는 함수이고, HookImportFunction()함수는
실제로 임포트된 함수를 후크하는 함수이다. 이 두 함수를 이용해서 MessageBox API를 후킹해보자.

// MessageBox API 원형
typedef int (WINAPI *PROC_MESSAGEBOX)(HWND, PSTR, PSTR, UINT);

PROC_MESSAGEBOX pfnOrgMessageBox = NULL;

// MessageBox를 대체할 훅함수
int WINAPI MyMessageBoxA(HWND hWnd, PSTR pszText, PSTR pszTitle, UINT uType)
{
    ASSERT(NULL != pfnOrgMessageBox);

    return pfnOrgMessageBox(NULL, "Hooked MessageBox !!", "Hooked !!", MB_ICONINFORMATION);
}

// Main 프로세스
int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
     // TODO: Place code here.

    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);

    // hook !!
    pfnOrgMessageBox = (PROC_MESSAGEBOX)HookImportFunction(
        GetModuleHandle(NULL), "user32.dll", "MessageBoxA", (PROC)MyMessageBoxA);
    if(NULL == pfnOrgMessageBox)
        return 0;

    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);

    // unhook !!
    pfnOrgMessageBox = (PROC_MESSAGEBOX)HookImportFunction(
        GetModuleHandle(NULL), "user32.dll", "MessageBoxA", (PROC)pfnOrgMessageBox);
    if(NULL == pfnOrgMessageBox)
        return 0;
    TRACE("replace orginal function \n");

    MessageBox(NULL, "Default MessageBox", "Windows 98", MB_ICONINFORMATION);

    return 0;
}

MyMessageBoxA 라는 함수를 보면 타이틀에 Hooked !! 라는 캡션을 가진 Hooked MessageBox !! 메시지를 출력하는 메시지 박스 를 보여주는 함수이다. 후킹이 성공적으로 이루어지면 MessageBox는 무조건 위와같은 형태로 보여지게 될것이다. MessageBoxA의 A 문자는 ANSI 문자열 버전으로서 Windows 9x 계열에서 주로 사용되며 1바이트 문자를 사용한 다. 반대로 W가 붙은 API는 Wide Charactor로서 2바 이트 문자를 사용하는 유니코드 사용 API이다. 실제로 Win9x 계열에서는 생략할경우 A문자 버전의 API가 호출되도록 재지정되어 있으며, NT4/W2K 계열에서는 내부적으로는 W문 자 버전의 API가 호출된다. 문자열을 인자로 사용하는 대부분의 API가 이와같이 두가지 버전으로 나뉘어지며 만약 훅함수를 설치한 다면 정확한 버전을 사용해야 할것이다.

실행시켜보면 실제로 중간의 MessageBox(NULL, "Default MessageBox", "Windows 98",
MB_ICONINFORMATION); 함수는 우리가 지정한 후킹함수로 대체되어 Hooked ... 라는 메시지 박스가 출력될것이다.

자, 원리를 알고 실행이 제대로 되는것을 확인했다면 문제점에 대해서 알아보자.
위 의 방법으로는 현재 내 프로그램만이 후킹될수 있다. 이유인즉슨 임포트 디스크립터 테이블이라는것이 프로그램마다 가지고 있는것이기 때 문에 당연히 내 프로그램만이 적용되어진다. 그렇다면 다른 프로그램들 도 모조리 임포트 디스크립터 테이블을 변경하면 될것이 아닌가? 라고 생각하는 사람이 있을지 모르겠지만, 그건 불가능하다. 왜냐하면 Win32 응용프로그램들은 각각 독립적인 주소공간에서 실행되기 때문에 기본적으로 서로 다른 프로그램의 영역을 침범할수 없다. 그러면 어떻게 다른 응용프로그램(정확히 말하면 프로세스)의 주소공 간 을 볼수있을까? 이 문제에 관해서는 다음 강좌에서 다루어 보도록 하자.

끝으로...
오 늘은 Win32 API 후 킹에 대한 맛보기였다. 사실 오늘 제시한 방법으로는 우리가 생각하는 것들을 하기에는 턱없이 모자란다. 그렇지만 모든것은 순서가 있 듯이 가장 기본적인 방법론부터 제시해보았다. 사실 오늘 강좌에도 따라오는 부수적인 내용은 상당히 방대하다. 일단 Windows 실 행파일구조인 PE구조에 대한 이해와 Windows 프로세스간 메모리 관리 또한 매우 중요한 내용이다. 지면상(또는 시간상 ^^) 여기서 다 다룰수는 없지만 참고할만한 서적을 소개하면 PE구조에 관해서는 MSJ나 마이크로소프트웨어 잡지를 검색하면 찾을 수 있을 것이다. 실제로 PEDUMP 같은 유틸리티를 작성해본다면 더없이 좋 을것이다. Win32 메모리관리에 관해서는 Jeffry Richter의 Advanced Windows 라는 책을 추천한다. 좀 오래된 책인데 필자가 공부할때는 가장 훌륭 했 다고 여겨지는 책이다. 아마 다른 시스템 프로그래밍을 다루는 책에서도 자료를 얻을 수 있을것이다. 또한 위 소스코드의 모체가된 John Robbins의 Debugging Applications라는 책을 참고하는 것도 좋을 것이다.

노 파심에서 사족을 달자면, 혹시 C/C++와 Windows 시스템과 메카니즘에 익숙하지 않다면 과감히 강좌를 보는것을 포기하길 권유한다. 적 어도 위의 소스코드정도는 쉽게 이해할 수 있어야 할것이다. 또한 필자의 사정상 빨라야 일주일에 두번정도 강좌를 올릴 수 있을것 같 다. 그래서 전체적인 커리를 다음과 같이 제시하니 필요한 부분은 미리미리 공부한다면 빨리 따라올 수 있을것이다.

1. Win32 API 후킹의 기본
2. 다른 프로세스의 주소공간으로 들어가자 !!
3. Win32 어셈블리 프로그래밍
4. Win9x 디바이스 드라이버(VxD) 모델
5. 기계어 프로그래밍 - Shell Code 작성
6. Win9x Global API Hooking
7. WinNT/2000 디바이스 드라이버 모델
8. WinNT/2000 Global API Hooking

앞 으로 전개될 강좌의 주요 테마이다. 하나같이 만만하지 않은 주제들로 이루어져있으며 한두회의 강좌로는 턱없이 부족한 주제도 대부분일 것이다. 그러나 차근차근 따라오다보면 어느새 자신의 실력이 부쩍 향상되어 있음을 피부로 느낄것이다. 이것은 필자가 이름을 걸고 맹세할 수 있다. 실제로 디바이스드라이버 프로그래밍과 어셈블리 프로 그래밍에 대한 경험이 있거나, 얼마전까지 유행하던 해킹기법인 Stack Overflow의 익스플로잇과 쉘코드를 직접 제작할 수 있는 능력이 있다면 강좌를 따라오기가 한결 수월할 것이 다.

P.S : 본 강좌는 얼마든지 다른 사이트로 복사되어질 수 있지만, 상업적인 용도로 사용되어질 경우, 원저자의 동의를 얻어야함을 명시합니다. 복사를 하실때는 예의상 원저자의 이름정도는 밝혀주시기 바랍니다.

Posted by skensita