알송의 데스크탑 가사창을 어떻게 구현했을까.

우선 알송의 가사창을 자세히 보면 Alpha Blending으로 윈도우를 반투명 처리 되었지만

마우스 이벤트가 가사창 윈도우에 먹지 않습니다.

클릭을 하면 가사창 아래의 창에 클릭이 되고, 마우스 포인터(보통 화살표)도 가사창 아래의 윈도우에 투영됩니다.

이런것은 어떻게 처리 했을까?

정답은, API 함수의 윈도우 생성 함수중

CreateWindowEx() 함수의 Extended Style 인자에

WS_EX_TRANSPARENT 와 WS_EX_LAYERED 속성을 추가하면 됩니다

각각, 이 윈도우는 투명화 처리, 윈도우 레이어 임의 설정이 됩니다 

출처: http://blog.daum.net/wooyoung

www.debuglab.com 에서 있는 자료들입니다.


이 자료를 정리하신 분은 서우석이라는 분인데..정말 대단하다라는 생각이 듭니다.

어쩜 이렇게 정리를 잘 하셨을까.. 부럽다는 생각만 드는군요..ㅡㅡ



-------------------------------------------------------------------

Home


lastest update : 2001.08.06




'C/C++언어 > 후킹' 카테고리의 다른 글

Hooking 을 사용하는 프로그램 내의 구현  (0) 2007.11.11
Global Hooking in Win32  (0) 2007.11.11
[본문스크랩] 메세지 후킹  (0) 2007.09.06
API Hooking Revealed  (0) 2007.09.06
동적 API 후킹의 구조  (0) 2007.09.06
출처 : http://www.devpia.com/Forum/BoardView.aspx?no=6753&page=3&Tpage=53&forumname=vc_lec&stype=&ctType=&answer=



API Hooking Revealed

[역자주] 이 글은 CodeProject 사이트에 Ivo Ivanov가 "API hooking revealed"라는 제목으로 게재한 글입니다. Win32 시스템에서 API를 후킹하는 방법에 대한 전반적인 기법들을 다루고 있으며 자세한 설명이나 코드보다는 개념적인 내용들이 많아 Win32 SDK에 익숙치 않은 개발자들도 쉽게 접근할 수 있는 좋은 글인 것 같습니다. 원래 소스 파일이 함께 첨부되어 게재된 글인데 이 파일을 번역한 글과 함께 올리는 것은 혹시 저작권 문제가 될 수도 있다고 생각되어 따로 첨부하지 않았습니다. 소스 파일이 필요하신 분은 원문인 "API hooking revealed"에서 다운받아 사용하시기 바랍니다.
이 글을 읽고 제대로 이해하려면 최소한 윈도우즈 메시지 후킹DLL의 기본 구조에 대해서는 알아야 합니다. 이런 내용들은 MSDN에 잘 설명되어 있고 codeproject, codeguru, devpia 등의 사이트에도 좋은 글들이 많이 게재되어 있으니 참고하시기 바랍니다.

어떤 윈도우즈 어플리케이션을 개발하기 위해서 우리들은 여러 종류의 언어나 도구들을 사용할 수 있습니다. Visual Basic, Visual C++, Delphi, C++ Builder, PowerBuilder 등 수많은 언어와 도구들이 있습니다.
이런 프로그램들에서 어떤 작업을 수행하기 위해서 사용하는 방법은 서로 다를지라도 그 내부로 들어가 보면 결국에는 윈도우즈 운영체제에서 제공하는 API를 호출하게 됩니다. 예를 들어 화면에 문자열을 출력하려고 한다면 MFC에서는 CDC::DrawText를 사용하고 Dephi에서는 TCanvas::TextOut을 사용합니다. 도구마다 사용하는 형태는 이렇게 다르지만 결국에는 TextOutA/W API를 호출합니다.
TextOutA/W API는 gdi32.dll에 구현되어 있는 함수이고 운영체제가 제공하는 API입니다. 윈도우즈는 기본적으로 3개의 DLL-kernel32.dll, user32.dll, gdi32.dll-에 대부분의 API를 구현하여 제공하고 어플리케이션은 실행시 자신의 프로세스 주소 공간으로 이들 DLL을 매핑한 후 사용합니다.
API 후킹은 어떤 프로그램에서 API 호출을 하는 것을 가로채서 개발자가 만든 프로그램의 함수가 처리할 수 있도록 하는 메카니즘을 말합니다. 반드시 윈도우즈의 API일 필요는 없고 단지 DLL에서 구현하여 제공하는 함수이면 됩니다.
API 후킹은 어떤 언어로 개발된 프로그램에도 적용될 수 있으며 디버깅이나 트레이싱 작업, 모니터링 작업 등에 사용할 수 있으며 소스 코드가 없는 프로그램에 기능을 추가하기 위한 용도로도 사용할 수 있습니다. 그외에도 여러가지 용도로 사용할 수 있지만 굳이 이런 프로그램을 개발할 계획이 아니더라도 윈도우즈 프로그램의 기본적인 원리를 이해하는데 많은 도움이 됩니다.
API 후킹을 적용하여 만든 프로그램 중에 주위에서 일반적으로 볼 수 있는 것으로는 myQuickFind, 아래한글사전과 같은 전자 사전 프로그램이 있습니다. 이런 프로그램들은 모두 다른 윈도우의 마우스 커서 밑에 있는 단어를 인식할 수 있는 기능을 가지고 있는데 이것은 윈도우즈 메시지 후킹과 API 후킹(TextOutA/W)을 이용한 것입니다.
또한 크래킹이나 스파이웨어를 개발하는데 악용될 수도 있고 반대로 방지하는 용도로도 사용될 수 있습니다. 어찌되었든 간에 이 글이 윈도우즈 환경에서 개발을 하시는 많은 분들에게 도움이 되기를 바랍니다.

번역해서 글을 올리는 것이 처음이어서 어색한 부분들이 많습니다. 게다가 저자가 쉼표를 생략하는 동격 명사 구문이나 분사 구문을 많이 사용하였고 번역이 표준화되지 않은 개발 관련 용어들도 많아 더욱 그렇습니다. 그래서 원문과 번역문을 함께 올렸고 번역이 미비하거나 어색한 부분에는 [역자주]를 달아 놓았습니다. 그래도 이상한 부분들은 코멘트를 달아 주시면 가능하면 수정해서 다시 올리도록 하겠습니다.

Introduction(소개)

Intercepting Win32 API calls has always been a challenging subject among most of the Windows developers and I have to admit, it's been one of my favorite topics. The term Hooking represents a fundamental technique of getting control over a particular piece of code execution. It provides an straightforward mechanism that can easily alter the operating system's behavior as well as 3rd party products, without having their source code available.

Win32 API 호출을 가로채는 것은 대다수의 윈도우즈 개발자들 사이에서 항상 도전하는 과제이었으며 내가 가장 좋아하는 주제 중의 하나가 되었다. 후킹이라는 단어는 코드 실행의 특정 부분을 제어할 수 있는 기반 기술을 의미한다. 후킹은 써드 파티 제품뿐 아니라 운영체제의 동작까지도 소스 코드 없이 쉽게 바꿀 수 있는 간편한 메카니즘을 제공한다.

Many modern systems draw the attention to their ability to utilize existing Windows applications by employing spying techniques. A key motivation for hooking, is not only to contribute to advanced functionalities, but also to inject user-supplied code for debugging purposes.

많은 현대적인 시스템들이 스파이 기술을 사용하여 기존의 윈도우즈 어플리케이션을 활용하는 것에 관심을 기울이고 있다. 후킹을 연구하는 주요 동기는 향상된 기능을 제공하는 것 뿐 아니라 디버깅을 목적으로 사용자 정의 코드를 침투시키는 것에 있다.

Unlike some relatively "old" operating systems like DOS and Windows 3.xx, the present Windows OS as NT/2K and 9x provide sophisticated mechanisms to separate address spaces of each process. This architecture offers a real memory protection, thus no application is able to corrupt the address space of another process or in the worse case even to crash the operating system itself. This fact makes a lot harder the development of system-aware hooks.

DOS, 윈도우즈3.XX와 같이 상대적으로 오래된 운영체제와 달리, NT/2K,9x와 같은 현재의 윈도우즈 OS는 각각의 프로세스의 주소 공간을 분리하는 정교한 메카니즘을 제공한다. 이러한 구조는 실제 메모리 보호를 제공하여 어떠한 어플리케이션도 다른 프로세스의 주소 공간을 변형시키거나 더 나아가 운영 체제 자체를 망가뜨리지 못하도록 한다. 이러한 이유로 시스템 후킹의 개발은 더욱 어려워졌다.

My motivation for writing this article was the need for a really simple hooking framework, that will offer an easy to use interface and ability to capture different APIs. It intends to reveal some of the tricks that can help you to write your own spying system. It suggests a single solution how to build a set for hooking Win32 API functions on NT/2K as well as 98/Me (shortly named in the article 9x) family Windows. For the sake of simplicity I decided not to add a support do UNICODE. However, with some minor modifications of the code you could easily accomplish this task.

이 글을 쓰게 된 동기는 사용하기 쉽고 다른 API들을 가로챌 수 있는 매우 간단한 후킹 프레임웍에 대한 필요성 때문이다. 이 글에서 스파이 시스템을 구축하기 위해 필요한 몇가지 트릭들을 밝힐 계획이다. 이 글은 98/Me와 NT/2K 에서 모두 사용할 수 있는 Win32 API 함수 후킹에 대한 하나의 솔루션을 제안한다. 단순하게 하기 위하여 UNICODE에 대한 지원은 추가하지 않기로 하였다. 하지만 코드를 약간 수정하면 UNICODE에 대한 지원을 쉽게 달성할 수 있을 것이다.

Spying of applications provides many advantages:

어플리케이션을 스파이하는 것은 다음과 같은 많은 이점을 제공한다:

  • API function's monitoring(API 함수의 감시)
    The ability to control API function calls is extremely helpful and enables developers to track down specific "invisible" actions that occur during the API call. It contributes to comprehensive validation of parameters as well as reports problems that usually remain overlooked behind the scene. For instance sometimes, it might be very helpful to monitor memory related API functions for catching resource leaks.

    API 함수 호출을 제어할 수 있는 능력은 API 호출시 발생하는 보이지 않는 특정 행위를 개발자가 추적할 수 있도록 도와준다. 또한 항상 지나칠 수 있는 문제점들을 보고할 뿐 아니라 매개변수에 대한 종합적인 검증을 할 수도 있다. 예를 들면 메모리와 관련된 API 함수들을 감시하여 리소스 누출을 잡아내는 것에 매우 유용하다.

  • Debugging and reverse engineering(디버깅과 역공학)
    Besides the standard methods for debugging API hooking has a deserved reputation for being one of the most popular debugging mechanisms. Many developers employ the API hooking technique in order to identify different component implementations and their relationships. API interception is very powerful way of getting information about a binary executable.

    디버깅을 위한 표준적인 방법들을 제외하면 API 후킹은 가장 인기있는 디버깅 메카니즘의 하나라는 평가를 받는다. 많은 개발자들이 API 후킹 기술을 콤포넘트의 서로 다른 구현들과 그들의 관계를 파악하기 위해 사용한다. API 가로채기는 이진 실행 파일에 대한 정보를 얻을 수 있는 매우 강력한 방법이다.

  • Peering inside operating system(운영체제 엿보기)
    Often developers are keen to know operating system in dept and are inspired by the role of being a "debugger". Hooking is also quite useful technique for decoding undocumented or poorly documented APIs.

    가끔 개발자들은 운영체제에 대해 이해하기를 갈망하고 "디버거"의 역할을 하게 된다. 후킹은 문서화되지 않았거나 빈약하게 문서화된 API를 해석하는데 매우 유용하다.

  • Extending originally offered functionalities(기존 기능의 확장) by embedding custom modules into external Windows applications Re-routing the normal code execution by injecting hooks can provide an easy way to change and extend existing module functionalities. For example many 3rd party products sometimes don't meet specific security requirements and have to be adjusted to your specific needs. Spying of applications allows developers to add sophisticated pre- and post-processing around the original API functions. This ability is an extremely useful for altering the behavior of the already compiled code.

    외부의 윈도우즈 어플리케이션에 사용자 모듈을 삽입하여 기존에 제공되던 기능을 확장하는, 후크를 침투시켜 기존의 코드 실행을 재편성하는 것은 기존 모듈의 기능을 수정하고 확장하는 쉬운 방법을 제공할 수 있다.(???) 예을 들면 많은 써드 파티 제품들이 특별한 보안적 요구 사항없이 특정 기능을 하도록 수정되어야 한다. 어플리케이션 스파이는 개발자가 원본 API 함수의 호출 전,후에 세련된 처리를 추가할 수 있도록 해준다. 이러한 능력은 이미 컴파일된 코드의 동작을 변경하는데 매우 유용하다.

    [역자주] 도저히 첫번째 문장은 매끄러운 번역을 못하겠습니다. 정확히 주어가 무엇인지를 모르겠지만 대략적인 의미는 API 후킹을 이용하여 소스 코드없이 기존의 어플리케이션에 새로운 기능을 추가하거나 동작을 변경할 수 있다는 것입니다.

    Functional requirements of a hooking system(후킹 시스템의 기능적 요구 사항)

    There are few important decisions that have to be made, before you start implementing any kind of API hooking system. First of all, you should determine whether to hook a single application or to install a system-aware engine. For instance if you would like to monitor just one application, you don't need to install a system-wide hook but if your job is to track down all calls to TerminateProcess() or WriteProcessMemory() the only way to do so is to have a system-aware hook. What approach you will choose depends on the particular situation and addresses specific problems.

    API 후킹 시스템 구현을 시작하기 전에 결정해야 할 몇가지 중요한 사항들이 있다. 무엇보다 먼저 하나의 어플리케이션을 후킹할 것인가 아니면 시스템 전역 엔진을 설치할 것인가를 결정해야 한다. 예를 들어 하나의 어플리케이션만을 감시하기 원한다면 시스템 전역 후크를 설치할 필요가 없지만 TerminateProcess() 함수나 WriteProcessMemory() 함수에 대한 모든 호출을 추적하는 업무라면 시스템 전역 후크를 설치해야만 한다. 어떤 접근 방법을 선택하는가는 어떠한 상황인가에 달려 있으며 접근 방법들은 각각 특별한 문제점들을 가지고 있다.

    [역자주] system-aware와 system-wide를 모두 "시스템 전역"으로 번역했습니다. system-wide hook는 MSDN에서 윈도우즈 메시지 후킹을 설명하면서 나오는 용어입니다. global hook라는 용어와 같은 의미이며 실행 중인 모든 어플리케이션에 적용되는 메시지 후크를 뜻합니다.
    글을 읽어 가면서 알게 되겠지만 이 글의 주제인 "API 후크"와 MSDN의 "윈도우 메시지 후크"는 관련이 있지만 서로 다른 용어입니다.

    General design of an API spying framework(일반적인 API 후킹 시스템의 설계)

    Usually a Hook system is composed of at least two parts - a Hook Server and a Driver. The Hook Server is responsible for injecting the Driver into targeted processes at the appropriate moment. It also administers the driver and optionally can receive information from the Driver about its activities whereas the Driver module that performs the actual interception.  
    This design is rough and beyond doubt doesn't cover all possible implementations. However it outlines the boundaries of a hook framework.

    Once you have the requirement specification of a hook framework, there are few design points you should take into account:

    • What applications do you need to hook
    • How to inject the DLL into targeted processes or which implanting technique to follow
    • Which interception mechanism to use

    I hope next the few sections will provide answers to those issues.

    일반적으로 후크 시스템은 최소한 2부분, 후크 서버와 드라이버로 구성된다. 후크 서버는 적절한 시점에 목표로 하는 프로세스에 드라이버를 침투시키는 기능을 한다. 또한 드라이버를 관리하며 선택적으로 드라이버에서 활동 정보를 받을 수 있으며 드라이버 모듈은 실질적인 가로채기를 수행한다.
    이러한 설계는 세련되지 않았고 의심할 바 없이 가능성있는 모든 구현을 처리할 수 없다. 하지만 후크 프레임웍의 범위의 윤곽은 잡아준다.

    일단 후크 프레임웍의 요구 사항이 정의되면 기록해 놓아야 할 몇가지 설계 포인트가 있다:

    • 어떤 어플리케이션을 후킹할 것인가
    • 어떻게 DLL을 목표로 하는 프로세스에 침투시킬 것인가 혹은 아래에 설명된 침투 기술 중 어떤 것을 사용할 것인가
    • 어떤 가로채기 메카니즘을 사용할 것인가

    다음의 몇개 섹션에서 이러한 사항들에 대해 설명하겠다.

    Injecting techniques(침투 기술)

    1. Registry(레지스트리)
      In order to inject a DLL into processes that link with USER32.DLL, you simply can add the DLL name to the value of the following registry key:

      어떤 DLL을 USER32.DLL과 링크되어 있는 프로세스에 침투시키기 위해서는 아래의 레지스트리 키의 값에 DLL의 이름을 추가하기만 하면 된다.

      HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

      Its value contains a single DLL name or group of DLLs separated either by comma or spaces. According to MSDN documentation [7], all DLLs specified by the value of that key are loaded by each Windows-based application running within the current logon session. It is interesting that the actual loading of these DLLs occurs as a part of USER32's initialization. USER32 reads the value of mentioned registry key and calls LoadLibrary() for these DLLs in its DllMain code. However this trick applies only to applications that use USER32.DLL. Another restriction is that this built-in mechanism is supported only by NT and 2K operating systems. Although it is a harmless way to inject a DLL into a Windows processes there are few shortcomings:

      • In order to activate/deactivate the injection process you have to reboot Windows.
      • The DLL you want to inject will be mapped only into these processes that use USER32.DLL, thus you cannot expect to get your hook injected into console applications, since they usually don't import functions from USER32.DLL.
      • On the other hand you don't have any control over the injection process. It means that it is implanted into every single GUI application, regardless you want it or not. It is a redundant overhead especially if you intend to hook few applications only. For more details see [2] "Injecting a DLL Using the Registry"

      이 키의 값에는 하나의 DLL 이름만 등록할 수도 있고 콤마나 스페이스로 구분하여 여러 개의 DLL들을 등록할 수도 있다. MSDN 문서 [7]에 의하면 이 키의 값으로 등록된 DLL들은 현재의 로그온 세션 내에서 윈도우즈 기반의 어플리케이션이 실행될 때마다 로드된다. 흥미롭게도 이 DLL들의 실제적인 로드는 USER32의 초기화 과정의 일부분으로서 수행된다. USER32는 상기의 레지스트리 키의 값을 읽어 DllMain 안에서 이들 DLL에 대해 LoadLibrary() 함수를 호출한다. 그러나 이 방법은 USER32.DLL을 사용하는 어플리케이션에 대해서만 적용할 수 있다. 또다른 제약은 이 메카니즘이 NT와 2K 운영체제에 대해서만 지원된다는 것이다. 이 방법이 어떤 윈도우즈 프로세스에 DLL을 침투시키는 유용한 방법이지만 몇가지 단점들이 있다:

      • 침투 과정을 활성/비활성시키기 위해서는 윈도우즈를 리부팅해야 한다.
      • 침투시킬 DLL은 USER32.DLL을 사용하는 프로세스에만 가능하다. 따라서 USER32.DLL에서 함수들을 임포트하지 않는 콘솔 어플리케이션에 대해서는 후크를 침투시킬 수 없다.
      • 한편으로는 침투 과정에 대한 제어를 전혀 할 수가 없다. 원하건 원하지 않건 간에 모든 GUI 어플리케이션들에 침투하게 된다. 소수의 어플라케이션만 후킹하기를 원하는 경우 이것은 상당한 오버헤드를 유발하게 된다. 보다 자세한 내용은 레퍼런스의 [2] "Injecting a DLL Using the Registry" 를 참조하기 바란다.

    2. System-wide Windows Hooks(시스템 전역 윈도우즈 후크)
      Certainly a very popular technique for injecting DLL into a targeted process relies on provided by Windows Hooks. As pointed out in MSDN a hook is a trap in the system message-handling mechanism. An application can install a custom filter function to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure.

      목표로 하는 프로세스에 DLL을 침투시키는 매우 인기있는 기술은 윈도우즈 후크를 이용하는 것이다. MSDN에서 설명하듯이 후크는 시스템 상의 메시지 처리 메카니즘을 가로채는 것이다. 어플리케이션은 사용자 정의 필터 함수를 설치하여 시스템에서의 메시지 교환을 감시하고 특정한 종류의 메시지가 목표로 하는 윈도우에 도달하기 전에 처리할 수 있다.

      [역지주] 여기서 말하는 윈도우즈 후크는 API 후크와 다른 용어로서 MSDN에서 설명하고 있는 메시지 후크를 뜻합니다. 혹시 메시지 후크를 전혀 모른다면 MSDN에서 대략적인 의미라도 파악하고서 이 글을 읽으시기 바랍니다.

      A hook is normally implemented in a DLL in order to meet the basic requirement for system-wide hooks. The basic concept of that sort of hooks is that the hook callback procedure is executed in the address spaces of each hooked up process in the system. To install a hook you call SetWindowsHookEx() with the appropriate parameters. Once the application installs a system-wide hook, the operating system maps the DLL into the address space in each of its client processes. Therefore global variables within the DLL will be "per-process" and cannot be shared among the processes that have loaded the hook DLL. All variables that contain shared data must be placed in a shared data section. The diagram bellow shows an example of a hook registered by Hook Server and injected into the address spaces named "Application one" and "Application two".

      후크는 시스템 전역 후크의 기본 요구 사항을 충족시키기 위해서는 보통 DLL로 구현된다. 이러한 종류의 후크는 기본적으로 후크 콜백 프로시져가 시스템 상에서 후크된 각각의 프로세스의 주소 공간에서 실행된다. 후크를 설치하기 위해서는 SetWindowsHookEx() 함수를 적절한 매개변수를 주어 호출하여야 한다. 일단 어플리케이션이 시스템 전역 후크를 설치하면 운영체제는 DLL을 클라이언트 프로세스의 각각의 주소 공간에 매핑하게 된다. 그러므로 DLL 내의 전역 변수는 프로세스마다 존재하게 되고 후크 DLL을 적재한 프로세스 사이에서 공유할 수 없게 된다. 데이터를 공유하는 모든 변수들은 공유 데이터 섹션에 정의되어야만 한다. 아래의 그림은 후크 서버에 의해 등록된 하나의 후크와 "Application one"과 "Application two"라는 이름의 주소 공간에 침투한 후크의 예를 보여준다.

      Figure 1

      A system-wide hook is registered just ones when SetWindowsHookEx() is executed. If no error occurs a handle to the hook is returned. The returned value is required at the end of the custom hook function when a call to CallNextHookEx() has to be made. After a successful call to SetWindowsHookEx() , the operating system injects the DLL automatically (but not necessary immediately) into all processes that meet the requirements for this particular hook filter. Let's have a closer look at the following dummy WH_GETMESSAGE filter function:

      시스템 전역 후크는 SetWindowsHookEx() 함수가 실행될 때 등록된다. 오류가 없으면 후크 핸들이 반환된다. 반환값은 사용자 정의 후크 함수의 끝부분에서 CallNextHookEx() 함수를 호출할 때 필요하다. SetWindowsHookEx() 함수가 성공적으로 호출되면 운영체제는 DLL을 특별한 후크 필터의 요구 사항을 충족시키는 모든 프로세스에 자동적으로(즉시는 아님) 침투시킨다. 아래의 아무것도 하지 않는 WH_GETMESSAGE 필터 함수를 보자:

      //---------------------------------------------------------------------------
      // GetMsgProc
      //
      // Filter function for the WH_GETMESSAGE - it's just a dummy function
      //---------------------------------------------------------------------------
      LRESULT CALLBACK GetMsgProc(
      	int code,       // hook code
      	WPARAM wParam,  // removal option
      	LPARAM lParam   // message
      	)
      {
      	// We must pass the all messages on to CallNextHookEx.
      	return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam);
      }
      

      A system-wide hook is loaded by multiple processes that don't share the same address space.

      시스템 전역 후크는 동일한 주소 공간을 공유하지 않는 여러 개의 프로세스들에 로드된다.

      For instance hook handle sg_hGetMsgHook, that is obtained by SetWindowsHookEx() and is used as parameter in CallNextHookEx() must be used virtually in all address spaces. It means that its value must be shared among hooked processes as well as the Hook Server application. In order to make this variable "visible" to all processes we should store it in the shared data section.

      예를 들면 SetWindowsHookEx() 호출로 얻어지고 CallNextHookEx() 호출의 매개변수로 사용되는 sg_hGetMsgHook는 모든 주소 공간에서 사용된다. 이것은 이 값이 후크 서버 어플리케이션 뿐만 아니라 후크된 프로세스 사이에서도 공유되어야 한다는 것을 의미한다. 이 변수를 모든 프로세스에서 사용 가능하도록 하기 위해서는 이 변수를 공유 데이터 섹션에 저장하여야만 한다.

      The following is an example of employing #pragma data_seg(). Here I would like to mention that the data within the shared section must be initialized, otherwise the variables will be assigned to the default data segment and #pragma data_seg() will have no effect.

      아래의 예제는 #pragma data_seg()의 사용을 보여준다. 공유 섹션의 데이터들은 반드시 초기화되어야 하며 그렇지 않으면 변수들은 디폴트 데이터 세그먼트에 할당되어 #pragma data_seg()는 아무런 효과도 갖지 않게 된다는 시실을 강조하고 싶다.

      //---------------------------------------------------------------------------
      // Shared by all processes variables
      //---------------------------------------------------------------------------
      #pragma data_seg(".HKT")
      HHOOK sg_hGetMsgHook       = NULL;
      BOOL  sg_bHookInstalled    = FALSE;
      // We get this from the application who calls SetWindowsHookEx()'s wrapper
      HWND  sg_hwndServer        = NULL; 
      #pragma data_seg()
      You should add a SECTIONS statement to the DLL's DEF file as well

      DLL의 DEF 파일에도 SECTIONS 문장을 추가하여야 한다.

      SECTIONS
      	.HKT   Read Write Shared
      
      or use
      #pragma comment(linker, "/section:.HKT, rws")
      

      Once a hook DLL is loaded into the address space of the targeted process, there is no way to unload it unless the Hook Server calls UnhookWindowsHookEx() or the hooked application shuts down. When the Hook Server calls UnhookWindowsHookEx() the operating system loops through an internal list with all processes which have been forced to load the hook DLL. The operating system decrements the DLL's lock count and when it becomes 0, the DLL is automatically unmapped from the process's address space.

      Here are some of the advantages of this approach:

      • This mechanism is supported by NT/2K and 9x Windows family and hopefully will be maintained by future Windows versions as well.
      • Unlike the registry mechanism of injecting DLLs this method allows DLL to be unloaded when Hook Server decides that DLL is no longer needed and makes a call to UnhookWindowsHookEx()

      후크 DLL이 목표로 하는 프로세스에 일단 로드되면 후크 서버가 UnhookWindowsHookEx() 함수를 호출하거나 후크된 어플리케이션을 종료하기 전에는 언로드할 방법이 없다. 후크 서버가 UnhookWindowsHookEx() 함수를 호출하면 운영체제는 내부의 리스트를 통해 후크 DLL을 로드하도록 지시했던 모든 프로세스들을 순회하게 된다. 운영체제는 DLL의 참조수를 감소시키고 참조수가 0이 되면 DLL은 자동적으로 프로세스의 주소 공간에서 언매핑된다.

      이러한 접근 방식은 다음과 같은 이점이 가진다:

      • 이 메카니즘은 NT/2K와 9X 윈도우즈 패밀리에서 지원되며 다행스럽게도 미래의 윈도우즈에서 계속 지원이 유지될 것이다.
      • DLL을 침투시키는 레지스트리 메카니즘과 달리 이 방법은 후크 서버가 DLL이 더이상 필요없다고 결정하여 UnhookWindowsHookEx() 함수를 호출하게 되면 DLL을 언로드할 수 있다.

      Although I consider Windows Hooks as very handy injection technique, it comes with its own disadvantages:

      • Windows Hooks can degrade significantly the entire performance of the system, because they increase the amount of processing the system must perform for each message.
      • It requires lot of efforts to debug system-wide Windows Hooks. However if you use more than one instance of VC++ running in the same time, it would simplify the debugging process for more complex scenarios.
      • Last but not least, this kind of hooks affect the processing of the whole system and under certain circumstances (say a bug) you must reboot your machine in order to recover it.

      윈도우즈 후크가 매우 편리한 침투 기술이지만 다음과 같은 단점이 있다:

      • 윈도우즈 후크는 시스템이 수행하여야 하는 메시지의 처리 과정을 증가시키므로 전체 시스템의 성능을 확연하게 저하시킨다.
      • 시스템 전역 후크를 디버그하는 것은 많은 노력을 필요로 한다. 하지만 동시에 하나 이상의 VC++ 인스턴스를 이용할 있다면 복잡한 디버그 과정을 단순화할 수 있을 것이다.
        [역자주] 어떻게 VC++을 2개 띄운다는 것인지 잘 모르겠습니다. 혹시 아시는 분은 코멘트를 달아 주세요.
      • 마지막이지만 사소하지 않은 것이 이러한 방식의 후크는 전체 시스템의 프로세스에 영향을 미치고 어떤 특별한 상황(버그 발생)에서는 복구하기 위해 시스템을 리부팅해야만 한다.
        [역자주] "Last but not least", 영문을 읽다 보면 자주 나오는 말인데 뭐라 번역을 하는게 좋은지 잘 모르겠습니다. 뜻은 "마지막 사항이지만 그렇다고 해서 덜 중요한 것이 아니다."인데 이렇게 번역하기는 좀 그렇네요.

    3. Injecting DLL by using CreateRemoteThread() API function (CreateRemoteThread() 함수를 이용한 DLL 침투)
      Well, this is my favorite one. Unfortunately it is supported only by NT and Windows 2K operating systems. It is bizarre, that you are allowed to call (link with) this API on Win 9x as well, but it just returns NULL without doing anything.

      이것이 내가 가장 선호하는 방법이다. 불행히도 이 방법은 오직 NT와 2K 운영체제에서만 지원된다. 윈도우즈 9x에서도 이 API를 호출할 수 있지만 아무 것도 하지 않고 그냥 NULL을 반환한다.

      Injecting DLLs by remote threads is Jeffrey Ritcher's idea and is well documented in his article [9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB".

      리모트 쓰레드를 이용하여 DLL을 침투시키는 것은 Jeffrey Ritcher의 아이디어이며 그의 기사인 [9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB"에 잘 설명되어 있다.

      The basic concept is quite simple, but very elegant. Any process can load a DLL dynamically using LoadLibrary() API. The issue is how do we force an external process to call LoadLibrary() on our behalf, if we don't have any access to process's threads? Well, there is a function, called CreateRemoteThread() that addresses creating a remote thread. Here comes the trick - have a look at the signature of thread function, whose pointer is passed as parameter (i.e. LPTHREAD_START_ROUTINE) to the CreateRemoteThread():

      기본 개념은 상당히 단순하지만 굉장히 멋지다. 어떤 프로세스도 LoadLibrary() API를 호출하여 DLL을 동적으로 로드할 수 있다. 문제는 어떻게 프로세스의 쓰레드에 대해 전혀 접근을 하지 못하면서 외부의 프로세스가 적절하게 LoadLibrary() 함수를 호출하도록 만드는가 하는 것이다. 하나의 리모트 쓰레드를 생성하는 CreateRemoteThread() 함수가 그 해답이다. 여기서 약간의 속임수가 필요하다. - 쓰레드 함수의 원형을 보라. 이 함수의 포인터가 매개변수(LPTHREAD_START_ROUTINE)로 CreateRemoteThread() 함수에 넘겨진다:

      DWORD WINAPI ThreadProc(LPVOID lpParameter);
      And here is the prototype of LoadLibrary API

      그리고 여기 LoadLibrary API의 원형이 있다.

      HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
      Yes, they do have "identical" pattern. They use the same calling convention WINAPI, they both accept one parameter and the size of returned value is the same. This match gives us a hint that we can use LoadLibrary() as thread function, which will be executed after the remote thread has been created. Let's have a look at the following sample code:

      그렇다, 이 함수들은 동일한 형태를 지닌다. 이 함수들은 같은 호출 규약 WINAPI를 사용하고, 하나의 매개변수를 받아 동일한 크기의 값을 반환한다. 이러한 일치점들은 LoadLibrary() 함수를 쓰레드 함수로 사용하여 리모트 쓰레드가 생성된 후에 실행시킬 수가 있다는 힌트를 준다. 아래의 예제 코드를 살펴보자.

      hThread = ::CreateRemoteThread(
      	hProcessForHooking, 
      	NULL, 
      	0, 
      	pfnLoadLibrary, 
      	"C:\\HookTool.dll", 
      	0, 
      	NULL);
        

      By using GetProcAddress() API we get the address of the LoadLibrary() API. The dodgy thing here is that Kernel32.DLL is mapped always to the same address space of each process, thus the address of LoadLibrary() function has the same value in address space of any running process. This ensures that we pass a valid pointer (i.e. pfnLoadLibrary) as parameter of CreateRemoteThread().

      GetProcAddress() API를 사용하여 우리는 LoadLibrary() API의 주소를 구할 수가 있다. 여기서 이상한 것은 Kernel32.DLL이 각각의 프로세스 공간에서 항상 동일한 주소로 매핑되어 실행 중인 어떤 프로세스의 주소 공간에서도 LoadLibrary()의 주소가 똑같다는 것이다. 이것은 CreateRemoteThread() 함수의 매개변수로서 유효한 포인터 (i.e. pfnLoadLibrary)를 넘길 수 있다는 확신을 준다.

      As parameter of the thread function we use the full path name of the DLL, casting it to LPVOID. When the remote thread is resumed, it passes the name of the DLL to the ThreadFunction (i.e. LoadLibrary). That's the whole trick with regard to using remote threads for injection purposes.

      쓰레드 함수의 매개변수로 DLL의 전체 경로명을 지정하고 LPVOID로 형변환한다. 리모트 쓰레드가 시작할 때 DLL의 이름이 쓰레드 함수(i.e. LoadLibrary)의 매개변수로 넘겨진다. 이것이 리모트 쓰레드를 사용하여 침투를 하는 속임수의 전부이다.

      [역자주] 혹시 이해가 안가시는 분들을 위해 부연 설명을 하겠습니다. 본인도 쓰레드 작업을 해본 적은 없지만 여기서 말하는 방법은 대강 다음과 같습니다. CreateRemoteThread라는 함수는 다른 프로세스에 쓰레드를 생성하는 함수인데 이 함수는 쓰레드 생성시 초기화를 위한 콜백 함수인 ThreadProc의 주소를 매개변수로 받습니다. 그런데 이 콜백 함수의 원형이 LoadLibrary와 매우 유사하고 ThreadProc는 생성시 단 한번 실행되므로 ThreadProc에 LoadLibrary의 주소를 넘겨 주면 결국 목표로 하는 프로세스가 LoadLibrary를 호출하여 프로세스의 주소 공간에 DLL을 로드하게 된다는 것입니다.

      There is an important thing we should consider, if implanting through CreateRemoteThread() API. Every time before the injector application operate on the virtual memory of the targeted process and makes a call to CreateRemoteThread(), it first opens the process using OpenProcess() API and passes PROCESS_ALL_ACCESS flag as parameter. This flag is used when we want to get maximum access rights to this process. In this scenario OpenProcess() will return NULL for some of the processes with low ID number. This error (although we use a valid process ID) is caused by not running under security context that has enough permissions. If you think for a moment about it, you will realize that it makes perfect sense. All those restricted processes are part of the operating system and a normal application shouldn't be allowed to operate on them. What would happen if some application has a bug and accidentally attempts to terminate an operating system's process? To prevent the operating system from that kind of eventual crashes, it is required that a given application must have sufficient privileges to execute APIs that might alter operating system behavior. To get access to the system resources (e.g. smss.exe, winlogon.exe, services.exe, etc) through OpenProcess() invocation, you must be granted the debug privilege. This ability is extremely powerful and offers a way to access the system resources, that are normally restricted. Adjusting the process privileges is a trivial task and can be described with the following logical operations:

      • Open the process token with permissions needed to adjust privileges
      • Given a privilege's name "SeDebugPrivilege", we should locate its local LUID mapping. The privileges are specified by name and can be found in Platform SDK file winnt.h
      • Adjust the token in order to enable the "SeDebugPrivilege" privilege by calling AdjustTokenPrivileges() API
      • Close obtained by OpenProcessToken() process token handle
      For more details about changing privileges see [10] "Using privilege".

      CreateRemoteThread() API를 사용하여 침투하는 경우에는 반드시 고려해야 하는 중요한 사항이 하나 있다. 침투 어플리케이션이 목표로 하는 프로세스의 가상 메모리에서 작동하고 CreateRemoteThread()를 호출하기 전에 OpenProcess() API에 PROCESS_ALL_ACCESS 플래그를 매개변수로 넘겨주어 먼저 프로세스를 오픈하여야 한다. 이 플래그는 프로세스에 대한 최대한의 접근 권한을 얻기 위해 사용된다. 이 경우 일부 작은 ID의 프로세스는 OpenProcess() 함수가 NULL을 반환한다. 유효한 프로세스 ID를 사용했음에도 이러한 오류가 나는 것은 충분한 권한을 가지는 보안 레벨에서 실행되지 않았기 때문이다. 이것에 대해 잠시 살펴 보면 정확한 의미를 알 수 있다. 이렇게 제한되는 모든 프로세스는 운영체제의 일부분이고 일반적인 어플리케이션이 이런 프로세스를 조작하는 것은 허용되지 않는다. 어떤 어플리케이션이 버그를 가지고 있어 운영체제의 프로세스를 중지시킬려고 한다면 어떻게 되겠는가? 운영체제를 이러한 유형의 사고로부터 보호하기 위해 어떤 어플리케이션이 운영체제의 동작을 변경할 수 있는 API들을 실행하기 위해서는 충분한 권한을 가져야만 가능하다. OpenProcess() 호출을 통해 시스템 자원(예: smss.exe, winlogon.exe, services.exe..)에 대해 접근하려면 디버그 권한을 가져야만 한다. 이 권한은 매우 강력하며 일반적으로 제한되는 시스템 자원에 대한 접근을 허용한다. 프로세스의 권한을 조정하는 것은 사소한 작업이며 아래에 논리적인 방법을 설명하였다.

      • 프로세스 토큰을 권한 조정을 위해 필요한 퍼미션으로 오픈한다.
      • 주어진 권한의 이름 "SeDebugPrivilege"으로 LUID매핑을 찾는다. 권한은 이름으로 구분되며 플랫폼 SDK 파일 winnt.h에서 찾을 수 있다.
      • "SeDebugPrivilege" 권한을 활성화하기 위해 AdjustTokenPrivileges() API를 호출하여 토큰을 조정한다.
      • OpenProcessToken()으로 얻은 프로세스 토큰 핸들을 닫는다.

      권한 변경에 관한 보다 자세한 설명은 [10] "Using privilege"를 참조한다.

    4. Implanting through BHO add-ins(BHO 애드인을 통한 침투)
      Sometimes you will need to inject a custom code inside Internet Explorer only. Fortunately Microsoft provides an easy and well documented way for this purpose - Browser Helper Objects. A BHO is implemented as COM DLL and once it is properly registered, each time when IE is launched it loads all COM components that have implemented IObjectWithSite interface.

      때때로 인터넷 익스플로러에만 사용자 정의 코드를 침투시키는 것이 필요할 수 있다. 다행스럽게도 마이크로소프트는 이러한 목적에 부합하는 쉽고 잘 문서화된 Brower Helper Object라는 방법을 제공한다. BHO는 COM DLL로 구현되며 적절하게 등록되기만 하면 IE가 실행될 때마다 IE가 IObjectWithSite 인터페이스를 구현한 모든 COM 객체를 로드한다.

    5. MS Office add-ins(오피스 애드인)
      Similarly, to the BHOs, if you need to implant in MS Office applications code of your own, you can take the advantage of provided standard mechanism by implementing MS Office add-ins. There are many available samples that show how to implement this kind of add-ins.

      BHO와 유사하게 MS 오피스에 자신의 코드를 침투시키려면 MS 오피스 애드인으로 구현되는 표준 메카니즘의 장점을 이용할 수 있다. 이러한 종류의 애드인을 구현하는 방법을 설명하는 예제들은 많이 있다.

    Interception mechanisms(가로채기 메카니즘)

    Injecting a DLL into the address space of an external process is a key element of a spying system. It provides an excellent opportunity to have a control over process's thread activities. However it is not sufficient to have the DLL injected if you want to intercept API function calls within the process.

    다른 프로세스의 주소 공간에 DLL을 침투시키는 것은 스파이 시스템의 중요한 요소이다. 이것은 프로세스의 쓰레드 활동에 대해 제어할 수 있는 훌륭한 기회를 제공한다. 그러나 프로세스 내의 API 함수 호출을 가로채려면 DLL을 침투시키는 것만으로는 충분치 않다.

    This part of the article intends to make a brief review of several available real-world hooking aspects. It focuses on the basic outline for each one of them, exposing their advantages and disadvantages.

    지금부터 실제 후킹의 여러가지 형태에 대해 간단하게 살펴 보겠다. 각각의 기본적인 윤곽에 초점을 맞추고 장점과 단점을 설명하겠다.

    In terms of the level where the hook is applied, there are two mechanisms for API spying - Kernel level and User level spying. To get better understanding of these two levels you must be aware of the relationship between the Win32 subsystem API and the Native API. Following figure demonstrates where the different hooks are set and illustrates the module relationships and their dependencies on Windows 2K:

    후크가 적용되는 레벨의 측면에서 보면 API 후킹은 커널 레벨과 사용자 레벨의 2가지 메카니즘으로 구분된다. 이 2가지 레벨에 대한 이해를 돕기 위해 설명하자면 Win32 서브시스템 API와 네이티브 API의 관계를 알아야만 한다. 아래의 그림은 다른 종류의 후크가 설치되는 위치를 보여주며 윈도우즈 2K에서 모듈 간의 관계와 의존성을 표시하고 있다.

    Figure 2

    The major implementation difference between them is that interceptor engine for kernel-level hooking is wrapped up as a kernel-mode driver, whereas user-level hooking usually employs user-mode DLL.

    이들을 구현함에 있어 가장 중요한 차이점은 커널 레벨 후킹의 가로채기 엔진은 커널 모드 드라이버로 포장되지만 사용자 레벨 후킹은 일반적으로 사용자 모드 DLL을 사용한다는 것이다.

    1. NT Kernel level hooking(NT 커널 레벨 후킹)

      There are several methods for achieving hooking of NT system services in kernel mode. The most popular interception mechanism was originally demonstrated by Mark Russinovich and Bryce Cogswell in their article [3] "Windows NT System-Call Hooking". Their basic idea is to inject an interception mechanism for monitoring NT system calls just bellow the user mode. This technique is very powerful and provides an extremely flexible method for hooking the point that all user-mode threads pass through before they are serviced by the OS kernel.

      커널 모드에서 NT 시스템 서비스의 후킹을 하기 위해서는 몇가지 방법들이 있다. 가장 인기있는 가로채기 메카니즘은 Mark Russinovich와 Bryce Cogswell이 그들의 글 [3] "Windows NT System-Call Hooking"에서 처음 소개하였다. 그들의 기본 아이디어는 NT 시스템 호출을 감시하기 위한 가로채기 메카니즘을 단지 사용자 모드 하부에 침투시키는 것이다.(???)

      [역자주] 도저히 해석을 못하겠습니다. 일단 bellow(소리치다,울부짖다)가 아니라 below(~밑에)인 것 같은데...
      이 기술은 매우 강력하며 모든 사용자 모드 쓰레드들이 OS 커널에 의해 서비스되기 전에 거쳐야 하는 지점을 후킹하는 굉장히 유연한 방법을 제공한다.

      You can find an excellent design and implementation in "Undocumented Windows 2000 Secrets" as well. In his great book Sven Schreiber explains how to build a kernel-level hooking framework from scratch [5].

      "Undocumented Windows 2000 Secrets" [5]에서도 뛰어난 설계와 구현을 발견할 수 있다. 이 훌륭한 책에서 Sven Schreiber는 어떻게 커널 레벨 후킹 프레임웍을 구축하는가를 개략적으로 설명하였다.

      Another comprehensive analysis and brilliant implementation has been provided by Prasad Dabak in his book "Undocumented Windows NT" [17].

      또다른 폭넓은 분석과 명석한 구현을 Prasad Dabak의 책 "Undocumented Windows NT" [17]에서도 찾을 수 있다.

      However, all these hooking strategies, remain out of the scope of this article.

      하지만 이러한 모든 후킹 전략들은 이 글의 범위를 벗어난다.

    2. Win32 User level hooking(Win32 사용자 레벨 후킹)
      1. Windows subclassing.(윈도우즈 서브클래싱)
        This method is suitable for situations where the application's behavior might be changed by new implementation of the window procedure. To accomplish this task you simply call SetWindowLongPtr() with GWLP_WNDPROC parameter and pass the pointer to your own window procedure. Once you have the new subclass procedure set up, every time when Windows dispatches a message to a specified window, it looks for the address of the window's procedure associated with the particular window and calls your procedure instead of the original one.

        이 방법은 어플리케이션의 동작이 윈도우 프로시저를 새롭게 구현하여 변경될 수 있는 상황에 적합하다. 이 작업을 하기 위해서는 GWLP_WNDPROC를 매개변수로 하여 SetWindowLongPtr() 함수를 호출하고 새로운 윈도우 프로시저의 포인터를 넘겨 주기만 하면 된다. 일단 새로운 서브클래스 프로시저가 설정되면 윈도우즈는 메시지를 특정 윈도우로 보낼 때마다 특정 윈도우와 연관된 윈도우 프로시저의 주소를 파악해서 기존의 프로시저 대신 새로운 프로시저를 호출하게 된다.

        The drawback of this mechanism is that subclassing is available only within the boundaries of a specific process. In other words an application should not subclass a window class created by another process.

        Usually this approach is applicable when you hook an application through add-in (i.e. DLL / In-Proc COM component) and you can obtain the handle to the window whose procedure you would like to replace.

        For example, some time ago I wrote a simple add-in for IE (Browser Helper Object) that replaces the original pop-up menu provided by IE using subclassing.

        이 메카니즘의 단점은 서브클래싱이 하나의 특정 프로세스의 영역으로만 제한된다는 것이다. 바꿔 말하면 하나의 어플리케이션은 다른 어플리케이션에 의해 생성된 윈도우 클래스는 서브클래싱을 할 수가 없다.

        일반적으로 이러한 접근 방법은 애드인(i.e. DLL / In-Proc COM component)을 통해 후킹하고 교체하려는 프로시저의 윈도우 핸들을 구할 수 있는 경우에 유용하다.

        예를 들면, 얼마 전에 나는 IE가 사용하는 원래의 팝업 메뉴를 서브클래싱을 이용하여 교체하는 간단한 IE 애드인(BHO) 작성하였다.

      2. Proxy DLL (Trojan DLL)(대리자 DLL)
        An easy way for hacking API is just to replace a DLL with one that has the same name and exports all the symbols of the original one. This technique can be effortlessly implemented using function forwarders. A function forwarder basically is an entry in the DLL's export section that delegates a function call to another DLL's function.

        API를 해킹하는 쉬운 방법은 같은 이름을 가지고 원본과 동일한 익스포트 심볼을 가지는 DLL로 바꿔치는 것이다. 이 기술은 함수 포워더를 이용하여 쉽게 구현할 수 있다. 함수 포워더는 기본적으로 함수에 대한 호출을 다른 DLL의 함수로 위임하는 DLL 익스포트 섹션의 엔트리이다.

        You can accomplish this task by simply using #pragma comment:

        이 작업은 단순히 #pragma comment을 이용하여 구현할 수 있다:

        #pragma comment(linker, "/export:DoSomething=DllImpl.ActuallyDoSomething")

        However, if you decide to employ this method, you should take the responsibility of providing compatibilities with newer versions of the original library. For more details see [13a] section "Export forwarding" and [2] "Function Forwarders".

        그러나 이 방법을 사용하기로 한다면 원본 라이브러리와 새로운 버전이 호환되도록 유지하여야 한다. 자세한 내용은 [13a] "Export forwarding" 과 [2] "Function Forwarders"을 참조하기 바란다.

      3. Code overwriting(코드 덮어쓰기)
        There are several methods that are based on code overwriting. One of them changes the address of the function used by CALL instruction. This method is difficult, and error prone. The basic idea beneath is to track down all CALL instructions in the memory and replace the addresses of the original function with user supplied one.

        코드 덮어쓰기에 기초한 몇가지 방법들이 있다. 그중 하나는 CALL 명령에 의해 사용되는 함수의 주소를 변경하는 것이다. 이 방법은 어렵고 오류를 발생시키기 쉽다. 기본적인 아이디어는 메모리 상의 모든 CALL 명령을 추적하고 원본 함수의 주소를 사용자가 정의한 주소로 바꾸는 것이다.

        [역자주] 이 부분은 본인이 어셈블리에 대한 지식이 미흡하고 어차피 내용 자체가 이 글의 주제를 벗어나기 때문에 대강 해석하였습니다.

        Another method of code overwriting requires a more complicated implementation. Briefly, the concept of this approach is to locate the address of the original API function and to change first few bytes of this function with a JMP instruction that redirects the call to the custom supplied API function. This method is extremely tricky and involves a sequence of restoring and hooking operations for each individual call. It's important to point out that if the function is in unhooked mode and another call is made during that stage, the system won't be able to capture that second call.
        The major problem is that it contradicts with the rules of a multithreaded environment.

        However, there is a smart solution that solves some of the issues and provides a sophisticated way for achieving most of the goals of an API interceptor. In case you are interested you might peek at [12] Detours implementation.

        코드 덮어쓰기의 또다른 방법은 보다 복잡한 구현을 필요로 한다. 간단하게 설명하면 이 접근의 개념은 원본 API 함수의 주소를 파악하고 이 함수의 첫번째 몇 바이트를 사용자 정의 API 함수로 연결시키는 JMP 명령으로 변경하는 것이다. 이 방법은 매우 교묘하고 각각의 개별적인 호출에 대해 일련의 복원과 후킹을 반복하게 한다. 만일 함수가 후크되지 않은 상태에서 다른 호출이 발생하면 시스템은 두번째 호출을 가로챌 수 없다는 것은 매우 중요하다.
        가장 큰 문제점은 멀티쓰레드 환경의 규칙에 위배된다는 것이다.

        하지만 이러한 몇가지 문제점들을 해결하고 API 가로채기의 대부분의 목적을 달성할 수 있도록 하는 세련된 방법이 있다. 만약 관심이 있다면 [12] Detours implementation를 참조하기 바란다.

      4. Spying by a debugger(디버거를 이용한 스파이)
        An alternative to hooking API functions is to place a debugging breakpoint into the target function. However there are several drawbacks for this method. The major issue with this approach is that debugging exceptions suspend all application threads. It requires also a debugger process that will handle this exception. Another problem is caused by the fact that when the debugger terminates, the debugger is automatically shut down by Windows.

        API 함수를 후킹하는 또다른 방법은 디버깅 멈춤점을 목표로 하는 함수에 위치시키는 것이다. 하지만 이 방법에는 몇가지 단점들이 있다. 이 방식의 가장 큰 문제는 디버깅 예외가 모든 어플리케이션 쓰레드를 대기시킨다는 것이다. 이것은 또한 예외를 조작할 디버거 프로세스를 필요로 한다. 또다른 문제는 디버거가 종료할 때 윈도우즈가 자동으로 디버거를 끝낸다는 사실에 기인한다.(???)

        [역자주] 이 부분 역시 의미를 잘 모르겠고 내용 자체가 이 글의 주제를 벗어나기 때문에 대강 해석하였습니다.
      5. Spying by altering of the Import Address Table(임포트 주소 테이블 수정을 이용한 스파이)
        This technique was originally published by Matt Pietrek and than elaborated by Jeffrey Ritcher ([2] "API Hooking by Manipulating a Module's Import Section") and John Robbins ([4] "Hooking Imported Functions"). It is very robust, simple and quite easy to implement. It also meets most of the requirements of a hooking framework that targets Windows NT/2K and 9x operating systems. The concept of this technique relies on the elegant structure of the Portable Executable (PE) Windows file format. To understand how this method works, you should be familiar with some of the basics behind PE file format, which is an extension of Common Object File Format (COFF). Matt Pietrek reveals the PE format in details in his wonderful articles - [6] "Peering Inside the PE.", and [13a/b] "An In-Depth Look into the Win32 PE file format". I will give you a brief overview of the PE specification, just enough to get the idea of hooking by manipulation of the Import Address Table.

        이 기술은 Matt Pietrek이 처음 발표하였고 그후에 Jeffrey Ritcher ([2] "API Hooking by Manipulating a Module's Import Section") 와 John Robbins ([4] "Hooking Imported Functions")에 의해 다듬어졌다. 이 방법 매우 견실하며 단순하고 구현하기 상당히 쉬운 방법이다. 또한 NT/2k와 9x 운영체제를 모두 지원하는 후킹 프레임웍의 요구사항의 대부분을 만족시킬 수 있다. 이 기술의 개념은 PE(Portable Executable) 윈도우즈 파일 포맷의 우아한 구조에 의존한다. 이 방법이 어떻게 적용되는가를 이해하려면 Common Object File Format(COFF)의 확장 형태인 PE 파일 포맷에 대한 기본 지식에 친숙해져야 한다. Matt Pietrek은 그의 멋진 글인 [6] "Peering Inside the PE." 와 [13a/b] "An In-Depth Look into the Win32 PE file format"에서 PE 포맷의 베일을 벗겼다. PE 특성의 전반적인 설명만으로도 임포트 주소 테이블을 조작하여 후킹을 구현하는 아이디어를 얻을 수 있다.

        In general an PE binary file is organized, so that it has all code and data sections in a layout that conform to the virtual memory representation of an executable. PE file format is composed of several logical sections. Each of them maintains specific type of data and addresses particular needs of the OS loader.

        일반적으로 PE 이진 파일이 생성되면 실행시의 가상 메모리 구조를 따르는 형태의 코드와 데이터 섹션을 갖게 된다. PE 파일 포맷은 몇가지 논리적인 섹션으로 구성된다. 그것들 각각은 특정 유형의 데이터를 유지하고 OS 로더에게 특별한 요구를 지시한다.(???)

        The section .idata, I would like to focus your attention on, contains information about Import Address Table. This part of the PE structure is particularly very crucial for building a spy program based on altering IAT.
        Each executable that conforms with PE format has layout roughly described by the figure below.

        .idata 섹션은 특별히 관심을 기울여야 하는데 이것은 임포트 주소 테이블(IAT)에 관한 정보를 담고 있다. PE 구조의 이 부분은 IAT를 변경을 기반으로 하는 스파이 프로그램을 작성하는데 매우 중요하다.
        아래의 그림은 PE 포맷의 실행 파일 구조를 개략적으로 나타내고 있다.

        Figure 3

        The program loader is responsible for loading an application along with all its linked DLLs into the memory. Since the address where each DLL is loaded into, cannot be known in advance, the loader is not able to determine the actual address of each imported function. The loader must perform some extra work to ensure that the program will call successfully each imported function. But going through each executable image in the memory and fixing up the addresses of all imported functions one by one would take unreasonable amount of processing time and cause huge performance degradation. So, how does the loader resolves this challenge? The key point is that each call to an imported function must be dispatched to the same address, where the function code resides into the memory. Each call to an imported function is in fact an indirect call, routed through IAT by an indirect JMP instruction. The benefit of this design is that the loader doesn't have to search through the whole image of the file. The solution appears to be quite simple - it just fixes-up the addresses of all imports inside the IAT. Here is an example of a snapshot PE File structure of a simple Win32 Application, taken with the help of the [8] PEView utility. As you can see TestApp import table contains two imported by GDI32.DLL function - TextOutA() and GetStockObject().

        프로그램 로더는 어플리케이션을 로드하면서 어플리케이션에 링크된 DLL들을 함께 메모리에 로드한다. 각각의 DLL이 로드되는 주소는 미리 알 수 없기 때문에 로더는 임포트된 각각의 함수들의 실제 주소를 알지 못한다. 로더는 프로그램이 임포트된 함수를 성공적으로 호출할 수 있도록 별도의 작업을 수행하여야만 한다. 하지만 메모리 상의 실행 이미지 각각을 훝으면서 모든 임포트된 함수의 주소를 하나하나 수정하는 것은 과도한 처리 시간을 요구하고 엄청난 성능 저하를 유발한다. 그렇다면 로더는 이러한 문제를 어떻게 해결할까? 중요한 사실은 임포트된 함수에 대한 각각의 호출이 메모리 상에서 함수 코드가 위치하는 동일한 주소로 전달되어야만 한다는 것이다. 임포트된 함수에 대한 각각의 호출은 사실상 IAT를 거쳐 간접 JMP 명령을 통하는 간접적인 호출이다. 이러한 디자인의 이점은 로더가 파일의 모든 이미지를 훝지 않아도 된다는 것이다. 해결책이 약간 단순해 보인다. 단지 IAT 내부의 모든 임포트 주소를 수정하기만 하는 것이다. 아래에 간단한 Win32 어플리케이션의 PE 파일 구조의 형태를 [8] PEView utility를 사용하여 보여주는 예가 있다. TestApp 임포트 테이블이 GDI32.DLL의 2개 함수, TextOutA()GetStockObject()를 포함하는 것을 확인할 수 있다.

        Figure 4

        Actually the hooking process of an imported function is not that complex as it looks at first sight. In a nutshell an interception system that uses IAT patching has to discover the location that holds the address of imported function and replace it with the address of an user supplied function by overwriting it. An important requirement is that the newly provided function must have exactly the same signature as the original one. Here are the logical steps of a replacing cycle:
        • Locate the import section from the IAT of each loaded by the process DLL module as well as the process itself
        • Find the IMAGE_IMPORT_DESCRIPTOR chunk of the DLL that exports that function. Practically speaking, usually we search this entry by the name of the DLL
        • Locate the IMAGE_THUNK_DATA which holds the original address of the imported function
        • Replace the function address with the user supplied one

        임포트된 함수를 후킹하는 과정은 처음 보았을 때 느끼는 것처럼 복잡하지 않다. 간단히 말하면 IAT를 수정하는 후킹 시스템은 임포트된 함수의 주소를 가지고 있는 위치를 찾아 사용자 정의 함수의 주소로 덮어써서 바꿔 주는 것이다. 이 과정에서 중요한 요구 사항은 새로 제공하는 함수가 기존의 함수와 동일한 형태이어야 한다는 것이다. 아래에 교체 싸이클의 논리적 단계를 설명하였다.

        • 프로세스와 프로세스가 로드한 DLL 모듈의 각각의 IAT에서 임포트 섹션을 찾는다.
        • DLL에서 함수를 익스포트하는 IMAGE_IMPORT_DESCRIPTOR 청크를 찾는다. 실제로는 이 엔트리를 DLL의 이름으로 찾는다.
        • 임포트된 함수의 원래 주소를 가지고 있는 IMAGE_THUNK_DATA를 찾는다.
        • 사용자 정의 함수의 주소로 함수의 주소를 바꾼다.

        By changing the address of the imported function inside the IAT, we ensure that all calls to the hooked function will be re-routed to the function interceptor.

        IAT 내부의 임포트된 함수의 주소를 변경함으로서 후킹된 함수에 대한 호출은 새로운 함수로 연결되게 된다.

        Replacing the pointer inside the IAT is that .idata section doesn't necessarily have to be a writable section. This requires that we must ensure that .idata section can be modified. This task can be accomplished by using VirtualProtect() API.

        IAT 내부의 포인터를 바꾸기 위해서 .idata 섹션이 반드시 쓰기 가능할 필요는 없다. .idata 섹션이 수정 가능하다는 것을 확신하는 것이 필요하다. 이 작업은 VirtualProtect() API를 사용하여 수행할 수 있다.

        Another issue that deserves attention is related to the GetProcAddress() API behavior on Windows 9x system. When an application calls this API outside the debugger it returns a pointer to the function. However if you call this function within from the debugger it actually returns different address than it would when the call is made outside the debugger. It is caused by the fact that that inside the debugger each call to GetProcAddress() returns a wrapper to the real pointer. Returned by GetProcAddress() value points to PUSH instruction followed by the actual address. This means that on Windows 9x when we loop through the thunks, we must check whether the address of examined function is a PUSH instruction (0x68 on x86 platforms) and accordingly get the proper value of the address function.

        관심을 가져야 하는 또다른 문제점은 윈도우즈9x에서 GetProcAddress() API의 동작과 관련이 있다. 어플리케이션이 이 API를 디버거 외부에서 호출하면 이 API는 함수에 대한 포인터를 반환한다. 하지만 디버거 내에서 이 함수를 호출하면 디버거 외부에서 호출할 때 반환하는 주소와 다른 값을 반환한다. 이러한 현상은 디버거 내에서의 GetProcAddress() 호출이 실제 포인터를 변환(wrapper)해서 반환하기 때문에 발생한다. GetProcAddress()가 반환하는 주소값은 PUSH 명령과 실제 주소가 나오는 위치를 가르킨다. 이것은 윈도우즈 9x에서는 청크를 순회할 때 반드시 검사한 함수의 주소가 PUSH 명령 (0x68 on x86 platforms)인가를 체크하여 주소 함수의 적합한 값을 얻어야 한다는 것을 의미한다.

        Windows 9x doesn't implement copy-on-write, thus operating system attempts to keep away the debuggers from stepping into functions above the 2-GB frontier. That is the reason why GetProcAddress() returns a debug thunk instead of the actual address. John Robbins discusses this problem in [4] "Hooking Imported Functions".

        윈도우즈 9x는 copy-on-write를 지원하지 않으므로 운영체제는 디버거가 2-GB 한계 상단의 함수에 접근하지 못하도록 만든다.

        [역자주] copy-on-write는 두개의 프로세스가 동일한 자원을 읽기 용도로만 사용한다면 자원을 공유하여 사용하다가 하나의 프로세스가 자원에 대해 쓰기 시도를 하면 자원의 복사본을 만들어서 사용하는 것을 의미합니다. 아마도 성능 향상을 위해 운영체제가 채택하는 기술로 생각되며 "쓰기 위에 복사하기"가 아니라 "쓰면 복사한다"로 이해하면 됩니다.
        이것이 GetProcAddress()가 실제 주소 대신 디버그 청크를 반환하는 이유이다. John Robbins는 이 문제에 대하여 [4] "Hooking Imported Functions".에서 다루었다.

    [역자주] 여기까지의 내용을 요약해 보면 다음과 같습니다.

    1. API 후킹을 위해서는 기본적으로 서버 프로그램과 드라이버 DLL이 필요하다.
    2. 서버 프로그램은 DLL을 다른 프로세스에 침투시키는 역할을 한다.
    3. 드라이버 DLL은 프로세스 내의 API 호출을 가로채는 역할을 한다.
    4. 침투시키는 방법은 여러 가지가 있지만 윈도우즈 메시지 후크를 사용하는 방법과 CreateRemoteThread를 사용하는 방법이 제일 유리하다.
    5. API 호출을 가로채는 방법도 여러 가지가 있지만 IAT를 수정하는 방법이 제일 유리하다.
    6. 모든 OS를 지원하려면 서버는 메시지 후크를 이용하는 방법을 사용하고 드라이버는 IAT를 수정하는 방법을 채택해야 한다. (뒤에 나오겠지만 CreateRemoteThread를 이용하는 방법은 생각보다 훨씬 복잡합니다.)

    Figuring out when to inject the hook DLL(후킹 DLL 침투 과정의 이해)

    That section reveals some challenges that are faced by developers when the selected injection mechanism is not part of the operating system's functionality. For example, performing the injection is not your concern when you use built-in Windows Hooks in order to implant a DLL. It is an OS's responsibility to force each of those running processes that meet the requirements for this particular hook, to load the DLL [18]. In fact Windows keeps track of all newly launched processes and forces them to load the hook DLL. Managing injection through registry is quite similar to Windows Hooks. The biggest advantage of all those "built-in" methods is that they come as part of the OS.

    지금까지의 섹션에서 선택한 침투 메카니즘이 운영체제가 제공하는 기능이 아닌 경우 개발자들이 직면하게 되는 몇가지 문제점들에 대해 설명하였다. 예를 들면, DLL을 주입시키기 위해 운영체제가 제공하는 윈도우즈 후크를 사용한다면 침투를 수행하는 것은 관심의 대상이 아니다. 특정 후크의 조건에 부합하는 실행 프로세스가 DLL을 로드[18]하도록 하는 것은 운영체제의 몫이다. 사실상 윈도우즈 새로 적재된 모든 프로세스를 추적하고 프로세스들이 후킹 DLL을 로드하도록 강제한다. 레지스트리를 통한 침투 관리는 윈도우즈 후크와 약간 유사하다. 운영체제가 제공하는 모든 방법들의 가장 큰 장점은 운영체제의 일부분으로서 제공된다는 것이다.

    Unlike the discussed above implanting techniques, to inject by CreateRemoteThread() requires maintenance of all currently running processes. If the injecting is made not on time, this can cause the Hook System to miss some of the calls it claims as intercepted. It is crucial that the Hook Server application implements a smart mechanism for receiving notifications each time when a new process starts or shuts down. One of the suggested methods in this case, is to intercept CreateProcess() API family functions and monitor all their invocations. Thus when an user supplied function is called, it can call the original CreateProcess() with dwCreationFlags OR-ed with CREATE_SUSPENDED flag. This means that the primary thread of the targeted application will be in suspended state, and the Hook Server will have the opportunity to inject the DLL by hand-coded machine instructions and resume the application using ResumeThread() API. For more details you might refer to [2] "Injecting Code with CreateProcess()".

    상기의 침투 기술과 달리 CreateRemoteThread()를 이용하는 침투는 현재 실행 중인 모든 프로세스에 대한 관리를 요구한다. 만일 침투가 적절한 때에 이루어지지 않는다면 후크 시스템이 가로채기로 지정한 호출의 일부를 수행하지 못할 수가 있다. 후킹 서버 어플리케이션이 새로운 프로세스가 시작되거나 종료할 때마다 통보를 받을 수 있도록 세련된 메카니즘을 구현하는 것은 매우 중요하다. 이러한 경우 제안되는 방법 중의 하나가 CreateProcess() API 패밀리의 함수를 가로채서 감시하는 것이다. 그리고 후킹된 사용자 정의 함수가 호출될 때 원본 CreateProcess() 함수를 dwCreationFlagsCREATE_SUSPENDED 플래그를 OR 연산하여 호출한다. 이것은 목표로 하는 어플리케이션의 프라이머리 쓰레드를 대기 상태로 하여 후크 서버가 DLL을 침투시키도록 하고 ResumeThread() API로 어플리케이션을 기동시키는 것을 의미한다. 보다 자세한 정보를 원한다면 레퍼런스의 [2] "Injecting Code with CreateProcess()"를 참조하라.

    The second method of detecting process execution, is based on implementing a simple device driver. It offers the greatest flexibility and deserves even more attention. Windows NT/2K provides a special function PsSetCreateProcessNotifyRoutine() exported by NTOSKRNL. This function allows adding a callback function, that is called whenever a process is created or deleted. For more details see [11] and [15] from the reference section.

    프로세스의 실행을 감지하는 두번째 방법은 간단한 디바이스 드라이버를 구현하는 것이다. 이 방법은 가장 유연하고 큰 관심을 가질 만하다. 윈도우즈 NT/2K는 NTOKKRNL에서 익스포트된 PsSetCreateProcessNotifyRoutine()라는 특별한 함수를 제공한다. 이 함수는 프로세스의 생성이나 소멸 시에 호출되는 콜백 함수를 추가하도록 해준다. 보다 자세한 내용은 레퍼런스의 [11]과 [15]를 참조하라.

    Enumerating processes and modules(프로세스와 모듈을 나열하기)

    Sometimes we would prefer to use injecting of the DLL by CreateRemoteThread() API, especially when the system runs under NT/2K. In this case when the Hook Server is started it must enumerate all active processes and inject the DLL into their address spaces. Windows 9x and Windows 2K provide a built-in implementation (i.e. implemented by Kernel32.dll) of Tool Help Library. On the other hand Windows NT uses for the same purpose PSAPI library. We need a way to allow the Hook Server to run and then to detect dynamically which process "helper" is available. Thus the system can determine which the supported library is, and accordingly to use the appropriate APIs.

    때때로 CreateRemoteThread() API를 이용하여 DLL을 침투시키는 방법이 선호된다. 특히 시스템이 NT/2K인 경우에 그러하다. 이 경우, 후크 서버가 시작될 때 모든 활성 프로세스를 나열하고 각각의 프로세스의 주소 공간에 DLL을 침투시켜야 한다. 윈도우즈 9x와 2K는 Tool Help Library의 내장 구현(Kernel32.dll로 구현된)을 제공한다. 윈도우즈 NT에서는 PSAPI 라이브러리를 같은 목적으로 사용할 수 있다. 그러므로 후크 서버는 실행된 후 어떤 프로세스 헬퍼 라이브러리를 사용할 수 있지를 판단하여 적절한 API들을 사용할 수 있도록 만들어져야 한다.

    I will present an object-oriented architecture that implements a simple framework for retrieving processes and modules under NT/2K and 9x [16]. The design of my classes allows extending the framework according to your specific needs. The implementation itself is pretty straightforward.

    이제부터 NT/2K와 9x [16] 환경에서 프로세스와 모듈을 추출하는 간단한 프레임웍에 대한 객체 지향 구조에 대해 설명하겠다. 이 클래스들의 디자인은 특정 요구 사항에 맞춰 확장이 용하도록 되어있다. 구현 자체는 매우 수월하다.

    CTaskManager implements the system's processor. It is responsible for creating an instance of a specific library handler (i.e. CPsapiHandler or CToolhelpHandler) that is able to employ the correct process information provider library (i.e. PSAPI or ToolHelp32 respectively). CTaskManager is in charge of creating and marinating a container object that keeps a list with all currently active processes. After instantiating of the CTaskManager object the application calls Populate() method. It forces enumerating of all processes and DLL libraries and storing them into a hierarchy kept by CTaskManager's member m_pProcesses.

    Following UML diagram shows the class relationships of this subsystem:

    CTaskManager는 시스템의 프로세서를 담당한다. 이 클래스는 운영체제가 제공하는 라이브러리((i.e. PSAPI or ToolHelp32 respectively)를 취사선택하여 특정 라이브러리 핸들러(CPsapiHandler or CToolhelpHandler)의 인스턴스를 생성하는 역할을 한다. CTaskManager는 현재 활성화된 모든 프로세스의 리스트를 유지하는 컨테이너 객체의 생성을 관리한다. CTaskManager 객체가 생성된 후에는 어플리케이션은 Populate() 메소드를 호출할 수 있다. 이 메소드는 모든 프로세스와 DLL 라이브러리를 나열하고 그 정보를 CTaskManager의 멤버 변수인 m_pProcesses에 계층적으로 저장한다.

    아래의 UML 다이어그램은 이러한 서브 시스템의 클래스 관계를 보여준다.

    Figure 5

    It is important to highlight the fact that NT's Kernel32.dll doesn't implement any of the ToolHelp32 functions. Therefore we must link them explicitly, using runtime dynamic linking. If we use static linking the code will fail to load on NT, regardless whether or not the application has attempted to execute any of those functions. For more details see my article "Single interface for enumerating processes and modules under NT and Win9x/2K.".

    NT의 Kernel32.dll이 ToolHelp32 함수의 어떤 것도 구현하지 않는다는 것은 매우 중요한 사실이다. 그러므로 실행시 동적 링크를 이용하여 명시적으로 DLL들을 링크하여야 한다. 만일 정적 링크를 사용한다면 어플리케이션이 그 함수들을 사용하는가에 무관하게 그 코드는 NT에서 로드에 실패할 것이다. 보다 자세한 내용은 "Single interface for enumerating processes and modules under NT and Win9x/2K."을 참조하기 바란다.

    Requirements of the Hook Tool System(후크 툴 시스템의 요구 사항)

    Now that I've made a brief introduction to the various concepts of the hooking process it's time to determine the basic requirements and explore the design of a particular hooking system. These are some of the issues addressed by the Hook Tool System:

    지금까지 후킹 프로세스의 다양한 개념들을 간략하게 설명하였다. 이제부터는 기본 요구 사항을 결정하고 후킹 시스템을 설계하는 것을 연구해 보겠다. 다음의 사항들은 후크 툴 시스템에서 제기되는 이슈들이다:

    • Provide a user-level hooking system for spying any Win32 API functions imported by name
    • Provide the abilities to inject hook driver into all running processes by Windows hooks as well as CreateRemoteThread() API. The framework should offer an ability to set this up by an INI file
    • Employ an interception mechanism based on the altering Import Address Table
    • Present an object-oriented reusable and extensible layered architecture
    • Offer an efficient and scalable mechanism for hooking API functions
    • Meet performance requirements
    • Provide a reliable communication mechanism for transferring data between the driver and the server
    • Implement custom supplied versions of TextOutA/W() and ExitProcess() API functions
    • Log events to a file
    • The system is implemented for x86 machines running Windows 9x, Me, NT or Windows 2K operating system
    • 이름으로 임포트된 어떤 Win32 API 함수도 후킹할 수 있는 사용자 레벨 후킹 시스템을 제공한다.
    • CreateRemoteThread() API뿐 아니라 윈도우즈 후크를 사용하여 모든 실행 프로세스에 후크 드라이버를 침투시킬 수 있도록 한다. 프렉임웍은 이것을 INI 파일로 설정할 수 있어야 한다.
    • 임포트 주소 테이블을 변경하는 것에 기초한 가로채기 메카니즘을 사용한다.
    • 객체 지향적인 재사용 가능하고 확장성 있는 계층 구조를 사용한다.
    • API 함수를 후킹하는 효율적이고 단계적인 메카니즘을 제공한다.
    • 성능적인 요구 사항을 충족시킨다.
    • 드라이버와 서버 간의 데이터 전송에 신뢰할 수 있는 교환 메카니즘을 제공한다.
    • TextOutA/W()ExitProcess() API 함수에 대한 사용자 정의 버전을 구현한다.
    • 이벤트를 파일에 기록한다.
    • 시스템은 윈도우즈 9x, Me, NT or 윈도우즈 2K를 운영체제로 하는 x86 머신에 대해 구현한다.

    Design and implementation(설계와 구현)

    This part of the article discusses the key components of the framework and how do they interact each other. This outfit is capable to capture any kind of WINAPI imported by name functions.

    이 섹션에서는 프레임웍의 주요 컴포넌트와 서로 간의 상호 작용에 대해 살펴 보겠다. 이 시스템은 이름으로 임포트되는 어떠한 종류의 WINAPI 함수도 가로챌 수가 있다.

    Before I outline the system's design, I would like to focus your attention on several methods for injecting and hooking.

    시스템의 설계를 요점을 설명하기 전에 침투와 후킹의 여러 방법들에 관심을 기울이기 바란다.

    First and foremost, it is necessary to select an implanting method that will meet the requirements for injecting the DLL driver into all processes. So I designed an abstract approach with two injecting techniques, each of them applied accordingly to the settings in the INI file and the type of the operating system (i.e. NT/2K or 9x). They are - System-wide Windows Hooks and CreateRemoteThread() method. The sample framework offers the ability to inject the DLL on NT/2K by Windows Hooks as well as to implant by CreateRemoteThread() means. This can be determined by an option in the INI file that holds all settings of the system.

    무엇보다 먼저 DLL 드라이버를 모든 프로세스에 침투시키는 요구 사항을 충족하는 주입 방법을 선택하는 것이 필요하다. 그래서 INI 파일의 설정과 운영체제(i.e. NT/2K or 9x)의 종류에 따라 적용할 2개의 침투 기술을 가지고 추상적인 접근을 설계했다. 그것은 시스템 전역 윈도우즈 후크와 CreateRemoteThread()이다. 예제 프레임웍은 CreateRemoteThread() 함수를 사용해서 주입할 수 있을 뿐 아니라 NT/2K에서 윈도우즈 후크를 사용하여 DLL을 침투시킬 수도 있는 능력을 제공한다. 어떠한 것을 사용할 것인가는 시스템의 모든 설정을 가지고 있는 INI 파일의 설정에 따라 결정된다.

    Another crucial moment is the choice of the hooking mechanism. Not surprisingly, I decided to apply altering IAT as an extremely robust method for Win32 API spying.

    또다른 중요한 결정은 후킹 메카니즘을 선택하는 것이다. 당연히 Win32 API를 후킹하는 매우 견실한 방법인 IAT 변경을 적용하기로 결정하였다.

    To achieve desired goals I designed a simple framework composed of the following components and files:

    • TestApp.exe - a simple Win32 test application that just outputs a text using TextOut() API. The purpose of this app is to show how it gets hooked up.
    • HookSrv.exe - control program
    • HookTool .DLL - spy library implemented as Win32 DLL
    • HookTool.ini - a configuration file
    • NTProcDrv.sys - a tiny Windows NT/2K kernel-mode driver for monitoring process creation and termination. This component is optional and addresses the problem with detection of process execution under NT based systems only.

    요구되는 목표를 달성하기 위해 다음의 콤포넌트와 파일로 구성되는 단순한 프레임웍을 설계하였다:

    • TestApp.exe - TextOut() API를 사용하여 텍스트를 출력하는 단순한 Win32 테스트 어플리케이션. 이 어플리케이션의 목적은 어떻게 후킹이 되는가를 보여주는 것이다.
    • HookSrv.exe - 제어 프로그램.
    • HookTool .DLL - Win32 DLL로 구현된 스파이 라이브러리.
    • HookTool.ini - 설정 파일.
    • NTProcDrv.sys - 프로세스의 생성과 소멸을 감시하는 작은 윈도우즈 NT/2K 커널 모드 드라이버. 이 컴포넌트는 선택 사항이며 NT 시스템에서만 프로세스의 실행을 감지하여 문제점을 찾아낸다.
      [역자주] 이 드라이버를 컴파일하기 위해서는 윈도우즈 DDK(Device Driver Kit)가 설치되어 있어야 한다. DDK는 무료가 아니므로 MSDN 가입자가 아니면 마이크로소프트에서 다운로드 받을 수가 없다. 하지만 이 드라이버는 모니터하는 용도의 선택 사항이므로 후킹 예제를 테스트하기 위해 반드시 컴파일이 필요하지는 않다.

    HookSrv is a simple control program. Its main role is to load the HookTool.DLL and then to activate the spying engine. After loading the DLL, the Hook Server calls InstallHook() function and passes a handle to a hidden windows where the DLL should post all messages to.

    HookSrv는 단순한 제어 프로그램이다. 이것의 주요 임무는 HookTool.DLL을 로드하여 스파이 엔진을 활성화시키는 것이다. DLL을 로드한 후에 후크 서버는 InstallHook() 함수를 호출하고 DLL이 모든 메시지를 전달해야 하는 숨겨진 윈도우에 핸들을 넘겨준다.

    HookTool.DLL is the hook driver and the heart of presented spying system. It implements the actual interceptor and provides three user supplied functions TextOutA/W() and ExitProcess() functions.

    HookTool.DLL은 후크 드라이버이고 스파이 시스템의 핵심이다. 이것은 실질적인 가로채기를 구현하고 TextOutA/W()ExitProcess()에 대한 3개의 사용자 정의 함수를 제공한다.

    Although the article emphasizes on Windows internals and there is no need for it to be object-oriented, I decided to encapsulate related activities in reusable C++ classes. This approach provides more flexibility and enables the system to be extended. It also benefits developers with the ability to use individual classes outside this project.

    이 글에서 윈도우즈 내부적인 측면에 대해 강조하였고 반드시 시스템이 객체 지향이어야 할 이유는 없지만 상호 간의 동작을 재사용 가능한 C++ 클래스에 캡슐화하기로 결정하였다. 이러한 접근은 더많은 유연성을 제공하고 시스템이 확장 가능하도록 만들어 준다. 또한 개발자가 다른 프로젝트에서도 개별적인 클래스를 사용할 수 있다는 장점이 있다.

    Following UML class diagram illustrates the relationships between set of classes used in HookTool.DLL's implementation.

    아래의 UML 다이어그램은 HookTool.DLL의 구현에 사용되는 일련의 클래스들의 관계를 나타낸다.

    [역자주] 저자는 이 글의 목표를 범용적인 프레임웍의 설계에 두었고 클래스 설계에 싱클턴 패턴이나 템플릿 메쏘드 패턴 같은 디자인 패턴을 많이 적용하였기 때문에 객체 지향 설계나 디자인 패턴에 대한 지식이 없다면 구조나 흐름을 이해하기가 상당히 어렵습니다. 기회가 된다면 이 예제를 조금 단순화시킨 프로그램을 만들어서 올리도록 하겠습니다.

    Figure 6

    In this section of the article I would like to draw your attention to the class design of the HookTool.DLL. Assigning responsibilities to the classes is an important part of the development process. Each of the presented classes wraps up a specific functionality and represents a particular logical entity.

    이 섹션에서는 HookTool.DLL의 클래스 설계에 관심을 가지기 바란다. 클래스에 역할을 할당하는 것은 개발 과정에서 매우 중요한 부분이다. 제시된 클래스들 각각은 특별한 기능을 감추고 있고 특별한 논리적 개체로 표현된다.

    CModuleScope is the main doorway of the system. It is implemented using "Singleton" pattern and works in a thread-safe manner. Its constructor accepts 3 pointers to the data declared in the shared segment, that will be used by all processes. By this means the values of those system-wide variables can be maintained very easily inside the class, keeping the rule for encapsulation.

    CModuleScope는 시스템의 주 출입구이다. 이 클래스는 싱글턴 패턴을 사용하여 구현되었고 thread-safe한 방식으로 동작한다. 이 클래스의 생성자는 공유 세그먼트에 선언되어 있는 3개의 데이터에 대한 포인터를 매개변수로 넘겨받고 이 데이터는 모든 프로세스에서 사용하게 된다. 이것은 시스템 전역 변수의 값이 클래스 내부에서 매우 쉽게 관리된다는 것을 의미하고 캡슐화의 규칙을 유지하게 된다.

    When an application loads the HookTool library, the DLL creates one instance of CModuleScope on receiving DLL_PROCESS_ATTACH notification. This step just initializes the only instance of CModuleScope. An important piece of the CModuleScope object construction is the creation of an appropriate injector object. The decision which injector to use will be made after parsing the HookTool.ini file and determining the value of UseWindowsHook parameter under [Scope] section. In case that the system is running under Windows 9x, the value of this parameter won't be examined by the system, because Window 9x doesn't support injecting by remote threads.

    어떤 어플리케이션이 HookTool 라이브러리를 로드할 때 DLL은 DLL_PROCESS_ATTACH 통지를 받고 CModuleScope 객체를 하나 생성한다. CModuleScope 객체 생성의 중요한 부분은 적합한 침투 객체를 생성하는 것이다. 어떤 침투 클래스를 사용할 것인가는 HookTool.ini 파일을 읽어 [Scope] 섹션의 UseWindowsHook 항목의 값을 확인한 후에 결정된다. 시스템이 윈도우즈 9x에서 실행되는 경우에는 리모트 쓰레드를 사용하는 침투가 지원되지 않으므로 이 항목은 시스템이 무시한다.

    After instantiating of the main processor object, a call to ManageModuleEnlistment() method will be made. Here is a simplified version of its implementation:

    주 처리 객체가 생성된 후에 ManageModuleEnlistment() 메소드를 호출하게 된다. 아래에 이 메소드의 구현을 단순화한 소스가 있다.

    // Called on DLL_PROCESS_ATTACH DLL notification
    BOOL CModuleScope::ManageModuleEnlistment()
    {
    	BOOL bResult = FALSE;
    	// Check if it is the hook server 
    	if (FALSE == *m_pbHookInstalled)
    	{
    		// Set the flag, thus we will know that the server has been installed
    		*m_pbHookInstalled = TRUE;
    		// and return success error code
    		bResult = TRUE;
    	}
    	// and any other process should be examined whether it should be
    	// hooked up by the DLL
    	else
    	{
    		bResult = m_pInjector->IsProcessForHooking(m_szProcessName);
    		if (bResult)
    			InitializeHookManagement();
    	}
    	return bResult;
    }
    

    The implementation of the method ManageModuleEnlistment() is straightforward and examines whether the call has been made by the Hook Server, inspecting the value m_pbHookInstalled points to. If an invocation has been initiated by the Hook Server, it just sets up indirectly the flag sg_bHookInstalled to TRUE. It tells that the Hook Server has been started.

    ManageModuleEnlistment() 메소드의 구현은 간단하다. m_pbHookInstalled가 가르키는 값을 검사하여 후크 서버에 의한 호출인가를 확인한다. 만일 후크 서버에 의한 실행이라면 단순히 sg_bHookInstalled를 TRUE로 설정한다. 이것은 후크 서버가 이미 시작되었음을 나타낸다.

    The next action taken by the Hook Server is to activate the engine through a single call to InstallHook() DLL exported function. Actually its call is delegated to a method of CModuleScope - InstallHookMethod(). The main purpose of this function is to force targeted for hooking processes to load or unload the HookTool.DLL.

    후크 서버가 취하는 다음 행동은 DLL의 익스포트된 함수인 InstallHook()를 한번 호출하여 엔진을 활성화시키는 것이다.

     // Activate/Deactivate hooking
    engine BOOL	CModuleScope::InstallHookMethod(BOOL bActivate, HWND hWndServer)
    {
    	BOOL bResult;
    	if (bActivate)
    	{
    		*m_phwndServer = hWndServer;
    		bResult = m_pInjector->InjectModuleIntoAllProcesses();
    	}
    	else
    	{
    		m_pInjector->EjectModuleFromAllProcesses();
    		*m_phwndServer = NULL;
    		bResult = TRUE;
    	}
    	return bResult;
    }
    

    HookTool.DLL provides two mechanisms for self injecting into the address space of an external process - one that uses Windows Hooks and another that employs injecting of DLL by CreateRemoteThread() API. The architecture of the system defines an abstract class CInjector that exposes pure virtual functions for injecting and ejecting DLL. The classes CWinHookInjector and CRemThreadInjector inherit from the same base - CInjector class. However they provide different realization of the pure virtual methods InjectModuleIntoAllProcesses() and EjectModuleFromAllProcesses(), defined in CInjector interface.

    HookTool.DLL은 다른 프로세스의 주소 공간에 스스로 침투하는 2가지의 메카니즘 -윈도우즈 후크를 이용하는 하는 방법과 CreateRemoteThread() API를 이용하여 DLL을 침투시키는 방법- 을 제공한다. 시스템의 구조는 DLL을 주입하고 뽑아내는 순수 가상 함수를 가지는 추상 클래스 CInjector를 정의한다. CWinHookInjectorCRemThreadInjector는 같은 부모 클래스 CInjector를 상속한다. 하지만 이들 자식 클래스들은 CInjector 인터페이스에 정의된 순수 가상 메소드인 CWinHookInjectorCRemThreadInjector를 다른 방식으로 구현한다.

    CWinHookInjector class implements Windows Hooks injecting mechanism. It installs a filter function by the following call

    CWinHookInjector 클래스는 윈도우즈 후크를 이용하는 침투 메카니즘으로 구현된다. 이 클래스는 아래의 소스와 같이 필터 함수를 설치한다.

    // Inject the DLL into all running processes
    BOOL CWinHookInjector::InjectModuleIntoAllProcesses()
    {
    	*sm_pHook = ::SetWindowsHookEx(
    		WH_GETMESSAGE,
    		(HOOKPROC)(GetMsgProc),
    		ModuleFromAddress(GetMsgProc), 
    		0
    		);
    	return (NULL != *sm_pHook);
    }
    

    As you can see it makes a request to the system for registering WH_GETMESSAGE hook. The server executes this method only once. The last parameter of SetWindowsHookEx() is 0, because GetMsgProc() is designed to operate as a system-wide hook. The callback function will be invoked by the system each time when a window is about to process a particular message. It is interesting that we have to provide a nearly dummy implementation of the GetMsgProc() callback, since we don't intend to monitor the message processing. We supply this implementation only in order to get free injection mechanism provided by the operating system.

    보는 바와 같이 이 소스는 시스템에 WH_GETMESSAGE 후크를 등록하도록 요청한다. 서버는 이 메소드를 단한번 실행한다. GetMsgProc()는 시스템 전역 후크로 동작하도록 설계되었으므로 SetWindowsHookEx() 함수의 마지막 매개변수는 0이다. 콜백 함수는 윈도우가 특별한 메시지를 처리하려고 할 때 마다 실행된다. 메시지 처리 과정을 감시하는 것을 의도하지 않는다면 GetMsgProc() 콜백 함수를 거의 아무 것도 하지 않게 구현하여 제공하여야 한다는 것은 흥미로운 일이다. 운영체제가 제공하는 쉬운 침투 메카니즘을 이용하기 위해서는 이렇게만 구현하면 된다.

    After making the call to SetWindowsHookEx(), OS checks whether the DLL (i.e. HookTool.DLL) that exports GetMsgProc() has been already mapped in all GUI processes. If the DLL hasn't been loaded yet, Windows forces those GUI processes to map it. An interesting fact is, that a system-wide hook DLL should not return FALSE in its DllMain(). That's because the operating system validates DllMain()'s return value and keeps trying to load this DLL until its DllMain() finally returns TRUE.

    SetWindowsHookEx() 함수를 호출하면 OS는 GetMsgProc()를 익스포트하고 있는 DLL(i.e. HookTool.DLL)이 모든 GUI 프로세스에 이미 매핑이 되어 있는가를 검사한다. DLL이 아직 로드되지 않았다면 윈도우즈는 GUI 프로세스가 DLL을 매핑하도록 명령한다. 흥미로운 사실은 시스템 전역 후크 DLL은 DllMain()에서 절대로 FALSE를 반환하지 않는다는 것이다. 이것은 운영체제가 DllMain()의 반환값을 검사하여 DllMain()TRUE를 반환할 때까지 로드를 시도하기 때문이다.

    A quite different approach is demonstrated by the CRemThreadInjector class. Here the implementation is based on injecting the DLL using remote threads. CRemThreadInjector extends the maintenance of the Windows processes by providing means for receiving notifications of process creation and termination. It holds an instance of CNtInjectorThread class that observes the process execution. CNtInjectorThread object takes care for getting notifications from the kernel-mode driver. Thus each time when a process is created a call to CNtInjectorThread ::OnCreateProcess() is issued, accordingly when the process exits CNtInjectorThread ::OnTerminateProcess() is automatically called. Unlike the Windows Hooks, the method that relies on remote thread, requires manual injection each time when a new process is created. Monitoring process activities will provide us with a simple technique for alerting when a new process starts.

    CRemThreadInjector 클래스는 아주 다른 방식으로 접근한다. 이제부터는 리모트 쓰레드를 사용하여 DLL을 침투시키는 방식에 기초한 구현을 설명하겠다. CRemThreadInjector는 프로세스의 생성과 소멸에 관한 통지를 받는 방법을 이용하여 윈도우즈 프로세스의 관리를 확장시킨다. 이 클래스는 프로세스의 실행을 감시하는 CNtInjectorThread 클래스 객체를 멤버 변수로 가진다. CNtInjectorThread 객체는 커널 모드 드라이버로부터 통지를 받는 것을 감시한다. 어떤 프로세스가 생성될 때 마다 CNtInjectorThread ::OnCreateProcess() 함수가 호출되고 프로세스가 종료할 때 CNtInjectorThread ::OnTerminateProcess() 함수가 호출된다. 윈도우즈 후크와 다르게 리모트 쓰레드에 의존하는 방식은 새로운 프로세스가 생성될 때 마다 침투 작업이 필요하다. 프로세스의 활동을 감시하는 것은 새로운 프로세스가 시작될 때 마다 변경 작업을 하는 간단한 방법을 제공한다.

    CNtDriverController class implements a wrapper around API functions for administering services and drivers. It is designed to handle the loading and unloading of the kernel-mode driver NTProcDrv.sys. Its implementation will be discussed later.

    CNtDriverController 클래스는 서비스와 드라이버를 관리하는 API 함수로 구현된다. 이 클래스는 커널 모드 드라이버 NTProcDrv.sys의 로드와 언로드를 조작하도록 설계되었다. 이것의 구현은 나중에 논의하겠다.

    After a successful injection of HookTool.DLL into a particular process, a call to ManageModuleEnlistment() method is issued inside the DllMain(). Recall the method's implementation that I described earlier. It examines the shared variable sg_bHookInstalled through the CModuleScope 's member m_pbHookInstalled. Since the server's initialization had already set the value of sg_bHookInstalled to TRUE, the system checks whether this application must be hooked up and if so, it actually activates the spy engine for this particular process.

    어떤 특정 프로세스로 HookTool.DLL을 침투시키는 것이 성공하면 DllMain()에서 ManageModuleEnlistment() 함수를 호출하게 된다. 위에서 설명한 이 메소드의 구현을 생각해 보자. 이 함수는 CModuleScope의 멤버 변수인 m_pbHookInstalled로 저장되는 공유하는 변수인 sg_bHookInstalled를 검사한다. 서버의 초기화에서 이미 sg_bHookInstalled의 값을 TRUE로 설정하였으므로 시스템은 이 어플리케이션이 후크되었는가를 검사하고 그렇다면 이 프로세스에 스파이 엔진을 실질적으로 활성화시킨다.

    Turning the hacking engine on, takes place in the CModuleScope::InitializeHookManagement()'s implementation. The idea of this method is to install hooks for some vital functions as LoadLibrary() API family as well as GetProcAddress(). By this means we can monitor loading of DLLs after the initialization process. Each time when a new DLL is about to be mapped it is necessary to fix-up its import table, thus we ensure that the system won't miss any call to the captured function.

    후킹 엔진이 활성화되었으면 CModuleScope::InitializeHookManagement()의 구현이 실행된다. 이 방식에서는 GetProcAddress()LoadLibrary() API 계열의 함수에 후크를 설치한다. 이것은 초기화 과정 후에 DLL의 로드를 감시할 수 있다는 것을 의미한다. 어떤 새로운 DLL이 매핑될 때 마다 그것의 임포트 테이블을 수정하는 작업이 필요하고 그렇게 함으로서 시스템은 가로챈 함수의 호출을 놓치지 않게 된다.

    At the end of the InitializeHookManagement() method we provide initializations for the function we actually want to spy on.

    InitializeHookManagement() 메소드의 끝부분에서 실제로 스파이하기를 원하는 함수의 초기화를 하게 된다.

    Since the sample code demonstrates capturing of more than one user supplied functions, we must provide a single implementation for each individual hooked function. This means that using this approach you cannot just change the addresses inside IAT of the different imported functions to point to a single "generic" interception function. The spying function needs to know which function this call comes to. It is also crucial that the signature of the interception routine must be exactly the same as the original WINAPI function prototype, otherwise the stack will be corrupted. For example CModuleScope implements three static methods MyTextOutA(),MyTextOutW() and MyExitProcess(). Once the HookTool.DLL is loaded into the address space of a process and the spying engine is activated, each time when a call to the original TextOutA() is issued, CModuleScope:: MyTextOutA() gets called instead.

    예제 코드가 여러 개의 사용자 정의 함수로 가로채는 것을 보여주므로 후크 함수 각각을 처리할 수 있는 하나의 공통된 함수로 구현하여야 한다. 이것은 이러한 방식을 사용해서는 서로 다른 임포트된 함수의 IAT 내부의 주소들을 하나의 가로채기 함수를 가리키도록 바꿀 수는 없다는 것을 의미한다.

    [역자주] 여러 개의 함수를 후킹하여야 하므로 후킹하는 루틴을 하나의 함수로 만들어서 사용한다는 뜻입니다. 실제 소스에서는 BOOL CApiHookMgr::HookImport(PCSTR pszCalleeModName, PCSTR pszFuncName, PROC pfnHook)로 구현하여 아래와 같이 사용합니다.
    HookImport("Kernel32.dll", "LoadLibraryA", (PROC) CApiHookMgr::MyLoadLibraryA);
    HookImport("Kernel32.dll", "LoadLibraryW", (PROC) CApiHookMgr::MyLoadLibraryW);
    HookImport("Kernel32.dll", "LoadLibraryExA", (PROC) CApiHookMgr::MyLoadLibraryExA);

    스파이 함수는 호출하는 함수에 대해 알아야만 한다. 또한 가로채기 루틴의 형태가 원본 WINAPI 함수의 원형과 일치해야 한다는 것은 매우 중요하다. 그렇지 않으면 스택이 손상될 것이다. 예를 들면 CModuleScopeMyTextOutA(),MyTextOutW(),MyExitProcess(), 3개의 전역 함수를 구현하고 있다. HookTool.DLL이 어떤 프로세스의 주소 공간에 로드되고 스파이 엔진이 활성화 되면 원본 TextOutA()의 호출이 요청될 때 마다 CModuleScope:: MyTextOutA()가 대신 호출된다.

    Proposed design of the spying engine itself is quite efficient and offers great flexibility. However, it is suitable mostly for scenarios where the set of functions for interception is known in advance and their number is limited.

    스파이 엔진 자체의 설계는 매우 효율적이고 상당한 유연성을 제공한다. 하지만 가로채려는 함수를 미리 알 수 있는 경우에 적합한데 그러한 함수의 수는 한정되어 있다.

    If you want to add new hooks to the system you simply declare and implement the interception function as I did with MyTextOutA/W() and MyExitProcess(). Then you have to register it in the way shown by InitializeHookManagement() implementation.

    시스템에 새로운 후크를 추가하려면 샘플의 MyTextOutA/W()MyExitProcess()처럼 단순히 가로채기 함수를 선언하고 구현하기만 하면 된다. 그리고 나서 InitializeHookManagement()의 구현에서 처럼 그 함수를 등록하여야 한다.

    Intercepting and tracing process execution is a very useful mechanism for implementing systems that require manipulations of external processes. Notifying interested parties upon starting of a new processes is a classic problem of developing process monitoring systems and system-wide hooks. The Win32 API provides a set of great libraries (PSAPI and ToolHelp [16]) that allow you to enumerate processes currently running in the system. Although these APIs are extremely powerful they don't permit you to get notifications when a new process starts or ends up. Luckily, NT/2K provides a set of APIs, documented in Windows DDK documentation as "Process Structure Routines" exported by NTOSKRNL. One of these APIs PsSetCreateProcessNotifyRoutine() offers the ability to register system-wide callback function which is called by OS each time when a new process starts, exits or has been terminated. The mentioned API can be employed as a simple way to for tracking down processes simply by implementing a NT kernel-mode driver and a user mode Win32 control application. The role of the driver is to detect process execution and notify the control program about these events. The implementation of the Windows process's observer NTProcDrv provides a minimal set of functionalities required for process monitoring under NT based systems. For more details see articles [11] and [15]. The code of the driver can be located in the NTProcDrv.c file. Since the user mode implementation installs and uninstalls the driver dynamically the currently logged-on user must have administrator privileges. Otherwise you won't be able to install the driver and it will disturb the process of monitoring. A way around is to manually install the driver as an administrator or run HookSrv.exe using offered by Windows 2K "Run as different user" option.  

    프로세스의 실행을 가로채고 추적하는 것은 외부 프로세스를 조작하는 시스템을 구현하는 것에 매우 유용한 메카니즘이다. 관심이 있는 새로운 프로세스의 시작을 통지하는 것은 프로세스 감시 시스템과 시스템 전역 후크를 개발할 때 제기되는 고전적인 문제이다. Win32 API는 현재 시스템에서 실행되고 있는 프로세스를 나열할 수 있도록 하는 강력한 라이브러리(PSAPI와 ToolHelp [16])를 제공한다. 이 API들이 매우 강력하지만 새로운 프로세스가 생성되거나 소멸하는 것을 통지하지는 못한다. 다행스럽게도 NT/2K는 윈도우즈 DDK 문서에 설명되어 있고 NTOSKRNL에 익스포트되어 있는 "Process Structure Routines"라는 일련의 API들을 제공한다. 이 API 중의 하나인 PsSetCreateProcessNotifyRoutine()은 새로운 프로세스가 생성되거나 종료, 강제 종료될 때마다 OS가 호출하는 시스템 전역 콜백 함수를 등록할 수 있도록 해준다. 이 API는 NT 커널 모드 드라이버와 사용자 모드 Win32 제어 어플리케이션을 구현하여 쉽게 프로세스를 추적할 수 있는 방법으로 사용될 수 있다. 드라이버의 역할은 프로세스의 실행을 감지하여 이 이벤트를 제어 프로그램에 통지하는 역할을 한다. 윈도우즈 프로세스 감시자인 NTProcDrv는 NT 환경에서 프로세스 감시를 수행하기에 필요한 최소한의 기능들을 제공하도록 구현되었다. 보다 자세한 내용은 레퍼런스의 [11]과 [15]의 글을 참조하기 바란다. 드라이버의 코드는 NTProcDrv.c 파일에 있다. 사용자 모드 프로그램이 드라이버를 동적으로 설치, 삭제를 하므로 현재 로그온한 사용자는 관리자 권한을 가져야 한다. 그렇지 않으면 드라이버를 설치할 수 없고 프로세스를 감시할 수 없을 것이다. 다른 방법으로는 관리자로서 드라이버를 수동으로 설치하거나 윈도우즈 2K에서 제공하는 "다른 사용자로 실행하기" 옵션으로 HookSrv.exe를 실행하는 것이 있다.

    Last but not least, the provided tools can be administered by simply changing the settings of an INI file (i.e. HookTool.ini). This file determines whether to use Windows hooks (for 9x and NT/2K) or CreateRemoteThread() (only under NT/2K) for injecting. It also offers a way to specify which process must be hooked up and which shouldn't be intercepted. If you would like to monitor the process there is an option (Enabled) under section [Trace] that allows to log system activities. This option allows you to report rich error information using the methods exposed by CLogFile class. In fact ClogFile provides thread-safe implementation and you don't have to take care about synchronization issues related to accessing shared system resources (i.e. the log file). For more details see CLogFile and content of HookTool.ini file.

    마지막이지만 사소하지 않은 것이 제공되는 도구들이 단순히 INI 파일(i.e. HookTool.ini)의 설정을 바꿈으로서 관리된다는 것이다. 이 파일은 침투를 위해 윈도우즈 후크(9x,NT/2K)를 사용할 것인가 아니면 CreateRemoteThread()(NT/2K에서만)를 사용할 것인가를 결정한다. 또한 어떤 프로세스를 후크하고 어떤 프로세스는 후크하지 않을 것인가를 설정할 수도 있다. 만일 프로세스를 감시하기를 원한다면 [Trace] 섹션의 (Enabled)의 값을 세팅하여 시스템의 활동을 기록할 수도 있다. 이 옵션은 CLogFile 클래스의 메소드를 사용하여 상세한 오류 정보를 기록한다. 실제로 CLogFile 클래스는 thread-safe하게 구현되었고 공유 시스템 자원(즉 로그 파일) 접근과 관련된 동기화 문제에 신경쓰지 않아도 된다. 보다 자세한 내용은 CLogFile과 HookTool.ini 파일을 참조하기 바란다.

    Sample code(예제 코드)

    The project compiles with VC6++ SP4 and requires Platform SDK. In a production Windows NT environment you need to provide PSAPI.DLL in order to use provided CTaskManager implementation.

    이 프로젝트는 VC6++ SP4에서 컴파일되고 플랫폼 SDK를 필요로 한다. 윈도우즈 NT 환경에서 실행되는 경우 CTaskManager의 구현을 사용하기 위해 PSAPI.DLL이 필요하다.

    Before you run the sample code make sure that all the settings in HookTool.ini file have been set according to your specific needs.

    예제 코드를 실행하기 전에 특정 요구 사항에 맞게 HookTool.ini 파일이 제대로 설정되었는가를 확인해야 한다.

    For those that will like the lower-level stuff and are interested in further development of the kernel-mode driver NTProcDrv code, they must install Windows DDK.

    저수준의 방법을 선호하고 커널 모드 드라이버 NTProcDrv의 코드를 개발할 계획이라면 윈도우즈 DDK가 요구된다.

    Out of the scope(이글의 범위를 벗어나는 것들)

    For the sake of simplicity these are some of the subjects I intentionally left out of the scope of this article:

    • Monitoring Native API calls
    • A driver for monitoring process execution on Windows 9x systems.
    • UNICODE support, although you can still hook UNICODE imported APIs

    단순하게 하기 위해 아래의 주제들은 이글에서 다루지 않았다:

    • 네이티브 API 호출의 감시
    • 윈도우즈 9x 시스템에서 프로세스 실행을 감시하는 드라이버
    • UNICODE 지원

    Conclusion(결론)

    This article by far doesn't provide a complete guide for the unlimited API hooking subject and without any doubt it misses some details. However I tried to fit in this few pages just enough important information that might help those who are interested in user mode Win32 API spying.

    이 글은 절대로 무제한의 API 후킹에 대한 완벽한 가이드가 아니며 의심할 바 없이 일부 자세한 내용들이 빠져있다. 하지만 몇 페이지의 글에 사용자 모드 Win32 API 후킹에 관심이 있는 사람들이 중요한 정보를 주기에 충분하도록 노력하였다.

    References

    [1] "Windows 95 System Programming Secrets", Matt Pietrek
    [2] "Programming Application for MS Windows" , Jeffrey Richter
    [3] "Windows NT System-Call Hooking" , Mark Russinovich and Bryce Cogswell, Dr.Dobb's Journal January 1997
    [4] "Debugging applications" , John Robbins
    [5] "Undocumented Windows 2000 Secrets" , Sven Schreiber
    [6] "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format" by Matt Pietrek, March 1994
    [7] MSDN Knowledge base Q197571
    [8] PEview Version 0.67 , Wayne J. Radburn
    [9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB" MSJ May 1994
    [10] "Programming Windows Security" , Keith Brown
    [11] "Detecting Windows NT/2K process execution" Ivo Ivanov, 2002
    [12] "Detours" Galen Hunt and Doug Brubacher
    [13a] "An In-Depth Look into the Win32 PE file format" , part 1, Matt Pietrek, MSJ February 2002
    [13b] "An In-Depth Look into the Win32 PE file format" , part 2, Matt Pietrek, MSJ March 2002
    [14] "Inside MS Windows 2000 Third Edition" , David Solomon and Mark Russinovich
    [15] "Nerditorium", James Finnegan, MSJ January 1999
    [16] "Single interface for enumerating processes and modules under NT and Win9x/2K." , Ivo Ivanov, 2001
    [17] "Undocumented Windows NT" , Prasad Dabak, Sandeep Phadke and Milind Borate
    [18] Platform SDK: Windows User Interface, Hooks

출처 : Tong - 장우나라님의 [펌]정보통

'C/C++언어 > 후킹' 카테고리의 다른 글

Hooking 을 사용하는 프로그램 내의 구현  (0) 2007.11.11
Global Hooking in Win32  (0) 2007.11.11
[본문스크랩] 메세지 후킹  (0) 2007.09.06
[본문스크랩] Knowledge Base API  (0) 2007.09.06
동적 API 후킹의 구조  (0) 2007.09.06

Dynamic API Hooking Architecture

Written by X-Type

User Level (ring 3)

 

목차

머리말

API Hooking…?

Dynamic API Hooking…?

API 후킹 방식

동적 API 후킹의 구현

문제 해결하기

코딩하기

결론

참고 자료

 

머리말

글에서는 Windows NT 9x 대해 정적(Static) 아닌 동적(Dynamic) API Hooking 하는 방법에 대해 자세히 알아보고자 한다.

 

API Hooking?

본론으로 들어가기 전에 먼저 API Hooking 무엇인지 짚고 넘어가자.

API Hooking 어떤 특정한 프로그램 또는 시스템 전역에서 API 호출되는 것을 가로채는 것을 말한다. (그러나 반드시 API 대상으로 하지는 않는다) 개발자들이 문제에 도전하는 이유는, API Hooking 해서 얻는 것이 많기 때문이다. API 호출을 가로챈다면, 주어진 인자 또는 리턴값을 수정하여 프로그램의 동작을 제어할 있게 된다. 또한 API 호출을 로그(Log) 남긴다면, 프로그램이 어떤 식으로 동작하는지 있다. (의외로 효과는 크다)

그렇다면, 정적 API Hooking 무엇이고 동적 API Hooking 무엇인가? 둘의 차이는 API 어느 정도 후킹할 있는지의 차이이다. 정적 API 후킹의 경우, 특정한 코드로 짜여져서 특정 API 후킹하는 것이다. 동적 API 후킹은 런타임(Run-Time)에서 사용자의 요구에 따라 API 상황에 따라 다르게 후킹하도록 것이다.

 

Static API Hooking(정적 API 후킹)

Dynamic API Hooking(동적 API 후킹)

구현이 쉽다.

구현이 다소 어렵다.

후킹할 API 관리하기가 까다롭다.

(코드를 작성하고 다시 컴파일해야 )

유저에 의해 후킹할 API 관리된다.

(, 런타임상에서 관리된다)

API 따라서 가로챈 후의 동작이 다양하게 구현되는 경우가 많다.

API 후킹하는 목적에 따라 가로챈 후의 동작이 고정되어있는 경우가 많다.

<동적 API 후킹과 정적 API 후킹의 차이>

 

동적 API 후킹은 불안전한 면을 다소 가지고 있다. 예를 들어, 유저가 인자 개수를 틀리게 작성해주는 경우가 있다. 이럴 경우에는 스택이 잘못 정리되므로, 후킹 도중에 프로그램이 튕기게 된다. (디스어셈블러를 이용하여 보정해 수는 있다. 그러나 완벽하지는 않다.)

 

Dynamic API Hooking?

이번에는 동적 API 후킹이 무엇인지 알아보자. 정의에 관해서는 앞에서 말했기 때문에, 여기서는 구현 방식에 대해 간단하게 설명하도록 하겠다.

우선 정적 API 후킹 방식에 대해 이해해야 한다. 정적 API 후킹은 이런 방식으로 작성된다:

 

typedef int (WINAPI *API_MESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);

int WINAPI MessageBoxAProc(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

API_MESSAGEBOX pfnOriginalMessageBoxA;

 

LPVOID HookAPI(PROC Api, PROC ApiProc, DWORD Argc){

// API 후킹 구현

}

 

void main(){

             pfnOriginalMessageBoxA = HookAPI(

                                                                  GetProcAddress(LoadLibrary(“User32.dll”), “MessageBoxA”),

                                                                  (PROC)&MessageBoxAProc,

                                                                  4

                                                                  );

}

 

int WINAPI MessageBoxAProc(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType){

            

             return pfnOriginalMessageBoxA(hWnd, lpText, lpCaption, uType);

}

 

이런 식으로 API 후킹 프로시저(API 후킹을 가로챈 뒤에 어떠한 작업을 해주는 함수) 선언, 정의하고 API 후킹을 시도하는 것을 있다. 그렇다면 API 프로시저를 런타임에서 생성할 있다면 동적 API 후킹이 되는 것인가? 가능은 하겠지만 비효율적인 면이 있다. C/C++ 코드는 옮기면 깨질 가능성이 크기 때문에 어셈블리로 짜야 한다. 또한 스택 정리를 위해 함수가 약간씩 수정되어야 한다. ( 과정에서 디스어셈블러가 필요하다.) 1)API 프로시저의 크기가 크다면 이러한 동적 API 후킹 방식은 매우 비효율적인 방법이 수밖에 없다. 그렇다면 어떤 식으로 구현되어야 하는가? 필자는 동적 API 후킹을 하려면 함수의 동적인 생성(복사) 필연적이라고 생각한다. 2) 만약에 함수를 런타임에서 생성시킨다면 크기를 최소화하는 것이 좋을 것이다. 그러나, API 후킹 프로시저를 복사하는 것은 앞에서도 말했듯이 비효율적이다. 그렇다면 어떻게 해야 할까? 필자는 API 프로시저를 하나만 두고도 여러 가지 API 후킹이 가능하도록, 동적으로 생성된 여러 작은 함수들과, 1개의 내부3) API 프로시저로 외부4) API 프로시저를 연결시켰다. 과정이 약간 어렵다. 왜냐하면 순수 C/C++로는 구현이 불가능하기 때문이다. , 어셈블리를 써야 한다. 자세한 것은 다음에 말하도록 하겠다.

 

1)          이것을 구현하기에는 문제가 많다. C/C++에서 함수를 호출하면 보통 컴파일러가 절대 주소 참조를 하지 않고 상대 주소 참조를 한다. 이러한 코드는 코드의 위치를 옮기게 되면 모두 깨진다. Volatile 키워드가 붙은 함수 포인터를 사용하여 절대 주소 참조로 만들 수는 있다. (그러나 방법으로는 속도, 용량에 모두 손해를 본다.) 그리고 컴파일러에 따라서 같은 함수라도 어셈블리 상의 구현이 다를 있다. , 함수의 크기가 일정하지 않기 때문에 주의를 요한다.

2)          다수의 API 가로채어 같은 API 프로시저에 오게 한다면 API 구분할 없다. API 구분할 있게 하는 루틴이 API마다 필요하다.

3)          필자는 API 후킹이 쉽도록 API 후킹 객체를 작성하고 있다. , 객체 내부에서 구현하고 있는 API 프로시저를 말한다.

4)          객체 바깥에서 구현한 API 프로시저를 말한다.

 

API 후킹 방식

API 후킹하는 방법에 대해 간단히 알아보자. 동적인 후킹이든 정적인 후킹이든 모두 일정한 API 후킹 방법을 기초로 하여 작성된 것들이기 때문에 알아둘 필요가 있다.

DLL 필요한 방법은 DLL 이용하여 다른 프로세스의 메모리에 침투할 필요가 있는 방법들이다.

 

a. Hook Imported Function

방법은 임포트 테이블(Import Table)이라는 것을 수정하는 방법이다. 프로그램이 특정 API1) 호출하는 코드를 작성했다면, 보통 API 주소를 담고 있는 테이블을 이용하여 API 호출하는데 테이블을 수정하여 API 후킹을 시도하는 것이다. 방법은 특정 모듈2) 대해서만 API 후킹이 가능하다. 또한 임포트 테이블을 통하지 않고 API 주소를 알아내어 함수를 호출할 있으므로, 3) 모든 API 호출이 후킹되지 않는다. 그러나 방법은 다른 방법에 비해 비교적 간단하고 안정적으로 동작한다.

 

1) API뿐만 아니라 외부 DLL에서 export 모든 함수들이 대상이다.

2) 모듈(Module): 프로세스에 로드(load) 프로그램(exe, dll, …) 말한다.

3) LoadLibrary, GetProcAddress API 이용하면 API 주소를 알아낼 있다.

 

예를 들어 보자.

 

GetModuleHandleA(NULL);

00404663 6A 00                push        0

00404665 FF 15 50 12 42 00    call        dword ptr [__imp__GetModuleHandleA@4 (00421250)]

 

여기서 __imp__GetModuleHandleA@4 , 메모리 주소 0x421250 값을 읽어서 호출하는 것을 있다. 0x421250 값을 수정한다면, 함수의 호출을 가로챌 있을 것이다. (자세한 구현 방법은 찾아보면 나온다. 찾기가 귀찮다면 이곳(1) 참고하기 바란다.)

 

Windows에서는 각각의 프로세스의 메모리가 분리되어 있기 때문에, 다른 프로세스의 임포트 테이블을 건드리려면 DLL 대상 프로세스에 삽입(Inject)하여야 한다. DLL 삽입하지 않게 수도 있지만 구현이 훨씬 복잡하게 된다. 여기서는 DLL 삽입(DLL Injection) 관한 방법은 다루지 않는다. 이것에 관한 부분은 API Hooking Revealed(2)에서 찾아볼 있고, CreateRemoteThread( API NT 계열에서만 구현되어 있다. 그러나 9x에서도 가능하다) 이용한 DLL Injection 기법은 Remote Library(3)에서 모든 Windows 대해(9x, NT, …) 다루고 있다.

 

b. Trampoline

방법은 API 함수 자체의 코드를 변경시키는 방법이다. 그래서 구현이 까다롭다. 방법은 API 함수의 부분에 jmp 명령어를 넣어서 API 프로시저로 오게 하여 호출을 가로채는 방식이다. 원본 API 호출하기 위해서 복잡한 과정을 필요로 한다. 이렇게 복잡하지만 프로세스 내의 모든 API 호출이 후킹되는 장점이 있다. 타겟 API jmp 명령어가 들어갈 있는 충분한 크기를 가져야 하기 때문에, 함수의 크기가 jmp 명령어의 크기인 5 바이트보다 작을 경우 후킹할 없다는 단점이 있다.

 

예를 들어 보자.

 

// FindWindowA API 원본 (편의상 뒷부분을 생략했다)

77CFE8FF 33 C0                xor         eax,eax

77CFE901 50                   push        eax

77CFE902 FF 74 24 0C          push        dword ptr [esp+0Ch]

77CFE906 FF 74 24 0C          push        dword ptr [esp+0Ch]

// FindWindowA API 후킹

77CFE8FF E9 DC 2F C1 88       jmp         009118E0

77CFE904 90                   nop

77CFE905 90                   nop

77CFE906 FF 74 24 0C          push        dword ptr [esp+0Ch]

// Trampoline 함수 (원본 API 이것을 통해 호출)

00911920 33 C0                xor         eax,eax

00911922 50                   push        eax

00911923 FF 74 24 0C          push        dword ptr [esp+0Ch]

00911927 E9 DA CF 3E 77       jmp         77CFE906

 

어떤 방식인지 감이 것이다. 주의할 점이 있다면 jmp 명령어를 삽입할 원본 명령어 크기를 고려하여 nop 넣어주어야 한다는 것이다. Jmp 명렁어의 크기인 5바이트만을 복사하게 되면 위의 함수와 같은 경우에는 명령어가 깨질 수가 있다.

 

77CFE8FF E9 DC 2F C1 88       jmp         009118E0

77CFE904 24 0C                 and al, 0Ch (잘못됨)

77CFE906 FF 74 24 0C          push dword ptr [esp+0Ch]

 

함수의 경우, 이렇게 잘못 수정되면 인수 하나가 넘겨지지 못하게 된다. Access Violation(엑세스 위반) 발생할 것은 뻔하다. 그러나 디스어셈블러를 쓴다면 문제를 해결할 있다. 디스어셈블러 없이 구현하려면 원본 API 호출시 잠깐 API 복원했다가 다시 패치해주는 방법이 있으나 API 복원되었을 , 다른 쓰레드에서 API 호출한다면 후킹이 되지 않는 문제가 발생한다. 이런 방법은 멀티쓰레드 환경에 맞지 않다.

 

방법도 마찬가지로 방법으로 다른 프로세스의 API 호출을 잡아내려면 DLL 삽입해야 한다.

( 방법의 자세한 구현 방법은 뒤에서 말할 것이다.)

 

c. Debugging

방법은 디버거를 이용하는 것이다. 방법은 DLL 사용하지 않는 방법이다. 우선, 대상 프로세스에 디버거를 활성(Active)시킨다. 다음에 함수 부분에 브레이크 포인트(BreakPoint, 중단점) 심어둔다. 함수가 호출되면 EXCEPTION_BREAKPOINT 디버그 이벤트가 발생하며 모든 쓰레드가 중지된다. 제어권이 넘어오게 되는데 이때 각종 작업을 해주는 것이다. 방법의 경우 프로세스 통신 구현이 따로 필요가 없다. 단점은 많은 편이다. 우선 느리고, 디버거를 종료시키면 디버깅되는 프로세스도 같이 종료되어 버린다. 중단점에 이르면 모든 쓰레드가 멎어버리기 때문에 예기치 않은 상황이 발생할 가능성을 배제할 없다.

 

대상 프로세스를 디버그시킨다. -> 중단점(int 3) 심는다. -> 중단점 디버그 메시지를 받는다. -> (작업) -> API 복원 -> 중단점(int 3) 다시 심는다.

 

요약하자면 이정도가 된다.

 

(참고) Win9x API Hooking

Windows 9x(95, 98, Me)에서는 API 후킹이 더더욱 어렵다. System Dll(Kernel32.Dll, User32.dll, Gdi32.dll, …)들은 Windows 9x에만 존재하는항상 공유되는 메모리 영역 맵핑되어있기 때문에 문제가 발생한다. Windows 9x에서는 가상 메모리 0x80000000 ~ 0xC0000000 영역은 모든 프로세스에서 동일하다. , System Dll API 코드를 수정하였을 경우에 문제가 된다. 모든 프로세스에 적용되기 때문에, 기존의 Trampoline API 후킹을 사용하면 에러가 난다. 기존의 디버거를 이용한 방법은 모든 프로세스에 중단점이 존재하지만 디버그하는 프로세스는 하나이다. 만약에 디버그 중이 아닌 프로세스에서 해당 API 호출하게 되면, 중단점에서 프로그램이 튕겨버린다. 중단점도 인터럽트1) 중의 하나이기 때문에, 핸들링되어있지2) 않으면 기본 핸들러3) 프로세스를 강제 종료시켜 버린다. 7) 이런 이유로, 필자가 아는 바로는 9x에서 안전한 동작을 보장할 있는 API 후킹 방법은 Hook Imported Function 하나밖에 없다.

 

참고로, Windows 9x에서는 공유 메모리 영역의 Protection 변경할 없다. 공유 메모리에 위치한 System Dll 코드는 Protection 읽기 전용이기 때문에 수정이 불가능한 것처럼 보인다. 그러나 int 0x2E4), VxD Service5) 이용하면 수정할 있다. (자세한 내용은 뒤에 언급하겠다)

 

1) 인터럽트(Interrupt): 예외 상황이 발생하더라도 계속해서 작업이 가능하도록 하는 운영체제의 기능. 예외 처리 외에도 다른 기능이 있다.

2) 핸들링(Handling): 예외 상황을 계속해서 작업이 가능하도록 유도하는 작업.

3) 핸들러(Handler): 예외 상황에 대응하는 루틴. 기본 핸들러(Default Handler) 핸들링이 되어있지 않을 때에 호출된다.

4) Int 0x2E: 인터럽트 번호 0x2E 인터럽트를 말한다. NT 2000 ring 06) 서브루틴을 호출할 사용된다. (9x에서도 일부가 지원된다)

5) VxD Service: 운영체제 내부 구현이나 드라이버 구현에서 쓰인다. VxD(Virtual x Device) 제공하는 서비스를 말한다.

6) Ring 0: Intel 계열의 CPU 지원하는특권 레벨” 0 말한다. 특권 레벨은 0부터 3까지 있으며, 0 가장 높고, 3 가장 낮다. Windows 0 3만을 사용하는데, 흔히 ring 0 커널 모드, ring 3 유저 모드라고 부른다.

7) 아직 시험해 적이 없어서 확신할 없다.

 

동적 API 후킹의 구현

이번에는 동적 API 후킹이 어떤 식으로 구현되는지 알아보자. 앞으로 말할 내용은 동적 API 후킹 방법 중에 가지 방법일 뿐이며, 다른 방법도 있다는 것을 명심하기 바란다. 필자는 Trampoline API 후킹 방식을 기초로 한다. (하지만 다른 후킹 방법을 있다) 다수의 API 후킹하되 하나의 후킹 프로시저로 연결되도록 하는 것이 핵심이다. 그렇다면, 후킹 프로시저는 호출된 API 무엇인지 구분할 있어야 한다. 따라서 동적 API 후킹에는 후킹된 API 관한 정보를 저장해둘 필요가 있다. 하지만, 어떻게 자료를 넘겨야 할까? 필자는 API 호출된 직후에 eax 레지스터를 이용하여 자료의 포인터를 넘기는 방법을 선택했다. (필자는 역할을 하는 코드를 API Specifier라고 명명했다) 다음에는 필자는 유저 프로시저에 직접 데이터의 포인터를 넘기는 방법을 택했다. (유저 프로시저를 C/C++ 코드로 작성하기 위해서는 방법이 가장 안전하다) 그러면 유저 프로시저를 연결시켜주는 과정에 대해 생각해 보자. 필자는 과정 작성하기 전에 미리 가지를 정해두었다.

 

-1. 구현하는 필요한 어셈블리 코드를 최소화한다.

- 2. 유저 프로시저에는 최소한의 데이터만을 넘긴다.

- 3. 함수의 복사는 최대한 사용하지 않는다.

 

어셈블리 코드는 크기가 커지면 유지/보수가 곤란하다. 유저 프로시저에 데이터를 많이 넘기면 넘길수록 속도에 손해를 보게 되며, 함수의 복사는 안전하지 못하다. (구현하기도 어렵고 메모리를 쓸데없이 먹는다.)

API Specifier 유저 프로시저로 연결시켜주기 위해 특정한 함수(Connector) 점프한다. Connector에서는 일단 데이터 포인터(eax) 스택에 백업한다. Eax 레지스터는 차후 리턴값으로 쓰이게 되기 때문이다. 다음으로, 유저 프로시저를 실제로 호출하는 함수(Linker) 따로 두게 되면 함수(Linker) 구현을 좀더 자유롭게 있게 되고, Connector 함수의 코드의 양을 어느 정도 줄일 수가 있다. 따라서 Connector Linker 함수를 호출한다. Connector Linker에서 반환한 값으로 리턴을 한다.

Connector API 넘겨진 인수를 쓰지 않는다. User API Procedure API 넘겨진 인수를 쉽게 조작할 있도록, 이를 위해 Connector 번째 인수의 포인터를 후킹 데이터와 함께 Linker 넘겨주는 방법을 택했다. (어셈블리 코드를 줄이기 위해서)

문제는 스택 정리이다. API stdcall 호출 규약1) 사용하며 API 마다 인수의 개수가 약간씩 다르다. 그러므로 API마다 맞게 스택을 정리해줘야 한다. 유저 프로시저에 연결시켜주는 함수를 복사해서 스택 정리 부분만 수정시켜주는 방법은 곤란하다. (, Connector 복사하는 방법) 메모리 효율이 떨어지기 때문이다. 필자는 스택을 정리하는 부분만을 API마다 따로 두고(필자는 API Specifier 두었다) Connector 사용한 스택을 모두 정리한 , 데이터에 포인터로 기록되어 있는 스택을 정리하는 부분으로 점프한다. (이것 때문에 Connector C/C++ 구현될 없다) 이렇게 위의 조건을 최대한 따르면서 API 후킹을 구현해 보았다. 지금까지의 과정을 요약하면 다음과 같다.

 

(구현 과정을 알아보기 쉽도록 C 코드와 어셈블리를 섞어 썼다.)

참고: cdecl 호출 규약1) 가진 모든 함수에서의 스택 정리는 ret 0이나 ret 하면 된다.

 

API Specifier, Connector 모두 어셈블리로 작성되며, Linker C/C++ 작성해도 무방하다.

참고로 리턴 과정 이러하다.

User Proc -> Linker -> Connector -> API Specifier -> (End)

위에서는 원본 API 호출하는 것이 보이지 않는다. 그것은 User API Procedure 몫이다. 데이터를 통해 Trampoline 함수의 포인터가 제공되며, User API Procedure에서는 그것을 이용하여 원본 API 호출할 있다. ( 과정은 인라인 어셈블리를 필요로 하므로 따로 함수를 만들어 두는 것이 좋다.)

 

주의: User API Procedure에서 후킹된 API 호출할 경우에도 위의 순서대로 수행되어 무한 루프 빠질 있다. 해결책은 뒤에 언급하겠다.

 

1) 호출 규약: 함수를 어떻게 호출할 것인지 정해놓은 일종의 약속이다. stdcall 호출 규약은 함수에 인수를 넘기는 스택을 함수 내에서 정리하는 호출 규약을 말한다. 반대로 cdecl 호출 규약은 함수를 호출한 쪽에서 스택을 정리하는 것이다.

 

여기까지의 구현으로는 Windows NT에서만 동작한다. Windows 9x에서 System DLL API 호출을 가로채려면 복잡한 구현이 필요하다.

그렇다면 Windows 9x에서는 어떻게 해야 하는지 알아보자.

 

우선, System DLL API 수정하는 방법에 대해 알아보자.

Windows 9x VxD 서비스 중에 RtlCopyMemory라는 것이 있다. 이것을 이용하면 읽기 전용의 메모리 영역이라도 메모리 입출력을 있다.

 

push nSize                                    // 원본, 대상의 크기

push pSrc                                      // 원본 (Source) 메모리 주소

push pDest                                   // 대상 (Destination) 메모리 주소

mov edx, esp                                // edx: 인수 포인터

mov eax, 0x10A                            // eax: RtlCopyMemory 서비스 번호

int 0x2E                                        // RtlCopyMemory(pDest, pSrc, nSize)

add esp, 12                                  // 스택 정리

 

간단한 어셈블리 코드이다. 우선 인수들을 넘기고 가장 첫번째 인수의 주소를 edx 레지스터에, 서비스 번호를 eax 레지스터에 넘기고 인터럽트 0x2E 발생시키면 nSize만큼의 pSrc pDest 복사된다. 참고로, 원본이나 대상의 메모리를 읽을 없을 경우 무서운 블루스크린 뜨니 코드를 실행하기 전에 VirtualQuery함수로 해당 메모리가 존재하는지 체크하는 것이 좋다. 또한 코드는 NT에서 동작하지 않는다. (NT에서는 필요가 없겠지만)

 

하지만 공유 영역에 있는 System DLL API 수정하면 모든 프로세스에 적용되기 때문에 추가로 구현해주어야 것이 있다. 일단은 현재 프로세스가후킹되었는지검사해야 한다. 만약 후킹되었으면 API Specifier jump하고, 후킹되지 않았으면 Trampoline 함수로 jump하면 된다. 일단 후킹된 프로세스 리스트(9x Stub Data), API Specifier, Trampoline 함수를 공유 영역에 할당할 필요가 있다.

 

주의: System DLL 공유 메모리 영역에 위치하기 때문에 수정에 신중을 기하여야 한다. 잘못된 코드를 실행하게 되면 실행중인 모든 프로세스가 튕기는 현상이 발생한다. 또한 모든 작업을 마친 코드를 원래대로 돌려놓지 않을 경우에도 문제가 발생할 있다.

 

메모리를 직접 할당하는데 쓰이는 VirtualAlloc함수의 fAllocationType에는 VA_SHARED라는 Flag 있어서 이것을 쓰면 공유 영역에 메모리를 할당할 있다. (NT에서는 당연히 안된다) VA_SHARED 값은 0x8000000이다. 이렇게 할당해주면 것이다.

 

#define VA_SHARED 0x8000000

LPVOID pSharedData = ::VirtualAlloc(NULL, dwSize, fAllocationType | VA_SHARED, flProtect);

 

이렇게 공유 영역에 할당한 데이터를 프로세스 검사 루틴에 넘겨주기 위해서는 마찬가지로 eax 레지스터를 통해 넘겨주면 된다. 1) 이렇게 넘겨진 데이터로 후킹되었는지를 검사해서 적절히 jump하면 된다. 주의할 점은 Trampoline 함수는 후킹되지 않은 프로세스에서도 쓰이므로 공유 영역에 할당3)하고, API Specifier 프로세스별로 다르게 할당2)되어 있으므로 프로세스 리스트와 묶어서 관리해야 한다. 그리고 이렇게 할당한 데이터를 해제하기 위해서는 개의 프로세스가 후킹되었는지 저장해둘 필요가 있다. 후킹을 해제할 마지막 후킹된 프로세스일 경우에만 공유 메모리에 할당된 데이터를 최종적으로 해제해야 한다.

 

1) 필자는 이러한 역할을 수행하는 코드를 9x Specifier라고 이름 붙였다. 9x Specifier 모든 프로세스에서 유효해야 하므로 공유 메모리에 할당되어야 한다. 또한 프로세스 검사 루틴 역시 공유 메모리에 위치해야 한다)

2) 이것도 공유 메모리에 할당할 필요는 없다. 공유 메모리에 할당하는 것은 좁은 공유 영역을 낭비하는 것이다.

3) Trampoline 함수, 9x Specifier 함수는 공유 영역에 위치하기 때문에 한번만 할당해도 여러 프로세스에서 써먹을 있다.

 

정리하자면, 공유 메모리에 할당되어야 것들은 9x Specifier, Trampoline 함수, 프로세스 검사 루틴, 프로세스 리스트이다.

프로세스 리스트에 포함될 데이터는 Trampoline 함수 주소 ( 프로세스 공통), 후킹된 프로세스 개수, API Specifier 포함된 프로세스 리스트이다.

프로세스 검사 루틴은 공유 메모리에 할당되어야 하기 때문에 함수를 복사해야 한다. 안전한 복사를 하려면, 모두 어셈블리로 짜야 하고, 직접적인 jmp, call 구문을 쓰지 않아야 한다. 주소값을 담고 있는 포인터나 레지스터를 이용해서 jmp, call 해야 한다.

 

프로세스 리스트에서, Windows 9x에서는 프로세스 ID 대신 프로세스 Database 포인터를 있다. 번거롭게 GetCurrentProcessId 호출하는 것보다 훨씬 구현이 쉽다.

 

LPVOID pCurrentProcessDatabase;

__asm{

mov eax, fs:[0x30]                                   // Current Process Database = fs:[0x30]

mov [pCurrentProcessDatabase], eax

}

 

이런 식으로 현재 프로세스의 데이터베이스 주소를 구할 있다.

지금까지의 과정을 그림으로 나타내 보면 이러하다.

위의 그림에서는 빠뜨렸는데, 프로세스 체크 함수도 공유되어야 한다. (위에도 언급했었지만)

 

주의: 프로그램이 예기치 못한 에러로 종료될 경우, API 제대로 복원되지 않아 문제를 발생시킬 있다. (메모리 누수, 전체 프로세스 종료 현상) 따라서 SetUnhandledExceptionFilter 써서 예외 상황에서 API 복원해 주는 것이 좋다. 방식으로 예외 처리를 , API 후킹을 객체로 경우, 인스턴스를 개로 제한할 필요가 있다.

 

문제 해결하기

이번에는 위에서 언급했던 문제에 관해 알아보자.

첫번째로 API 코드 덮어쓰기에 문제가 있었다. 명령어가 깨지는 문제이다. 디스어셈블러를 이용하여 API 명령어 단위로 분석하여 최소 5 바이트를 확보하면 문제는 해결된다. 디스어셈블러는 어떻게 써야 하는 것인가? ……물론 직접 만들거나 다른 디스어셈블러 소스를 쓰면 된다. 디스어셈블러를 만들려면 기계어의 해독 방법을 알아야 한다. 별로 어렵지는 않다. CPU 제조 회사 홈페이지에 찾아가면 어렵지 않게 관련 문서를 구할 있다. (모두 영어지만) 필자가 하나 번역해 (4) 있으니 참고하기 바란다.

두번째로 User API Procedure 의해 무한 루프에 빠져버리는 문제이다. 먼저 생각해 있는 방법은 API 후킹되었는지 확인하고, 후킹되었으면 Trampoline 함수를 호출하고, 그렇지 않으면 원본 API 바로 호출하는 것이다. API 후킹 데이터와 일일이 비교하는 방법도 있으나, 필자의 후킹 방식을 경우에, API 부분에 jmp 있는지 확인하고, jmp 있으면 주소를 추적한다. 주소가 Specifier라면, (mov 시작하고, 다음 명령어가 jmp인지 체크해본다.) 데이터 주소를 얻어낼 있다. 이때 데이터를 이용하여 Trampoline 함수의 주소를 알아내고 그것을 호출하는 것이다. 물론 후킹된 API 아니면 그대로 호출해주면 된다. 모두 인라인 어셈블리를 필요로 한다. 그러나 방법으로는 겉으로 보이는 호출만 막을 있지 다른 라이브러리를 통한 API 호출은 막을 없다.1) 자세한 방법은 뒤에 언급할 것이다. 다른 방법으로는 리턴 주소를 체크하여 API 프로시저에서 호출한 것이라면 바로 원본 API 호출하고 리턴하는 방법이 있다. 2)

 

1)          예를 들어서 malloc같은 함수가 있다. 내부적으로 HeapAlloc API 호출한다. 만약에 API 후킹 프로시저에서 malloc 호출하고 HeapAlloc 함수가 후킹되어 있다면 방법으로는 막을 없다.

2)          방법도 마찬가지다. 라이브러리를 정적 링크하고 DLL 전체에서 검사한다면 막을 수는 있다.

3)          그러나 위의 방법 모두 Win9x에서 디버깅되는 프로세스에서는 통하지 않는다. Win9x에서 디버그중인 프로세스에서는 직접적으로 System DLL API 호출하지 않고 간접적으로 API 호출하도록 Windows에서 조치를 취하기 때문이다.

 

코딩하기

이제 코딩을 보자. 일단 누구나 쉽게 API 후킹을 있도록, 객체로 짜는 것이 좋겠다.

우선 필요한 데이터들을 알아보자. (1 바이트 구조체 정렬을 했다)

 

typedef struct _API{

             api_hook *HookObj;                     // 00      Hooking Object

             const _APIHOOK *HookInfo;         // 04      Hooking Information

             void *ApiCaller;                             // 08      Return Address               

}API;

 

API 프로시저에 넘겨질 데이터이다. 후킹 객체, API 정보, 리턴 주소(함수 수행이 끝났을 리턴되는 ) 있다.

후킹 객체는 API 정보에도 담겨 있지만, 편의상 제공했다.

 

typedef struct _APIHOOK{

             PROC fnTrampoline;         // 00      trampoline 함수

             PROC fnApi;                     // 04      원본 api 함수

             PROC fnSpecifer;             // 08      API Specifier

             APIPROC fnProc;              // 12      User API procedure

             DWORD fnArgc;                // 16      API 인수 개수

             DWORD fnApiOffset;        // 20      API 코드에 덮어쓴 크기

             LPVOID UserData;           // 24      user data

             api_hook *HookObj;        // 28      후킹 객체

             _9XSTUB *pStub;            // 32      stub data (9x)

             DWORD idxDat;               // 36      pStub->Data[idxDat]

             PROC fn9xSpecifier;         // 40      9x Specifier

}APIHOOK;

 

API 후킹 정보이다.

 

typedef struct __9XDATA{

             LPVOID pPDB;                  // 00      Process DataBase

             LPVOID pSpec;                // 04      API Specifier

}_9XDATA;

 

typedef struct __9XSTUB{

             LPVOID pTrampoline;       // 00      Trampoline Function (공유)

             DWORD nProcHooked;     // 04      후킹된 프로세스 개수

             _9XDATA Data[1];            // 08(+8n)          프로세스 리스트 본체

}_9XSTUB;

 

9x 데이터이다.

9x data 프로세스를 구분하는데 쓰일 프로세스 데이터베이스(PDB) API Specifier 있다. 여기서 주의할 것은 API Specifier 공유 메모리에 할당되지 않는다는 것이다. (, 프로세스마다 따로 할당)

 

class api_hook{

private:

             static const int procchk_len;         // Process Checker 길이

             static const int stubdata_len;       // Stub Data 크기

             list<APIHOOK> m_data;               // APIHOOK DATA

             disasm m_disObj;                         // Disassembler

            

             // Skeleton Codes

             static BYTE ApiPatchCode[];          // API 덮어쓸 코드

             static BYTE ApiSpecifer[];              // api specifier

             static BYTE Api9xSpecifier[];          // 9x api specifier

 

             PROC m_ProcChecker;                  // 9x process checker (공유)

 

             // 예외 처리

             static LPTOP_LEVEL_EXCEPTION_FILTER pTopFilter;

             static api_hook *pInstance;

             static LONG __stdcall UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo);

public:

             static ah_osver osver;                  // Operating System Version Checker

             // simple VirtualProtect

             static inline DWORD VirtualProtect(void *lpAddress, DWORD dwSize, DWORD flNewProtect){

                           DWORD flOldProtect = 0;

                           ::VirtualProtect(lpAddress, dwSize, flNewProtect, &flOldProtect);

                           return flOldProtect;

             }

            

             // Intercept Function Calls (stdcall, cdecl)

             const APIHOOK *InterceptApi(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData = 0);

             const APIHOOK *InterceptCdecl(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData = 0);

 

             // stop Intercepting (stdcall, cdecl)

             int RestoreApi(PROC fnApi);

 

             // api hook data

             const list<APIHOOK> &GetData() const{return m_data;}

 

             // call trampoline function

             int CallTrampolineAPI(const API *api, LPVOID pFirstArg);                  // stdcall

             int CallTrampolineCdecl(const API *api, LPVOID pFirstArg);  // cdecl

 

             // copy memory (9x, NT)

             static void _stdcall _RtlCopyMemory(void *pDest, const void *pSrc, unsigned int nSize);

 

             api_hook();

             virtual ~api_hook();

};

 

클래스 선언문이다. 지금까지 설명한 내용이 모두 들어가 있음을 있다. 부분은 별로 어렵지 않으니 넘어가도록 하겠다. 이제 connector, linker, Process Checker 보자.

 

//////////////////////////////////////////////////////////////////////////

// 내부 함수 정의 부분 (헤더 파일에 포함)

 

// connector에서 데이터를 받아 API 프로시저를 호출하는 linker 함수.

static int __stdcall api_hook_linker(APIHOOK *pSpecApi, PVOID pStackObject){

             // pStackObject -> 첫번째 인수의 주소이다. [첫번째 인수의 주소]-4 리턴 주소를 나타낸다.

             API api = {pSpecApi->HookObj, pSpecApi, (PVOID)(*((DWORD *)((BYTE *)pStackObject-4)))};

 

             return pSpecApi->fnProc(&api, pStackObject);

}

 

#define pos_spec 8                      // APIHOOK에서의 Specifier 위치

#define pos_spec_2 10                // Specifier에서의 ret xx위치

 

// connector 함수 (Specifier->Linker)

static int __declspec(naked) __stdcall api_hook_connector(){

             __asm{

                           // API Hooking 데이터 백업

                           push eax

 

                           // api_hook_linker(pSpecApi, pStackObject);

                           lea ecx, [esp+8]

                           push ecx

                           push eax

                           call api_hook_linker

 

                           // Specifier ret xx 위치 추적

                           mov ecx, dword ptr [esp]

                           mov ecx, dword ptr [ecx+pos_spec]

                           add ecx, pos_spec_2

 

                           // 스택 정리 점프

                           add esp, 4

                           jmp ecx

             }

}

#undef pos_spec

#undef pos_spec_2

 

// Process Checker 함수

// 지역 변수

#define pCurPDB ecx

#define idx edx

#define i edi

#define pScanPDB esi

#define pStub ebp-4

#define local_size 4        // 지역 변수 크기

 

static int __declspec(naked) __stdcall api_hook_proc_checker(){

             __asm{

                           // EAX: APIHOOK::pStub

                           // 레지스터 백업

                           push edx

                           push edi

                           push esi

                           push ebp

                           mov ebp, esp

                           sub esp, local_size

 

                           // pStub = APIHOOK::pStub;

                           mov [pStub], eax

 

                           // pCurPDB = fs:[0x30];

                           mov pCurPDB, fs:[0x30]

 

                           // idx = -1;

                           mov idx, -1

 

                           // i = 0;

                           xor i, i

 

                           // pScanPDB = &pStub->Data[0].pPDB;

                           mov esi, [pStub]

             }

MAIN_LOOP:

             __asm{

                           // to next pPDB

                           add pScanPDB, 8

 

                           // if (pStub->Data[i].pPDB == pCurPDB) goto MATCH_PROC;

                           cmp pCurPDB, dword ptr [pScanPDB]

                           je MATCH_PROC

 

                           // i++

                           inc i

                           // if (i<1024) goto MAIN_LOOP;

                           cmp i, 1024

                           jl MAIN_LOOP

                           // else goto LOOP_END;

                           jmp LOOP_END

             }

MATCH_PROC:

             __asm{

                           // idx = I;

                           mov idx, i

             }

LOOP_END:

             __asm{

                           cmp idx, 0xFFFFFFFF                      // cmp idx, -1

                           je TRAMPOLINE

                           jmp SPECIFIER

                           // if (idx == -1) goto TRAMPOLINE

                           // else goto SPECIFIER

             }

TRAMPOLINE:

             __asm{

                           // eax = TRAMPOLINE FUNCTION

                           mov eax, [pStub]

                           mov eax, [eax]

                           jmp END

             }

SPECIFIER:

             __asm{

                           // eax = API SPECIFIER FUNCTION

                           mov eax, [pScanPDB+4]

             }

END:

             __asm{

                           add esp, local_size

                           pop ebp

                           pop esi

                           pop edi

                           pop edx

                           // jump TRAMPOLINE or SPECIFIER

                           jmp eax

             }

}

 

#undef pCurPDB

#undef idx

#undef i

#undef pStub

#undef pScanPDB

#undef local_size

 

여기까지가 헤더 파일(.h) 들어갈 내용들이다. 주석에 설명이 되어있으니 다음으로 넘어가자.

이제 .cpp 작성해 보자.

 

#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr)+(DWORD)(addValue))

// 후킹될 있는 최대 프로세스 개수 ( 수정시 Process Checker 수정 필요)

#define STUB_DATA_COUNT 1024

// 공유 메모리 영역에 메모리를 할당하도록 하는 Flag (VirtualAlloc)

#define VA_SHARED 0x8000000

 

// 운영체제 버전 체크. IsWinxx 형식의 함수들이 있음.

ah_osver api_hook::osver;

// 9x Stub Data 크기

const int api_hook::stubdata_len = sizeof(_9XSTUB)+(sizeof(_9XDATA)*(STUB_DATA_COUNT)-1);

// 9x Process Checker 크기 (직접 측정. VC++ Disassembly 썼다.)

const int api_hook::procchk_len = 75;

 

// 예외 처리

LPTOP_LEVEL_EXCEPTION_FILTER api_hook::pTopFilter = NULL;

api_hook *api_hook::pInstance = NULL;

 

// Api 쓰여질 코드. 명령어 길이를 맞추기 위해서 0x90 (nop-아무 동작도 수행하지 않음) 넣었다.

BYTE api_hook::ApiPatchCode[]={

             0xE9, 0x00, 0x00, 0x00, 0x00,                   // jmp rel32 (Api Specifier)

             0x90, 0x90, 0x90, 0x90, 0x90

};

 

// Api Specifier

BYTE api_hook::ApiSpecifer[]={

             0xB8, 0x00, 0x00, 0x00, 0x00,                   // mov eax, imm32 (data ptr)

             0xE9, 0x00, 0x00, 0x00, 0x00,                   // jmp rel32 (connector)

             0xC2, 0x00, 0x00                                                    // ret xx

};

 

// 9x Api Specifier

BYTE api_hook::Api9xSpecifier[]={

             0xB8, 0x00, 0x00, 0x00, 0x00,                   // mov eax, imm32 (9x stub ptr)

             0xE9, 0x00, 0x00, 0x00, 0x00                    // jmp rel32 (proc_checker)

};

 

여기까지가 선언문이다. 이제 본격적으로 코딩에 들어가 보자.

 

// 예외 처리. 예외 발생시 파괴자 호출 프로세스 종료

LONG api_hook::UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo){

    pInstance->~api_hook();

 

    return EXCEPTION_CONTINUE_SEARCH;

}

 

// 생성자

api_hook::api_hook(){

    // 인스턴스 제한

    ASSERT(!pInstance);

 

    // UnhandledExceptionFilter 설정

    pInstance = this;

    pTopFilter = SetUnhandledExceptionFilter(&api_hook::UnhandledExceptionFilter);

 

    // Win9x: Process Checker 공유 메모리에 할당한다.

    if (osver.IsWin9x()){

        m_ProcChecker = (PROC)

                           VirtualAlloc(NULL, procchk_len, MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE);

        memcpy(m_ProcChecker, &api_hook_proc_checker, procchk_len);

    }else{

        // WinNT: Process Checker 없음

        m_ProcChecker = 0;

    }

}

 

// 파괴자

api_hook::~api_hook(){

    // UnhandledExceptionFilter 복원

    SetUnhandledExceptionFilter(pTopFilter);

 

    list<APIHOOK>::iterator it = m_data.begin();

 

    // 후킹된 API 복원한다

    while(m_data.size()){

        const APIHOOK &data = *it;

        RestoreApi(data.fnApi);

        it = m_data.begin();

    }

 

    // Win9x: Process Checker 해제한다.

    if (m_ProcChecker){

        VirtualFree(m_ProcChecker, NULL, MEM_RELEASE);

        m_ProcChecker = 0;

    }

}

 

// __stdcall 호출 규약의 함수 호출을 후킹한다.

// fnApi: 후킹할 함수 주소, fnProc: API 후킹 프로시저, fnArgc: 후킹될 함수의 인수 개수, UserData: 사용자 정의 데이터

// 리턴값: API 후킹 데이터

const APIHOOK *api_hook::InterceptApi(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData){

    // 함수가 모두 주어졌는가?

    if (!fnApi || !fnProc) return NULL;

 

    // Win9x: '진짜' API 함수 주소를 얻어 온다.

    // Windows 9x에서는 System DLL 디버깅되는 것을 방지하기 위해,

    // 디버거가 실행중일 때에는 GetProcAddress()

    // 동적 할당된 코드의 주소를 리턴하며, 임포트 테이블의 내용도 모두 바뀐다.

    // 코드의 가장 부분은 API 주소를 push 하는 명령으로 되어 있다.

    if (osver.IsWin9x()){

        if (*(PBYTE)fnApi == 0x68)

        {

            fnApi = (PROC)*(DWORD *)((PBYTE)fnApi + 1);

        }

    }

 

    // 증분 링크 체크

    // Microsoft Visual C++ 실행중에도 코드를 수정할 있게끔

    // 증분 링크를 사용하여 함수의 주소가 실제 함수를 가리키지 않고, 실제 함수로 jmp 하는

    // 명령의 주소로 되어 있다.

    // 참고: System DLL 경우에 무시

    if ((DWORD)fnApi < 0x80000000 && *(PBYTE)fnApi == 0xE9){

        fnApi = (PROC)((int)(fnApi) + (*(int *)((PBYTE)fnApi+1)) + 5);

    }

 

    // 중복 후킹은 에러의 원인 하나다. (중복 후킹 막기)

    // fnApi 이미 후킹되었는가? (데이터 검색)

    list<APIHOOK>::iterator it;

    list<APIHOOK>::iterator end = m_data.end();

    for (it=m_data.begin(); it!=end; it++){

        if (it->fnApi == fnApi)

            return 0;

    }

 

    // list 추가할 데이터

    APIHOOK data;

 

    // 데이터 셋팅

    data.fnArgc = fnArgc;       // 인수 개수

    data.fnApi = fnApi;         // API 주소

    data.fnProc = fnProc;       // API Procedure

    data.UserData = UserData;       // 사용자 정의

    data.HookObj = this;        // 후킹 객체

 

    // API 덮어쓸 코드의 크기를 측정한다.

    // 디스어셈블러를 사용하여 명령 크기 단위로 5 바이트 이상의 공간을 확보한다.

    int WriteSize = 0;

    while (WriteSize < 5)

        WriteSize += m_disObj.GetLineSize(((BYTE *)fnApi)+WriteSize);

 

    // 0x90 (nop) 모자랄 경우.

    // 이론상 에러는 나지 않지만 예외 상황을 대비하여 0 리턴함.

    if (WriteSize > sizeof(ApiPatchCode)) return 0;

    // API 복원을 위해 덮어쓴 코드 크기를 저장해 둔다.

    data.fnApiOffset = WriteSize;

 

    BYTE *TrampCode = new BYTE [WriteSize+5];       // Trampoline 함수 할당

    BYTE *SpecCode = new BYTE [sizeof(ApiSpecifer)];    // api specifier 할당

    BYTE *PatchCode = new BYTE [WriteSize]; // API 덮어쓸 코드 할당

 

    // 메모리 할당 실패

    if (!PatchCode || !TrampCode || !SpecCode){

        delete [] PatchCode;

        delete [] TrampCode;

        delete [] SpecCode;

        return 0;

    }

 

    // PatchCode: jmp SpecCode

    memcpy(PatchCode, ApiPatchCode, WriteSize);

    *((int *)&PatchCode[1]) = SpecCode -((BYTE *)fnApi) -5;

   

    // SpecCode:        mov eax, data

    //          jmp api_hook_connector

    //          ret argc*sizeof(int)

    memcpy(SpecCode, ApiSpecifer, sizeof(ApiSpecifer));

    *((WORD *)(SpecCode+11)) = fnArgc * sizeof(int);

    *((int *)(SpecCode+6)) = ((BYTE *)(void *)&api_hook_connector) -(&SpecCode[6]) -4;

 

    // TrampCode:           <원본 API 코드> (API 복원시에도 활용)

    //              jmp <덮어써지지 않은 API 코드 시작 부분>

    memcpy(TrampCode, fnApi, WriteSize+5);

    TrampCode[WriteSize] = 0xE9;

    *((int *)&TrampCode[WriteSize+1]) = ((BYTE *)fnApi+WriteSize+1) -(&TrampCode[WriteSize+1]) -5;

 

    // 동적 할당한 함수들의 메모리 Protection 재설정

    VirtualProtect(PatchCode, WriteSize, PAGE_EXECUTE_READWRITE);

    VirtualProtect(SpecCode, sizeof(SpecCode), PAGE_EXECUTE_READWRITE);

    VirtualProtect(TrampCode, WriteSize+5, PAGE_EXECUTE_READWRITE);

 

    // 함수가 System DLL 속해 있지 않거나 WinNT 경우

    if (osver.IsWinNT() || (DWORD)fnApi < 0x80000000){

        // 메모리 Protection 변경 코드를 덮어씀. (안정성을 위해 가장 마지막에 덮어씀)

        // DWORD oldProtect = VirtualProtect(fnApi, WriteSize, PAGE_EXECUTE_READWRITE);

        // memcpy(fnApi, PatchCode, WriteSize);

        // VirtualProtect(fnApi, WriteSize, oldProtect);

    // Win9x에서 함수가 System DLL 속해 있는 경우

    }else{

        PVOID pfn9xSpec = 0;    // 9x Specifier

        _9XSTUB *p9xStub = 0;   // 9x Stub Data

        PVOID pfn9xTramp = 0;   // 9x Shared Trampoline Function

       

        // System DLL 함수는 모든 프로세스에 적용된다.

        // 프로세스 별로 API 후킹하려면, Stub 데이터를 한번만 할당해도 된다.

        // 따라서 중복 할당을 막기 위해 Stub 데이터 주소를 추적한다.

        // 9x Specifier 추적 (API 첫부분에 jmp 코드가 있을 경우)

        if (*(PBYTE)fnApi == 0xE9){

            // 9x Specifier

            pfn9xSpec = (PROC)((int)(fnApi) + (*(int *)((PBYTE)fnApi+1)) + 5);

 

            // 올바른 9x Specifier인가? ( 명령이 mov인가?)

            if (*(BYTE *)pfn9xSpec != 0xB8)

                pfn9xSpec = 0;

 

            // 9x Stub Data 추적

            p9xStub = (_9XSTUB *)(*(DWORD *)((BYTE *)pfn9xSpec+1));

        }

        // 9x Specifier 존재하지 않는 경우, 아직 후킹되지 않은 API이다.

        if (!pfn9xSpec){

            // 9x Specifier 할당 (공유)

            pfn9xSpec = VirtualAlloc(NULL, sizeof(Api9xSpecifier),

                                                     MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE);

            memcpy(pfn9xSpec, Api9xSpecifier, sizeof(Api9xSpecifier));

 

            // 9x stub data 할당 (공유)

            p9xStub = (_9XSTUB *)VirtualAlloc(NULL, stubdata_len,

                                                     MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE);

            memset(p9xStub, 0, stubdata_len);

 

            // Trampoline 함수 할당 (공유)

            // 기존 힙에 할당한 코드를 해제하고 다시 공유 메모리에 할당한다.

            pfn9xTramp = VirtualAlloc(NULL, WriteSize+5,

                                                     MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE);

            memcpy(pfn9xTramp, TrampCode, WriteSize+5);

            delete [] TrampCode;

            TrampCode = (BYTE *)pfn9xTramp;

 

            // Trampoline 코드가 옮겨졌으므로 jmp 명령어가 다시 설정되어야 한다.

            *((int *)&TrampCode[WriteSize+1]) = ((BYTE *)fnApi+WriteSize+1) -(&TrampCode[WriteSize+1]) -5;

 

            // 9x Stub Data 설정

            *((DWORD *)(((BYTE *)pfn9xSpec)+1)) = (DWORD)p9xStub;

            // Process Checker 설정

            *((DWORD *)(((BYTE *)pfn9xSpec)+6)) = (int)m_ProcChecker -(int)((BYTE *)pfn9xSpec+6) -4;

        }

 

        int i, idx=-1;

 

        // 프로세스 리스트에서 공간 찾음

        for (i=0; i<STUB_DATA_COUNT; i++){

            if (p9xStub->Data[i].pPDB == 0){ idx = i; break; }

        }

 

        // 프로세스 리스트가

        if (idx == -1) return 0;

 

        // 현재 Process DB 읽어와서 항목에 셋팅한다.

        PVOID PDB;

        __asm{

            mov eax, fs:[0x30]

            mov [PDB], eax

        }

 

        p9xStub->Data[i].pPDB = PDB;

 

        // API Specifier 설정

        p9xStub->Data[i].pSpec = SpecCode;

 

        // API 후킹 데이터에 저장

        data.fn9xSpecifier = (PROC)pfn9xSpec;

        data.pStub = p9xStub;

 

        // 첫번째 후킹 => TrampCode 공유 메모리에 있음 (위에서 할당함)

        if (p9xStub->nProcHooked < 1){

            data.pStub->pTrampoline = TrampCode;

        // 두번째 이상 => TrampCode 힙에 할당됨.

        // 공유된 Trampoline 함수로 설정

        }else{

            delete [] TrampCode;

            TrampCode = (BYTE*)(data.pStub->pTrampoline);

        }

 

        // 프로세스 리스트의 index 저장

        data.idxDat = idx;

 

        // 후킹 횟수 증가

        p9xStub->nProcHooked++;

    }

   

    // API 후킹 데이터에 저장한다.

    data.fnTrampoline = (PROC)TrampCode;

    data.fnSpecifer = (PROC)SpecCode;

 

    // 데이터 추가

    m_data.push_back(data);

   

    // API Specifier 해당 데이터 적용

    *((DWORD *)(SpecCode+1)) = (DWORD)&(m_data.back());

 

    // API 코드를 패치해 준다.

    if (osver.IsWinNT() || (DWORD)fnApi < 0x80000000){

        DWORD oldProtect = VirtualProtect(fnApi, WriteSize, PAGE_EXECUTE_READWRITE);

        memcpy(fnApi, PatchCode, WriteSize);

        VirtualProtect(fnApi, WriteSize, oldProtect);

    }else{

        // Win9x: 첫번째 후킹일때만 패치시켜준다.

        if (data.pStub->nProcHooked <= 1){

            *((int *)&PatchCode[1]) = (BYTE *)(data.fn9xSpecifier) -((BYTE *)fnApi) -5;

            _RtlCopyMemory(fnApi, PatchCode, WriteSize);

        }

    }

   

    // API 패치 PatchCode 해제해 준다.

    delete [] PatchCode;

 

   

    // 새로 패치한 명령어가 적용되도록 명령어 캐쉬를 비운다.

    FlushInstructionCache(GetCurrentProcess(), NULL, NULL);

 

    // API Hooking 성공!

    return (APIHOOK *)(*((DWORD *)(SpecCode+1)));

}

 

// cdecl 호출 규약의 함수 후킹

// 아래와 같이 구현하면 안전하지 않다.

// InterceptAPI 똑같이 구현하되 Specifier 마지막 스택 정리 부분을 ret 0으로 하면 된다.

// 코드의 길이가 길어져서 생략했다.

const APIHOOK *api_hook::InterceptCdecl(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData){

    APIHOOK *pHookDat = (APIHOOK *)InterceptApi(fnApi, fnProc, fnArgc, UserData);

 

    if (!pHookDat) return 0;

 

    // Specifier 스택 정리 부분을 수정한다 -> ret 0

    *((WORD *)((BYTE *)(pHookDat->fnSpecifer)+11)) = 0;

    return (APIHOOK *)(pHookDat);

}

 

// 후킹한 API 복원

int api_hook::RestoreApi(PROC fnApi){

    list<APIHOOK>::iterator it_data;

    list<APIHOOK>::iterator it_end = m_data.end();

   

    // list에서 맞는 데이터를 찾는다.

    for (it_data = m_data.begin(); it_data != it_end; it_data++){

        const APIHOOK &data = *it_data;

        // 데이터가 주어진 함수의 주소와 맞는가?

        if (data.fnApi == fnApi){

            // Win9x System API 아니거나 NT 경우

            if (osver.IsWinNT() || (DWORD)data.fnApi < 0x80000000){

                // API 코드 복원

                DWORD oldp = VirtualProtect(data.fnApi, data.fnApiOffset, PAGE_EXECUTE_READWRITE);

                memcpy(data.fnApi, data.fnTrampoline, data.fnApiOffset);

                VirtualProtect(data.fnApi, data.fnApiOffset, oldp);

 

                // 메모리 해제

                delete [] (BYTE *)data.fnTrampoline;

                delete [] (BYTE *)data.fnSpecifer;

                m_data.erase(it_data);

            }else{

                // 다른 프로세스가 후킹하고 있는 중일 경우

                if (data.pStub->nProcHooked > 1){

                    // 쓰고 있는 프로세스 리스트를 초기화시킨다.

                    memset(&data.pStub->Data[data.idxDat], 0, sizeof(_9XDATA));

                    // 후킹 횟수를 줄인다.

                    data.pStub->nProcHooked--;

 

                    // 메모리를 해제한다.

                    delete [] (BYTE *)data.fnSpecifer;

                    m_data.erase(it_data);

                }

                // 다른 프로세스가 후킹중이지 않은 경우

                else{

                    // API 복원한다.

                    _RtlCopyMemory(data.fnApi, data.fnTrampoline, data.fnApiOffset);

 

                    // 공유 메모리를 해제한다.

                    VirtualFree(data.pStub, NULL, MEM_RELEASE);

                    VirtualFree(data.fn9xSpecifier, NULL, MEM_RELEASE);

                    VirtualFree(data.fnTrampoline, NULL, MEM_RELEASE);

 

                    // 메모리를 해제한다.

                    delete [] (BYTE *)data.fnSpecifer;

                    m_data.erase(it_data);

                }

            }

            break;

        }

    }

   

    return 1;

}

 

// Trampoline 함수 호출 (__stdcall)

// API Procedure 인수를 그대로 넘겨주면 된다.

int api_hook::CallTrampolineAPI(const API *api, LPVOID pFirstArg){

    // 후킹 데이터를 읽는다.

    const APIHOOK *data = api->HookInfo;

 

    // 인수를 차례로 push Trampoline 함수를 호출한다.

    // (push 순서는 거꾸로다. 가장 마지막 인수 -> 가장 첫번째 인수로 push)

    int *pCurParam = ((int *)pFirstArg)+(data->fnArgc)-1;

 

    int _var, _proc, _ret;

 

    for (int i=0; i<data->fnArgc; i++){

        _var = (*(pCurParam-i));

        __asm push _var

    }

    _proc = (int)(data->fnTrampoline);

    __asm{

        call _proc

        mov _ret, eax

    }

 

    // 원본 API 함수의 리턴값

    return _ret;

}

 

// Trampoline 함수 호출 (cdecl, stdcall 공용)

int api_hook::CallTrampolineCdecl(const API *api, LPVOID pFirstArg){

    const APIHOOK *data = api->HookInfo;

    int *pCurParam = ((int *)pFirstArg)+(data->fnArgc)-1;

 

    int _var, _proc, _ret, _esp;

 

    __asm mov _esp, esp

   

    for (int i=0; i<data->fnArgc; i++){

        _var = (*(pCurParam-i));

        __asm push _var

    }

    _proc = (int)(data->fnTrampoline);

    __asm{

        call _proc

        mov esp, _esp

        mov _ret, eax

    }

    return _ret;

}

 

// 참고: Hook Imported Function 방식의 API Hooking.

// 수정한 부분의 주소를 반환한다. (, 함수의 주소가 기록되어 있는 메모리 주소)

// System DLL Import Table 수정할 없다.

/*PDWORD api_hook::HookImportedFunction(HMODULE hModule, PROC fnApi, PROC fnProc){

    PROC pfnOriginalProc;

    PIMAGE_DOS_HEADER pDosHeader;

    PIMAGE_NT_HEADERS pNTHeader;

    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;

    PIMAGE_THUNK_DATA pThunk;

    PVOID pBaseAddr; 

 

    // Verify that a valid pfn was passed

    if ( IsBadCodePtr(fnProc) ) return 0; 

 

    // Get API Address to Hook

    pfnOriginalProc = fnApi;

    if(!pfnOriginalProc) return 0; 

 

    // Approach the address of import table

    pBaseAddr   = (PVOID)hModule;

    pDosHeader  = (PIMAGE_DOS_HEADER)pBaseAddr; 

    pNTHeader   = (PIMAGE_NT_HEADERS) ( (DWORD)pDosHeader + pDosHeader->e_lfanew); 

    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ( (DWORD)pBaseAddr +  

   (DWORD) (pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)); 

 

    // Verify that the module is valid

    if ( pDosHeader->e_magic != IMAGE_DOS_SIGNATURE ) // "MZ"

        return 0;

    if ( pNTHeader->Signature != IMAGE_NT_SIGNATURE ) // "PE\0\0"

        return 0;

 

    // the end of dll chain is always NULL

    while ( pImportDesc->Name ) //Name is a DWORD (RVA, to a DLL name)

    {

        PSTR pszModName = MakePtr(PSTR, pDosHeader, pImportDesc->Name);

        

//      if ( stricmp(pszModName, szDllName) == 0 )  // find dll

        {

            pThunk = (PIMAGE_THUNK_DATA) ( (DWORD)pBaseAddr + pImportDesc->FirstThunk); 

 

            // the end of API address chain in dll is always NULL

            while ( pThunk->u1.Function )

            {

                PDWORD pImpFunction = pThunk->u1.Function;

 

                if (osver.IsWin9x()){

                    if (*(PBYTE)pImpFunction == 0x68){

                        pImpFunction = (PDWORD)*(DWORD *)((PBYTE)pImpFunction + 1);

                    }

                }

               

                if ( (DWORD)pImpFunction == (DWORD)pfnOriginalProc ) // find api

                {  // FOUND!

                    DWORD oldp = 0;

                    ::VirtualProtect(&pThunk->u1.Function, sizeof(PDWORD), PAGE_EXECUTE_READWRITE, &oldp);

                    pThunk->u1.Function = (PDWORD)fnProc; // intercept target api address field

                    ::VirtualProtect(&pThunk->u1.Function, sizeof(PDWORD), oldp, &oldp);

                    return (PDWORD)&pThunk->u1.Function;

                }

                pThunk++;  

            }

        }

        pImportDesc++;

    }

 

    return 0;

}*/

 

// 메모리 복사

// 메모리 Protection 무시하고 복사한다.

void _stdcall api_hook::_RtlCopyMemory(void *pDest, const void *pSrc, unsigned int nSize){

    // 메모리가 실제로 존재하는 부분인가?

    MEMORY_BASIC_INFORMATION mbi={0};

    if (VirtualQuery(pDest, &mbi, sizeof(mbi)) == sizeof(mbi)){

        if (mbi.State == MEM_FREE) return;

    }

    if (VirtualQuery(pSrc, &mbi, sizeof(mbi)) == sizeof(mbi)){

        if (mbi.State == MEM_FREE) return;

    }

 

    // WinNT: 메모리 Protection 잠시 바꾸어서 복사

    if (osver.IsWinNT()){

        DWORD oldp = VirtualProtect(pDest, nSize, PAGE_EXECUTE_READWRITE);

        DWORD oldp_2 = VirtualProtect((void *)pSrc, nSize, PAGE_EXECUTE_READWRITE);

        memcpy(pDest, pSrc, nSize);

        VirtualProtect((void *)pSrc, nSize, oldp_2);

        VirtualProtect(pDest, nSize, oldp);

        return;

    }

    // Win9x: int 2E 이용

    __asm{

        push nSize

        push pSrc

        push pDest

        mov edx, esp

        mov eax, 0x10A          // int2E_RtlCopyMemory

        int 0x2E                // RtlCopyMemory(pDest, pSrc, nSize)

        add esp, 12

    }

}

 

결론

API 후킹은 API 호출을 가로채는 것이고 동적 API 후킹은 API 후킹 기법을 활용하여 여러 API 호출을 하나의 프로시저로 연결시키는 것이다.

 

참고 자료

1. API Hooking 기초강좌

2. API Hooking Revealed (한글 번역)

3. Remote Library (한글 번역)

4. IA-32 Intel Architecture Software Developer’s Manual Volume 2A: Instruction Set Reference, A-M; CHAPTER 2, Instruction Format (한글 번역)


출처 : http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=7267&ref=7267#IDynAPIHook

'C/C++언어 > 후킹' 카테고리의 다른 글

Hooking 을 사용하는 프로그램 내의 구현  (0) 2007.11.11
Global Hooking in Win32  (0) 2007.11.11
[본문스크랩] 메세지 후킹  (0) 2007.09.06
[본문스크랩] Knowledge Base API  (0) 2007.09.06
API Hooking Revealed  (0) 2007.09.06

이건 Windows API정복 -가남사 : 김상형저- 에 있던 소스 입니다..


대략 API의 정복이라 불리우는 책이죠.. 좋은책입니다.

게임을 만들기 위해.. DirectX를 해야 하지만.. 이 DirectX를 받쳐주는게 API 방식의 프로그래밍과
MFC의 프로그래밍...


물론 각각 C언어와 C++ 언어가 되야 하지만.. 이것만 어떻게든 하면 DirectX 는 상당히 쉬운편이라

생각되네요..


뭐 대략 요령을 깨우치지 못하면.. 무척 어렵지만....


----------------- 소스.. ------------------

// 밑에 부분 복사후 Alt + F8 인가로 소스 정렬후 봅시다.. 그게 편합니다..

// 나중에 자기가 짠 소스에 허우적 거리지 않기를..


#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;
LPCTSTR lpszClass=TEXT("Class");

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
   ,LPSTR lpszCmdParam,int nCmdShow)
{
 HWND hWnd;
 MSG Message;
 WNDCLASS WndClass;
 g_hInst=hInstance;
 
 WndClass.cbClsExtra=0;
 WndClass.cbWndExtra=0;
 WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
 WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
 WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
 WndClass.hInstance=hInstance;
 WndClass.lpfnWndProc=(WNDPROC)WndProc;
 WndClass.lpszClassName=lpszClass;
 WndClass.lpszMenuName=NULL;
 WndClass.style=CS_HREDRAW | CS_VREDRAW;
 RegisterClass(&WndClass);

 hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
  NULL,(HMENU)NULL,hInstance,NULL);
 ShowWindow(hWnd,nCmdShow);
 hWndMain=hWnd;
 
 while(GetMessage(&Message,0,0,0)) {
  TranslateMessage(&Message);
  DispatchMessage(&Message);
 }
 return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
 HDC hdc;
 PAINTSTRUCT ps;

 switch(iMessage) {
 case WM_CREATE:
  return 0;
 case WM_PAINT:
  hdc=BeginPaint(hWnd, &ps);
  EndPaint(hWnd, &ps);
  return 0;
 case WM_DESTROY:
  PostQuitMessage(0);
  return 0;
 }
 return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}


//---------------- 이하 소스 끝 -------------------


실행하면 화면에 박스(프로그램 창)가 하나 뜹니다. 이 박스를 생성할때 박스 생성 조절은...

WinMain 함수의 CreateWindow 함수에서 조정하면 되는것이고요..


이 박스는 WndProc 함수안의 switch 문 안의 여러 이벤트(환경 변화..

대략 프로그램 실행시 입력하는.. 마우스 움직임 이라던지.. 키보드 눌림 변화..)

에 동작 하게 만듭니다.

+ Recent posts