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(침투 기술)
- 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" 를 참조하기 바란다.
- 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
필터 함수를 보자:
LRESULT CALLBACK GetMsgProc(
int code,
WPARAM wParam,
LPARAM lParam
)
{
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()
는 아무런 효과도 갖지 않게 된다는 시실을 강조하고 싶다.
#pragma data_seg(".HKT")
HHOOK sg_hGetMsgHook = NULL;
BOOL sg_bHookInstalled = FALSE;
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", 영문을 읽다 보면 자주 나오는 말인데 뭐라 번역을 하는게 좋은지 잘 모르겠습니다. 뜻은 "마지막 사항이지만 그렇다고 해서 덜 중요한 것이 아니다."인데 이렇게 번역하기는 좀 그렇네요.
- 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"를 참조한다.
- 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 객체를 로드한다.
- 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을 사용한다는 것이다.
- 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.
하지만 이러한 모든 후킹 전략들은 이 글의 범위를 벗어난다.
- 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 with CreateProcess()"
.
상기의 침투 기술과 달리 CreateRemoteThread()
를 이용하는 침투는 현재 실행 중인 모든 프로세스에 대한 관리를 요구한다. 만일 침투가 적절한 때에 이루어지지 않는다면 후크 시스템이 가로채기로 지정한 호출의 일부를 수행하지 못할 수가 있다. 후킹 서버 어플리케이션이 새로운 프로세스가 시작되거나 종료할 때마다 통보를 받을 수 있도록 세련된 메카니즘을 구현하는 것은 매우 중요하다. 이러한 경우 제안되는 방법 중의 하나가 CreateProcess()
API 패밀리의 함수를 가로채서 감시하는 것이다. 그리고 후킹된 사용자 정의 함수가 호출될 때 원본 CreateProcess()
함수를 dwCreationFlags
에 CREATE_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.
요구되는 목표를 달성하기 위해 다음의 콤포넌트와 파일로 구성되는 단순한 프레임웍을 설계하였다:
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()
메소드를 호출하게 된다. 아래에 이 메소드의 구현을 단순화한 소스가 있다.
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 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()
를 한번 호출하여 엔진을 활성화시키는 것이다.
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, 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
함수의 원형과 일치해야 한다는 것은 매우 중요하다. 그렇지 않으면 스택이 손상될 것이다. 예를 들면 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) 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