본문 바로가기
IT-개발,DB

[개발/MFC] 따라해보는 키보드 후킹

by SB리치퍼슨 2011. 9. 30.
따라해보는 후킹
작성자 : 이은규
작성일 : 2003.11.02
홈페이지 : http://unkyulee.net/
 
목차
1. 들어가는 글
2. 후킹이란?
3. 후킹 프로시져를 만들어 보자.
4. 후킹 프로시져를 시작, 종료하는 함수
5. 프로시져 내에서 다른 윈도우로 데이터 전송하기
6. 간단한 샘플 프로그램
 

1. 들어가는 글
 
 "그냥 실행되는 걸 보고 싶었다."
  예전에 했던 프로젝트의 내용 중에 사용자가 키보드로 입력하는 내용을 얻어와서 처리해야 되는 부분이 있었다. 이러한 기능을 구현하기 위해서는 후킹 이라는 기술을 사용해야 한다. 그래서 관련된 내용을 인터넷에서 찾아봤는데, 왜 이렇게 알아야 되는 내용이 많은지… 또 내용들은 어찌나 어렵던지… 후킹 구현한답시고 한달 내내 문서 읽고 인터넷 뒤지고 엄청 고생 했었다. 결국 간단한 샘플 코드를 구해서 원하는 기능을 구현 했던 기억이 있다.
  프로젝트 내에서 그리 중요한 부분도 아니였고, 그냥 호기심에 후킹이라는 걸 실제로 구현해보고 싶었던 것 뿐이였는데, 정말 어렵게 공부했던 것 같다.
  이 강좌는 순전히 호기심으로 혹은 그냥(?) 한번 후킹을 실제로 구현해 보고자 하는 사람들을 대상으로 한다. 후킹에 대한 자세한 이론은 대부분 생략할 예정이다. 대신에 이 강좌를 다 읽고 나면 후킹을 사용한, 일단 돌아가는 코드를 작성 할 수 있을 것이다.
  일단 돌아가는 코드를 작성하고 나면, 그 외의 내용을 익히는 것은 시간 문제일 뿐이지 않을까 생각한다. ^0^)
 

2. 후킹이란?
 
 "후킹은 도청하는 걸 예로 들면 쉽게 이해할 수 있다."
  후킹이란 다른 프로세스에 걸려서(Hooked) 해당 프로세스의 정보를 얻어 오거나, 변경하는 것이 가능한 기술이다. 여기서 프로세스란 좁게 봐서 "윈도우 프로시져" 혹은 "윈도우 메시지" 라고 생각하면 되겠다.
  예를 들어 보자. 한 아파트 건물이 있다고 하자. 그리고 이 건물을 도청하고 싶다. 그럼 전화선들이 지나가는 곳(Window Process) 에 도청 장치(후킹 프로시져)를 설치한다. 그럼 도청장치 사이로 전화 내용들이(Window Message) 지나간다. 도청 장치는 그 중 필요한 메시지를 저장하고 있거나, 필요한 곳으로 전송한다.
  위의 과정들에 의해서 빌딩은 도청을 당한다. 그럼 실제 후킹의 경우를 살펴보자. 일단 윈도우 메시지(전화 내용)를 후킹 한다고 하면 윈도우 메시지를 발생시키는 곳(전화선들이 지나가는 곳)에 후킹 프로시져(도청 장치)를 설치한다. 그럼 해당 윈도우에서 발생하는 메시지가 후킹 프로시져를 거쳐가게 된다. 그럼 후킹 프로시져는 메시지들을 보고 필요한 내용을 저장하던지 필요한 곳에 전달을 하면 되는 것이다.
  그림 한번 보자. 위 그림은 Ivo Ivanov 라는 사람이 쓴 API Hooking Reveal 라는 문서에서 퍼 가지고 왔다. 그림을 보면 Hook Driver 라는 것들이 3개가 보인다. 그림에서 현재 윈도우가 3 개가 떠있는데 각각의 윈도우에 하나씩 Hook Driver 가 붙어 있는 모양이다. 따라서 이 드라이버(후킹 프로시져) 들이 각각의 윈도우에서 오는 메시지들을 받을 수가 있는 것이다.
  뭔가 굉장히 많아 보인다. 하지만 결국 개발자가 만들어 주는 것은 후킹 프로시져 하나이다.
  여기서 키포인트는 특정 위도우가 받는 메시지를 후킹 프로시져도 받을 수 있다는 것이다. 이런 기술을 바로 후킹이라고 한다.
 
3. 후킹 프로시져를 만들어 보자.
 
 "이론은 끝났다. 이제 만들어 보자."
  후킹 프로시져… 아까부터 프로시져라는 단어가 계속 나오는데 결국 프로시져는 함수와 그 의미가 비슷하다. 따라서 후킹 프로시져라고 함은 후킹을 하는 함수라고 생각하자. 따라서 후킹 프로시져를 만든다는 것은 함수를 하나 만든다고 생각하면 된다.
  후킹 함수를 만들기 위해서는 지켜야 하는 규칙들이 있다.
1. Call Back 함수이여야 한다.
2. 함수의 마지막 부분에서는 CallNextHookEx() 함수를 호출한다. 
3. 함수가 받는 인자는 정해져 있다.
4. 후킹 프로시져는 DLL 안에 있어야 한다.
 
Ex)
//--------------------------------------------------------------
// Hook Procedure - Keyboard
//--------------------------------------------------------------
LRESULT CALLBACK KeyboardProcedure(int nCode, WPARAM wParam, LPARAM lParam)
{
             if( nCode >= 0 )
             {           
             }
             // We must pass the all messages on to CallNextHookEx.
           return ::CallNextHookEx( g_Hook , nCode , wParam , lParam );
}
 위의 규칙들을 지켜서 만든 후킹 프로시져 함수이다.
 4번째 규칙에 의하면 후킹 함수는 DLL 내부에 있어야 한다. Visual C++ 6.0 에서 DLL 프로젝트를 하나 만들어 보자.
Step 1. Win32 Dynamic-Link Library 프로젝트 생성
Step 2. Simple 프로젝트 선택 후 Finish
Step 3. cpp 파일에 위의 후킹 함수를 만들어 준다.
위의 프로젝트를 컴파일 하면 HookDll.dll 파일이 생성된다.

전체 프로젝트 파일 내용
// HookDll.cpp : Defines the entry point for the DLL application.
//
#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, 
                       LPVOID lpReserved)
{
    return TRUE;
}
 
//--------------------------------------------------------------
// Hook Procedure - Keyboard
//--------------------------------------------------------------
LRESULT CALLBACK KeyboardProcedure(int nCode, WPARAM wParam, LPARAM lParam)
{
             if( nCode >= 0 )
             {                          
             }
             // We must pass the all messages on to CallNextHookEx.
             return ::CallNextHookEx( g_Hook , nCode , wParam , lParam );
}

 
4. 후킹 프로시져를 시작, 종료하는 함수
 
 이번 단계에서는 앞서 만든 후킹 프로시져를 설치하고, 제거하는 함수를 만들어 보겠다. 후킹 프로시져는 특정 윈도우에 설치가 되어야 제 역할을 할 수 있게 된다. 이때 후킹 프로시져를 설치해주는 함수가 바로 SetWindowsHookEx() 이다. 그 다음에 설치된 후킹 프로시져를 제거하기 위해서는 UnhookWindowsHookEx() 가 쓰이게 된다. 각각의 사용법을 알아보고 앞에서 만든 프로젝트에 이어서 적용시켜 보자.
SetWindowsHookEx()의 사용법
HHOOK SetWindowsHookEx(
    int idHook,
    HOOKPROC lpfn,
    HINSTANCE hMod,
    DWORD dwThreadId
);
 
 첫번째 패러메터는 후킹 필터를 설정한다. 후킹 프로시져가 받은 메시지의 종류를 설정하는 항목이다. 예를 들어 WH_GETMESSAGE로 설정하면 모든 메시지를 받게 되고, WH_KEYBOARD로 설정하게 되면 키보드 관련 메시지만 전달 받게 된다. 더 자세한 내용은 MSDN 을 참고하기 바란다.
 두번재 패러메터는 후킹 프로시져의 포인터를 지정해야 한다. 간단하게 후킹 프로시져 함수 이름 써주면 된다.  
 세번째 패러메터는 DLL 의 핸들을 넘겨줘야 된다. 이 값은 앞에서 만든 프로젝트에서 DLlMain() 함수를 보면 HANDLE hModule 값이 넘어오는데 이걸 저장 해놨다가 넘겨주면 된다.
 네번째 패러메터는 후킹 프로시져를 설치할 윈도우 값을 넘겨준다. 이번 예에서는 0 을 넘겨준다. 0 을 넘겨주면 후킹 프로시져가 모든 윈도우에 설치가 된다.
 이 함수가 리턴하는 값을 잘 저장 해 놓자. 나중에 후킹을 해제할 때 필요하게 된다.
Ex) g_HookKeyboard = SetWindowsHookEx( WH_KEYBOARD , KeyboardProcedure , (HINSTANCE)g_Module , 0 ) ;
 
UnhookWindowsHookEx() 의 사용법

BOOL UnhookWindowsHookEx(          
             HHOOK hhk
);
 
 이 함수는 후킹 핸들을 받아서 해당 후킹 프로시져를 해제한다. 이때 받는 핸들은 SetWindowsHookEx() 함수가 리턴한 값을 넣어주면 된다.
프로젝트를 계속 진행 해보자.
1. 필요한 전역 변수를 만들어 준다.
//---------------------------------------------------
// Global Variables
// 공용 메모리
//---------------------------------------------------

#pragma data_seg(".HKT")
HINSTANCE g_Module = NULL ;     // DLL Handle 
HHOOK g_Hook = NULL ;  // Hook Handle
HWND g_HwndServer = NULL ;      // Hook Server Window Handle
#pragma data_seg()
 
2. SetHook, Remove 이라는 함수를 만든다.
//------------------------------------------------------------------// Set Hook
//------------------------------------------------------------------BOOL     SetHook( HWND hWnd ) 
{
             g_HwndServer = hWnd ;                // Set Hook Server
             g_Hook = SetWindowsHookEx( WH_KEYBOARD , KeyboardProcedure , (HINSTANCE)g_Module , 0 ) ;
             return false ;
}
//-------------------------------------------------------// Remove Hook
//------------------------------------------------------------------BOOL     RemoveHook() 
{
             UnhookWindowsHookEx( g_Hook ) ;
             return false;
}
// HookDll.def 파일 부분
LIBRARY   HookDll
SECTIONS
             .HKT   Read Write Shared
EXPORTS
             SetImeWindow                 @1 
             SetHook                                        @2
             RemoveHook                                 @3


3.. 위의 함수와 전역 변수의 세팅을 위한 [프로젝트명].def 파일을 만들어 준다.
LIBRARY   [프로젝트명]
SECTIONS
             .HKT   Read Write Shared
EXPORTS
             SetHook                                        @2
             RemoveHook                                 @3
 
4. DllMail 함수에서 핸들을 저장한다.
//---------------------------------------------------// DllMain : Entry point
//------------------------------------------------------------------BOOL 
APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, 
                       LPVOID lpReserved)
{
             switch (ul_reason_for_call)
             {
             case DLL_PROCESS_ATTACH:
                           g_Module = (HINSTANCE)hModule; // Save Dll Handle
                           break;
              case DLL_PROCESS_DETACH:
                           RemoveHook();
                           break;
             }
             return TRUE;
}
전체 프로젝트 파일

1. HookDll.cpp

// HookDll.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

//---------------------------------------------------
// Global Variables
// 공용 메모리
//---------------------------------------------------
#pragma data_seg(".HKT")
HINSTANCE g_Module = NULL ;     // DLL Handle 
HHOOK g_Hook = NULL ;  // Hook Handle
HWND g_HwndServer = NULL ;      // Hook Server Window Handle
#pragma data_seg()
 
BOOL    RemoveHook() ;
BOOL    SetHook( HWND hWnd ) ;
 
//------------------------------------------------------------------
// DllMain : Entry point
//------------------------------------------------------------------

BOOL APIENTRY DllMain( 
                                                                                HANDLE hModule, 
                                                                                DWORD  ul_reason_for_call, 
                                                                                LPVOID lpReserved
                                                                  )
{
             switch (ul_reason_for_call)
             {
             case DLL_PROCESS_ATTACH:
                           g_Module = (HINSTANCE)hModule; // Save Dll Handle
                           break; 
             case DLL_PROCESS_DETACH:
                           RemoveHook();
                           break;
             }
             return TRUE;
}
 
//--------------------------------------------------------------
// Hook Procedure - Keyboard
//--------------------------------------------------------------
LRESULT CALLBACK KeyboardProcedure(int nCode, WPARAM wParam, LPARAM lParam)
{
             if( nCode >= 0 )
             {                       
             }
             // We must pass the all messages on to CallNextHookEx.
             return ::CallNextHookEx( g_Hook , nCode , wParam , lParam );
}
 
//------------------------------------------------------------------
// Set Hook
//------------------------------------------------------------------
BOOL    SetHook( HWND hWnd ) 
{
             g_HwndServer = hWnd ;                // Set Hook Server
             g_Hook = SetWindowsHookEx( WH_KEYBOARD , KeyboardProcedure , (HINSTANCE)g_Module , 0 ) ;
             return false ;
}
 
//------------------------------------------------------------------
// Remove Hook
//------------------------------------------------------------------
BOOL    RemoveHook() 
{
             UnhookWindowsHookEx( g_Hook ) ;
             return true ;
}
 

2. HookDll.def 파일
LIBRARY   HookDll
SECTIONS
.HKT   Read Write Shared
EXPORTS
SetHook                                        @2
RemoveHook                                 @3

프로젝트를 컴파일 하면 HookDll.lib 와 HookDll.dll 파일이 생성이 된다.
5. 프로시져 내에서 다른 윈도우로 데이터 전송하기
 이번에는 앞서 만든 후킹 프로시져를 좀더 강화해보기로 하자. 후킹 프로시져 내에서 메시지를 받고 받은 메시지를 복사하여 특정 윈도우에게 보낼 것이다.  
 이때 WM_COPYDATA 메시지를 생성 할 것이다.
1. WM_COPYDATA 로 보낼 데이터의 구조를 정의한다.
// 메시지를 저장하는 구조체

typedef struct
{
             int                       Type ;
             WPARAM            Data ;
             LPARAM              lParam ;
} HEVENT;

 윈도우 메시지의 내용을 저장할 구조체

2. 현재의 메시지를 복사한다.

COPYDATASTRUCT  CDS;
HEVENT          Event;
// Set CDS
CDS.dwData = 0 ;
CDS.cbData = sizeof(Event);
CDS.lpData = &Event;
// 메시지의 내용을 저장한다.
Event.Type = 1 ;               // It's WM_KEY.. 
Event.Data = wParam ;     // Send CharCode
Event.lParam = lParam ;
3. g_HwndServer 에게로 메시지를 전달한다.
// g_HwndServer  에게로 메시지를 날린다.
// g_HwndServer 는 SetHook 함수 호출시 저장한 윈도우 핸들이다.
::SendMessage( g_HwndServer , WM_COPYDATA , 0 , (LPARAM)(VOID*)&CDS ) ;
프로젝트를 계속 진행 해보자.
1. 앞선 프로젝트의 내용 중 KeyboardProcedure() 함수의 내용을 수정한다.
//------------------------------------------------------------------
// Hook Procedure - Keyboard
//------------------------------------------------------------------
LRESULT CALLBACK KeyboardProcedure(int nCode, WPARAM wParam, LPARAM lParam)
{
             if( nCode >= 0 )
             {
                           // Send To HookServer
                           COPYDATASTRUCT  CDS;
                           HEVENT          Event;
                           // Set CDS
                           CDS.dwData = 0 ;
                           CDS.cbData = sizeof(Event);
                           CDS.lpData = &Event;
                           // Set Variables
                           Event.Type = 1 ;               // It's WM_KEY.. 
                           Event.Data = wParam ;     // Send CharCode
                           Event.lParam = lParam ;
                           ::SendMessage( g_HwndServer , WM_COPYDATA , 0 , (LPARAM)(VOID*)&CDS ) ;     
             }
             // We must pass the all messages on to CallNextHookEx.
             return ::CallNextHookEx( g_Hook , nCode , wParam , lParam );
}
 
2. Event 구조체를 정의 해준다.
typedef struct
{
             int                       Type ;
             WPARAM            Data ;
             LPARAM              lParam ;
} HEVENT;
 
6. 간단한 샘플 프로그램

 지금까지 만들어 본 후킹 프로시져를 가지고 실제로 샘플 프로그램을 작성해보자.

1. 프로젝트를 생성한다.
             - MFC 다이얼로그
2. 위의 프로젝트를 컴파일 하면 HookDll.lib 와 HookDll.dll 파일이 생성이 된다. 이 파일을 새롭게 생성할 프로젝트 폴더에 복사한다.
 
3. 프로젝트를 세팅한다.
             - 메뉴에서 Project -> Setting -> Link 탭에서 Object/Modules Library 칸에 HookDll.lib 를 지정한다.
 
4 . 다이얼로그를 위의 모양같이 만들어 주고 에디트 컨트롤을 생성하고 다음과 같이 변수를 연결해준다..
 ID : IDC_EDIT_CNT
 Value : m_Cnt 
 Type : int
5. 클래스 위저드로 WM_COPYDATA 메시지 핸들러를 생성한다.
BOOL CHookTestDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) 
{
             // TODO: Add your message handler code here and/or call default
             // WM_COPYDATA 메시지가 올때마다 카운트를 증가한다.
             m_Cnt++ ;
             UpdateData( false ) ;
             return CDialog::OnCopyData(pWnd, pCopyDataStruct);
}
위와 같이 수정해준다.
6. DLL 에 있는 함수를 사용하기 위해 헤더 파일에 다음을 추가한다.
BOOL    RemoveHook() ;
BOOL    SetHook( HWND hWnd ) ;

7. InitDialog() 함수에서 SetHook() 함수를 호출한다.
BOOL CHookTestDlg::OnInitDialog()
{
             CDialog::OnInitDialog();
             // Add "About..." menu item to system menu.
             // IDM_ABOUTBOX must be in the system command range.
             ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
             ASSERT(IDM_ABOUTBOX < 0xF000);
             CMenu* pSysMenu = GetSystemMenu(FALSE);
             if (pSysMenu != NULL)
             {
                           CString strAboutMenu;
                           strAboutMenu.LoadString(IDS_ABOUTBOX);
                           if (!strAboutMenu.IsEmpty())
                           {
                                        pSysMenu->AppendMenu(MF_SEPARATOR);
                                        pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
                           }
             }
             // Set the icon for this dialog.  The framework does this automatically
             //  when the application's main window is not a dialog
             SetIcon(m_hIcon, TRUE);                           // Set big icon
             SetIcon(m_hIcon, FALSE);                          // Set small icon
             
             // TODO: Add extra initialization here
             SetHook( this->GetSafeHwnd() ) ; // 후킹 프로시져 설치
             
             return TRUE;  // return TRUE  unless you set the focus to a control
}
 
자 이제 실행하면 키보드를 칠 때마다 카운트가 증가하는 프로그램이 완성이 되었다.




## 출처 ##

작성자 : 이은규
작성일 : 2003.11.02
홈페이지 : http://unkyulee.net/

반응형

댓글