강좌와 팁

저수준 키보드 후킹 예제 및 강좌 날짜:2022-2-28 3:58:37 조회수:381
작성자 : daypark
포인트 : 1039
가입일 : 2020-02-14 10:42:05
방문횟수 : 217
글 153개, 댓글 26개
소개 : 자기소개를 입력하십시오.
작성글 보기
쪽지 보내기
시스템 전역적인 키보드 메시지를 가로채는 키보드 후킹은 두가지 방법이 있다. 두 방식은 메시지를 언제 꺼내는지가 다르며 이 차이로 인해 여러가지 특성과 활용처가 다르다.

-고수준 훅 프로시저는 메시지를 꺼낼때 호출되는 반면 저수준 훅 프로시저는 키보드 메시지를 스레드 큐에 붙일 때 호출된다. 
- 고수준은 타겟이 꺼낸 메시지를 들여다 봐야 하므로 타겟 프로세스의 주소 공간에서 실행해야 하지만 저수준은 시스템이 메시지 큐에 넣을 메시지를 보므로 그럴 필요가 없다. DLL이 타겟 프로세스에 주입되지 않으며 대신 컨텍스트 스위칭만 잠시 발생한다.
-고수준은 지역 훅이 가능하지만 저수준은 원칙적으로 전역 훅만 가능하다. 대신 저수준은 DLL로 분리하지 않더라도 전역 후킹이 가능한 이점이 있다.
-고수준은 keybd_event로 발생시킨 이벤트와 키보드로부터 발생한 이벤트를 구분할 수 없지만 저수준은 플래그의 LLKHF_INJECTED 비트를 점검하여 인위적인 메시지인지를 구분할 수 있다.
-wParam, lParam으로 전달되는 정보가 다르다. 저수준은 wParam으로 메시지의 종류가 오고 lParam에는 가상키, 스캔 코드, 플래그, 시간, 여분 정보 등을 멤버로 가진 구조체가 온다.

두 방법 모두 모니터링만 가능하며 메시지 자체를 조작하는 것은 안된다. 그러나 메시지를 아예 먹어 버리고 새로운 메시지를 밀어 넣는 방식으로 어느 정도는 조작할 수 있다. 
고수준 후킹은 자료가 많으므로 여기서는 저수준 후킹을 중점적으로 알아 보자. 들여다 보는 시점이 더 빠르고 메시지를 원형 그대로 볼 수 있다는 것일 뿐 어렵거나 난해하지는 않다. 다음은 저수준 후킹 테스트 예제이다. 

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;
LPCTSTR lpszClass = TEXT("KeyHookLL");

int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
    , _In_ LPSTR lpszCmdParam, _In_ int nCmdShow)
{
    HWND hWnd;
    MSG Message;
    WNDCLASS WndClass;
    g_hInst = hInstance;

    WndClass.cbClsExtra = 0;
    WndClass.cbWndExtra = 0;
    WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    WndClass.hInstance = hInstance;
    WndClass.lpfnWndProc = WndProc;
    WndClass.lpszClassName = lpszClass;
    WndClass.lpszMenuName = NULL;
    WndClass.style = CS_HREDRAW | CS_VREDRAW;
    RegisterClass(&WndClass);

    hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, (HMENU)NULL, hInstance, NULL);
    ShowWindow(hWnd, nCmdShow);
    hWndMain = hWnd;

    while (GetMessage(&Message, NULL, 0, 0)) {
        TranslateMessage(&Message);
        DispatchMessage(&Message);
    }
    return (int)Message.wParam;
}

HHOOK hKeyHookLL = NULL;
TCHAR MesName[111];
TCHAR log[256];
TCHAR output[65000];
LRESULT CALLBACK KeyHookProcLL(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0) {
        KBDLLHOOKSTRUCT* kb = (KBDLLHOOKSTRUCT*)lParam;
        if (wParam == WM_KEYDOWN) lstrcpy(MesName, "Down");
        if (wParam == WM_KEYUP) lstrcpy(MesName, "Up");
        if (wParam == WM_SYSKEYDOWN) lstrcpy(MesName, "SysDown");
        if (wParam == WM_SYSKEYUP) lstrcpy(MesName, "SysUp");
        if ((kb->flags & LLKHF_INJECTED) != 0) lstrcat(MesName, "(Injected)");

        wsprintf(log, "%s - vk = %x(%c), scan = %x, flag=%x, time = %d\r\n", 
            MesName, kb->vkCode, kb->vkCode, kb->scanCode, kb->flags, kb->time);
        lstrcat(output, log);

        // ESC를 누르면 메시지를 리셋한다.
        if (wParam == WM_KEYDOWN && kb->vkCode == VK_ESCAPE) {
            lstrcpy(output, "");
        }
        InvalidateRect(hWndMain, NULL, TRUE);

        // 0이 아닌 값을 리턴하여 특정 키는 입력을 금지한다. 
        if (kb->vkCode == '1') {
            return 1;
        }

        // 다른 키로 바꾸는 기능은 안된다. 
        if (kb->vkCode == '2') {
            kb->vkCode = '3';
        }

        // 키 입력을 금지하고 새로운 키 입력을 발생할 수는 있다. 
        if (kb->vkCode == '4') {
            keybd_event('5', 0, wParam == WM_KEYDOWN ? 0: KEYEVENTF_KEYUP, 0);
            return 1;
        }
    }
    return CallNextHookEx(hKeyHookLL, nCode, wParam, lParam);
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT crt;

    switch (iMessage) {
    case WM_CREATE:
        hKeyHookLL = SetWindowsHookEx(WH_KEYBOARD_LL, KeyHookProcLL, g_hInst, NULL);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        GetClientRect(hWnd, &crt);
        DrawText(hdc, output, -1, &crt, 0);
        EndPaint(hWnd, &ps);
        return 0;
    case WM_DESTROY:
        if (hKeyHookLL != NULL) UnhookWindowsHookEx(hKeyHookLL);
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}

설치는 다른 훅과 같되 훅 타입을 WH_KEYBOARD_LL로 지정한다. DLL로 설치할 필요 없으므로 주체는 항상 현재 인스턴스이며 전역만 가능하므로 대상 스레드는 언제나 NULL이다. 훅 프로시저는 메시지 전송을 통해 호출되므로 주체는 반드시 메시지 루프가 있어야 한다. WM_CREATE에서 설치하고 WM_DESTROY에서 해제하면 실행중인동안 모든 키보드 메시지를 훅 프로시저로 먼저 받는다. 
훅 프로시저는 각 키의 상태를 갱신하기 전에 호출되므로 이때는 GetAsyncKeyState 함수로 키의 상태를 정확히 알 수 없다. 만약 키 상태가 필요하면 메시지를 받을 때마다 키의 상태를 자체적으로 관리해야 한다. wParam으로 메시지의 종류가 전달되며 lParam으로는 다음 구조체를 전달한다. 

typedef struct tagKBDLLHOOKSTRUCT {
  DWORD     vkCode;
  DWORD     scanCode;
  DWORD     flags;
  DWORD     time;
  ULONG_PTR dwExtraInfo;
} KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

이 정보를 읽어 어떤 키를 눌렀는지 뗐는지 파악한다. 메시지를 본 후 0을 리턴하면 다음 훅 체인을 거쳐 타겟 윈도우로 메시지를 전달하여 아무 일 없이 처리된다. 메시지를 없애 버리려면 0이 아닌 값, 통상 1을 리턴하여 다음 체인으로 가지 않도록 한다. 
훅 프로시저는 제한시간내로 메시지를 보거나 처리해야 하며 만약 시간을 초과하면 메시지는 다음 체인으로 강제 전달된다. 제한시간은 레지스트리에 지정되어 있는데 디폴트값은 1초이다.
예제 코드는 메시지를 받은 직후 로그를 출력하여 어떤 키 메시지인지 보여준다. 다른 프로그램의 키 입력을 훤히 들여다 볼 수 있다. 대개의 경우 다음 체인으로 메시지를 그냥 보내지만 필요하다면 메시지를 없애 버리거나 바꿀 수 있다. esc를 누르면 로그를 리셋하는데 포커스를 가지지 않아도 이 동작이 가능하다.
1키에 대해서는 1을 리턴하여 메시지를 먹어 버린다. 다음 체인으로 메시지를 보내지 않으면 이 키는 사라진다. 따라서 어떤 윈도우도 1키에 대한 메시지를 받을 수 없다. 게임중에 win키를 금지할 때 이 방법을 사용하면 된다. 
2키를 3키 입력으로 바꾸는 것은 안된다. vkCode는 눌러진 키에 대한 정보일 뿐이며 이 값을 바꾼다고 해서 타겟이 받는 메시지가 달라지지는 않는다. 즉 훅 프로시저로 전달되는 파라미터는 읽기 전용이다.
메시지를 정 바꾸고 싶으면 일단 먹어 버리고 keybd_event함수로 새로운 키입력을 생성한다. 4를 누르면 5로 바꿔 버린다. 새로 발생한 5 입력도 훅 프로시저로 오며 이 입력을 다음 체인으로 보내면 최종 타겟으로 전달된다. 키보드로 직접 입력하지 않은 메시지를 구분해 내려면 flag의 LLKHF_INJECTED 플래그를 점검한다.



예제를 실행해 보고 다른 윈도우에서 1, 2, 4 를 각각 눌러 보면 훅 프로시저가 메시지를 어떻게 조작하는지 알 수 있다. 다른 윈도우에 입력되는 키를 모두 볼 수 있으며 아주 특수한 몇가지를 제외하고는 대부분의 변형도 가능하다. 
 

목록보기 삭제 수정 신고 스크랩


로그인하셔야 댓글을 달 수 있습니다.