윈도우 환경에서 Python을 사용하여 키보드 후킹을 하는 방법 관련하여 좀더 쓰기 편하게 코드 정리를 하였다. 특수키를 확인하는 방법도 추가되었다.



소스


KeyRogue.py
# -*- coding: utf-8 -*-
#!/usr/bin/python

import sys
from ctypes import *
from ctypes.wintypes import MSG
from ctypes.wintypes import DWORD

class KeyRogue:
    user32 = windll.user32
    kernel32 = windll.kernel32

    WH_KEYBOARD_LL = 13
    WM_KEYDOWN = 0x0100
    WM_SYSKEYDOWN = 0x0104
    
    VK_CONTROL = 0x11
    VK_MENU = 0x12
    VK_SHIFT = 0x10
    
    hooked = None
    hookProc = None
    pointer = None
    
    @staticmethod
    def installHookProc(hookProc):
        if KeyRogue.hooked is not None:
            print("already installed. uninstalling..")
            KeyRogue.uninstallHookProc()
        KeyRogue.hookProc = hookProc
        KeyRogue.pointer = KeyRogue.getFPTR(KeyRogue.hookProcInternal)
        
        KeyRogue.hooked = KeyRogue.user32.SetWindowsHookExA(
            KeyRogue.WH_KEYBOARD_LL,
            KeyRogue.pointer,
            KeyRogue.kernel32.GetModuleHandleW(None),
            0
        )
        if not KeyRogue.hooked:
            return False
        return True

    @staticmethod
    def uninstallHookProc():
        if KeyRogue.hooked is None:
            return
        KeyRogue.user32.UnhookWindowsHookEx(KeyRogue.hooked)
        KeyRogue.hooked = None

    @staticmethod
    def bytes(integer):
        return divmod(integer, 0x10000)
            
    @staticmethod
    def getFPTR(fn):
        CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
        return CMPFUNC(fn)

    @staticmethod
    def startKeyLog():
        msg = MSG()
        KeyRogue.user32.GetMessageA(byref(msg), 0, 0, 0)

    @staticmethod
    def getKeyCode(lParam):
        high, low = KeyRogue.bytes(lParam[0])
        return low

    @staticmethod
    def getKeyInfo(lParam):
        keyCode = KeyRogue.getKeyCode(lParam)
        return (keyCode, chr(keyCode))

    @staticmethod
    def checkModifier(lParam, vKey):
        if KeyRogue.user32.GetAsyncKeyState(vKey) & 0x8000:
            return True
        return False

    @staticmethod
    def hookProcInternal(nCode, wParam, lParam):
        if wParam != KeyRogue.WM_KEYDOWN and wParam != KeyRogue.WM_SYSKEYDOWN:
            return KeyRogue.user32.CallNextHookEx(KeyRogue.hooked, nCode, wParam, lParam)
        if KeyRogue.hookProc:
            KeyRogue.hookProc(nCode, wParam, lParam)
        return KeyRogue.user32.CallNextHookEx(KeyRogue.hooked, nCode, wParam, lParam)

    @staticmethod
    def start(hookProc):
        if KeyRogue.installHookProc(hookProc):
            KeyRogue.startKeyLog()
        else:
            print("install failed")

    @staticmethod
    def stop():
        KeyRogue.uninstallHookProc()

client.py
from KeyRogue import *

def hookProc(nCode, wParam, lParam):
    key = KeyRogue.getKeyInfo(lParam)
    print("key code = %d" % key[0])
    print("key char = %c" % key[1])
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_CONTROL):
        print("CTRL pressed")
        if (key[1] == 'Q'):
            print("exit key pressed, call stop()")
            KeyRogue.stop()
            sys.exit(-1)
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_MENU):
        print("ALT pressed")
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_SHIFT):
        print("SHIFT pressed")

KeyRogue.start(hookProc)

  • Hook 설치 성공시 .start() 함수는 리턴되지 않는다.(심지어 stop() 함수를 사용하여 언인스톨되었을 경우에도) 따라서 GUI가 있는 프로그램에 키보드 후킹 기능을 추가하려면 별도의 쓰레드에서 실행되도록 처리가 필요하다.


참고사항

<테스트 환경>
- OS : Windows 10
- Python 버전 : 3.6.5(32/64bit), 3.7.5(32bit)


,