Win32 User level hooking(Win32 사용자 레벨 후킹)
- 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) 작성하였다.
- 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"을 참조하기 바란다.
- 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를 참조하기 바란다.
- 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 함수를 후킹하는 또다른 방법은 디버깅 멈춤점을 목표로 하는 함수에 위치시키는 것이다. 하지만 이 방법에는 몇가지 단점들이 있다. 이 방식의 가장 큰 문제는 디버깅 예외가 모든 어플리케이션 쓰레드를 대기시킨다는 것이다. 이것은 또한 예외를 조작할 디버거 프로세스를 필요로 한다. 또다른 문제는 디버거가 종료할 때 윈도우즈가 자동으로 디버거를 끝낸다는 사실에 기인한다.(???)
[역자주] 이 부분 역시 의미를 잘 모르겠고 내용 자체가 이 글의 주제를 벗어나기 때문에 대강 해석하였습니다.
- 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".에서 다루었다.
[역자주] 여기까지의 내용을 요약해 보면 다음과 같습니다.
- API 후킹을 위해서는 기본적으로 서버 프로그램과 드라이버 DLL이 필요하다.
- 서버 프로그램은 DLL을 다른 프로세스에 침투시키는 역할을 한다.
- 드라이버 DLL은 프로세스 내의 API 호출을 가로채는 역할을 한다.
- 침투시키는 방법은 여러 가지가 있지만 윈도우즈 메시지 후크를 사용하는 방법과 CreateRemoteThread를 사용하는 방법이 제일 유리하다.
- API 호출을 가로채는 방법도 여러 가지가 있지만 IAT를 수정하는 방법이 제일 유리하다.
- 모든 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 withCreateProcess()"
.
상기의 침투 기술과 달리 CreateRemoteThread()
를 이용하는 침투는 현재 실행 중인 모든 프로세스에 대한 관리를 요구한다. 만일 침투가 적절한 때에 이루어지지 않는다면 후크 시스템이 가로채기로 지정한 호출의 일부를 수행하지 못할 수가 있다. 후킹 서버 어플리케이션이 새로운 프로세스가 시작되거나 종료할 때마다 통보를 받을 수 있도록 세련된 메카니즘을 구현하는 것은 매우 중요하다. 이러한 경우 제안되는 방법 중의 하나가 CreateProcess()
API 패밀리의 함수를 가로채서 감시하는 것이다. 그리고 후킹된 사용자 정의 함수가 호출될 때 원본 CreateProcess()
함수를 dwCreationFlags
에CREATE_SUSPENDED
플래그를 OR 연산하여 호출한다. 이것은 목표로 하는 어플리케이션의 프라이머리 쓰레드를 대기 상태로 하여 후크 서버가 DLL을 침투시키도록 하고 ResumeThread()
API로 어플리케이션을 기동시키는 것을 의미한다. 보다 자세한 정보를 원한다면 레퍼런스의 [2] "Injecting Code withCreateProcess()
"를 참조하라.
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 byCTaskManager
's member m_pProcesses
.
Following UML diagram shows the class relationships of this subsystem:
CTaskManager
는 시스템의 프로세서를 담당한다. 이 클래스는 운영체제가 제공하는 라이브러리((i.e. PSAPI or ToolHelp32 respectively)를 취사선택하여 특정 라이브러리 핸들러(CPsapiHandler
orCToolhelpHandler
)의 인스턴스를 생성하는 역할을 한다. 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.
요구되는 목표를 달성하기 위해 다음의 콤포넌트와 파일로 구성되는 단순한 프레임웍을 설계하였다:
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 ofCModuleScope
. 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()
메소드를 호출하게 된다. 아래에 이 메소드의 구현을 단순화한 소스가 있다.
BOOL CModuleScope::ManageModuleEnlistment() { BOOL bResult = FALSE; if (FALSE == *m_pbHookInstalled) { *m_pbHookInstalled = TRUE; bResult = TRUE; } 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 flagsg_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 toInstallHook()
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()
를 한번 호출하여 엔진을 활성화시키는 것이다.
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
를 정의한다. CWinHookInjector
와 CRemThreadInjector
는 같은 부모 클래스 CInjector
를 상속한다. 하지만 이들 자식 클래스들은 CInjector
인터페이스에 정의된 순수 가상 메소드인 CWinHookInjector
와CRemThreadInjector
를 다른 방식으로 구현한다.
CWinHookInjector
class implements Windows Hooks injecting mechanism. It installs a filter function by the following call
CWinHookInjector
클래스는 윈도우즈 후크를 이용하는 침투 메카니즘으로 구현된다. 이 클래스는 아래의 소스와 같이 필터 함수를 설치한다.
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, becauseGetMsgProc()
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 toManageModuleEnlistment()
method is issued inside the DllMain()
. Recall the method's implementation that I described earlier. It examines the shared variable sg_bHookInstalled
through theCModuleScope
'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
함수의 원형과 일치해야 한다는 것은 매우 중요하다. 그렇지 않으면 스택이 손상될 것이다. 예를 들면CModuleScope
는 MyTextOutA(),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) orCreateRemoteThread()
(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