Global API Hooking 에 대한 기본 개념을 정리합니다. 또한 ntdll!ZwResumeThread() API 의 후킹을 통한 Global API Hooking 기법의 원리를 살펴봅니다.



<Global API Hooking>


본 내용을 읽기 전에 이전 포스트를 참고하세요. 같이 이어지는 내용입니다.


* 참고!
모든 소스 코드는 MS Visual C++ 2008 Express Edition 으로 개발 되었으며, Windows 7 32bit & IE 8 에서 테스트 되었습니다.



Global API Hooking 


Global API Hooking 에 대해서 다시 한번 간단히 정리하고 넘어가겠습니다.

지금까지의 강좌를 통해서 우리는 특정 프로세스에 대해 원하는 API 의 후킹을 간단하게 구현할 수 있게 되었습니다.

* 참고 


#1. 일반적인 API Hooking

일반적인 API 후킹의 문제는 후킹을 원하는 프로세스가 생성될 때마다 매번 API 후킹을 해줘야 한다는 것입니다. 아래 그림은 DLL Injection 기법을 이용한 일반적인 API Hooking 을 표현한 것입니다.


<Fig. 1>

위 그림에서 후킹 대상 프로세스는 Test.exe(PID:2492) 입니다. InjDll.exe 프로그램을 이용해서 Hook.dll 을 Test.exe 프로세스에 인젝션 시켜서 원하는 API 를 후킹 하였습니다. (1)

그런데 이후에 또 다른 Test.exe(PID:3796) 프로세스가 생성되었다면 이 프로세스에도 역시 Hook.dll 을 인젝션 시켜줘야 (PID:3796 프로세스에 대한) 정상적인 API 후킹이 이루어 질 것입니다. (2)

즉, 후킹 대상 프로세스가 새로 생성 될 때마다 계속해서 수동으로 API 후킹을 시켜야 합니다.


#2. Global API Hooking

이번에는 Global API Hooking 을 살펴보도록 하겠습니다.


<Fig. 2>

InjDll.exe 는 Windows 운영체제의 기본 쉘(Shell) 인 Explorer.exe 프로세스에 gHook.dll 을 인젝션 시킵니다. (후킹하고자 하는 Test.exe 가 아닌, Test.exe 를 실행시켜주는 프로세스인 Explorer.exe 를 후킹한다는 것이 핵심입니다.)

gHook.dll 은 <Fig. 1> 의 Hook.dll 의 기능에다가 자식 프로세스 생성에 관련된 API 를 후킹하여 자식 프로세스를 생성할 때마다 자신(gHook.dll)을 인젝션 시키는 기능을 가지고 있습니다. (위 <Fig. 2> 참조)

따라서 Windows 쉘인 Explorer.exe 프로세스에 gHook.dll 을 한번 인젝션 시켜놓으면 그 이후 Explorer.exe 에서 생성되는 모든 자식 프로세스들에게 자동으로 gHook.dll 이 인젝션 됩니다.

이것이 자동 API Hooking 의 기본 개념이며, 이를 시스템에 실행중인 모든 프로세스를 대상으로 확장한 것이 바로 Global API Hooking 입니다.
 
* 참고!
Explorer.exe 외에 다른 프로세스들도 자식 프로세스를 생성할 수 있습니다. 따라서 원칙적으로 Global API Hooking 을 완벽히 구현하려면 현재 실행중인 모든 프로세스들을 후킹 해야 합니다. 
하지만 시스템 안정성과 불필요한 오버헤드를 막기 위해 작업 내용에 따라서 특정 프로세스만 후킹 하는 경우도 있습니다. (제가 실습 예제로 준비한 IE 후킹이 대표적인 경우입니다.)

이상으로 Global API Hooking 개념에 대해서 정리해 보았습니다.

이제부터 어떤 API 를 후킹해야 Global API Hooking 을 쉽게 구현할 수 있는지 알아보도록 하겠습니다.



ntdll!ZeResumeThread() API


자식 프로세스를 생성하는 API 에 대해서 생각해 보도록 하겠습니다.
프로세스를 생성하는 API 는 단연 kernel32!CreateProcess() API 가 대표적입니다.

* 참고 – kernel32!CreateProcess() API 를 이용한 Global API Hooking 방법

CreateProcess() API 의 디버깅을 위하여 다음과 같이 간단한 프로그램을 만들어 보겠습니다.

// cptest.cpp

#include "windows.h"
#include "tchar.h"

void main()
{
    STARTUPINFO             si = {0,};
    PROCESS_INFORMATION     pi = {0,};
    TCHAR                   szCmd[MAX_PATH] = {0,};

    si.cb = sizeof(STARTUPINFO);
    _tcscpy(szCmd, L"notepad.exe");

    if( !CreateProcess(NULL,                      // lpApplicationName
                       szCmd,                     // lpCommandLine
                       NULL,                      // lpProcessAttributes
                       NULL,                      // lpThreadAttributes
                       FALSE,                     // bInheritHandles
                       NORMAL_PRIORITY_CLASS,     // dwCreationFlags
                       NULL,                      // lpEnvironment
                       NULL,                      // lpCurrentDirectory
                       &si,                       // lpStartupInfo
                       &pi) )                     // lpProcessInformation
        return;

    if( pi.hProcess != NULL )
        CloseHandle(pi.hProcess);
}


위 코드로 빌드 시킨 프로그램입니다.

cptest.exe 를 디버깅 해보면 프로세스 생성과 관련된 API 들의 호출 흐름을 알 수 있습니다.
아래 그림은 cptest.exe 의 kernel32!CreateProcessW() 호출 코드입니다.

* 참고 : CreateProcessW 는 CreateProcess 의 Wide Character (유니코드)버전입니다.


<Fig. 3>

kernel32!CreateProcessW() 내부로 따라 들어가면 아래 그림과 같이 kernel32!CreateProcessInternalW() 호출 코드를 볼 수 있습니다.


<Fig. 4>

위 그림에서 아래쪽의 스택 메모리를 보시면 <Fig. 3> 의 스택(함수 파라미터)이 거의 동일하게 넘어온걸 알 수 있습니다.

kernel32!CreateProcessInternalW() 내부로 들어가 보겠습니다.
 

<Fig. 5>

kernel32!CreateProcessInternalW() 는 상당히 큰 함수입니다. 아래로 쭉 스크롤을 내리면 아래 그림과 같이 ntdll!ZwCreateProcess() 를 호출하는 코드가 나타납니다.


<Fig. 6>

위 그림에서 아래쪽의 스택을 보시면 <Fig. 4> 에서의 스택과는 많이 다른 형태인 것을 알 수 있습니다. 2번째 파라미터(Arg2)는 어떤 구조체 인데 왼쪽의 Hex dump 창을 보시면 구조체 멤버 중에 12F950 주소의 12FD38(“notepad”) 문자열 주소를 확인 하실 수 있습니다. (<Fig. 3> 스택 참조)

Ntdll!ZwCreateUserProcess() 가 호출되면 자식 프로세스가 SUSPEND 모드로 실행됩니다. 


<Fig. 7>

notepad.exe 프로세스는 생성되었지만 아직 EP 코드가 실행되지 않은 상태입니다.
<Fig. 6> 코드에서 계속 아래로 진행하면 ntdll!ZwResumeThread() API 호출 코드가 나타납니다.


<Fig. 8>

ntdll!ZwResumeThread() 는 함수 이름 그대로 스레드를 RESUME 시켜줍니다. 이 스레드가 바로 자식 프로세스(notepad.exe) 의 메인 스레드입니다. 따라서 이 API 가 호출되면 비로소 자식 프로세스의 EP 코드가 실행됩니다.


<Fig. 9>

지금까지의 API 호출 흐름을 정리하면 아래와 같습니다. 

kernel32!CreateProcessW
    kernel32!CreateProcessInternalW
        ntdll!ZwCreateUserProcess      // 프로세스 생성됨 (메인 스레드는 suspend 상태)
        ntdll!ZwResumeThread        // 메인 스레드 resume 시킴 (프로세스 실행됨)

자식 프로세스 생성에 있어서 가장 마지막에 호출되는 API 가 바로 ntdll!ZwResumeThread() 입니다. 따라서 우리는 이 API 를 후킹함으로써 자식 프로세스의 EP 코드가 실행되기 직전에 제어를 가로챈 후 원하는 API 를 후킹시킬 수 있습니다.

ntdll!ZwResumeThread() 는 undocumented API 이며, 함수 정의는 아래와 같습니다.

NTSTATUS NtResumeThread(
  IN    HANDLE ThreadHandle,
  OUT     PULONG SuspendCount OPTIONAL 
);

* 출처

* 참고
User Mode 에서 ntdll!ZwResumeThread() API ntdll!NtResumeThread() API 는 동일합니다.


위에서 소개한 4 개의 API (CreateProcessW, CreateProcessInternalW, ZwCreateUserProcess, ZwResumeThread) 중에서 어떤걸 후킹해도 Global API Hooking 이 가능합니다. 

다만 상위에 위치한 CreateProcessW() 함수를 후킹하면 특정 경우(CreateProcessInternalW 를 직접 호출하는 경우)에 후킹이 되지 않을 수 있습니다. 따라서 CreateProcessInternalW() 이하를 후킹하는 것이 더 좋은 방법입니다. (각자 장단점이 있을 수 있으므로 모두 연습해 두시는 것이 좋을 것 같습니다.)
 
다음 시간에는 Global 후킹 실습을 한 후 실제 코드를 살펴보도록 하겠습니다.


ReverseCore


위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

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

  1. 철이 2010/04/26 03:50 댓글주소 | 수정 | 삭제 | 댓글

    항상감사합니다. 하루 빨리 커널 모드 강의 듣고 싶습니다. ;;

    빨리 고~!

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

      안녕하세요~ 철이님~ ^^
      철이님 께서는 커널 관련 내용에 목말라 하시는것 같아요~
      유저 모드는 이미 마스터 하셨다는 말씀이신거죠? ^^
      즐거운 하루 되세요~

  2. AhnMaru 2010/04/27 06:59 댓글주소 | 수정 | 삭제 | 댓글

    구글링하다가 이런 좋은 사이트를 발견한 것 행운인 것 같습니다.
    질문이 좀 막연한데요. ZwResumeThread()가 실행하는 스레드를 OllyDBG로 디버깅해 보는 방법이 있을 까요?

    제가 악성코드를 분석하는데 이 녀석이 notepad.exe에 자신의 코드를 주입시킨 후에 ZwReSumeThread()를 사용해서 주입된 코드를 실행해 버리네요.

    한수 가르침부탁드립니다. (_ _)

    • reversecore 2010/04/27 23:43 댓글주소 | 수정 | 삭제

      안녕하세요.
      악성코드 분석업무를 하시는군요. ^^

      코드 인젝션 후 ZwResumeThread() 를 이용해서 실행한다는 말씀이시죠?

      어차피 코드 인젝션 후 리모트 스레드를 실행시키는 방법은 CreateRemoteThread() 를 호출하는 것입니다. 이 API 내부적으로 ZwCreateThreadEx 와 ZwResumeThread 가 호출 되는 것이지요.

      직접 ZwCreateThreadEx 와 ZwResumeThread 를 호출해도 되겠지요. 프로그래밍을 정교하게 해준다면 말이죠.

      인젝션된 코드를 알고 계실테니 인젝션 시키기 전에 시작코드 첫바이트를 CC (INT 3) 로 변경합니다. 그후 코드가 인젝션 된후 CreateRemoteThread (혹은 ZwResumeThread) 를 호출하면 바로 notepad 프로세스에서 run time 에러가 발생합니다. (INT 3)

      만약 just in time debugger 로 OllyDbg 를 등록하셨다면 OllyDbg 가 자동으로 실행되면서 notepad.exe 를 바로 디버깅 하실 수 있으실 겁니다. 이때 CC 를 원래 바이트로 변경하시는거 잊지 마시구요.

      이 방법이 제일 간단한것 같네요.

      아니면... 디버거에 익숙하시다면...
      해당 코드에 바로 "New origin here" 명령으로 (인젝션 시키지 않고) 디버깅 할 수도 있겠지요. 물론 적절히 스레드 파라미터는 맞춰 놓으셔야 하구요. 다양한 방법이 가능할 겁니다.

      악성코드 분석업무를 하신다니 좀 편하게 설명드려봤네요~ ^^

      잘 안되시면 다시 질문 올려주세요~~~

      감사합니다.

  3. Ezbeat 2010/04/28 01:03 댓글주소 | 수정 | 삭제 | 댓글

    이 전글들을 꾸준히 보고 응용도 해본 덕인지.. 글만 봐도 쉽게 이해가 되네요 ^^ 다음 글도 기대할께요~!

    아~ 그런데 문서 쓰실때 그림같은 것은 어떠한 툴을 사용해서 만드시나요?? ㅠㅠ

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

      Ezbeat님, 안녕하세요~

      와~ 대단하세요. 열심히 노력하시는 모습이 저에게도 큰 동기 부여가 됩니다.

      문서 그림은 OFFICE 2007 의 PowerPoint 를 사용하였습니다.

      감사합니다.

  4. 2010/04/30 02:28 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

  5. mscproject 2010/05/04 08:46 댓글주소 | 수정 | 삭제 | 댓글

    항상 글 잘 보고 있습니다. 특히 PE구조와 MUP분야에서 많은 도움이 되었습니다.

    지금 강좌도 열심히 보고 있습니다. 운영자님 글을 보면서 저도 실력이 조금씩 늘어가는 것을

    느끼며 감사하다는 말씀을 전하고 싶습니다.

    다음편에서 실 코드를 보게 될걸 생각하니 두근두근 거리네요!!


◀ PREV : [1] : ... [14] : [15] : [16] : [17] : [18] : [19] : [20] : [21] : [22] : ... [91] : NEXT ▶