.NET에서의 Low-Level 키보드 입력 후킹
닷 넷 프레임워크에서는 Windows Form 기반의 프로그램을 작성할 때 KeyPreview 속성을 사용해서 자신에게 돌아오는 키보드 메시지의 처리를 제어할 수 있습니다. 하지만 시스템으로 들어오는 모든 키보드 메시지를 처리하려면 winapi에 의존할 수 밖에 없습니다. 이 아티클은 SetWindowsHookEx winapi 함수를 이용해서 키보드를 후킹하는 클래스에 대한 설명입니다.
안녕하세요 : )
제 예전 아티클(Windows Form 기반의 프로그램에서, 폼에서 컨트롤의 키 입력을 가로채기)의 연장으로 작성된 내용입니다. 키보드를 후킹해서 이번에야말로 꼼짝달싹못하게 어플리케이션을 묶어 보도록 하겠습니다 '')/
그럼 시작합니다.
Just hook!
첨 부된 파일을 먼저 확인하시라. 컴파일을 수행하고 Hook! 버튼을 누르면 해당 폼에서뿐만 아니라 전체 윈도우에서 delete, tab, esc 키 등이 입력되지 않는 것을 확인할 수 있다. (alt + tab 등도 tab키가 입력되는 조합이므로 입력되지 않는다)
실제 코드를 살펴보자. (Form1.cs 참조) KeyboardHooker의 사용 방법은 실로 간단한데, 단순히 KeyboardHooker.HookedKeyboardUserEventHandler 딜리게이트를 처리할 수 있는 이벤트를 만들고, 이벤트 핸들러를 KeyboardHooker.HookedKeyboardUserEventHandler에 등록하면 된다.
private void Form1_Load(object sender, System.EventArgs e) {
this.HookedKeyboardNofity += new KeyboardHooker.HookedKeyboardUserEventHandler(Form1_HookedKeyboardNofity);
}
event KeyboardHooker.HookedKeyboardUserEventHandler HookedKeyboardNofity;
이벤트 핸들러를 등록
그 리고 이벤트 핸들러를 구현하자. 지금 구현된 이벤트 핸들러는 tab, delete, esc 키의 입력을 막고 있으나, 사용하기에 따라서는 조합키(ctrl + a같은)의 입력을 핸들링하는데 사용하거나, KeyLogger의 용도로써도 사용할 수 있다. 이벤트 핸들러의 구현은 다음과 같다 :
private long Form1_HookedKeyboardNofity(
bool bIsKeyDown, bool bAlt, bool bCtrl, bool bShift, bool bWindowKey, int vkCode ) {
long lResult = 0;
// 입력을 막고 싶은 키가 있을 경우, 해당 키가 입력되었을 때
// 0이 아닌 값을 리턴하면 다른 프로세스가 해당 키보드 메시지를 받지 못하게 된다.
// 지금의 예처럼 코딩하면 Tab,Delete,Esc 키의 입력을 막게 된다.
if(
(vkCode == (int)System.Windows.Forms.Keys.Tab) ||
(vkCode == (int)System.Windows.Forms.Keys.Delete) ||
(vkCode == (int)System.Windows.Forms.Keys.Escape)) {
lResult = -1;
}
return lResult;
}
이벤트 핸들러 구현
실제 후킹을 시작/중지하는 코드는 다음과 같다 :
private void button2_Click(object sender, System.EventArgs e) {
// 후킹 여부 검사
if(KeyboardHooker.Hooked)
// 후킹 중지
KeyboardHooker.UnHook();
else
// 후킹 시작(이벤트를 넘겨줘서, KeyboardHooker에서 해당 이벤트를 이벤트 핸들)
KeyboardHooker.Hook(HookedKeyboardNofity);
if(KeyboardHooker.Hooked)
this.button2.Text = "Unhook";
else
this.button2.Text = "Hook!";
this.textBox1.Focus();
}
후킹 시작/중지
KeyboardHooker.Hook() 메서드
KeyboardHooker 클래스에서 가장 핵심적인 부분이라고 할 수 있다. 일단 소스를 보면 다음과 같다 :
public static bool Hook(HookedKeyboardUserEventHandler callBackEventHandler)
{
bool bResult = true;
m_hDllKbdHook = SetWindowsHookEx(
(int)WH_KEYBOARD_LL,
m_LlKbEh,
Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]).ToInt32(),
0);
if(m_hDllKbdHook == 0)
{
bResult = false;
}
// 외부에서 KeyboardHooker의 이벤트를 받을 수 있도록 이벤트 핸들러를 할당함
KeyboardHooker.m_fpCallbkProc = callBackEventHandler;
m_Hooked = bResult;
return bResult;
}
이 중 실제로 hook chain에 hook procedure (소스에서는 HookKeyboardProc() 메서드)를 등록하는 함수인 SetWindowsHookEx()의 원형은 MSDN을 참고하면 다음과 같다 :
HHOOK SetWindowsHookEx(
int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId );
WH_KEYBOARD_LL은 상수로써, 저수준(low-level)의 키보드 입력 메시지를 후킹하게 되는 것을 나타낸다.
Km_LlKbEh 는 HookedKeyboardEventHandler 딜리게이트로써, HookKeyboardProc() 함수를 이벤트 핸들러로 등록하고 있음을 알 수 있다. Kenial도 WinAPI를 호출할 경우 콜백 함수를 어떤 식으로 등록해야 하는지 몰라서 헤맸던 경험이 있는데, 이와 같이 해당 함수를 이벤트 핸들러로 갖는 딜리게이트를 선언한 후에 해당 딜리게이트의 인스턴스를 콜백 함수 인자로 넘겨주면, 닷넷 프레임워크(아마도 System.Runtime.InteropServices)에서 해당 딜리게이트 인스턴스를 변환, 콜백 함수 주소를 넘겨주는 것으로 보인다.
Marshal.GetHINSTANCE() 함수는 현재 모듈의 인스턴스 핸들을 얻는 함수이다.
마지막 0은 hook procedure가 적용될 쓰레드의 id를 나타내는 인자인데, KeyboardHooker는 윈도우 내 모든 프로그램의 키 입력을 후킹하기 위해 만들어졌으므로 0으로 설정한다.
callBackEventHandler는 KeyboardHooker 객체에 이벤트 핸들러를 등록하게 함으로써, 키 입력이 있을 때 Hook() 메서드를 호출한 어플리케이션에 해당 이벤트를 넘겨주기 위해서 등록하는 것이다.
후 킹 기능을 제공하는 클래스는 어차피 여러 개의 인스턴스가 생성될 필요는 없다고 판단해서, Kenial은 이 클래스를 전체가 정적 메서드로 구성된 클래스로 만들었다. 하지만 event는 정적 필드(static)로 사용할 수 없으므로, 이처럼 Form에서 event를 선언해서 유저용 이벤트 핸들러를 KeyboardHooker의 정적 필드 이벤트 핸들러에 등록할 수 있는 형태로 만들게 되었다.
KeyboardHooker.HookedKeyboardProc() 메서드
이 메서드는 실제로 low-level 키보드 메시지를 처리하는 프로시저로써 hook chain에 등록된다. (hook chain에 대해서는 msdn의 win32 hooks, about hooks 항목을 읽어보기 바란다)
C# 언어의 (WinAPI 프로그래밍을 하기 위해서) 중요한 키워드 중 하나인 unsafe가 나온다. unsafe 키워드를 처음 접한다면, 여기에서는 단지 포인터를 사용하기 위해서 필요한 것이라고 이해하시고 넘어가면 되겠다.(CopyMemory로 lParam으로 넘어온 KBDLLHOOKSTRUCT의 주소에 접근해 해당 객체를 m_KbDllHs로 복사하고 있다.
그 다음 Hook()에서 등록한 이벤트 핸들러인 m_fpCallbkProc으로 키보드 메시지를 넘겨주게 되어 콜백 함수를 호출하는 것과 같은 형태의 코드가 되었다.
그리고 CallNextHookEx()은 hook chain에 등록된 다른 hook procedure를 호출하는 명령인데, msdn에는 다음과 같이 설명되어 있다 :
If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx.
If nCode is greater than or equal to zero, and the hook procedure did not process the message, it is highly recommended that you call CallNextHookEx and return the value it returns; otherwise, other applications that have installed WH_KEYBOARD_LL hooks will not receive hook notifications and may behave incorrectly as a result. If the hook procedure processed the message, it may return a nonzero value to prevent the system from passing the message to the rest of the hook chain or the target window procedure.
nCode가 HC_ACTION이 아닐 때, (low level 키보드 메시지가 아닌 다른 후킹된 메시지일 때) 다음 hook procedure로 해당 메시지를 넘겨줌으로써 hook chain이 제대로 동작할 수 있도록 하는 코드이다.
참고
GotDotNet User Sample : Low Level Keyboard Capture
Key Support, Keyboard Scan Codes, and Windows
http://www.microsoft.com/whdc/device/input/Scancode.mspx
정리
간략하게 쓰고 싶었습니다만, 쉽지 않군요.
여 기서 설명되지 않은 부분은, msdn을 참고하시면서 보시면 쉽게 이해하실 수 있는 내용이라고 생각합니다. hooks에 전혀 관심이 없으시다면 할 수 없지만.. 이 아티클을 통해 한 번 접근해보실 수 있는 기회가 됐으면 좋겠습니다.
그럼 : )
'TechLog' 카테고리의 다른 글
맥북 극빈유저를 위한 아이팟 터치 거치대 (0) | 2009.03.28 |
---|---|
LINUX HATER'S BLOG : 우분투 유저의 진화 (1) | 2009.03.26 |
닷넷에서의 더블 버퍼링(GDI+) (0) | 2009.02.11 |
string.Format의 활용 (0) | 2009.01.29 |
MS, PerformancePoint Planning 단종 발표 (0) | 2009.01.29 |