'3. Implementation/Hooking Windows'에 해당되는 글 9건
- 2010.03.11 Recording mouse and keyboard events and playing them back
- 2009.10.22 Manipulating Windows using messages and simple CBT hooking
- 2008.08.23 Detours - Microsoft Research
- 2008.08.11 Dll Injection Tutorial by Darawk
- 2008.08.06 마이크로소프트웨어 연재글
- 2008.08.05 API Hooking Revealed
- 2008.08.04 Win32 Hook (MSDN' 2부)
- 2008.08.04 Win32 Hook (MSDN' 1부)
- 2008.08.04 SDT Hooking 무력화에 대한 연구
Recording mouse and keyboard events and playing them back

The code is organized into two projects. One being the application that controls start and stop of record and play events, and other is the DLL which actually does the job.
For WH_JOURNALRECORD hooks, DLL may not be required and the core logic can embedded in the application source itself.
Following is how the application works:
For starting recording: Click File->Start recording and then press Start button on the menu.
all the events get recorded in c:\recording.txt
To stop recording, click File->stop recording.
To play back the recording, click File->Play and then press the start button on the menu.
Following is the hook procedure that records the events
......
http://www.codeproject.com/KB/winsdk/WIN_RECORDER.aspx
Manipulating Windows using messages and simple CBT hooking

Introduction
Windows is essentially a message driven Operating System in the sense that, the majority of actions that take place are responses to messages sent to the main window procedure of an application. Whether you press a key, or move the mouse or drag a window the application receives messages through it's message queue and reacts accordingly. Now this results in a rather interesting corollary that can be taken advantage of by us, developers; by sending the correct messages to a window or it's child windows in the proper order, we can actually simulate human actions on an application. And this has it's uses in various scenarios. Obviously the first one that comes to mind is the ability to automate a task, like for example opening a document in word, left justifying the entire text and taking a print out.
But for me a more interesting usage of this technique is when it's applied to take advantage of the Windows user interface to quickly do tasks, which might otherwise require a lot of programming calls and access to undocumented information. This includes changing various system properties, making changes through the control panel applets or even changing display properties for the desktop. In this article I will randomly select one such scenario ( a test case scenario ) and see how to go about automating the task by using some simple windows techniques like posting messages to a window, enumerating child windows and elementary CBT hooking.
Test scenario
I am going to be using Windows XP Professional as my test platform and therefore my example scenario is only meaningful in an XP context. Users of other Operating Systems might have to make suitable changes to my example code snippets to get the same end result as in this article.
By default, the keyboard navigation short cuts for menus are not shown in the XP operating system ( something that both puzzled and annoyed a lot of users when they first encountered this in Windows 2000 ). Having long abandoned Windows 2000, I do not remember if there was a documented way of changing this setting in 2000, other than by editing the registry or using some tweaking application, but in XP this setting can be easily changed by using the Display Properties control panel applet. All you need to do is select the Appearances tab from the Display Properties dialog box, bring up the Effects sub-window and uncheck the check box that says "Hide underlined letters for keyboard navigation until I press the Alt key". Now I am wholly sure that this setting can probably be changed by modifying a trivial registry entry; but for the sake of this test case scenario and the article, let's assume that we do not know how to achieve the same programmatically.
The human approach
Let's see how we'd do this had we done this manually sitting in front of the machine. We'd probably have to follow these steps ( or something very similar ) :-
- Right click on the desktop and bring up the Display Properties control panel applet
- Chose the Appearances tab
- Bring up the Effects sub-window by clicking on the Effects button
- Check/Uncheck the corresponding check box depending on what we are trying to do
- Click OK to dismiss the Effects sub-window
- Click OK to dismiss the Display Properties and apply our changes globally
The solution in code
Now we need to decide what we need to do to achieve the same sequence of events through code.
- Bringing up the Display Properties window and choosing the Appearances tab
can be done in just one step because we know that the Display Properties control
panel applet is called desk.cpl and that it takes command line arguments
that can be used to dictate which tab comes up by default. In fact we need to
call it like this :-
control.exe desk.cpl Display,@Appearance
Control.exe is used to bring up the control panel applet passed to it as first argument, and the additional arguments are used to force it to start with the Appearance tab selected.
- Now we need to enumerate the child windows ( controls ) on the Appearance
tab till we find the Effects button and this can be achieved using
EnumChildWindows
. Once we obtain the Effects button's handle we can send a button click message to it and bring up the Effects sub-window. - To locate the required check box on the Effects sub-window we would need to
first get the handle to the sub-window that just popped up. We achieve this by
setting up a global CBT hook ( this means we'd need to put all put code into a
DLL ), and watching for all newly activated windows. We know the title text for
the Effect sub-window and thus we obtain the handle to the window the moment it
gets activated. Now we do the same as previous, i.e. we use
EnumChildWindows
to retrieve the handle to the check box, and then send a button click message to it. - We post a
WM_COMMAND
message to the Effects sub-window with a command ID ofIDOK
which is the equivalent of closing the window by clicking on the OK button. - We do the same for the main Display Properties window.
Implementation details
Bringing up the Display Properties window
BOOL BringUpDisplayAppearance() |
The code is quite simple and straightforward, we simply use
ShellExecute
to bring up the Display Properties applet window with
the default tab set to the Appearances tab. I have used SW_SHOW
here because using SW_HIDE
will have no effect on the display
properties window ( I believe the control.exe program or perhaps
desk.cpl itself will later call ShowWindow(hWnd, SW_SHOW)
somewhere in the code ). We do our window hiding in the hook procedure ( but
even this is not fully effective and there is a short flash on screen, but then
our aim is not really to hide what we are doing from our end user, but to try
and make things as lucid as possible, which we achieve by reducing the time the
window remains visible to a few milliseconds ).
Getting the handle to the Effects button
HWND GetEffectsButton(HWND hWndParent) |
Using Spy++ we extract the exact text associated with the Effects button which happens to be "&Effects..." and we use this knowledge to compare the text of each child control with this text repeatedly till we get the button control we want.
Getting the check box handle
HWND GetMenuUnderlineCheck(HWND hWndParent) |
This is very similar to how we obtained the handle to the Effects button.
The CBT hook procedure
LRESULT CALLBACK CBTProc(int nCode, |
The HCBT_ACTIVATE
code indicates that a window is about to be
activated. We compare the title text of this window with "Effects" and if they
match, we know that this is the window we were searching for. If you are
wondering why we had to install a CBT hook, instead of using
FindWindow
using the title text; this is to make sure that even if
there was already an existing window with the same title text, it won't
interfere with our search because we are only checking newly activated windows.
We set the CBT hook quite late into the code and uninstall it the moment we get
the window we want. The duration the hook is active is from the time we bring up
the Display Properties window till the Effects sub-window is just about to be
activated, and under most circumstances this shouldn't be more than a few
milliseconds.
We also use the hook procedure to hide the windows that pop up, which include both the main Display Properties window as well as the Effects sub-window. The infinitesimal flash still exists and if anyone has any ideas on further reducing this, they are welcome to make suggestions.
The main function ( exported )
DEMODLL_API BOOL ToggleMenuUnderline(void) |
We first set up our CBT hook and then call the
BringUpDisplayAppearance
function to bring up the Display
Properties window with the Appearance tab selected. Once the window comes up we
obtain the handle to the Effects button using the GetEffectsButton
function, and then post a BM_CLICK
message to the Effects button
using the handle we just obtained. Almost instantly the Effect sub-window pops
up and we retrieve it's handle through our CBT hook procedure which also
uninstalls the hook since it's no longer of any use to us. We wait for the
Effects window to come up, using the following while loop :-while(!IsWindow(g_hWndEffects)) |
This way we avoid sleeping for too long or for too less. Now we obtain the
handle to the check box using the GetMenuUnderlineCheck
function
and post a BM_CLICK
message to the check box which effectively
toggles it's state which is just what we are trying to do. Now we simply post
WM_COMMAND
messages with wParam
set to
IDOK
to the Effects sub-window as well as to the Display Properties
main window. That's all; we have now successfully toggled the state of the
"Underline keyboard navigation shortcuts for menus" system-wide property.
Conclusion
The test scenario we considered was perhaps too simplistic to reveal the actual power of this technique, but when you consider that you can now do anything from your program that a user can do manually using the Windows GUI, you'll be slowly impressed by the awesome possibilities of the technique. You can use this technique to enumerate Windows themes, change the current theme, change display settings, change system settings, automate your own applications etc. The only tool you'd need in addition to Everett is Spy++ or some such similar application. Good luck with your own message based Windows automation attempts.
History
- August 8th 2003 - First written
From : http://www.voidnish.com/Articles/ShowArticle.aspx?code=manipwindows
Detours - Microsoft Research

원본 링크 : http://research.microsoft.com/sn/detours/#pubs
아래글 출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=7080&ref=7080
[MVP]신형철 (krispy)
어떤 프로그램 작동방식이 궁금해 이것 저것 뒤져 보던중 detour 라는 MS Research 팀에서 만드는 툴에 대해 알게 되었다. 이 툴엔 여러가지 기능이 있는데 Dll Injection 툴, Api Trace 툴, Memory Allocation Trace 툴 등등.. 무엇 보다 좋았던건 소스도 포함되어 있거니와 PPT 파일로 작동 방식도 설명 해주고 있다.
이 중 가장 애용(?) 했던 것이 API 함수가 호출 되는 것을 Trace 로그로 남겨 주는 traceapi 툴이다. 이를 이용해 특정 프로그램을 실행 하면 이 프로그램이 로더에 의해 로드 되어 쓰레드가 시작 될 때 부터의 모든 API를 로그로 찍어 준다.(모든 API는 아니고 거의 모든. 이에 대해서는 다시 설명 한다.) 몇 십초만 실행 시켜도 로그 파일이 몇 메가 단위로 생긴다. (요즘 CPU 참 빠르다...)
detour를 다운로드 받을 수 있는 경로는 http://research.microsoft.com/sn/detours/#pubs
다운로드 받고 나서 압축을 풀면 몇개의 폴더안의 소스코드와 Readme 파일과 설명 ppt, pdf 파일, 그리고 make 파일이 나온다. 그런데 실행 파일은 하나도 없다. 직접 빌드 해야 한다. 그러나 Visual Studio 용 프로젝트 파일 같은건 없기 때문에 makefile을 이용해 직접 빌드를 해야 한다. 빌드를 위해서는 detour가 위치한 경로에서 command 창으로
nmake
를 가볍게 쳐준다.(VC 환경 변수 세팅이 되어 있다고 가정 한다.) 그럼 자동으로 빌드가 되고 bin 폴더 안에 모든 실행 파일들이 들어간다. (간혹, pdb 파일이 뭐라 뭐라 하면서 빌드 중간에 종료 되는 경우가 있는데 이런 경우엔 lib 폴더에 detour.pdb 파일을 삭제 하고 다시 빌드 하면 된다.) 이렇게 빌드가 끝나면 각 툴들의 폴더 안에 가면 test.bat 가 있는데 이를 간단히 실행 해 봄으로써 테스트를 해 볼 수 있다.
우선 TraceApi를 보자. traceapi 폴더 내에 보면 cpp 파일 몇개와 makefile 그리고 test.bat 파일이 하나 있다. 그럼 먼저 test.bat 파일을 열어 보자.
start ..\bin\syelogd.exe
..\bin\withdll -d:traceapi.dll ..\bin\sleepold.exe
열어 보면 위와 같이 두개의 커맨드 명령이 있다. 첫째 라인은 syelogd.exe 라는 로그 프로그램을 띄우는 것이고 두번째 라인은 withdll 이라는 프로그램으로 sleepold.exe라는 프로그램을 띄우는데 이 프로세스에 traceapi.dll을 삽입 시켜 실행 하라는 명령이다. 이 batch 파일을 커맨드 콘솔에서 실행 시켜 보면 두개의 콘솔이 뜨고 하나는 로그창 다른 하나는 withdll를 이용한 sleepold.exe의 실행화면이 뜬다. 그러나 로그가 너무 빨리 지나가기 때문에 분석하기에 힘들다. 그래서 다음과 같이 커맨드 콘솔을 두개 띄우고 한개의 콘솔에는
..\bin\syelogd.exe >> trace.txt
다른 콘솔에는
..\bin\withdll -d:traceapi.dll ..\bin\someprogram.exe
이렇게 해주면 someprogram.exe가 실행되는 동안 로그는 trace.txt에 들어가게 된다. 물론 로그가 파일로 가기 때문에 someprogram.exe 가 실행 되는 동안에는 약간의 성능 저하가 있다. 그리고 로그가 금방 쌓이기 때문에 몇십초만 실행 해도 로그 파일의 크기는 메가 단위로 커진다. 우선 사용 방법은 됐는데 내가 원하는 API가 호출 되는 것에 대한 로그가 안찍히는 경우엔? traceapi 폴더 안에 보면 무려 1메가가 넘는 cpp 파일 하나가 있다. 이 _win32.cpp 파일을 보면 traceapi 툴이 훅킹 하는 모든 API 함수들의 stub을 볼 수 있는데 이 툴이 배포용으로 만들어 졌을 당시의 API 함수들만이 들어 있기 때문에 XP 나 2003 Server에 새로 추가된 함수들은 없다. 앞에서 거의 모든 API 함수들에 대한 로그라고 한 이유가 여기 있다. 그럼 여기에 내가 원하는 API 함수를 추가 해 보자.
예로 SetLayeredWindowAttributes() 함수를 추가 해 보자. 우선 함수의 prototype을 선언 해준다.
extern "C"
BOOL WINAPI SetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags);
그리고
DETOUR_TRAMPOLINE(int __stdcall Real_SetLayeredWindowAttributes(HWND a0, COLORREF a1, BYTE a2, DWORD a3), SetLayeredWindowAttributes);
와 같이 DETOUR_TRAMPOLINE 매크로를 이용해 훅킹거는 대상의 함수와 stub 함수를 연결 시켜 준다. 그리고 나서
int __stdcall Mine_SetLayeredWindowAttributes(HWND a0, COLORREF a1, BYTE a2, DWORD a3)
{
_PrintEnter("SetLayeredWindowAttributes(%lx,%lx,%lx,%lx)\n", a0, a1, a2, a3);
int rv = 0;
__try {
rv = Real_SetLayeredWindowAttributes(a0, a1, a2, a3);
} __finally {
_PrintExit("SetLayeredWindowAttributes() -> %lx\n", rv);
};
return rv;
}
를 추가 해준다. 여기서 _PrintEnter()를 보면 이 함수가 호출 되었을 때 찍히는 로그 형식을 볼 수 있는데 이를 적절히 고치면 로그의 형식을 내가 원하는 방식으로 변경이 가능 하다. 그리고 나서 VOID TrampolineWith(VOID) 함수 안에
DetourFunctionWithTrampoline((PBYTE)Real_SetLayeredWindowAttributes, (PBYTE)Mine_SetLayeredWindowAttributes);
를 추가 해준다. 이 과정은 _win32.cpp 파일의 코드를 조금만 들여다 보면 금방 알 수 있으니 직접 코드를 보면서 하면 쉽다. 이 과정이 모두 끝났으면 다시 한번 nmake를 이용해 빌드 해주고 나서 다시 실행 하면 추가한 API 함수가 로그에 찍힌다.
이제 할일은... 수만 라인의 로그를 분석 하는 일만.. ㅜ.ㅜ
Dll Injection Tutorial by Darawk

The CreateRemoteThread method
The SetWindowsHookEx method
The code cave method
Appendix A - Methods of obtaining a process ID
Appendix B - Methods of obtaining a thread ID
Appendix C - Complete CreateRemoteThread example source code
Appendix D - Complete SetWindowsHookEx example source code
Appendix E - Complete code cave example source code
Introduction
In this tutorial i'll try to cover all of the known methods(or at least, those that I know =p) of injecting dll's into a process.
Dll injection is incredibly useful for TONS of stuff(game hacking, function hooking, code patching, keygenning, unpacking, etc..).
Though there are scattered tutorials on these techniques available throughout the web, I have yet to see any complete tutorials detailing
all of them(there may even be more out there than I have here, of course), and comparing their respective strength's and weakness's.
This is precisely what i'll attempt to do for you in this paper. You are free to reproduce or copy this paper, so long as proper
credit is given and you don't modify it without speaking to me first.
The CreateRemoteThread method
I've used this in tons of stuff, and I only recently realized that a lot of people have never seen it, or know how to do it.
I can't take credit for thinking it up...I got it from an article on codeproject, but it's a neat trick that I think more
people should know how to use.
The trick is simple, and elegant. The windows API provides us with a function called CreateRemoteThread(). This allows you
to start a thread in another process. For our purposes, i'll assume you know how threading works, and how to use functions like
CreateThread(if not, you can go here ). The main disadvantage of this method is that it will work only on windows NT and above.
To prevent it from crashing, you should use this function to check to make sure you're on an NT-based system(thanks to CatID for
pointing this out):
Code:
bool IsWindowsNT()
{
// check current version of Windows
DWORD version = GetVersion();
// parse return
DWORD majorVersion = (DWORD)(LOBYTE(LOWORD(version)));
DWORD minorVersion = (DWORD)(HIBYTE(LOWORD(version)));
return (version < 0x80000000);
}
The MSDN definition for CreateRemoteThread is as follows:
Code:
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags,
LPDWORD lpThreadId );
So, it's essentially CreateThread, with an hProcess argument, so that we can tell it in which process to create the new thread.
Now, normally we would want to start the thread executing on some internal function of the process that we are interacting with.
However, to inject a dll, we have to do something a little bit different.
Code:
BOOL InjectDLL(DWORD ProcessID)
{
HANDLE Proc;
char buf[50]={0};
LPVOID RemoteString, LoadLibAddy;
if(!ProcessID)
return false;
Proc = OpenProcess(CREATE_THREAD_ACCESS, FALSE, ProcessID);
if(!Proc)
{
sprintf(buf, "OpenProcess() failed: %d", GetLastError());
MessageBox(NULL, buf, "Loader", NULL);
return false;
}
LoadLibAddy = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
RemoteString = (LPVOID)VirtualAllocEx(Proc, NULL, strlen(DLL_NAME), MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(Proc, (LPVOID)RemoteString, DLL_NAME,strlen(DLL_NAME), NULL);
CreateRemoteThread(Proc, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibAddy, (LPVOID)RemoteString, NULL, NULL);
CloseHandle(Proc);
return true;
}
This code, calls CreateRemoteThread() with a lpStartAddress of LoadLibrary(). So, it starts a new thread in the remote process
and executes the LoadLibrary() function. Luckily for us, this function takes only one argument, the name of the dll to load. We can
pass this in the arg field of CreateRemoteThread(). However, there is a minor dilemma. Since this thread will not be executing in
our address space, it won't be able to refer to strings(such as the name of the dll) that are in our address space. So, before calling
CreateRemoteThread(), we have to allocate space in the other process, using VirtualAllocEx(), and write our string there. Finally,
we pass the pointer to the string inside the remote process in the single arg field of CreateRemoteThread(), and voila...Our dll is
now loaded and running smoothly within the remote process. This is the generic loader program I use whenever I need to load a dll.
The SetWindowsHookEx method
The SetWindowsHookEx method is a little bit more intrusive than the first, and creates more of a commotion in the injected
process, which we normally don't want. However, it is a little bit easier to use than the first, and does have it's own advantages
(like being able to inject into every process on the system in one shot). The SetWindowsHookEx() function is designed to allow you
to "hook" windows messages for a given thread. This requires that you inject a dll into that process's address space, so
SetWindowsHookEx() handles all that for us. The dll must have a function for the hook that it created though, otherwise it will
crash.
Code:
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
idHook is just that, the ID of the message that we want to hook. There are many of them(for a complete list, go
here ), however we'll want to use one that's as unintrusive as possible, and has the least likelihood of causing alarm bells to
go off in any AV software(SetWindowsHookEx is the staple of all ring3 keyloggers). The WH_CBT message seems innocuous enough.
Quote:
WH_CBT
Installs a hook procedure that receives notifications useful to a computer-based training (CBT) application. For more information,
see the CBTProc hook procedure.
--MSDN
So, we'll need to create a placebo CBT hook proc in our dll, so that when the hook is called, we can handle it properly.
Code:
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(0, nCode, wParam, lParam);
};
All we're doing is calling the next hook in the chain of hooks that exist for this message. Getting back to the SetWindowsHookEx()
function, the next parameter we see is lpfn. lpfn is exactly as it sounds "long pointer to function". That's a pointer to our CBT
hook proc function. So, to get this, we'll have to either hardcode the address, or load the dll first ourselves. Hardcoding anything
is a bad idea, so we'll load the dll using LoadLibrary(), and use GetProcAddress() to get the address of our function.
Code:
HMODULE hDll;
unsigned long cbtProcAddr;
hDll = LoadLibrary("injected.dll");
cbtProcAddr = GetProcAddress(hDll, "CBTProc");
Now, in cbtProcAddr we have the address of our function. Parameter 3, of SetWindowsHookEx() is a handle to our dll, we've
already obtained this in the process of getting the address of CBTProc(hDll is a handle to our dll, returned by LoadLibrary). Now,
there is only one parameter left in the SetWindowsHookEx() function, the dwThreadId parameter. If you want to inject your dll into
every process on the system(useful for global function hooks, keyloggers, trojans, rootkits, etc..) you can simply specify 0 for
this parameter. If you want to target a specific process, you'll need to get the ID of one of it's threads. There are many ways
of doing this, and i'll try to enumerate as many as I can think of in Appendix B. So,
to put it all together into one neat little function:
Code:
BOOL InjectDll(char *dllName)
{
HMODULE hDll;
unsigned long cbtProcAddr;
hDll = LoadLibrary(dllName);
cbtProcAddr = GetProcAddress(hDll, "CBTProc");
SetWindowsHookEx(WH_CBT, cbtProcAddr, hDll, GetTargetThreadIdFromWindow("targetApp"));
return TRUE;
}
The code cave method
Instead of exploiting a windows API function to force the process to load our Dll, this time we'll allocate a little chunk
memory inside the target application, and inject a little stub that loads our dll. The advantage of this approach is that it
will work on any version of windows, and it's also the least detectable of any of the methods mentioned thus far. Our stub will
look like this:
Code:
__declspec(naked) loadDll(void)
{
_asm{
// Placeholder for the return address
push 0xDEADBEEF
// Save the flags and registers
pushfd
pushad
// Placeholder for the string address and LoadLibrary
push 0xDEADBEEF
mov eax, 0xDEADBEEF
// Call LoadLibrary with the string parameter
call eax
// Restore the registers and flags
popad
popfd
// Return control to the hijacked thread
ret
}
}
0xDEADBEEF is just there to mark addresses that we can't know beforehand, and have to patch-in at runtime. Ok, let's make a list
of the things that we need to do to make this work:
- Allocate space for the stub
- Allocate space for the name of the dll
- Suspend the main thread of our target
- Get the address of the next instruction to be executed(need this for the next step)
- Patch the proper address to return to in the stub
- Patch the address of the dll name
- Patch the address of LoadLibrary
- Set the address of the next instruction to be executed in our target's thread, to the address of the beginning of our stub
- Resume the target's thread
To allocate space inside the target, we'll use VirtualAllocEx(). We'll need to open a handle to the process
with the VM_OPERATION privelege specified, in order to do this. For our dllName string, we'll only need read and write priveleges.
For the stub however, we'll need read, write, and execute priveleges. Then we'll write in our dllName string, so that we can reference
it from the stub once it's inserted.
Code:
void *dllString, *stub;
unsigned long wowID;
HANDLE hProcess
//See Appendix A for
//this function
wowID = GetTargetProcessIdFromProcname(PROC_NAME);
hProcess = OpenProcess((PROCESS_VM_WRITE | PROCESS_VM_OPERATION), false, wowID);
dllString = VirtualAllocEx(hProcess, NULL, (strlen(DLL_NAME) + 1), MEM_COMMIT, PAGE_READWRITE);
stub = VirtualAllocEx(hProcess, NULL, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, dllString, DLL_NAME, strlen(DLL_NAME), NULL);
To accomplish our next few tasks, we'll need a handle to one of our target's threads. We can use the functions from Appendix B
to get the ID of one such thread, and then use the OpenThread API to get a handle. We'll need to be able to get and set context, and
also suspend and resume the thread.
Code:
unsigned long threadID;
HANDLE hThread;
threadID = GetTargetThreadIdFromProcname(PROC_NAME);
hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, threadID);
Now, we need to pause the thread in order to get it's "context". The context of a thread is the current state of all of it's
registers, as well as other peripheral information. However, we're mostly concerned with the EIP register, which points to the
next instruction to be executed. So, if we don't suspend the thread before retrieving its context information, it'll continue
executing and by the time we get the information, it'll be invalid. Once we've paused the thread, we'll retrieve it's context
information using the GetThreadContext() function. We'll grab the value
of the current next instruction to be executed, so that we know where our stub should return to. Then it's just a matter of
patching up the stub to have all of the proper pointers, and forcing the thread to execute it:
Code:
SuspendThread(hThread);
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
oldIP = ctx.Eip;
//Set the EIP of the context to the address of our stub
ctx.Eip = (DWORD)stub;
ctx.ContextFlags = CONTEXT_CONTROL;
//Right now loadDll is code, which isn't writable. We need
//to change that.
VirtualProtect(loadDll, stubLen, PAGE_EXECUTE_READWRITE, &oldprot);
//Patch the first push instruction
memcpy((void *)((unsigned long)loadDll + 1), &oldIP, 4);
//Patch the 2nd push instruction
memcpy((void *)((unsigned long)loadDll + 8), &dllString, 4);
//Patch the mov eax, 0xDEADBEEF to mov eax, LoadLibrary
memcpy((void *)((unsigned long)loadDll + 13), &loadLibAddy, 4);
WriteProcessMemory(hProcess, stub, loadDll, stubLen, NULL); //Write the stub into the target
//Set the new context of the target's thread
SetThreadContext(hThread, &ctx);
//Let the target thread continue execution, starting at our stub
ResumeThread(hThread);
All that's left now, is to cleanup the evidence. Before we do that though, we should pause the injector for a bit, to be sure
that the target has time to execute our stub(don't want any nasty race conditions). We'll use Sleep() to pause for 8 seconds before
unmapping the memory that we allocated, and exiting the injector.
Code:
Sleep(8000);
VirtualFreeEx(hProcess, dllString, strlen(DLL_NAME), MEM_DECOMMIT);
VirtualFreeEx(hProcess, stub, stubLen, MEM_DECOMMIT);
CloseHandle(hProcess);
CloseHandle(hThread);
This method should work on any version of windows, and should be the least likely to trigger any A/V alarms or cause the program
to malfunction. If you can understand it and implement it properly, this is definitely the best of the three methods.
Appendix A - Methods of obtaining a process ID
If the process you're targeting has a window, you can use the FindWindow function, in conjunction with GetWindowThreadProcessId,
as shown here:
Code:
unsigned long GetTargetProcessIdFromWindow(char *className, char *windowName)
{
unsigned long procID;
HWND targetWnd;
targetWnd = FindWindow(className, windowName);
GetWindowThreadProcessId(targetWnd, &procId);
return procID;
}
If you only know the name of the executable file or it just doesn't have a window:
Code:
unsigned long GetTargetProcessIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot;
BOOL retval, ProcFound = false;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
return pe.th32ProcessID;
}
Appendix B - Methods of obtaining a thread ID
If the process you're targeting has a window, you can use the FindWindow function, in conjunction with GetWindowThreadProcessId
and the toolhelp API, as shown here:
Code:
unsigned long GetTargetThreadIdFromWindow(char *className, char *windowName)
{
HWND targetWnd;
HANDLE hProcess
unsigned long processId, pTID, threadID;
targetWnd = FindWindow(className, windowName);
GetWindowThreadProcessId(targetWnd, &processId);
_asm {
mov eax, fs:[0x18]
add eax, 36
mov [pTID], eax
}
hProcess = OpenProcess(PROCESS_VM_READ, false, processID);
ReadProcessMemory(hProcess, (const void *)pTID, &threadID, 4, NULL);
CloseHandle(hProcess);
return threadID;
}
If you only know the name of the executable of your target, then you can use this code to locate it:
Code:
unsigned long GetTargetThreadIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot, hProcess;
BOOL retval, ProcFound = false;
unsigned long pTID, threadID;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
CloseHandle(thSnapshot);
_asm {
mov eax, fs:[0x18]
add eax, 36
mov [pTID], eax
}
hProcess = OpenProcess(PROCESS_VM_READ, false, pe.th32ProcessID);
ReadProcessMemory(hProcess, (const void *)pTID, &threadID, 4, NULL);
CloseHandle(hProcess);
return threadID;
}
Appendix C - CreateRemoteThread complete example source code
Code:
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include <shlwapi.h>
#define PROCESS_NAME "target.exe"
#define DLL_NAME "injected.dll"
//I could just use PROCESS_ALL_ACCESS but it's always best to use the absolute bare minimum of priveleges, so that your code works in as
//many circumstances as possible.
#define CREATE_THREAD_ACCESS (PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ)
BOOL WriteProcessBYTES(HANDLE hProcess,LPVOID lpBaseAddress,LPCVOID lpBuffer,SIZE_T nSize);
BOOL LoadDll(char *procName, char *dllName);
BOOL InjectDLL(DWORD ProcessID, char *dllName);
unsigned long GetTargetProcessIdFromProcname(char *procName);
bool IsWindowsNT()
{
// check current version of Windows
DWORD version = GetVersion();
// parse return
DWORD majorVersion = (DWORD)(LOBYTE(LOWORD(version)));
DWORD minorVersion = (DWORD)(HIBYTE(LOWORD(version)));
return (version < 0x80000000);
}
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
if(IsWindowsNT())
LoadDll(PROCESS_NAME, DLL_NAME);
else
MessageBox(0, "Your system does not support this method", "Error!", 0);
return 0;
}
BOOL LoadDll(char *procName, char *dllName)
{
DWORD ProcID = 0;
ProcID = GetProcID(procName);
if(!(InjectDLL(ProcID, dllName)))
MessageBox(NULL, "Process located, but injection failed", "Loader", NULL);
return true;
}
BOOL InjectDLL(DWORD ProcessID, char *dllName)
{
HANDLE Proc;
char buf[50]={0};
LPVOID RemoteString, LoadLibAddy;
if(!ProcessID)
return false;
Proc = OpenProcess(CREATE_THREAD_ACCESS, FALSE, ProcessID);
if(!Proc)
{
sprintf(buf, "OpenProcess() failed: %d", GetLastError());
MessageBox(NULL, buf, "Loader", NULL);
return false;
}
LoadLibAddy = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
RemoteString = (LPVOID)VirtualAllocEx(Proc, NULL, strlen(DLL_NAME), MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(Proc, (LPVOID)RemoteString, dllName, strlen(dllName), NULL);
CreateRemoteThread(Proc, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibAddy, (LPVOID)RemoteString, NULL, NULL);
CloseHandle(Proc);
return true;
}
unsigned long GetTargetProcessIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot;
BOOL retval, ProcFound = false;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
return pe.th32ProcessID;
}
Appendix D - SetWindowsHookEx complete example source code
Code:
#include <windows.h>
#include <tlhelp32.h>
#define PROC_NAME "target.exe"
#define DLL_NAME "injected.dll"
void LoadDll(char *procName, char *dllName);
unsigned long GetTargetThreadIdFromProcname(char *procName);
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
LoadDll(PROC_NAME, DLL_NAME);
return 0;
}
void LoadDll(char *procName, char *dllName)
{
HMODULE hDll;
unsigned long cbtProcAddr;
hDll = LoadLibrary(dllName);
cbtProcAddr = GetProcAddress(hDll, "CBTProc");
SetWindowsHookEx(WH_CBT, cbtProcAddr, hDll, GetTargetThreadIdFromProcName(procName));
return TRUE;
}
unsigned long GetTargetThreadIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot, hProcess;
BOOL retval, ProcFound = false;
unsigned long pTID, threadID;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
CloseHandle(thSnapshot);
_asm {
mov eax, fs:[0x18]
add eax, 36
mov [pTID], eax
}
hProcess = OpenProcess(PROCESS_VM_READ, false, pe.th32ProcessID);
ReadProcessMemory(hProcess, (const void *)pTID, &threadID, 4, NULL);
CloseHandle(hProcess);
return threadID;
}
Appendix E - Code cave example source code
Code:
#include <windows.h>
#include <tlhelp32.h>
#include <shlwapi.h>
#define PROC_NAME "target.exe"
#define DLL_NAME "injected.dll"
unsigned long GetTargetProcessIdFromProcname(char *procName);
unsigned long GetTargetThreadIdFromProcname(char *procName);
__declspec(naked) loadDll(void)
{
_asm{
// Placeholder for the return address
push 0xDEADBEEF
// Save the flags and registers
pushfd
pushad
// Placeholder for the string address and LoadLibrary
push 0xDEADBEEF
mov eax, 0xDEADBEEF
// Call LoadLibrary with the string parameter
call eax
// Restore the registers and flags
popad
popfd
// Return control to the hijacked thread
ret
}
}
__declspec(naked) loadDll_end(void)
{
}
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
void *dllString;
void *stub;
unsigned long wowID, threadID, stubLen, oldIP, oldprot, loadLibAddy;
HANDLE hProcess, hThread;
CONTEXT ctx;
stubLen = (unsigned long)loadDll_end - (unsigned long)loadDll;
loadLibAddy = (unsigned long)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
wowID = GetTargetProcessIdFromProcname(PROC_NAME);
hProcess = OpenProcess((PROCESS_VM_WRITE | PROCESS_VM_OPERATION), false, wowID);
dllString = VirtualAllocEx(hProcess, NULL, (strlen(DLL_NAME) + 1), MEM_COMMIT, PAGE_READWRITE);
stub = VirtualAllocEx(hProcess, NULL, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, dllString, DLL_NAME, strlen(DLL_NAME), NULL);
threadID = GetTargetThreadIdFromProcname(PROC_NAME);
hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, threadID);
SuspendThread(hThread);
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
oldIP = ctx.Eip;
ctx.Eip = (DWORD)stub;
ctx.ContextFlags = CONTEXT_CONTROL;
VirtualProtect(loadDll, stubLen, PAGE_EXECUTE_READWRITE, &oldprot);
memcpy((void *)((unsigned long)loadDll + 1), &oldIP, 4);
memcpy((void *)((unsigned long)loadDll + 8), &dllString, 4);
memcpy((void *)((unsigned long)loadDll + 13), &loadLibAddy, 4);
WriteProcessMemory(hProcess, stub, loadDll, stubLen, NULL);
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
Sleep(8000);
VirtualFreeEx(hProcess, dllString, strlen(DLL_NAME), MEM_DECOMMIT);
VirtualFreeEx(hProcess, stub, stubLen, MEM_DECOMMIT);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
unsigned long GetTargetProcessIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot;
BOOL retval, ProcFound = false;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
CloseHandle(thSnapshot);
return pe.th32ProcessID;
}
unsigned long GetTargetThreadIdFromProcname(char *procName)
{
PROCESSENTRY32 pe;
HANDLE thSnapshot, hProcess;
BOOL retval, ProcFound = false;
unsigned long pTID, threadID;
thSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(thSnapshot == INVALID_HANDLE_VALUE)
{
MessageBox(NULL, "Error: unable to create toolhelp snapshot", "Loader", NULL);
return false;
}
pe.dwSize = sizeof(PROCESSENTRY32);
retval = Process32First(thSnapshot, &pe);
while(retval)
{
if(StrStrI(pe.szExeFile, procName) )
{
ProcFound = true;
break;
}
retval = Process32Next(thSnapshot,&pe);
pe.dwSize = sizeof(PROCESSENTRY32);
}
CloseHandle(thSnapshot);
_asm {
mov eax, fs:[0x18]
add eax, 36
mov [pTID], eax
}
hProcess = OpenProcess(PROCESS_VM_READ, false, pe.th32ProcessID);
ReadProcessMemory(hProcess, (const void *)pTID, &threadID, 4, NULL);
CloseHandle(hProcess);
return threadID;
}
출처 : http://www.dabouncer.net/uploads/dllinjection.txt
2008. 05 SDT 후킹의 창과 방패
윈도우 프로그래머를 위한 PE 포맷 가이드
2007. 08 실행파일 속으로(PDF) (바인드 정보) (소스)
2007. 09 DLL 로딩하기(PDF) (소스)
2007. 10 실행 파일 생성기의 원리
2007. 11 코드 패칭
2007. 12 바이러스
2008. 01 진화하는 코드
2008. 02 실행 압축의 원리
2008. 03 실행 파일 보안의 원리
2008. 04 실행 파일 프로텍터
윈도우 프로그래밍 테크닉
2007. 06 호출(calling)의 예술(PDF)
2007. 07 SpiderMonkey를 사용한 자바스크립트 임베딩(PDF) (소스)
개발자를 위한 윈도우 후킹 테크닉
2006. 05 키보드 훅을 통해 키로거 제작하기(PDF). (소스)
2006. 06 마우스 훅을 통한 화면 캡쳐 프로그램 제작하기(PDF). (소스)
2006. 07 메시지훅을 사용해 Spy++ 흉내내기(PDF). (보충) (소스 코드 버그 수정) (소스)
2006. 08 SendMessage 후킹하기(PDF). (보충) (소스)
2006. 09 Spy++ 클론 imSpy 제작하기(PDF). (소스) (2003 프로젝트) (보충)
2006. 10 저널 훅을 사용한 매크로 제작(PDF) (소스) (보충)
2006. 11 WH_SHELL 훅으로 다른 프로세스 윈도우 서브클래싱 하기(PDF) (소스) (보충)
2006. 12 WH_DEBUG 훅을 이용한 훅 탐지 방법(PDF) (소스) (보충)
2007. 01 OutputDebugString의 동작 원리(PDF) (소스) (보충)
마소 플러스
2006. 09 유니코드에 대비한 프로그램을 작성하는 여섯 가지 규칙(DOC).
2006. 09 VC++을 사용해 DLL을 만들때 지여야 할 일곱 가지 규칙(DOC).
2006. 10 ShellExecute의 단점, ShowBrowser로 메운다(DOC)
2006. 11 뮤텍스와 바이너리 세마포의 차이점(웹)
2006. 12 고급 매크로 표현식에 쓸 여섯 가지 테크닉(웹) (보충)
2007. 01 NIM게임으로 배워보는 알고리즘 디자인(웹)
2007. 02 ini 파일 헬퍼 클래스 만들기(웹)
2007. 02 STL 맵을 사용한 환경변수 관리(PDF) (소스)
2007. 03 CRT(C Runtime Library 이야기)(웹)
2007. 04 통찰력(insight)을 찾아서(웹)
2007. 06 메모리 사용법 1
2007. 07 메모리 사용법 2
2007. 08 복잡한 포인터 선언 이해하기
2007. 09 개발자의 시간을 갉아먹는 함정들
2007. 09 가변인자 포워딩 시키기
2007. 11 코드 리딩(reading)의 중요성
2007. 12 ‘분산투자’와 ‘몰빵’에서 배우는 교훈
2008. 01 참을 수 없는 예외처리의 편리함
2008. 02 개발자를 위한 드래곤볼
2008. 04 공명의 지혜
2008. 06 효율적으로 MSDN을 보는 방법
나만의 윈도우 라이브 가젯 만들기
2007. 02 이상한 나라의 자바스크립트
2007. 03 체스 기보 뷰어 만들기
2007. 04 Hello, World 가젯 만들기
2007. 05 StockViewer 가젯 만들기
책소개
2008. 03 Secure Coding in C and C++
출처 : http://www.jiniya.net/tt/271
[출처] 마이크로소프트웨어 연재글|작성자 마검린
출처 : http://blog.naver.com/mirnae?Redirect=Log&logNo=100051404897
API Hooking Revealed
Article Starts[역자주] 이 글은 CodeProject 사이트에 Ivo Ivanov가 "API hooking revealed"라는 제목으로 게재한 글입니다. Win32 시스템에서 API를 후킹하는 방법에 대한 전반적인 기법들을 다루고 있으며 자세한 설명이나 코드보다는 개념적인 내용들이 많아 Win32 SDK에 익숙치 않은 개발자들도 쉽게 접근할 수 있는 좋은 글인 것 같습니다. 원래 소스 파일이 함께 첨부되어 게재된 글인데 이 파일을 번역한 글과 함께 올리는 것은 혹시 저작권 문제가 될 수도 있다고 생각되어 따로 첨부하지 않았습니다. 소스 파일이 필요하신 분은 원문인 "API hooking revealed"에서 다운받아 사용하시기 바랍니다.
이 글을 읽고 제대로 이해하려면 최소한 윈도우즈 메시지 후킹과 DLL의 기본 구조에 대해서는 알아야 합니다. 이런 내용들은 MSDN에 잘 설명되어 있고 codeproject, codeguru, devpia 등의 사이트에도 좋은 글들이 많이 게재되어 있으니 참고하시기 바랍니다.
어떤 윈도우즈 어플리케이션을 개발하기 위해서 우리들은 여러 종류의 언어나 도구들을 사용할 수 있습니다. Visual Basic, Visual C++, Delphi, C++ Builder, PowerBuilder 등 수많은 언어와 도구들이 있습니다.
이런 프로그램들에서 어떤 작업을 수행하기 위해서 사용하는 방법은 서로 다를지라도 그 내부로 들어가 보면 결국에는 윈도우즈 운영체제에서 제공하는 API를 호출하게 됩니다. 예를 들어 화면에 문자열을 출력하려고 한다면 MFC에서는 CDC::DrawText를 사용하고 Dephi에서는 TCanvas::TextOut을 사용합니다. 도구마다 사용하는 형태는 이렇게 다르지만 결국에는 TextOutA/W API를 호출합니다.
TextOutA/W API는 gdi32.dll에 구현되어 있는 함수이고 운영체제가 제공하는 API입니다. 윈도우즈는 기본적으로 3개의 DLL-kernel32.dll, user32.dll, gdi32.dll-에 대부분의 API를 구현하여 제공하고 어플리케이션은 실행시 자신의 프로세스 주소 공간으로 이들 DLL을 매핑한 후 사용합니다.
API 후킹은 어떤 프로그램에서 API 호출을 하는 것을 가로채서 개발자가 만든 프로그램의 함수가 처리할 수 있도록 하는 메카니즘을 말합니다. 반드시 윈도우즈의 API일 필요는 없고 단지 DLL에서 구현하여 제공하는 함수이면 됩니다.
API 후킹은 어떤 언어로 개발된 프로그램에도 적용될 수 있으며 디버깅이나 트레이싱 작업, 모니터링 작업 등에 사용할 수 있으며 소스 코드가 없는 프로그램에 기능을 추가하기 위한 용도로도 사용할 수 있습니다. 그외에도 여러가지 용도로 사용할 수 있지만 굳이 이런 프로그램을 개발할 계획이 아니더라도 윈도우즈 프로그램의 기본적인 원리를 이해하는데 많은 도움이 됩니다.
API 후킹을 적용하여 만든 프로그램 중에 주위에서 일반적으로 볼 수 있는 것으로는 myQuickFind, 아래한글사전과 같은 전자 사전 프로그램이 있습니다. 이런 프로그램들은 모두 다른 윈도우의 마우스 커서 밑에 있는 단어를 인식할 수 있는 기능을 가지고 있는데 이것은 윈도우즈 메시지 후킹과 API 후킹(TextOutA/W)을 이용한 것입니다.
또한 크래킹이나 스파이웨어를 개발하는데 악용될 수도 있고 반대로 방지하는 용도로도 사용될 수 있습니다. 어찌되었든 간에 이 글이 윈도우즈 환경에서 개발을 하시는 많은 분들에게 도움이 되기를 바랍니다.
번역해서 글을 올리는 것이 처음이어서 어색한 부분들이 많습니다. 게다가 저자가 쉼표를 생략하는 동격 명사 구문이나 분사 구문을 많이 사용하였고 번역이 표준화되지 않은 개발 관련 용어들도 많아 더욱 그렇습니다. 그래서 원문과 번역문을 함께 올렸고 번역이 미비하거나 어색한 부분에는 [역자주]를 달아 놓았습니다. 그래도 이상한 부분들은 코멘트를 달아 주시면 가능하면 수정해서 다시 올리도록 하겠습니다.
Introduction(소개)
Intercepting Win32 API calls has always been a challenging subject among most of the Windows developers and I have to admit, it's been one of my favorite topics. The term Hooking represents a fundamental technique of getting control over a particular piece of code execution. It provides an straightforward mechanism that can easily alter the operating system's behavior as well as 3rd party products, without having their source code available.
Win32 API 호출을 가로채는 것은 대다수의 윈도우즈 개발자들 사이에서 항상 도전하는 과제이었으며 내가 가장 좋아하는 주제 중의 하나가 되었다. 후킹이라는 단어는 코드 실행의 특정 부분을 제어할 수 있는 기반 기술을 의미한다. 후킹은 써드 파티 제품뿐 아니라 운영체제의 동작까지도 소스 코드 없이 쉽게 바꿀 수 있는 간편한 메카니즘을 제공한다.
Many modern systems draw the attention to their ability to utilize existing Windows applications by employing spying techniques. A key motivation for hooking, is not only to contribute to advanced functionalities, but also to inject user-supplied code for debugging purposes.
많은 현대적인 시스템들이 스파이 기술을 사용하여 기존의 윈도우즈 어플리케이션을 활용하는 것에 관심을 기울이고 있다. 후킹을 연구하는 주요 동기는 향상된 기능을 제공하는 것 뿐 아니라 디버깅을 목적으로 사용자 정의 코드를 침투시키는 것에 있다.
Unlike some relatively "old" operating systems like DOS and Windows 3.xx, the present Windows OS as NT/2K and 9x provide sophisticated mechanisms to separate address spaces of each process. This architecture offers a real memory protection, thus no application is able to corrupt the address space of another process or in the worse case even to crash the operating system itself. This fact makes a lot harder the development of system-aware hooks.
DOS, 윈도우즈3.XX와 같이 상대적으로 오래된 운영체제와 달리, NT/2K,9x와 같은 현재의 윈도우즈 OS는 각각의 프로세스의 주소 공간을 분리하는 정교한 메카니즘을 제공한다. 이러한 구조는 실제 메모리 보호를 제공하여 어떠한 어플리케이션도 다른 프로세스의 주소 공간을 변형시키거나 더 나아가 운영 체제 자체를 망가뜨리지 못하도록 한다. 이러한 이유로 시스템 후킹의 개발은 더욱 어려워졌다.
My motivation for writing this article was the need for a really simple hooking framework, that will offer an easy to use interface and ability to capture different APIs. It intends to reveal some of the tricks that can help you to write your own spying system. It suggests a single solution how to build a set for hooking Win32 API functions on NT/2K as well as 98/Me (shortly named in the article 9x) family Windows. For the sake of simplicity I decided not to add a support do UNICODE. However, with some minor modifications of the code you could easily accomplish this task.
이 글을 쓰게 된 동기는 사용하기 쉽고 다른 API들을 가로챌 수 있는 매우 간단한 후킹 프레임웍에 대한 필요성 때문이다. 이 글에서 스파이 시스템을 구축하기 위해 필요한 몇가지 트릭들을 밝힐 계획이다. 이 글은 98/Me와 NT/2K 에서 모두 사용할 수 있는 Win32 API 함수 후킹에 대한 하나의 솔루션을 제안한다. 단순하게 하기 위하여 UNICODE에 대한 지원은 추가하지 않기로 하였다. 하지만 코드를 약간 수정하면 UNICODE에 대한 지원을 쉽게 달성할 수 있을 것이다.
Spying of applications provides many advantages:
어플리케이션을 스파이하는 것은 다음과 같은 많은 이점을 제공한다:
- API function's monitoring(API 함수의 감시)
The ability to control API function calls is extremely helpful and enables developers to track down specific "invisible" actions that occur during the API call. It contributes to comprehensive validation of parameters as well as reports problems that usually remain overlooked behind the scene. For instance sometimes, it might be very helpful to monitor memory related API functions for catching resource leaks.
API 함수 호출을 제어할 수 있는 능력은 API 호출시 발생하는 보이지 않는 특정 행위를 개발자가 추적할 수 있도록 도와준다. 또한 항상 지나칠 수 있는 문제점들을 보고할 뿐 아니라 매개변수에 대한 종합적인 검증을 할 수도 있다. 예를 들면 메모리와 관련된 API 함수들을 감시하여 리소스 누출을 잡아내는 것에 매우 유용하다.
- Debugging and reverse engineering(디버깅과 역공학)
Besides the standard methods for debugging API hooking has a deserved reputation for being one of the most popular debugging mechanisms. Many developers employ the API hooking technique in order to identify different component implementations and their relationships. API interception is very powerful way of getting information about a binary executable.
디버깅을 위한 표준적인 방법들을 제외하면 API 후킹은 가장 인기있는 디버깅 메카니즘의 하나라는 평가를 받는다. 많은 개발자들이 API 후킹 기술을 콤포넘트의 서로 다른 구현들과 그들의 관계를 파악하기 위해 사용한다. API 가로채기는 이진 실행 파일에 대한 정보를 얻을 수 있는 매우 강력한 방법이다.
- Peering inside operating system(운영체제 엿보기)
Often developers are keen to know operating system in dept and are inspired by the role of being a "debugger". Hooking is also quite useful technique for decoding undocumented or poorly documented APIs.
가끔 개발자들은 운영체제에 대해 이해하기를 갈망하고 "디버거"의 역할을 하게 된다. 후킹은 문서화되지 않았거나 빈약하게 문서화된 API를 해석하는데 매우 유용하다.
- Extending originally offered functionalities(기존 기능의 확장) by embedding custom modules into external Windows applications Re-routing the normal code execution by injecting hooks can provide an easy way to change and extend existing module functionalities. For example many 3rd party products sometimes don't meet specific security requirements and have to be adjusted to your specific needs. Spying of applications allows developers to add sophisticated pre- and post-processing around the original API functions. This ability is an extremely useful for altering the behavior of the already compiled code.
외부의 윈도우즈 어플리케이션에 사용자 모듈을 삽입하여 기존에 제공되던 기능을 확장하는, 후크를 침투시켜 기존의 코드 실행을 재편성하는 것은 기존 모듈의 기능을 수정하고 확장하는 쉬운 방법을 제공할 수 있다.(???) 예을 들면 많은 써드 파티 제품들이 특별한 보안적 요구 사항없이 특정 기능을 하도록 수정되어야 한다. 어플리케이션 스파이는 개발자가 원본 API 함수의 호출 전,후에 세련된 처리를 추가할 수 있도록 해준다. 이러한 능력은 이미 컴파일된 코드의 동작을 변경하는데 매우 유용하다.
[역자주] 도저히 첫번째 문장은 매끄러운 번역을 못하겠습니다. 정확히 주어가 무엇인지를 모르겠지만 대략적인 의미는 API 후킹을 이용하여 소스 코드없이 기존의 어플리케이션에 새로운 기능을 추가하거나 동작을 변경할 수 있다는 것입니다.Functional requirements of a hooking system(후킹 시스템의 기능적 요구 사항)
There are few important decisions that have to be made, before you start implementing any kind of API hooking system. First of all, you should determine whether to hook a single application or to install a system-aware engine. For instance if you would like to monitor just one application, you don't need to install a system-wide hook but if your job is to track down all calls to
TerminateProcess()
orWriteProcessMemory()
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 itsDllMain
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 toCallNextHookEx()
has to be made. After a successful call toSetWindowsHookEx()
, 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 dummyWH_GETMESSAGE
filter function:시스템 전역 후크는
SetWindowsHookEx()
함수가 실행될 때 등록된다. 오류가 없으면 후크 핸들이 반환된다. 반환값은 사용자 정의 후크 함수의 끝부분에서CallNextHookEx()
함수를 호출할 때 필요하다.SetWindowsHookEx()
함수가 성공적으로 호출되면 운영체제는 DLL을 특별한 후크 필터의 요구 사항을 충족시키는 모든 프로세스에 자동적으로(즉시는 아님) 침투시킨다. 아래의 아무것도 하지 않는WH_GETMESSAGE
필터 함수를 보자://--------------------------------------------------------------------------- // GetMsgProc // // Filter function for the WH_GETMESSAGE - it's just a dummy function //--------------------------------------------------------------------------- LRESULT CALLBACK GetMsgProc( int code, // hook code WPARAM wParam, // removal option LPARAM lParam // message ) { // We must pass the all messages on to CallNextHookEx. return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam); }
A system-wide hook is loaded by multiple processes that don't share the same address space.
시스템 전역 후크는 동일한 주소 공간을 공유하지 않는 여러 개의 프로세스들에 로드된다.
For instance hook handle
sg_hGetMsgHook
, that is obtained bySetWindowsHookEx()
and is used as parameter inCallNextHookEx()
must be used virtually in all address spaces. It means that its value must be shared among hooked processes as well as the Hook Server application. In order to make this variable "visible" to all processes we should store it in the shared data section.예를 들면
SetWindowsHookEx()
호출로 얻어지고CallNextHookEx()
호출의 매개변수로 사용되는sg_hGetMsgHook
는 모든 주소 공간에서 사용된다. 이것은 이 값이 후크 서버 어플리케이션 뿐만 아니라 후크된 프로세스 사이에서도 공유되어야 한다는 것을 의미한다. 이 변수를 모든 프로세스에서 사용 가능하도록 하기 위해서는 이 변수를 공유 데이터 섹션에 저장하여야만 한다.The following is an example of employing
#pragma data_seg()
. Here I would like to mention that the data within the shared section must be initialized, otherwise the variables will be assigned to the default data segment and#pragma data_seg()
will have no effect.아래의 예제는
#pragma data_seg()
의 사용을 보여준다. 공유 섹션의 데이터들은 반드시 초기화되어야 하며 그렇지 않으면 변수들은 디폴트 데이터 세그먼트에 할당되어#pragma data_seg()
는 아무런 효과도 갖지 않게 된다는 시실을 강조하고 싶다.//--------------------------------------------------------------------------- // Shared by all processes variables //--------------------------------------------------------------------------- #pragma data_seg(".HKT") HHOOK sg_hGetMsgHook = NULL; BOOL sg_bHookInstalled = FALSE; // We get this from the application who calls SetWindowsHookEx()'s wrapper HWND sg_hwndServer = NULL; #pragma data_seg()
You should add a SECTIONS statement to the DLL's DEF file as wellDLL의 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 callsUnhookWindowsHookEx()
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 returnsNULL
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 callLoadLibrary()
on our behalf, if we don't have any access to process's threads? Well, there is a function, calledCreateRemoteThread()
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 theCreateRemoteThread()
:기본 개념은 상당히 단순하지만 굉장히 멋지다. 어떤 프로세스도
LoadLibrary()
API를 호출하여 DLL을 동적으로 로드할 수 있다. 문제는 어떻게 프로세스의 쓰레드에 대해 전혀 접근을 하지 못하면서 외부의 프로세스가 적절하게LoadLibrary()
함수를 호출하도록 만드는가 하는 것이다. 하나의 리모트 쓰레드를 생성하는CreateRemoteThread()
함수가 그 해답이다. 여기서 약간의 속임수가 필요하다. - 쓰레드 함수의 원형을 보라. 이 함수의 포인터가 매개변수(LPTHREAD_START_ROUTINE
)로CreateRemoteThread()
함수에 넘겨진다:DWORD WINAPI ThreadProc(LPVOID lpParameter);
And here is the prototype ofLoadLibrary
API그리고 여기
LoadLibrary
API의 원형이 있다.HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
Yes, they do have "identical" pattern. They use the same calling conventionWINAPI
, they both accept one parameter and the size of returned value is the same. This match gives us a hint that we can useLoadLibrary()
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 theLoadLibrary()
API. The dodgy thing here is that Kernel32.DLL is mapped always to the same address space of each process, thus the address ofLoadLibrary()
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 ofCreateRemoteThread()
.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 toCreateRemoteThread()
, it first opens the process usingOpenProcess()
API and passesPROCESS_ALL_ACCESS
flag as parameter. This flag is used when we want to get maximum access rights to this process. In this scenarioOpenProcess()
will returnNULL
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) throughOpenProcess()
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 callingAdjustTokenPrivileges()
API - Close obtained by
OpenProcessToken()
process token handle
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 implementedIObjectWithSite
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 callSetWindowLongPtr()
withGWLP_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()
andGetStockObject()
.프로그램 로더는 어플리케이션을 로드하면서 어플리케이션에 링크된 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 usingVirtualProtect()
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 toGetProcAddress()
returns a wrapper to the real pointer. Returned byGetProcAddress()
value points toPUSH
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 aPUSH
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".에서 다루었다.
- Windows subclassing.(윈도우즈 서브클래싱)
[역자주] 여기까지의 내용을 요약해 보면 다음과 같습니다.
- 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 interceptCreateProcess()
API family functions and monitor all their invocations. Thus when an user supplied function is called, it can call the originalCreateProcess()
withdwCreationFlags
OR
-ed withCREATE_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
orCToolhelpHandler
) 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 theCTaskManager
object the application callsPopulate()
method. It forces enumerating of all processes and DLL libraries and storing them into a hierarchy kept byCTaskManager
's memberm_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()
andExitProcess()
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 byCreateRemoteThread()
means. This can be determined by an option in the INI file that holds all settings of the system.무엇보다 먼저 DLL 드라이버를 모든 프로세스에 침투시키는 요구 사항을 충족하는 주입 방법을 선택하는 것이 필요하다. 그래서 INI 파일의 설정과 운영체제(i.e. NT/2K or 9x)의 종류에 따라 적용할 2개의 침투 기술을 가지고 추상적인 접근을 설계했다. 그것은 시스템 전역 윈도우즈 후크와
CreateRemoteThread()
이다. 예제 프레임웍은CreateRemoteThread()
함수를 사용해서 주입할 수 있을 뿐 아니라 NT/2K에서 윈도우즈 후크를 사용하여 DLL을 침투시킬 수도 있는 능력을 제공한다. 어떠한 것을 사용할 것인가는 시스템의 모든 설정을 가지고 있는 INI 파일의 설정에 따라 결정된다.Another crucial moment is the choice of the hooking mechanism. Not surprisingly, I decided to apply altering IAT as an extremely robust method for Win32 API spying.
또다른 중요한 결정은 후킹 메카니즘을 선택하는 것이다. 당연히 Win32 API를 후킹하는 매우 견실한 방법인 IAT 변경을 적용하기로 결정하였다.
To achieve desired goals I designed a simple framework composed of the following components and files:
- TestApp.exe - a simple Win32 test application that just outputs a text using TextOut() API. The purpose of this app is to show how it gets hooked up.
- HookSrv.exe - control program
- HookTool .DLL - spy library implemented as Win32 DLL
- HookTool.ini - a configuration file
- NTProcDrv.sys - a tiny Windows NT/2K kernel-mode driver for monitoring process creation and termination. This component is optional and addresses the problem with detection of process execution under NT based systems only.
요구되는 목표를 달성하기 위해 다음의 콤포넌트와 파일로 구성되는 단순한 프레임웍을 설계하였다:
- TestApp.exe - TextOut() API를 사용하여 텍스트를 출력하는 단순한 Win32 테스트 어플리케이션. 이 어플리케이션의 목적은 어떻게 후킹이 되는가를 보여주는 것이다.
- HookSrv.exe - 제어 프로그램.
- HookTool .DLL - Win32 DLL로 구현된 스파이 라이브러리.
- HookTool.ini - 설정 파일.
- NTProcDrv.sys - 프로세스의 생성과 소멸을 감시하는 작은 윈도우즈 NT/2K 커널 모드 드라이버. 이 컴포넌트는 선택 사항이며 NT 시스템에서만 프로세스의 실행을 감지하여 문제점을 찾아낸다.
[역자주] 이 드라이버를 컴파일하기 위해서는 윈도우즈 DDK(Device Driver Kit)가 설치되어 있어야 한다. DDK는 무료가 아니므로 MSDN 가입자가 아니면 마이크로소프트에서 다운로드 받을 수가 없다. 하지만 이 드라이버는 모니터하는 용도의 선택 사항이므로 후킹 예제를 테스트하기 위해 반드시 컴파일이 필요하지는 않다.
HookSrv is a simple control program. Its main role is to load the HookTool.DLL and then to activate the spying engine. After loading the DLL, the Hook Server calls
InstallHook()
function and passes a handle to a hidden windows where the DLL should post all messages to.HookSrv는 단순한 제어 프로그램이다. 이것의 주요 임무는 HookTool.DLL을 로드하여 스파이 엔진을 활성화시키는 것이다. DLL을 로드한 후에 후크 서버는
InstallHook()
함수를 호출하고 DLL이 모든 메시지를 전달해야 하는 숨겨진 윈도우에 핸들을 넘겨준다.HookTool.DLL is the hook driver and the heart of presented spying system. It implements the actual interceptor and provides three user supplied functions
TextOutA/W()
andExitProcess()
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 receivingDLL_PROCESS_ATTACH
notification. This step just initializes the only instance ofCModuleScope
. An important piece of theCModuleScope
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 ofUseWindowsHook
parameter under [Scope] section. In case that the system is running under Windows 9x, the value of this parameter won't be examined by the system, because Window 9x doesn't support injecting by remote threads.어떤 어플리케이션이 HookTool 라이브러리를 로드할 때 DLL은
DLL_PROCESS_ATTACH
통지를 받고CModuleScope
객체를 하나 생성한다.CModuleScope
객체 생성의 중요한 부분은 적합한 침투 객체를 생성하는 것이다. 어떤 침투 클래스를 사용할 것인가는 HookTool.ini 파일을 읽어 [Scope] 섹션의UseWindowsHook
항목의 값을 확인한 후에 결정된다. 시스템이 윈도우즈 9x에서 실행되는 경우에는 리모트 쓰레드를 사용하는 침투가 지원되지 않으므로 이 항목은 시스템이 무시한다.After instantiating of the main processor object, a call to
ManageModuleEnlistment()
method will be made. Here is a simplified version of its implementation:주 처리 객체가 생성된 후에
ManageModuleEnlistment()
메소드를 호출하게 된다. 아래에 이 메소드의 구현을 단순화한 소스가 있다.// Called on DLL_PROCESS_ATTACH DLL notification BOOL CModuleScope::ManageModuleEnlistment() { BOOL bResult = FALSE; // Check if it is the hook server if (FALSE == *m_pbHookInstalled) { // Set the flag, thus we will know that the server has been installed *m_pbHookInstalled = TRUE; // and return success error code bResult = TRUE; } // and any other process should be examined whether it should be // hooked up by the DLL else { bResult = m_pInjector->IsProcessForHooking(m_szProcessName); if (bResult) InitializeHookManagement(); } return bResult; }
The implementation of the method
ManageModuleEnlistment()
is straightforward and examines whether the call has been made by the Hook Server, inspecting the valuem_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 to
InstallHook()
DLL exported function. Actually its call is delegated to a method ofCModuleScope
-InstallHookMethod()
. The main purpose of this function is to force targeted for hooking processes to load or unload the HookTool.DLL.후크 서버가 취하는 다음 행동은 DLL의 익스포트된 함수인
InstallHook()
를 한번 호출하여 엔진을 활성화시키는 것이다.// Activate/Deactivate hooking engine BOOL CModuleScope::InstallHookMethod(BOOL bActivate, HWND hWndServer) { BOOL bResult; if (bActivate) { *m_phwndServer = hWndServer; bResult = m_pInjector->InjectModuleIntoAllProcesses(); } else { m_pInjector->EjectModuleFromAllProcesses(); *m_phwndServer = NULL; bResult = TRUE; } return bResult; }
HookTool.DLL provides two mechanisms for self injecting into the address space of an external process - one that uses Windows Hooks and another that employs injecting of DLL by
CreateRemoteThread()
API. The architecture of the system defines an abstract classCInjector
that exposes pure virtual functions for injecting and ejecting DLL. The classesCWinHookInjector
andCRemThreadInjector
inherit from the same base -CInjector
class. However they provide different realization of the pure virtual methodsInjectModuleIntoAllProcesses()
andEjectModuleFromAllProcesses()
, defined inCInjector
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 callCWinHookInjector
클래스는 윈도우즈 후크를 이용하는 침투 메카니즘으로 구현된다. 이 클래스는 아래의 소스와 같이 필터 함수를 설치한다.// Inject the DLL into all running processes BOOL CWinHookInjector::InjectModuleIntoAllProcesses() { *sm_pHook = ::SetWindowsHookEx( WH_GETMESSAGE, (HOOKPROC)(GetMsgProc), ModuleFromAddress(GetMsgProc), 0 ); return (NULL != *sm_pHook); }
As you can see it makes a request to the system for registering
WH_GETMESSAGE
hook. The server executes this method only once. The last parameter ofSetWindowsHookEx()
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 theGetMsgProc()
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 exportsGetMsgProc()
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 returnFALSE
in itsDllMain()
. That's because the operating system validatesDllMain()
's return value and keeps trying to load this DLL until itsDllMain()
finally returnsTRUE
.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 ofCNtInjectorThread
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 toCNtInjectorThread ::OnCreateProcess()
is issued, accordingly when the process exitsCNtInjectorThread ::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 theDllMain()
. Recall the method's implementation that I described earlier. It examines the shared variablesg_bHookInstalled
through theCModuleScope
's memberm_pbHookInstalled
. Since the server's initialization had already set the value ofsg_bHookInstalled
toTRUE
, 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 asLoadLibrary()
API family as well asGetProcAddress()
. 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 exampleCModuleScope
implements three static methodsMyTextOutA(),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 originalTextOutA()
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()
andMyExitProcess()
. 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
Article Ends
[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이 글에 평점 주기:
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=6753&ref=6753
========================================
필터 함수
필터 함수는 훅에 설정되는 함수이다. 필터 함수는 윈도우에 의해 호출되고 응용 프로
그램에 의해 호출되는 것이 아니다. 따라서 필터 함수는 callback functions이라고도
불린다. 일관성을 유지하기 위해 여기에서는 필터 함수라는 말을 사용한다.
필터 함수는 다음과 같은 형태를 가진다:
LRESULT CALLBACK FilterFunc( nCode, wParam, lParam )
int nCode;
WORD wParam;
DWORD lParam;
필터 함수는 LONG 형 반환값을 가지며 FilterFunc에 실제 필터 함수의 이름이 온다.
Parameters
필터 함수는 3개의 매개변수를 받는다: ncode (훅 코드), wParam, lParam. 훅 코드는
정수값으로 추가적인 데이터를 담고 있다. 예를 들어 훅 코드는 어떤 동작 또는 이벤
트가 훅을 호출했는 지에 대한 정보를 담고 있다.
3.1 버전 이전의 윈도우에서 훅 코드는 필터 함수가 그 이벤트를 처리할지 또는
DefHookProc 함수를 호출할지 결정하기 위해 사용되었다. 훅 코드가 영(zero)보다 작
은 경우 필터 함수는 그 이벤트를 처리해서는 안된고 넘겨 받은 3개의 매개변수를 수
정하지 않고 그대로 DefHookProc 함수를 호출하는데 넘겨주어야 했다. 윈도우는 음수
값의 훅 코드를 사용하여 필터 함수열을 유지하였다.
윈도우 3.1에서도 음의 훅 코드가 필터 함수로 보내지는 경우 필터 함수가
CallNextHookEx 함수를 호출하도록 하였다. 또한 필터 함수는 CallNextHookEx 함수에
서 반환된 값을 반환하여야 한다. 하지만 윈도우 3.1이 음의 훅 코드를 필터 함수로
전달하는 일은 없다.
두 번째와 세 번째 매개변수는 각각 WPARAM, LPARAM 형이다. 이들 매개변수는 필터 함
수에 필요한 정보를 전달한다. 각각의 훅은 wParam과 lParam에 각기 다른 의미를 부여
한다. 예를 들어 WH_KEYBOARD 훅에 설치된 필터 함수는 wParam에 가상 키 코드를 받
고 lParam에는 키 이벤트가 발생했을 때의 키보드 상태를 설명하는 값을 받는다.
WH_MSGFILTER 훅에 설치된 필터 함수의 경우 wParam은 NULL 값을, lParam에는
message structure에 대한 포인터를 받는다. 몇몇 훅은 wParam과 lParam에 훅을 호출
한 원인에 따라 다른 값들을 넘겨준다. 훅의 종류에 따른 매개 변수의 의미는 윈도우
NT를 위한 Win32 SDK를 참고하면 된다.
Hook
Filter function documentation
WH_CALLWNDPROC
CallWndProc
WH_CBT
CBTProc
WH_DEBUG
DebugProc
WH_GETMESSAGE
GetMsgProc
WH_JOURNALRECORD
JournalRecordProc
WH_JOURNALPLAYBACK
JournalPlaybackProc
WH_SHELL
ShellProc
WH_KEYBOARD
KeyboardProc
WH_MOUSE
MouseProc
WH_MSGFILTER
MessageProc
WH_SYSMSGFILTER
SysMsgProc
필터 함수열에서 다음 함수 호출하기
훅이 설정된 경우 윈도우는 훅의 필터 함수열에서 첫 번째 함수를 호출하고 이로써 윈
도우의 책임은 끝이 난다. 필터 함수열에서 다음 필터 함수를 호출하는 책임은 필터
함수 자체에 있다. 필터 함수열에서 다음 필터 함수를 호출하기 위해 윈도우는
CallNextHookEx 함수를 제공한다. CallNextHookEx 함수는 4개의 매개변수를 가진다.
LRESULT CallNextHookEx(
HHOOK hhk, // handle to current hook
int nCode, // hook code passed to hook procedure
WPARAM wParam, // value passed to hook procedure
LPARAM lParam // value passed to hook procedure
);
첫 번째 매개변수는 SetWindowsHookEx 함수의 결과로 반환된 값입니다. 현재 이 값은
무시되지만 이후에는 어떻게 변할지 알 수 없습니다.
다음 세 개의 매개변수 nCode, wParam, lParam은 윈도우가 필터 함수로 건네준 매개변
수들입니다.
윈도우는 필터 함수열을 내부적으로 저장하고 어느 필터 함수를 호출하는지 관리합니
다. CallNextHookEx 함수가 호출될 때 윈도우는 다음 필터 함수를 함수열에서 결정하
고 그 함수를 호출합니다.
때때로 필터 함수는 동인한 열에 있는 다른 필터 함수도 이벤트를 넘겨주지 않기를 바
랄 수도 있습니다. 훅이 필터 함수의 이벤트를 제거를 허용하고 필터 함수가 이벤트
를 제거하려고 하는 경우 필터 함수는 CallNextHookEx 함수를 호출해서는 안됩니다.
필터 함수가 메시지를 수정했을 경우, 수정한 메시지를 필터 함수열의 나머지 함수들
로는 전해주지 않을 것입니다.
필터 함수는 특정한 순서로 설치되지 않으므로 새로 설치하려는 필터 함수가 필터 함
수열에서 설치하는 바로 그 순간을 제외하고는 어디에 위치할지 알 수 없다. 따라서
새로 설치하는 필터 함수가 발생하는 모든 이벤트를 받을 것이라고 확신할 수 없다.
새로운 필터 함수를 설치하기 이전에 설치된 어떤 필터 함수가 새로 설치하는 필터 함
수로 이벤트를 보내지 않을 수도 있다.
DLL에 있는 필터 함수
시스템 범위의 필터 함수는 DLL에 있어야만 한다. 16 비트 윈도우에서는 추천되지는
않지만 시스템 훅을 응용 프로그램 내의 필터 함수에 설치하는 것이 가능했다. 하지
만 Win32에서는 이러한 방식이 동작하지 않는다. 특정한 시스템에서 동작하는 것처럼
보일지도 모르지만 DLL에 있지 않은 시스템 범위의 필터 함수를 설치해서는 안된다.
journal 훅인 WH_JOURNALRECORD와 WH_JOURNALPLAYBACK는 이 규칙의 예외이다. 윈도우
가 이들 훅은 다른 훅과 다른 방식으로 처리하기 때문에 꼭 DLL에 있어야 할 필요는
없다.
시스템 범위의 훅을 위한 필터 함수는 그 함수가 실행되는 프로세스와 다른 프로세스
에서 필요로 하는 데이터를 공유할 수 있는 방법을 마련해야 한다. DLL은 DLL을 호출
한 클라이언트 프로세스 공간에 포함된다. 전역 변수라 하더라도 공유 데이터에 있지
않으면 특정 인스턴스에서만, 즉 DLL을 호출한 클라이언트에서만 사용할 수 있다. 예
를 들어 훅 예제에 있는 HOOKSDLL.DLL 라이브러리는 두 개의 데이터를 공유한다:
메시지를 표시할 윈도우의 핸들
윈도우에 있는 문자열의 높이
이 데이터를 공유하기 위해서는 이들 데이터를 공유 데이터 섹션에 위치시켜야 한다.
HOOKSDLL이 데이터를 공유하기 위해서는 다음과 같은 순서를 따라야 한다:
pragmas를 사용하여 데이터를 named data segment에 위치시킨다. 데이터는 반드시 초
기화되어야 한다.
// Shared DATA
#pragma data_seg(".SHARDATA")
static HWND hwndMain = NULL; // Main hwnd. We will get this from the app.
static int nLineHeight = 0; // Height of lines in window.
#pragma data_seg()
DLL의 .DEF 파일에 SECTIONS 구문을 첨가한다.
SECTIONS
.SHARDATA Read Write Shared
.DEF 파일에서 .EXP 파일을 생성한다.
hooksdll.exp: hooksdll.obj hooksdll.def
$(implib) -machine:$(CPU) -def:hooks.def hooksdll.obj -out:hooksdll.lib
HOOKSDLL.EXP 파일과 연결시키다.
hooksdll.dll: hooksdll.obj hooksdll.def hooksdll.lib hooksdll.exp
$(link) $(linkdebug) -base:0x1C000000 -dll -entry:LibMain$(DLLENTRY) -out:hooksdll.dll hooksdll.exp hooksdll.obj hooksdll.rbj $(guilibsdll)
훅의 종류
WH_CALLWNDPROC
운영 체제는 윈도우의 SendMessage 함수가 호출될 때 이 훅을 호출한다. 필터 함수는
현재 쓰레드에서 이 메시지가 발생했는지를 나타내는 훅 코드와 실제 메시지를 포함하
고 있는 구조체의 포인터를 받는다.
CWPSTRUCT 구조체는 다음과 같다.
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
DWORD message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT, NEAR *NPCWPSTRUCT, FAR *LPCWPSTRUCT;
필터는 메시지를 처리할 수 있지만 메시지를 수정할 수는 없다. 16 비트 윈도우에서
는 수정도 가능했따. 메시지는 원래 전해질 함수로 전달된다. 이 훅은 특히 시스템 범
위의 훅으로 설치된 경우 시스템 성능을 저해하기 때문에 개발이나 디버깅 도구로만
사용하는 것이 좋다.
WH_CBT
CBT 응용 프로그램을 작성하기 위해서 개발자는 CBT 프로그램과 그 프로그램의 대상
이 되는 프로그램을 결합시켜야 한다. 이러한 작업이 가능하도록 윈도우는 WH_CBT 훅
을 제공한다. 윈도우는 필터 함수로 훅 코드와 발생한 이벤트의 종류 그리고 이벤트
에 필요한 데이터를 넘겨준다.
WH_CBT 훅에 설치된 필터 함수는 다음의 10가지 훅 코드 처리할 수 있다.
HCBT_ACTIVATE
HCBT_CREATEWND
HCBT_DESTROYWND
HCBT_MINMAX
HCBT_MOVESIZE
HCBT_SYSCOMMAND
HCBT_CLICKSKIPPED
HCBT_KEYSKIPPED
HCBT_SETFOCUS
HCBT_QS
HCBT_ACTIVATE
운영 체제는 윈도우가 활성화되려고 할 때 이 훅 코드와 함께 WH_CBT 훅을 호출한다.
쓰레드 훅의 경우 생성되는 윈도우는 같은 쓰레드에 있어야 한다. 필터 함수가 TRUE
를 반환하면 윈도우는 활성화되지 않는다.
wParam 매개변수는 활성화되려고 하는 윈도우의 핸들을 가지고 있다. lParam 매개변수
는 구조체 CBTACTIVATESTRUCT에 대한 포인터를 가지고 있다.
typedef struct tagCBTACTIVATESTRUCT
{
BOOL fMouse; // 마우스로 클릭하여 활성화되면 TRUE,
// 그렇지 않으면 FALSE.
HWND hWndActive; // 현재 활성화되려고 하는
// 윈도우에 대한 핸들.
} CBTACTIVATESTRUCT, *LPCBTACTIVATESTRUCT;
HCBT_CREATEWND
윈도우가 생성되려고 할 때 이 훅 코드를 사용하여 WH_CBT 훅을 호출한다. 쓰레드 훅
의 경우 동일한 쓰레드에서 윈도우가 생성되는 경우이다. WH_CBT 훅은
WM_GETMINMAXINFO, WM_NCCREATE, WM_CREATE 메시지를 윈도우로 보내기 전에 호출된
다. 따라서 필터 함수는 TRUE를 반환하여 윈도우가 생성되지 않도록 할 수 있다.
wParam 매개변수는 생성되려고 하는 윈도우의 핸들을 가지고 있다. lParam 매개변수
는 구조체 CBT_CREATEWND에 대한 포인터를 가지고 있다.
struct CBT_CREATEWND
{
struct tagCREATESTRUCT *lpcs; // 새로 생성되는 윈도우의
// 생성 인자
HWND hwndInsertAfter; // Z-order 상에서
// 생성되는 윈도우의 앞에 오는 윈도우
} CBT_CREATEWND, *LPCBT_CREATEWND;
필터 함수는 구조체의 값을 변경할 수 있다.
HCBT_DESTROYWND
윈도우가 파괴되려고 할 때 이 훅 코드를 이용하여 WH_CBT를 호출한다. 쓰레드 훅의
경우 동일한 쓰레드에 있는 윈도우이어야 한다. 운영 체제는 WM_DESTROY 메시지를 보
내기 전에 WH_CBT 훅을 호출한다. 필터 함수가 TRUE를 반환하면 윈도우는 파괴되지 않
는다.
wParam 매개변수는 파괴되려고 하는 윈도우의 핸들을 가지고 있다. lParam 매개변수
는 영(zero)의 값을 갖는다.
HCBT_MINMAX
윈도우가 최소화 또는 최대화되려고 할 때 이 훅 코드를 사용하여 WH_CBT 훅을 호출한
다. 쓰레드 훅의 경우 동일한 쓰레드에 있는 윈도우이어야 한다. 필터 함수가 TRUE를
반환하면 최소화 또는 최대화가 일어나지 않는다.
wParam 매개변수는 최대화 또는 최소화되려고 하는 윈도우에 대한 핸들을 가지고 있
다. lParam 매개변수는 발생한 동작을 지시하는 값으로 WINUSER.H에 정의된 SW_* 값
들 중 하나의 값을 가진다.
HCBT_MOVESIZE
윈도우가 이동하거나 크기가 변하려고 할 때 그리고 사용자가 윈도우의 위치나 크기
변화를 끝냈을 때 이 훅 코드를 사용하여 WH_CBT를 호출한다. 쓰레드 훅의 경우 동일
한 쓰레드에 있는 윈도우이어야 한다. 필터 함수가 TRUE를 반환하면 변화가 일어나지
않는다.
wParam 매개변수는 이동되거나 크기가 변하는 윈도우에 대한 핸들을 가지고 있다.
lParam 매개변수는 끌기 직사각형(drag rectangle)에 대한 포인터로 LPRECT 형 값을
가진다.
HCBT_SYSCOMMAND
운영 체제가 시스템 명령어(system command)를 처리할 때 이 훅 코드를 사용하여
WH_CBT 훅을 호출한다. 쓰레드 훅의 경우 시스템 메뉴가 사용되는 윈도우는 같은 쓰레
드에 있어야 한다. WH_CBT 훅은 DefWindowsProc 함수에서 호출된다. 응용 프로그램이
WH_SYSCOMMAND 메시지를 DefWindowsProc 함수로 보내지 않으면 이 훅은 호출되지 않는
다. 필터 함수가 TRUE를 반환하면 시스템 명령은 처리되지 않는다.
wParam 매개변수는 수행되려고 하는 시스템 명령어(SC_TASKLIST, SC_HOTKEY 등)를 가
지고 있다. wParam의 값이 SC_HOTKEY인 경우 lParam의 LOWORD는 hotkey가 적용되는 윈
도우의 핸들을 가지고 있다. wParam의 값이 SC_HOTKEY 이외의 값이고 시스템 명령어
를 마우스로 선택한 경우 lParam의 LOWORD는 커서의 수평 위치를 HIWORD는 커서의 수
직 위치를 가지고 있다.
다음의 시스템 명령어들은 DefWindowProc 함수에서 이 훅을 호출한다. SC_CLOSE
윈도우를 닫는다.
SC_HOTKEY
응용 프로그램이 hot key에 의해 활성화된다.
SC_HSCROLL
수평 방향으로 스크롤한다.
SC_KEYMENU
키 입력에 의해 메뉴를 연다.
SC_MAXIMIZE
윈도우를 최대화한다.
SC_MINIMIZE
윈도우를 최소화한다.
SC_MOUSEMENU
마우스 입력에 의해 메뉴를 연다.
SC_MOVE
윈도우를 움직인다.
SC_NEXTWINDOW
다음 윈도우로 이동한다.
SC_PREVWINDOW
이전 윈도우로 이동한다.
SC_RESTORE
이전 위치로 복구한다.
SC_SCREENSAVE
화면 보호기를 실행한다.
SC_SIZE
윈도우의 크기를 조절한다.
SC_TASKLIST
윈도우의 작업 관리자(task manager)를 실행시키거나 활성화시키다.
SC_VSCROLL
수직 방향으로 스크롤한다.
HCBT_CLICKSKIPPED
마우스 이벤트가 쓰레드의 입력 큐에서 제거되고 마우스 훅이 설정되었을 때 이 훅 코
드를 사용하여 WH_CBT 훅을 호출한다. 운영 체제는 마우스 이벤트가 입력 큐에서 제거
되고 시스템 범위의 마우스 훅이나 현재의 쓰레드를 위한 쓰레드 훅이 설치된 경우 시
스템 범위의 훅을 호출한다. 이 훅 코드는 필터 함수가 WH_MOUSE 훅에 설치되지 않은
경우에는 발생하지 않는다. HCBT_CLICKSKIPPED는 마우스 이벤트가 버렸을 때 뿐만이
아니라 마우스 이벤트가 시스템 큐에서 제거될 때에도 호출된다. 이 값의 주요한 용도
는 마우스 이벤트에 반응하는 WH_JOURNALPLAYBACK 훅을 설치하는 것이다. (자세한 내
용은 아래 "WM_QUEUESYNC" 부분을 참고하면 된다.)
wParam 매개변수는 마우스 메시지의 메시지 ID를 가지고 있다. 예를 들면
WM_LBUTTONDOWN이나 WM_?BUTTON*고 같은 메시지들이다. lParam 매개변수는
MOUSEHOOKSTRUCT 구조체에 대한 포인터를 가지고 있다.
typedef struct tagMOUSEHOOKSTRUCT {
POINT pt; // 화면 좌표계에서의 마우스 위치
HWND hwnd; // 이 메시지를 받는 윈도우 핸들
UINT wHitTestCode; // hit-testing (HT_*) 결과
DWORD dwExtraInfo; // 현재 메시지와 관련된 부가 정보
} MOUSEHOOKSTRUCT, FAR *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;
HCBT_KEYSKIPPED
키보드 이벤트가 시스템 큐에서 제거되고 키보드 훅이 설치된 경우 이 훅 코드를 사용
하여 WH_CBT를 호출한다. 운영 체제는 키보드 이벤트가 입력 큐에서 제거되고 시스템
범위의 키보드 훅이나 현재의 쓰레드를 위한 쓰레드 훅이 설치된 경우 시스템 범위의
훅을 호출한다. 이 훅 코드는 필터 함수가 WH_KEYBOARD 훅에 설치되지 않은 경우에는
발생하지 않는다. HCBT_KEYSKIPPED는 키보드 이벤트가 버렸을 때 뿐만이 아니라 키보
드 이벤트가 시스템 큐에서 제거될 때에도 호출된다. 이 값의 주요한 용도는 키보드
이벤트에 반응하는 WH_JOURNALPLAYBACK 훅을 설치하는 것이다. (자세한 내용은 아
래 "WM_QUEUESYNC" 부분을 참고하면 된다.)
wParam 매개변수는 가상 키 코드를 가지고 있다. 이 값은 WM_KEY* 메시지를
GetMessage나 PeekMessage로 얻어올 때의 wParam과 같은 값이다. lParam 매개변수는
WM_KEY* 메시지를 GetMessage나 PeekMessage로 얻어올 때의 lParam과 같은 값을 가진
다.
WM_QUEUESYNC
실행 중에 CBT 응용 프로그램은 대상 프로그램의 이벤트에 반응해야 하는 경우가 종
종 있다. 보통 키보드나 마우스 이벤트가 이러한 이벤트를 발생시키다. 예를 들어 사
용자가 다이알로그 박스의 OK 버튼을 누르는 경우, CBT 응용 프로그램은 대상 프로그
램으로 일련의 키 입력을 보내려고 할 것이다. CBT 응용 프로그램은 마우스 훅을 사용
하여 OK 버튼이 눌러졌는지 결정할 수 있다. 대상 프로그램에 키 입력을 다시 재현하
고자 하는 경우 CBT 응용 프로그램은 대상 프로그램이 OK 버튼에 대한 처리를 끝내도
록 기다려야 한다. CBT 응용 프로그램이 다이알로그 박스에 키 입력을 해서는 안되는
것이다.
CBT 응용 프로그램은 WM_QUEUESYNC 메시지를 사용하여 대상 프로그램을 감시하고 언
제 동작이 끝나는지를 알아낼 수 있다. CBT 응용 프로그램은 대상 프로그램을 마우스
나 키보드 훅으로 감시하고 반응해야 하는 이벤트를 알아낸다. 대상 프로그램의 마우
스나 키보드 훅을 관찰함으로써 CBT 응용 프로그램은 언제 반응을 시작해야 하는지 알
게 된다. CBT 응용 프로그램은 이벤트 처리가 종료되고 이에 반응해야 할 때까지 기다
려야 한다.
이벤트 처리가 언제 끝나는지 결정하기 위해 CBT 응용 프로그램은 다음의 과정을 따른
다.
CBT 응용 프로그램은 운영 체제로부터 HCBT_CLICKSKIPPED이나 HCBT_KEYSKIPPED 훅 코
드를 가지는 WH_CBT 훅을 받을 때까지 기다린다. 이러한 WH_CBT 훅은 대상 프로그램
의 행동을 지시한 이벤트가 시스템 큐에서 제거될 때 발생한다.
CBT 응용 프로그램은 WH_JOURNALPLAYBACK 훅을 설치한다. CBT 응용 프로그램은
HCBT_CLICKSKIPPED이나 HCBT_KEYSKIPPED 훅 코드를 받을 때까지 WH_JOURNALPLAYBACK
훅을 설치할 수 없다. WH_JOURNALPLAYBACK 훅은 CBT 응용 프로그램에 WM_QUEUESYNC 메
시지를 재생한다. 이 메시지를 CBT 응용 프로그램이 받으면 원래의 이벤트에 CBT 응
용 프로그램이 반응할 수 있다. 예를 들어 CBT 응용 프로그램은 키 입력을 대상 프로
그램에 재현하게 된다.
HCBT_SETFOCUS
어떤 윈도우가 포커스를 받으려고 할 때 이 훅 코드로 WH_CBT를 호출한다. 쓰레드 훅
의 경우 윈도우는 동일한 쓰레드에 있어야 한다. 필터 함수가 TRUE를 반환하면 포커스
가 바뀌지 않는다.
wParam 매개변수는 포커스를 받을 윈도우의 핸들을 가지고 있다. lParam 매개변수는
포커스를 잃게 되는 윈도우의 핸들을 가지고 있다.
HCBT_QS
윈도우의 크기가 변화하거나 옮겨지는 동안 WM_QUEUESYNC 메시지가 시스템 큐에서 제
거되면 이 훅 코드로 WH_CBT를 호출한다. 다른 경우에는 호출되는 경우가 없다. 쓰레
드 훅의 경우 윈도우는 동일한 쓰레드에 있어야 한다.
wParam과 lParam 매개변수는 모두 영(zero)을 가지고 있다.
WH_DEBUG
필터 함수를 호출하려고 할 때 운영 체제가 이 훅을 호출한다. 필터는 훅의 값을 수정
할 수는 없지만, 영(zero)이 아닌 값을 반환함으로써 운영 체제가 원래의 필터 함수
를 호출하는 것을 중지시킬 수 있다.
wParam 매개변수는 호출되는 훅의 ID를 가지고 있다. lParam 매개변수는 다음 구조체
의 포인터를 가지고 있다.
typedef struct tagDEBUGHOOKINFO
{
DWORD idThread; // 현재 쓰레드의 ID
LPARAM reserved;
LPARAM lParam; // 목적지 필터 함수의 lParam
WPARAM wParam; // 목적지 필터 함수의 wParam
int code;
} DEBUGHOOKINFO, *PDEBUGHOOKINFO, NEAR *NPDEBUGHOOKINFO, FAR* LPDEBUGHOOKINFO;
WH_FOREGROUNDIDLE
현재 쓰레드에 처리할 사용자 입력이 없을 때 이 훅을 호출한다. 쓰레드 훅의 경우
그 쓰레드가 현재 쓰레드이고 쓰레드에 입력이 없을 때에만 이 훅을 호출한다. 이 훅
은 통지(notification)만 할 수 있다. 즉, 수정이나 제거는 할 수 없다. wParam과
lParam는 모두 영(zero)의 값을 갖는다.
WH_GETMESSAGE
GetMessage 함수나 PeekMessage 함수가 메시지를 반환하려고 할 때 이 훅을 호출한
다. 필터 함수는 실제 메시지에 대한 정보를 가지고 있는 구조체의 포인터를 받는다.
원본 그대로이거나 또는 수정된 경우에도 메시지는 원래의 목적지로 전달된다.
lParam 매개변수는 MSG 구조체에 대한 포인터를 가지고 있다.
typedef struct tagMSG { /* msg */
HWND hwnd; // 해당 메시지를 처리할 Winproc 함수를 가지고 있는 윈
도우
UINT message; // message number
WPARAM wParam;
LPARAM lParam;
DWORD time; // 메시지가 발생한 시간
POINT pt; // 화면 좌표계에서 메시지가 발생한 커서 위치
} MSG;
WH_HARDWARE
이 훅은 현재 Win32에서 구현되어 있지 않다.
Journal Hooks
Journal 훅은 이벤트를 기록하고 재생하기 위해 사용된다. 이들은 시스템 범위의 훅으
로만 사용되며 따라서 가능한 사용하지 않는 것이 좋다. 이들 훅은 모든 윈도우 기반
응용 프로그램에 영향을 미친다. 또 하나 journal 훅의 부작용은 모든 시스템 입력 큐
가 훅을 설치한 쓰레드에 집중된다는 점이다. 이는 모든 시스템 입력이 한 점을 통과
해야 한다는 것을 의미한다.
Win32는 이 훅이 시스템을 먹통으로 만들지 않도록 journal 훅을 취소하는 방법을 제
공한다. 운영 체제는 사용자가 CTRL+ESC, ALT+ESC, 또는 CTRL+ALT+DEL 키를 누르면 기
록이나 재생을 위한 journal 훅을 제거한다. 훅을 제거한 후에 운영 체제는
WM_CANCELJOURNAL 메시지를 보내 응용 프로그램에 훅이 제거되었다는 사실을 알린다.
Win32 takes special steps to allow a user to cancel a journal hook so that it
does not lock the system. Windows will uninstall a record or playback journal
hook when the user presses CTRL+ESC, ALT+ESC, or CTRL+ALT+DEL. Windows then
notifies the application that had a journal hook installed by posting a
WM_CANCELJOURNAL message.
WM_CANCELJOURNAL
이 메시지는 윈도우 프로시저로 전송되지 않도록 NULL 값을 갖는 윈도우 핸들과 더불
어 발생한다. 이 메시지를 알아챌 수 있는 가장 좋은 방법은 이 메시지를 감시하는
WH_GETMESSAGE 필터 함수를 설치하는 것이다. Win32 문서에 응용 프로그램이
WM_CANCELJOURNAL 메시지를 GetMessage (또는 PeekMessage) 함수와 DispatchMessage
함수를 호출하는 중간에 잡아낼 수 있다고 되어있다. 비록 이 시점에서 이 메시지를
잡아낼 수 있지만, 메시지가 보내졌을 때 응용 프로그램은 메시지를 잡아낼 수 있는
위치에 있지 않을 것이다. 예를 들어 응용 프로그램이 다이아로그 박스인 경우 main
message loop는 호출되지 않을 것이다.
CTRL+ESC, ALT+ESC, 그리고 CTRL+ALT+DEL 키 조합은 journal 훅을 중지시키기 위해 시
스템에 설치되어 있다. journal 훅을 사용하는 모든 응용 프로그램이 journaling을 중
지시키는 방법을 갖는 것은 바람직하다. journaling을 중지시키는 방법으로 권장되는
것은 VK_CANCEL (CTRL+BREAK) 키를 이용하는 방법이다.
WH_JOURNALRECORD
시스템 큐에서 이벤트를 제거할 때 이 훅을 호출한다. 따라서 필터 함수는 journal 재
생 훅에 의해 재생되는 경우를 제외하고는 모든 마우스와 키보드 이벤트에 대해 호출
된다. 필터 함수는 메시지를 처리한다. 즉 이벤트를 메모리나 디스크 또는 양 쪽 모두
에 저장할 수 있다. 하지만 메시지를 수정하거나 제거할 수는 없다. 이 훅을 위한 필
터 함수는 DLL에 있거나 .EXE 파일에 있어야 한다. HC_ACTION 훅 코드만이 Win32에서
구현되어 있다.
HC_ACTION
시스템 큐에 이벤트를 받았을 경우 이 훅 코드를 사용하여 WH_JOURNALRECORD 훅을 호
출한다. 이 훅 코드는 필터 함수에 발생한 이벤트가 정상적인 이벤트라는 것을 알린
다. [정상적이라는 말은 재생된 이벤트가 아니라는 말이다.] 필터 함수의 lParam 매개
변수는 EVENTMSG 구조체의 포인터를 가지고 있다. 보통의 이벤트 기록 방식은 훅으로
넘어온 EVENTMSG 구조체를 메모리나 디스크로 저장하는 것이다.
EVENTMSG 구조체는 WINDOWS.H에 정의되어 있다.
typedef struct tagEVENTMSG {
UINT message;
UINT paramL;
UINT paramH;
DWORD time;
HWND hwnd;
} EVENTMSG;
typedef struct tagEVENTMSG *PEVENTMSG, NEAR *NPEVENTMSG, FAR *LPEVENTMSG;
EVENTMSG 구조체에서 message는 메시지 ID로 WM_* 값을 가진다. paramL과 paramH는 발
생한 이벤트가 마우스 이벤트인지 키보드 이벤트인지에 따라 다르다. 마우스 이벤트
인 경우 이벤트가 발생한 위치의 좌표를 가지고 있다. 키보드 이벤트인 경우 paramL
은 HIBYTE에 scan code를 LOBYTE에 가상 키 코드를 가지고 있으며, paramH는 반복 횟
수를 가지고 있다. 반복 횟수의 15번째 비트는 그 이벤트가 확장 키인지를 나타낸다.
EVENTMSG 구조체의 시간 요소에는 이벤트가 발생한 시스템 시간이 GetTickCount 함수
를 사용하여 저장된다. hwnd는 이벤트가 발생한 윈도우의 핸들이다.
두 이벤트 사이의 시간은 시간 요소를 비교하여 얻을 수 있다. 이 값은 저장된 이벤트
를 재생할 때 필요하다.
WH_JOURNALPLAYBACK
이 훅은 마우스나 키보드 메시지가 실제로 발생한 것처럼 시스템 큐에 입력하기 위해
사용된다. 이 훅은 보통 WH_JOURNALRECORD 훅에 의해 저장된 이벤트를 재생하기 위해
사용되지만, 다른 응용 프로그램에 이벤트를 전달하기 위한 가장 좋은 방법이기도 하
다. 이 훅에 필터 함수가 설치되면, 운영체제는 필터 함수열에서 첫 번째 필터 함수
를 호출하여 이벤트를 얻는다. WH_JOURNALPLAYBACK 훅이 설치된 경우 마우스 움직임
은 무시된다. 키보드나 마우스 입력과 관련된 다른 메시지들은 WH_JOURNALPLAYBACK 훅
에 설치된 필터 함수가 더 이상 없을 때까지 큐에 저장된다. 이 훅을 위한 필터 함수
는 DLL이나 .EXE 파일에 있어야 한다. 이 훅에 설치된 필터 함수는 다음의 코드를 처
리할 수 있다.
HC_GETNEXT
HC_SKIP
HC_GETNEXT
운영 체제가 쓰레드의 입력 큐에 접근할 때 이 훅 코드로 WH_JOURNALPLAYBACK 훅을 호
출한다. 대부분의 경우 운영체제는 동일한 메시지에 대해 여러 번 동일한 호출을 한
다. 필터 함수의 lParam 매개변수는 EVENTMSG 구조체에 대한 포인터를 가지고 있다.
필터 함수는 EVENTMSG 구조체에서 message, paramL, paramH의 값을 설정하여야 한다.
이들 값은 보통 WH_JOURNALRECORD를 사용하여 기록한 이벤트에서 복사된다.
필터 함수는 운영 체제에게 필터 함수가 제공한 메시지를 언제 처리할지 알려 주어야
한다. 운영체제는 두 가지 값을 필요로 한다: (1) 메시지를 처리하기 전에 운영 체제
가 기다려야 하는 시간; (2) 메시지가 처리되어야 하는 시간. 기다리는 시간을 계산하
는 전형적인 방법은 EVENTMSG 구조체의 시간 요소를 사용하는 것이다. 이 방법은 이벤
트가 기록된 것과 동일한 속도로 재생할 수 있도록 해준다. 만약 메시지가 즉시 처리
되어야 한다면 영(zero)의 값을 반환할 것이다.
메시지가 처리되어야 하는 시간은 운영 체제가 메시지 처리를 위해 기다려야 하는 시
간과 GetTickCount에서 얻어진 현재 시스템 시간을 더해서 얻을 수 있다. 즉시 처리해
야 한다면 GetTickCount에서 반환된 값을 사용하면 된다.
시스템이 active하지 않으면 운영체제는 필터 함수가 이벤트 처리를 위해 제공한 값들
을 사용한다. 시스템이 active하면 운영체제는 시스템 큐를 검사한다. 각각의 경우
HC_GETNEXT 훅 코드로 동일한 이벤트를 탖느다. 필터 함수가 HC_GETNEXT를 받았을 때
에는 연속적인 호출 사이의 대기 시간을 반환해야 한다. EVENTMSG 구조체, 메시지,
paramH 값, paramL 값은 수정할 필요가 없다.
HC_SKIP
운영 체제가 WH_JOURNALPLAYBACK 훅에서 받은 메시지 처리를 끝냈을 때 이 훅 코드를
사용하여 WH_JOURNALPLAYBACK 훅을 호출한다. 만약 WH_JOURNALPLAYBACK 훅에 의해 생
성된 이벤트가 아니고 시스템 큐에 있던 이벤트라면 이 코드는 운영 체제가 시스템 큐
에서 이벤트를 제거했을 때 발생한다. 이 훅 코드는 필터 함수에게, 이전의
HC_GETNETXT 호출에서 필터 함수가 넘겨준 이벤트가 응용 프로그램에 전달되었다는 것
을 알려준다. 필터 함수는 다음 HC_GETEVENT 호출에서 다음 이벤트를 반환할 수 있는
준비를 해야 한다. 필터 함수가 더 이상 재생할 이벤트가 없는 경우에는 HC_SKIP 호출
에서 자신을 해제해야 한다.
WH_KEYBOARD
GetMessage 함수나 PeekMessage 함수가 WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP,
WM_SYSKEYDOWN, WM_CHAR 등의 메시지를 반환하려고 할 때 이 훅을 호출한다. 쓰레드
훅의 경우 이들 메시지는 쓰레드의 입력 큐에 있어야 한다. 필터 함수는 가상 키 코드
와 키보드 훅이 발생했을 때의 키보드 상태를 받는다. 이 훅에 설치된 필터 함수는 다
음 훅 코드를 처리할 수 있다.
HC_ACTION
HC_NOREMOVE
HC_ACTION
시스템 큐에서 이벤트가 제거될 때 이 훅 코드를 사용하여 WH_KEYBOARD 훅을 호출한
다.
HC_NOREMOVE
응용 프로그램이 PM_NOREMOVE 옵션을 사용하여 PeekMessage 함수를 호출하였기 때문
에 키보드 이벤트가 제거되지 않았을 때 이 훅 코드를 사용하여 WH_KEYBOARD 훅을 호
출한다. 이 훅 코드가 전달되면 키 상태 테이블(key-state table)은 이전의 키 상태
를 반영하지 않는다. 응용 프로그램은 이러한 옵션이 있다는 것을 알고 있어야 한다.
WH_MOUSE
GetMessage 함수나 PeekMessage 함수가 호출되었을 때 처리할 마우스 메시지가 있으
면 이 훅이 호출된다. WH_KEYBOARD 훅과 마찬가지로 이 필터 함수는 메시지가 제거되
었는지(HC_NOREMOVE)를 나타내는 훅 코드, 마우스 메시지 ID, 그리고 마우스의 좌표
를 받게 된다. 필터는 운영 체제에게 메시지를 버리도록 할 수 있다. 이 훅에 대한 필
터는 DLL에 있어야 한다.
WH_MSGFILTER
다이알로그 박스, 메시지 박스, 스크롤 바, 또는 메뉴가 메시지를 발생시켰을 경우 그
리고 훅을 설정한 응용 프로그램에서 사용자가 ALT+TAB이나 ALT+ESC 키를 눌렀을 때
이 훅을 호출한다. 이 훅은 쓰레드 훅으로만 사용될 수 있으므로 응용 프로그램이나
DLL에 필터 함수를 둘 수 있다. 필터 함수는 다음의 훅 코드들을 받는다.
MSGF_DIALOGBOX : 다이알로그 박스나 메시지 박스의 메시지.
MSGF_MENU : 메뉴 메시지
MSGF_SCROLLBAR : 스크롤 바 메시지
MSGF_NEXTWINDOW: 다음 윈도우러 전환하려고 할 때
MSGF_* 값으로 정의된 값들이 WINUSER.H에 보면 더 있지만 현재의 WH_MSGFILTER 훅에
는 사용되지 않는다.
lParam 매개변수는 메시지를 포함하고 있는 구조체의 포인터를 가지고 있다.
WH_SYSMSGFILTER 훅이 WH_MSGFILTER 훅 이전에 호출된다. 만약 WH_SYSMSGFILTER 훅이
TRUE를 반환하면 WH_MSGFILTER 훅은 호출되지 않을 것이다.
WH_SHELL
최상위 윈도우에서 어떤 동작이 발생했을 때 이 훅을 호출한다. 쓰레드 훅의 경우 동
일한 쓰레드에 속하는 윈도우에 대해서만 이 훅을 호출한다. 이 훅은 통지
(notification)만 할 수 있으므로 필터 함수는 이벤트를 수정하거나 제거할 수 없다.
wParam 매개변수는 윈도우의 핸들을 가지고 있고 lParam 매개변수는 사용되지 않는
다. WINUSER.H에 3가지 훅 코드가 정의되어 있다.
HSHELL_WINDOWCREATED: 최상위 윈도우가 생성되었을 때 이 훅 코드를 사용하여 호출한
다. 훅이 호출되었을 때 윈도우는 이미 화면에 보여지고 있다.
HSHELL_WINDOWDESTROYED: 최상위 윈도우가 파괴되려고 할 때 이 훅 코드를 사용하여
호출한다.
HSHELL_ACTIVATESHELLWINDOW: 이 훅 코드는 현재 사용되지 않는다.
WH_SYSMSGFILTER
이 훅은 시스템 범위의 훅이라는 점을 제외하고는 WH_MSGFILTER 훅과 동일하다. 다이
알로그 박스, 메시지 박스, 스크롤 바, 또는 메뉴가 메시지를 발생시켰을 경우 그리
고 사용자가 ALT+TAB이나 ALT+ESC 키를 눌렀을 때 이 훅을 호출한다. 필터 함수는
WH_MSGFILTER에서와 동일한 훅 코드를 받는다.
lParam 매개변수는 메시지를 포함하는 구조체의 포인터를 가지고 있다.
WH_SYSMSGFILTER 훅은 WH_MSGFILTER 훅 이전에 호출된다. WH_SYSMSGFILTER 훅의 필터
함수가 TRUE를 반환하면 WH_MSGFILTER 훅은 호출되지 않을 것이다.
첨부된 샘플 : From MSDN
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=1498&ref=660
훅에 관해 자세히 모르는 관계로 잘못된 부분이 많겠지만
대강의 개념을 이해할 수는 있을 것입니다.
잘못된 부분은 지적을 해주시고
다른 참고 자료를 가지신 분이 계시면 알려 주세요...
이게 내용이 길어서 그런지 한번에 올리려고 하니 계속 에러가 나는군요.. -_-
그리고 표는 깨져서 보일 테니 MSDN의 원본을 참조하세요
==================================================
Win32 Hooks
Created: July 29, 1993
Revised: February 1994
Translated : February 14, 2001 by hgycap!
요약
이 문서는 Win32 API에서 사용하는 훅(hook)에 대해 설명하고 있다. 여기서 다룰 내용
은 훅 함수, 필터 함수 그리고 다음과 같은 종류의 훅이다.
WH_CALLWNDPROC
WH_CBT
WH_DEBUG
WH_FOREGROUNDIDLE
WH_GETMESSAGE
WH_JOURNALPLAYBACK
WH_JOURNALRECORD
WH_KEYBOARD
WH_MOUSE
WH_MSGFILTER
WH_SHELL
WH_SYSMSGFILTER
용어 이 문서에서 말하는 "윈도우"라는 용어는 16 비트 윈도우, 윈도우 NT 등을 포함
하는 모든 윈도우 운영 체제를 포함한다. "윈도우 3.1"이라고 말하는 것은 그 버전을
지칭하는 말이다.
소개
윈도우 운영 체제에서 훅은 이벤트(메시지, 마우스 움직임, 키보드 입력)가 응용 프로
그램에 도착하기 전에 이벤트를 가로채는 방법을 제공한다. 이 방법을 통해 이벤트에
반응하여 동작하거나 이벤트를 수정 또는 삭제할 수 있다. 이벤트를 받는 함수를 필
터 함수(filter function)라고 부르며 함수가 가로채는 이벤트의 종류에 따라 나뉘어
진다. 예를 들어 어떤 필터 함수는 모든 종류의 키보드와 마우스 이벤트를 받을 수 있
다. 윈도우가 필터 함수를 부르기 위해서는 필터 함수가 윈도우의 훅에 설치, 다른 말
로 하자면 첨가되어 있어야 한다. 하나 이상의 필터 함수를 훅에 첨가하는 것을 "훅
을 설정한다(setting)"고 말한다. 하나의 훅에 둘 이상의 필터 함수가 첨가되어 있는
경우, 윈도우는 일련의 필터 함수로 연결하여 관리한다. 가장 최근에 설치된 함수는
함수열(chain)의 시작 부분에 위치하고 가장 먼저 설치된 함수는 함수열의 끝에 위치
한다.
훅이 하나 이상의 필터 함수를 가지고 있고 어떤 이벤트가 이 훅을 구동시킬 경우, 윈
도우는 필터 함수열의 첫 번째 필터 함수를 호출한다. 이러한 과정을 훅을 호출한다
(calling)고 말한다. 예를 들어 CBT 훅에 필터 함수가 설정되어 있고 이 훅을 구동하
는 이벤트(예를 들면 윈도우가 생성되려고 하는 경우)가 발생하였다면 윈도우는 필터
함수열의 첫 번째 필터 함수를 호출함으로써 훅을 호출한다.
필터 함수를 첨가하거나 제거하기 위해 응용 프로그램은 SetWindowsHookEx와
UnhookWindowsHookEx 함수를 사용한다.
훅은 윈도우 기반 응용 프로그램에 놀라운 능력을 제공한다. 다음의 응용 프로그램은
훅을 사용하여 구현할 수 있다:
응용 프로그램의 다이알로그 박스, 메시지 박스, 스크롤 바, 메뉴 등에 주어지는 모
든 메시지를 처리하거나 수정한다. (WH_MSGFILTER)
시스템의 다이알로그 박스, 메시지 박스, 스크롤 바, 메뉴 등에 주어지는 모든 메시지
를 처리하거나 수정한다. (WH_SYSMSGFILTER)
GetMessage나 PeekMessage 함수가 호출되는 경우 시스템의 모든 메시지를 처리하거나
수정한다. (WH_GETMESSAGE)
SendMessage 함수가 호출되는 경우 모든 메시지를 처리하거나 수정한다.
(WH_CALLWNDPROC)
키보드와 마우스의 이벤트를 기록하거나 재생한다. (WH_JOURNALRECORD,
WH_JOURNALPLAYBACK)
키보드 이벤트를 처리, 수정 또는 제거한다. (WH_KEYBOARD)
마우스 이벤트를 처리, 수정 또는 제거한다. (WH_MOUSE)
특정한 system action에 반응하도록 하여 응용 프로그램이 컴퓨터 기반 학습
(Computer Based Training, CBT)이 가능하도록 만들어준다. (WH_CBT)
다른 필터가 호출되지 않도록 한다. (WH_DEBUG)
응용 프로그램은 다음과 같은 경우 훅을 사용해 왔다:
메뉴, 다이알로그 박스, 메시지 박스 등에 F1 도움말 키를 지원하도록 한다.
(WH_MSGFILTER)
마우스와 키보드의 이벤트를 기록하고 재생하는 방법을 제공한다. 이를 종종 매크로라
고 한다. 예를 들어 Windows Recorder 보조 프로그램은 기록과 재생 기능을 위해 훅
을 사용한다. (WH_JOURNALRECORD, WH_JOURNALPLAYBACK).
메시지를 감시하여 어떤 메시지가 어떤 윈도우로 가는지 또는 특정 메시지가 어떤 동
작을 일으키는지 감시한다. (WH_GETMESSAGE, WH_CALLWNDPROC) 윈도우 NT의 Win32 SDK
에 있는 Spy 프로그램은 훅을 사용한다. Spy 프로그램의 소스는 SDK에 있다.
마우스나 키보드 입력을 흉내 낼 수 있다. (WH_JOURNALPLAYBACK) 훅은 이러한 행동을
흉내내기를 보장해줄 수 있는 유일한 방법이다. 이러한 이벤트를 SendMessage나
PostMessage 함수를 사용하여 흉내내려고 한다면 윈도우의 내부는 키보드나 마우스의
상태를 갱신하지 않아서 예기치 않는 행동을 보여주는 경우가 있다. 키보드나 마우스
이벤트를 재생하기 위해 훅을 사용하는 경우, 이들 이벤트는 실제의 키보드나 마우스
이벤트와 동일하게 동작한다. Excel 프로그램은 SEND.KEYS 매크로 함수를 구현하기 위
해 훅을 사용한다.
윈도우 환경에서 동작하는 응용 프로그램을 위한 CBT를 제공한다. WH_CBT 훅은 CBT 응
용 프로그램을 쉽게 만들 수 있는 방법을 제공한다.
훅의 사용 방법
훅을 사용하기 위해서는 다음 사항을 알아야 한다:
훅의 필터 함수열에 필터 함수를 추가하거나 제거하기 위해 윈도우의 훅 함수를 어떻
게 사용하는가?
설치하는 필터 함수는 어떤 동작을 수행하여야 하는가?
어떤 종류의 훅이 존재하며 이들은 어떤 일을 하는가 그리고 이들 훅은 필터 함수로
어떤 정보를 전달하는가?
윈도우 훅 함수
윈도우 기반의 응용 프로그램은 SetWindowsHookEx, UnhookWindowsHookEx,
CallNextHookEx 함수를 사용하여 필터 함수열을 관리한다. 3.1 버전 이전의 윈도우는
SetWindowsHook, UnhookWindowsHook, DefHookProc 함수를 사용하여 훅 관리를 했다.
이들 함수가 Win32에서도 존재하지만 새로운 버전의 Ex 계열 함수에 비해 할 수 있는
일이 적다. 기존의 코드도 새로운 함수를 사용하도록 바꾸는 것이 좋으며 앞으로도 새
로운 함수를 쓰는 것이 좋다.
SetWindowsHookEx와 UnhookWindowsHookEx 함수는 아래에 설명되어 있다.
CallNextHookEx 함수에 대해서는 별도의 문서 "필터 함수열에서 다음 함수의 호출
Calling the next function in the filter function chain"을 참고하면 된다.
SetWindowsHookEx
SetWindowsHookEx 함수는 훅에 필터 함수를 추가한다. 이 함수는 4개의 인자를 가지
고 있다:
HHOOK SetWindowsHookEx(
int idHook, // type of hook to install
HOOKPROC lpfn, // address of hook procedure
HINSTANCE hMod, // handle to application instance
DWORD dwThreadId // identity of thread to install hook for
);
필터 함수를 추가할 훅에 대한 정수값 코드. 코드 값은 WINUSER.H에 정의되어 있고 이
후에 설명할 것이다.
필터 함수의 주소. 필터 함수는 응용 프로그램이나 DLL의 모듈 정의 파일 EXPORTS 구
문에 필터 함수를 첨가하거나 적절한 컴파일러 플래그를 써서 export시켜야 한다.
필터 함수를 포함하는 모듈의 인스턴스 핸들. Win32에서는 (Win16과는 다르게) 특정
쓰레드에 사용되는 훅의 경우 이 값이 대부분 NULL이다. 하지만 꼭 NULL이어야 하는
것은 아니다. 시스템 훅이나 다른 프로세스에 있는 쓰레드에 대한 훅인 경우에는 필
터 함수가 있는 DLL의 인스턴스 핸들을 사용하여야 한다.
훅이 설치될 쓰레드의 ID. 쓰레드의 ID가 영(zero)이 아닌 경우, 설치된 필터 함수는
특정 쓰레드의 context에서만 호출된다. 쓰레드의 ID가 영(zero)인 경우, 설치된 필
터 함수는 시스템 전체 범위에 해당되며 시스템의 모든 쓰레드 context에서 호출된
다. 응용 프로그램이나 라이브러리는 훅을 설치할 쓰레드의 핸들을 얻기 위해
GetCurrentThreadId 함수를 사용할 수 있다.
몇몇 훅은 시스템 범위에서만 사용될 수 있고 몇몇 훅은 특정 쓰레드에서만 사용될
수 있다. 하지만 아래 표에서처럼 대부분이 시스템이나 쓰레드 범위에서 사용될 수 있
다.
Hook
Scope
WH_CALLWNDPROC
Thread or System
WH_CBT
Thread or System
WH_DEBUG
Thread or System
WH_GETMESSAGE
Thread or System
WH_JOURNALRECORD
System Only
WH_JOURNALPLAYBACK
System Only
WH_FOREGROUNDIDLE
Thread or System
WH_SHELL
Thread or System
WH_KEYBOARD
Thread or System
WH_MOUSE
Thread or System
WH_MSGFILTER
Thread or System
WH_SYSMSGFILTER
System Only
양 쪽 모두에 사용되는 경우 쓰레드 훅이 먼저 호출되고 시스템 훅이 다음에 호출된
다.
여러 가지 이유로 시스템 훅보다는 쓰레드 훅을 사용하는 것이 좋다. 쓰레드 훅의 장
점에는 다음과 같은 것들이 있다:
훅 사용과 무관한 응용 프로그램에 오버헤드를 주지 않는다.
훅을 위한 모든 이벤트가 직렬화되지 않아도 된다. 예를 들어, 응용 프로그램이 시스
템 키보드 훅을 설치하면 모든 응용 프로그램을 위한 모든 키보드 메시지가 키보드 필
터 함수로 집중되어 시스템에서 제공하는 다중 입력 큐 기능을 쓸 수 없게 된다. 만
약 필터 함수가 키보드 이벤트 처리를 멈추면, 실제로는 그렇지 않다고 해도 시스템
이 정지한 것처럼 보일 것이다. 사용자는 이 때에도 CTRL+ALT+DEL 키를 사용하여 log-
out하면 이 문제를 해결할 수 있지만 이러한 혼란이 달가울 리는 없다. 또한 사용자
가 log-out과 log-on을 통해 시스템을 원상태로 할 수 있다는 사실을 모를 수도 있다.
필터 함수 구현을 별도의 DLL로 만들 필요가 없다. 모든 시스템 훅과 다른 응용 프로
그램의 특정 쓰레드를 위한 훅은 DLL로 구현하여야 한다.
서로 다른 프로세스에서 사용되고 있는 동일한 DLL들이 데이터를 공유하지 않아도 된
다. 시스템 범위의 필터 함수는 DLL로 구현하여야 하고 따라서 당여히 다른 프로세스
와 공유할 필요가 있는 데이터는 공유하여야 한다. 하지만 DLL은 기본적으로 데이터
를 공유하지 않으므로 시스템 범위의 필터 함수를 작성할 때는 주의가 필요하다. 데이
터 공유를 잘못한 경우에는 프로세스가 파괴될 수 있다.
SetWindowsHookEx 함수는 설치된 훅에 대한 핸들(HHOOK 형)을 반환한다. 응용 프로그
램이나 라이브러리는 이 핸들을 UnhookWindowsHookEx 함수를 사용하여 훅을 해제할
때 사용한다. 훅에 필터 함수를 추가할 수 없는 경우 SetWindowsHookEx 함수는 NULL
을 반환한다. 또한 SetWindowsHookEx 함수는 수행에 실패한 원인을 나타내기 위해 아
래의 값들 중 하나로 last error를 설정한다. 이들 정보를 얻기 위해서는
GetLastError 함수를 사용하면 된다.
ERROR_INVALID_HOOK_FILTER: 훅 코드가 유효하지 않다.
ERROR_INVALID_FILTER_PROC: 필터 함수가 유효하지 않다.
ERROR_HOOK_NEEDS_HMOD: 전역적인 즉, 시스템 범위의 훅이 hInstance 인수를 NULL로
하여 설정되었거나, 쓰레드 훅이 훅을 설치하는 응용 프로그램과 동일한 쓰레드에 있
지 않다.
ERROR_GLOBAL_ONLY_HOOK: 시스템 훅으로만 사용할 수 있는 훅을 쓰레드 훅으로 설치하
였다.
ERROR_INVALID_PARAMETER: 쓰레드 ID가 유효하지 않다.
ERROR_JOURNAL_HOOK_SET: journal 훅 형식으로 이미 설치된 필터 함수가 있다.
journal record나 journal playback 훅은 한 번에 하나만 설치될 수 있다. 화면 보호
기가 동작하고 있는 경우 응용 프로그램이 journal 훅을 설치하고자 하는 경우 이런
에러가 발생할 수도 있다.
ERROR_MOD_NOT_FOUND: 전역 훅을 위한 hInstance 인수가 라이브러리가 아니다. (실제
로 이 값은 모듈 핸들을 찾을 수 없을 경우에 해당한다.)
Any other value: 보안 문제로 훅이 설치될 수 없거나 시스템의 메모리가 부족하다.
윈도우는 필터 함수열을 내부적으로 유지하고 다음 필터 함수를 찾기 위해 필터 함수
에 의존하지 않는다. 윈도우 3.1 이전 버전에서는 필터 함수들 자체가 연결되어 있었
다. 따라서 훅은 윈도우 3.1 버전보다 훨씬 견고해졌다. 또한 필터 함수열이 내부적으
로 관리됨으로써 성능도 향상되었다.
윈도우 3.1에서의 필터 함수열
UnhookWindowsHookEx
훅의 함수열에서 필터 함수를 제거하기 위해서는 UnhookWindowsHookEx 함수를 사용한
다. 이 함수는 SetWindowsHookEx 함수에서 반환된 훅의 핸들을 사용하고 훅이 제거되
었는 지의 여부를 반환한다. 영(zero)을 반환하는 경우 제거에 실패한 것으로
GetLastError 함수를 사용하여 자세한 정보를 얻을 수 있다.
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=1497&ref=659
SSDT란 System Service Descriptor Table의 약자로써 Window의 API들의
실제 함수의 주소들이 저장되어 있는 Table입니다. Kernel단에서의 처리가
필요한 API들은 해당 API의 Service Index를 이용하여 SSDT에서 해당 함수
의 실제 주소를 얻어 호출합니다. SSDT는 Kernel단의 Memory에 존재 하는
데, SSDT는 Process 독립적인 부분이 아닙니다. 즉 어떠한 조작을 하지 않는 한
모든 Application들은 모두 같은 Table을 참조하고 있는 것입니다. 그럼으로
SSDT를 수정하여 주면 시스템 전역 적인 API Hooking을 할 수 있습니다.
이러한 이유로 수많은 Rootkit들이 SSDT Hooking을 사용 하고 있으며,
Virus Engine의 동작기반 Virus탐지 기술에도 사용되고 있습니다.
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=7573&ref=7573