본문 바로가기

.NET(C#,ASP)

키보드 전역 후킹(Low Level Hooking) - Alt + 1같은 키조합 후킹하기


닷넷에서는 기본적으로 후킹을 지원하지 않으므로 WinAPI를 이용하여 후킹을 해야 하는데 지식이 짧아서 모르겠다.

여기저기서 비슷한 소스가 상당히 많이 나왔는데 일부분만 언급이 되어서 실력이 미천한 나로써는 써먹기 힘들었다.
아무튼 그러다가 간신히 거의 풀 소스를 찾았는데 잘 되서 그나마 다행이었다.

이 잘되는 소스를 이용하여 원하는 매크로 프로그램도 만들었다.
그러나 이와 같은 매크로 프로그램을 2004년인가 2005년쯤에도 비베로 만든적이 있었는데, 완전히 까먹는 바람에 이렇게 다시 만드는데 한참 고생했다.

그래서 매크로 프로그램을 만들면서 사용된 기능을 좀 나누어서 샘플코드로 만들어서 나중에 내가 필요할 때 기능별로 꺼내 쓸 수 있도록 하였다. 물론 다른 사람들도 나눠쓰면 좋고..



위의 파일은 테스트 프로그램의 프로젝트 파일을 통째로 압축한 파일이고, 압축풀면 바로 사용이 가능하다.


아래는 Form1.cs 파일의 전체 소스이다.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Reflection;

namespace HookingTest
{
    /// <summary>
    /// 이 프로그램의 디버깅 모드에서는 후킹이 정상적으로 작동되지 않으므로 Ctrl+F5로 실행시키고
    /// 아래 디버그용 코드의 주석을 풀어서 디버깅하도록 하자.
    /// </summary>
    public partial class Form1 : Form
    {
        /////////////////////////////////////////////////////////////////////////////////
        //
        // 위에서 정의한 KeyboardHooker class를 이용하여 후킹을 구현한다.
        // 여기서는 ALT키와 숫자 1키의 조합(ALT+1)을 인식하도록 한다.
        // 사람 손이 아무리 정확해도 키조합에서는 먼저 KeyUp되고 나중에 KeyUp되는 키가 있다.
        // 이것을 캐치하여 키조합이 눌린 후 둘 다 떨어지는 순간에 캐치한 이벤트에 대한
        // 특정 행위를 실행하도록 한다.
        //
        /////////////////////////////////////////////////////////////////////////////////
        private bool bAltAndNum;//Alt+숫자가 같이 눌린 상태(나중에 1 이외의 숫자도 쓸지 모르니..)
        private bool bAltOrNum;//Alt+숫자 이후 Alt만 남거나 숫자키만 남거나 한 상태

        //1. 후킹할 이벤트를 등록한다.
        event KeyboardHooker.HookedKeyboardUserEventHandler HookedKeyboardNofity;

        //2 .이벤트가 발생하면 호출된다.
        private long Form1_HookedKeyboardNofity(int iKeyWhatHappened, int vkCode)
        {
            //일단은 기본적으로 키 이벤트를 흘려보내기 위해서 0으로 세팅
            long lResult = 0;

            /////////////////////////////////////////////////////////////////////////////////
            //
            // Hook은 디버그모드에서 잡을 수 없기 때문에
            // 디버그용으로 다음의 코드를 사용한다.
            //textBox1.Text += Environment.NewLine + "iKeyWhatHappened=" + iKeyWhatHappened.ToString();
            //textBox1.Text += Environment.NewLine + "vkCode=" + vkCode.ToString();
            //
            /////////////////////////////////////////////////////////////////////////////////
            //
            // 키 조합에 의해 키가 눌리는 순간 동시에 눌린것을 확인 후 조치를 취한다.
            // L-Alt의 KeyDown: iKeyWhatHappened=32
            // L-Alt의 KeyUp: iKeyWhatHappened=160
            // 후킹은 했지만 키 이벤트는 얌전히 보내준다.
            // 만약 ALT+1에 대한 키 이벤트를 현재 활성화된 윈도우에 보내고싶지 않으면
            // 아래if문들의 lResult값을 모두 1을 주도록 하자.
            //
            /////////////////////////////////////////////////////////////////////////////////
            if (vkCode == 49 && iKeyWhatHappened == 32) //ALT + 1
            {
                bAltAndNum = true;
                bAltOrNum = false;
                lResult = 0;
                textBox1.Text = "ALT + 1이 눌렸습니다.";
            }
            else if (bAltAndNum && iKeyWhatHappened == 128)
            {
                bAltAndNum = false;
                bAltOrNum = true;
                lResult = 0;
                textBox1.Text += Environment.NewLine + "1은 눌려있고 ALT가 떨어졌다.";
            }
            else if (bAltAndNum && vkCode == 49)
            {
                bAltAndNum = false;
                bAltOrNum = true;
                lResult = 0;
                textBox1.Text += Environment.NewLine + "ALT는 눌려있고 1이 떨어졌다.";
            }
            else if (!bAltAndNum && bAltOrNum && (vkCode == 49 || vkCode == 164))
            {
                bAltOrNum = false;
                lResult = 0;
                textBox1.Text += Environment.NewLine + "키 조합이 완료되었다.";
                timer1.Interval = 50;
                timer1.Start();
            }
            else
            {
                //나머지 키들은 얌전히 보내준다.
                bAltAndNum = false;
                bAltOrNum = false;
                lResult = 0;
            }


            return lResult;
        }

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //3. 후크 이벤트를 연결한다.
            HookedKeyboardNofity += new KeyboardHooker.HookedKeyboardUserEventHandler(Form1_HookedKeyboardNofity);

            //4. 자동으로 훅을 시작한다. 여기서 훅에 의한 이벤트를 연결시킨다.
            KeyboardHooker.Hook(HookedKeyboardNofity);
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            KeyboardHooker.UnHook();
        }

        /// <summary>
        /// 핫키를 실행시킨다. 약간의 시간 딜레이를 주고 실행시키기 위하여 별도의 쓰레드에서 실행시킨다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer1_Tick(object sender, EventArgs e)
        {

            /////////////////////////////////////////////////////////////////////////////////
            //
            // ALT+1키 조합이 완성되었을 때 어떤 행위를 해주기 위한 쓰레드 부분이다.
            // 윈도우 폼에서는 타이머가 쓰레드역할을 잘 해주므로 편하게 이렇게 쓴다.
            // 일반적으로 키보드 후킹 후 매크로 명령을 많이 사용하므로
            // 이 부분은 각자 알아서 매크로 명령을 쓰도록 한다.
            // SendMessage/PostMessage/keybd_event등을 사용하면 된다.
            //
            /////////////////////////////////////////////////////////////////////////////////

            textBox1.Text += Environment.NewLine + "키조합에 의한 특수한 행위를 해보자.";
            timer1.Stop(); //타이머가 반복해서 동작하지 않도록 한다.
        }

    }//end of class


    public class KeyboardHooker
    {
        // 후킹된 키보드 이벤트를 처리할 이벤트 핸들러
        private delegate long HookedKeyboardEventHandler(int nCode, int wParam, IntPtr lParam);

        /// <summary>
        /// 유저에게 노출할 이벤트 핸들러
        /// iKeyWhatHappened : 현재 입력이 KeyDown/KeyUp인지 여부 - Key별로 숫자가 다르다.
        /// vkCode : virtual key 값, System.Windows.Forms.Key의 상수 값을 int로 변환해서 대응시키면 된다.
        /// </summary>
        /// <param name="iKeyWhatHappened"></param>
        /// <param name="bAlt"></param>
        /// <param name="bCtrl"></param>
        /// <param name="bShift"></param>
        /// <param name="bWindowKey"></param>
        /// <param name="vkCode"></param>
        /// <returns></returns>
        public delegate long HookedKeyboardUserEventHandler(int iKeyWhatHappened, int vkCode);

        // 후킹된 모듈의 핸들. 후킹이 성공했는지 여부를 식별하기 위해서 사용
        private const int WH_KEYBOARD_LL = 13;		// Intalls a hook procedure that monitors low-level keyboard input events.
        private static long m_hDllKbdHook;
        private static KBDLLHOOKSTRUCT m_KbDllHs = new KBDLLHOOKSTRUCT();
        private static IntPtr m_LastWindowHWnd;
        public static IntPtr m_CurrentWindowHWnd;

        // 후킹한 메시지를 받을 이벤트 핸들러
        private static HookedKeyboardEventHandler m_LlKbEh = new HookedKeyboardEventHandler(HookedKeyboardProc);

        // 콜백해줄 이벤트 핸들러 ; 사용자측에 이벤트를 넘겨주기 위해서 사용
        private static HookedKeyboardUserEventHandler m_fpCallbkProc = null;



        #region KBDLLHOOKSTRUCT Documentation
        /// <summary>
        /// The KBDLLHOOKSTRUCT structure contains information about a low-level keyboard input event. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/hooks_0cxe.htm">KBDLLHOOKSTRUCT</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        /// typedef struct KBDLLHOOKSTRUCT {
        ///     DWORD     vkCode;
        ///     DWORD     scanCode;
        ///     DWORD     flags;
        ///     DWORD     time;
        ///     ULONG_PTR dwExtraInfo;
        ///     ) KBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        private struct KBDLLHOOKSTRUCT
        {
            #region vkCode
            /// <summary>
            /// Specifies a virtual-key code. The code must be a value in the range 1 to 254. 
            /// </summary>
            #endregion
            public int vkCode;
            #region scanCode
            /// <summary>
            /// Specifies a hardware scan code for the key. 
            /// </summary>
            #endregion
            public int scanCode;
            #region flags
            /// <summary>
            /// Specifies the extended-key flag, event-injected flag, context code, and transition-state flag.
            /// </summary>
            /// <remarks>
            /// For valid flag values and additional information, see <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/hooks_0cxe.htm">MSDN Documentation for KBDLLHOOKSTRUCT</a>
            /// </remarks>
            #endregion
            public int flags;
            #region time
            /// <summary>
            /// Specifies the time stamp for this message. 
            /// </summary>
            #endregion
            public int time;
            #region dwExtraInfo
            /// <summary>
            /// Specifies extra information associated with the message. 
            /// </summary>
            #endregion
            public IntPtr dwExtraInfo;

            #region ToString()
            /// <summary>
            /// Creates a string representing the values of all the variables of an instance of this structure.
            /// </summary>
            /// <returns>A string</returns>
            #endregion
            public override string ToString()
            {
                string temp = "KBDLLHOOKSTRUCT\r\n";
                temp += "vkCode: " + vkCode.ToString() + "\r\n";
                temp += "scanCode: " + scanCode.ToString() + "\r\n";
                temp += "flags: " + flags.ToString() + "\r\n";
                temp += "time: " + time.ToString() + "\r\n";
                return temp;
            }
        }//end of structure

        #region CopyMemory Documentation
        /// <summary>
        /// The CopyMemory function copies a block of memory from one location to another. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/memory/memman_0z95.htm">CopyMemory</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        /// VOID CopyMemory(
        ///		PVOID Destination,   // copy destination
        ///		CONST VOID* Source,  // memory block
        ///		SIZE_T Length        // size of memory block
        ///		);
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"kernel32.dll", CharSet = CharSet.Auto)]
        private static extern void CopyMemory(ref KBDLLHOOKSTRUCT pDest, IntPtr pSource, long cb);

        #region GetForegroundWindow Documentation
        /// <summary>
        /// The GetForegroundWindow function returns a handle to the foreground window (the window with which the user is currently working).
        /// The system assigns a slightly higher priority to the thread that creates the foreground window than it does to other threads. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/windows_4f5j.htm">GetForegroundWindow</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        /// HWND GetForegroundWindow(VOID);
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr GetForegroundWindow();

        #region GetAsyncKeyState
        /// <summary>
        /// The GetAsyncKeyState function determines whether a key is up or down at the time the function is called,
        /// and whether the key was pressed after a previous call to GetAsyncKeyState. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/keybinpt_1x0l.htm">GetAsyncKeyState</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        ///	SHORT GetAsyncKeyState(
        ///		int vKey   // virtual-key code
        ///		);
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"user32.dll", CharSet = CharSet.Auto)]
        private static extern uint GetAsyncKeyState(int vKey);

        #region CallNextHookEx Documentation
        /// <summary>
        /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
        /// A hook procedure can call this function either before or after processing the hook information. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/hooks_57aw.htm">CallNextHookEx</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        /// 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
        ///    );
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"user32.dll", CharSet = CharSet.Auto)]
        private static extern long CallNextHookEx(long hHook, long nCode, long wParam, IntPtr lParam);

        #region SetWindowsHookEx Documentation
        /// <summary>
        /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
        /// You would install a hook procedure to monitor the system for certain types of events.
        /// These events are associated either with a specific thread or with all threads in the same
        /// desktop as the calling thread. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/hooks_7vaw.htm">SetWindowsHookEx</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        ///  HHOOK SetWindowsHookEx(
        ///		int idHook,        // hook type
        ///		HOOKPROC lpfn,     // hook procedure
        ///		HINSTANCE hMod,    // handle to application instance
        ///		DWORD dwThreadId   // thread identifier
        ///		);
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"user32.dll", CharSet = CharSet.Auto)]
        private static extern long SetWindowsHookEx(int idHook, HookedKeyboardEventHandler lpfn, long hmod, int dwThreadId);

        #region UnhookWindowsEx Documentation
        /// <summary>
        /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// See <a href="ms-help://MS.VSCC/MS.MSDNVS/winui/hooks_6fy0.htm">UnhookWindowsHookEx</a><BR/>
        /// </para>
        /// <para>
        /// <code>
        /// [C++]
        /// BOOL UnhookWindowsHookEx(
        ///    HHOOK hhk   // handle to hook procedure
        ///    );
        /// </code>
        /// </para>
        /// </remarks>
        #endregion
        [DllImport(@"user32.dll", CharSet = CharSet.Auto)]
        private static extern long UnhookWindowsHookEx(long hHook);


        // Valid return for nCode parameter of LowLevelKeyboardProc
        private const int HC_ACTION = 0;
        private static long HookedKeyboardProc(int nCode, int wParam, IntPtr lParam)
        {
            long lResult = 0;

            if (nCode == HC_ACTION) //LowLevelKeyboardProc
            {
                //visusl studio 2008 express 버전에서는 빌드 옵션에서 안전하지 않은 코드 허용에 체크
                unsafe
                {
                    //도대체 어디서 뭘 카피해놓는다는건지 이거 원..
                    CopyMemory(ref m_KbDllHs, lParam, sizeof(KBDLLHOOKSTRUCT));
                }

                //전역 후킹을 하기 위해서 현재 활성화 된 윈도우의 핸들값을 찾는다.
                //그래서 이 윈도우에서 발생하는 이벤트를 후킹해야 전역후킹이 가능해진다.
                m_CurrentWindowHWnd = GetForegroundWindow();

                //후킹하려는 윈도우의 핸들을 방금 찾아낸 핸들로 바꾼다.
                if (m_CurrentWindowHWnd != m_LastWindowHWnd)
                    m_LastWindowHWnd = m_CurrentWindowHWnd;

                // 이벤트 발생
                if (m_fpCallbkProc != null)
                {
                    lResult = m_fpCallbkProc(m_KbDllHs.flags, m_KbDllHs.vkCode);
                }

            }
            else if (nCode < 0) //나머지는 그냥 통과시킨다.
            {
                #region MSDN Documentation on return conditions
                // "If nCode is less than zero, the hook procedure must pass the message to the 
                // CallNextHookEx function without further processing and should 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;"
                #endregion
                return CallNextHookEx(m_hDllKbdHook, nCode, wParam, lParam);
            }

            //
            //lResult 값이 0이면 후킹 후 이벤트를 시스템으로 흘려보내고
            //0이 아니면 후킹도 하고 이벤트도 시스템으로 보내지 않는다.
            return lResult;
        }

        // 후킹 시작
        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;

            return bResult;
        }

        // 후킹 중지
        public static void UnHook()
        {
            //프로그램 종료 시점에서 호출해주자.
            UnhookWindowsHookEx(m_hDllKbdHook);
        }

    }//end of class(KeyboardHooker)

}//end of namespace