Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 10개 있습니다.)
Windows: 148. Windows - Raw Input의 Top level collection 의미
; https://www.sysnet.pe.kr/2/0/11612

.NET Framework: 788. RawInput을 이용한 키보드/마우스 입력 모니터링
; https://www.sysnet.pe.kr/2/0/11615

개발 환경 구성: 488. (User-mode 코드로 가상 USB 장치를 만들 수 있는) USB/IP PROJECT 소개
; https://www.sysnet.pe.kr/2/0/12213

개발 환경 구성: 490. C# - (Wireshark의) USBPcap을 이용한 USB 패킷 모니터링
; https://www.sysnet.pe.kr/2/0/12215

.NET Framework: 904. USB/IP PROJECT를 이용해 C#으로 USB Keyboard 가상 장치 만들기
; https://www.sysnet.pe.kr/2/0/12216

.NET Framework: 905. C# - DirectX 게임 클라이언트 실행 중 키보드 입력을 감지하는 방법
; https://www.sysnet.pe.kr/2/0/12218

.NET Framework: 917. C# - USB 관련 ETW(Event Tracing for Windows)를 이용한 키보드 입력을 감지하는 방법
; https://www.sysnet.pe.kr/2/0/12246

.NET Framework: 990. C# - SendInput Win32 API를 이용한 가상 키보드/마우스
; https://www.sysnet.pe.kr/2/0/12469

.NET Framework: 1062. Windows Forms - 폼 내에서 발생하는 마우스 이벤트를 자식 컨트롤 영역에 상관없이 수신하는 방법
; https://www.sysnet.pe.kr/2/0/12660

개발 환경 구성: 607. 로컬의 USB 장치를 원격 머신에 제공하는 방법 - usbip-win
; https://www.sysnet.pe.kr/2/0/12858




C# - DirectX 게임 클라이언트 실행 중 키보드 입력을 감지하는 방법

각종 키보드 입력 감지 기술들이,

Low level keyboard input from Windows
; https://stackoverflow.com/questions/310576/low-level-keyboard-input-from-windows

DirectX 게임에 대해서는 통하지 않습니다. 예전에 소개한 RawInput 조차도,

RawInput을 이용한 키보드/마우스 입력 모니터링
; https://www.sysnet.pe.kr/2/0/11615

마찬가지인데요, 이것이 가능하려면 보다 하위 기술로 내려가야 하는데 일례로 커널 레벨에서 키보드 장치에 대한 driver를 직접 필터링하는 식이어야 합니다.

Keyboard Hooking In Kernel
; https://www.codeproject.com/Articles/194729/Keyboard-Hooking-In-Kernel

문제는, 저런 DDK 소스 코드를 빌드해 정상적인 인증서로 서명하지 않으면 윈도우 환경을 "testsigning" 모드로 두어야 하는데 이것 또한 은근 보안 위협에 노출될 수 있고 게다가 특정 게임 보안 프로그램에서는 아예 testsigning 환경에서는 게임을 실행하지 못하게 하는 경우도 있습니다.




이런 상황에 대한 우회 해결책으로 지난 글에서 설명한,

C# - (Wireshark의) USBPcap을 이용한 USB 패킷 모니터링
; https://www.sysnet.pe.kr/2/0/12215

USBPcap이 답이 될 수 있습니다. 실제로 그 글에서 소개한 소스 코드로 빌드한 ConsoleApp1.exe는 키보드 입력에 대해 DirectX 게임 실행 중에도 잘 동작하는 것을 확인할 수 있습니다.

"C:\Program Files\Wireshark\extcap\USBPcapCMD.exe" -d \\.\USBPcap1 --devices 1 -o - | ConsoleApp1.exe

게다가 USBPcap 자체에서 이미 정식 인증서로 서명한 드라이버 파일을 제공하므로 현실적으로 사용하는 데에도 아무런 불편함이 없습니다. 한 가지 아쉬운 점이 있다면, USBPcapCMD.exe의 동작 방식으로 인해 표준 입출력에 대한 redirection을 처리해야 한다는 것인데 개발하면서 디버깅하는 중에는 이 과정이 꽤나 귀찮다는 점입니다.

이런 불편함을 없애려면 USBPcapCMD.exe처럼 직접 USBPcap device driver에 접근해 필터링하는 코드를 작성하는데, 음... C/C++로 하려니 역시나 귀찮고 아무도 C#으로 컨버팅하지도 않은 듯하니... 어쩔 수 없이 그냥 제가 만들었습니다. ^^; 소스 코드는 기존 usbcap을 fork한,

stjeong / usbpcap
; https://github.com/stjeong/usbpcap

windows 하위 폴더에 csUSBPcapCMD C# 프로젝트로 만들어 두었고,

\usbpcap\windows\csUSBPcapCMD
; https://github.com/stjeong/usbpcap/tree/master/windows/csUSBPcapCMD

장치 선택을 쉽게 할 수 있도록 Wireshark에서 보여주는 번호와 일치하는 "device id"를 함께 출력하도록 했습니다.

D:\usbpcap\windows\csUSBPcapCMD\bin\Debug\netcoreapp3.1> csUSBPcapCMD.exe
Following filter control devices are available:
1 \\.\USBPcap1
  \??\USB#ROOT_HUB30#4&1148bc98&0&0#{f18a0e88-c30c-11d0-8815-00a0c906bed8}
    [Port 7]  (device id: 1) USB Composite Device
      USB Input Device
        HID Keyboard Device
      USB Input Device
        HID-compliant consumer control device
        HID-compliant system controller
    [Port 8]  (device id: 2) Generic USB Hub
      [Port 1]  (device id: 6) USB Input Device
        HID-compliant mouse
    [Port 9]  (device id: 3) USB Mass Storage Device
      ST1000LM 024 HN-M101MBB USB Device
    [Port 12]  (device id: 4) Intel(R) Wireless Bluetooth(R)
      Microsoft Bluetooth LE Enumerator
      Bluetooth Device (RFCOMM Protocol TDI)
      Microsoft Bluetooth Enumerator
        ...[생략]...
      Bluetooth Device (Personal Area Network)
    [Port 13]  (device id: 5) Wacom Tablet
      HID-Compliant Mouse
      HID-compliant vendor-defined device
      HID-compliant pen
      HID-compliant digitizer

따라서, 위와 같은 상황에서 "HID Keyboard Device"를 모니터링하고 싶다면 "csUSBPcapCMD.exe 0 1"과 같이 실행하면 됩니다.

참고로, 대부분의 기능을 별도로 USBPcapClient 라이브러리로 만들어 두었고,

usbpcap/windows/USBPcapLib/
; https://github.com/stjeong/usbpcap/tree/master/windows/USBPcapLib

NuGet에도 올렸으니,

USBPcapLib
; https://www.nuget.org/packages/USBPcapLib/

라이브러리 참조만 추가하고,

Install-Package USBPcapLib -Version 1.0.3

다음과 같은 식으로 코딩하기만 하면 됩니다. ^^ (물론, 사전에 USBPcap을 시스템에 설치해 두어야 하고, 프로그램은 반드시 관리자 권한으로 실행해야 합니다.)

using System;
using System.Collections.Generic;
using USBPcapLib;

namespace ConsoleApp1
{
    // Install-Package USBPcapLib -Version 1.0.3
    // https://github.com/stjeong/usbpcap/tree/master/windows/csUSBPcapCMD
    class Program
    {
        static void Main(string[] args)
        {
            List<string> filters = USBPcapClient.find_usbpcap_filters();

            if (filters.Count == 0)
            {
                Console.WriteLine("No filter control devices are available.");

                if (USBPcapClient.is_usbpcap_upper_filter_installed() == true)
                {
                    Console.WriteLine("Please reinstall USBPcapDriver.");
                }

                Console.WriteLine($@"USBPcap UpperFilter entry appears to be present.
Most likely you have not restarted your computer after installation.
It is possible to restart all USB devices to get USBPcap working without reboot.
{Environment.NewLine}WARNING:{Environment.NewLine}  Restarting all USB devices can result in data loss.
  If you are unsure please answer 'n' and reboot in order to use USBPcap.");

                return;
            }

            // how can I precisely specify a USB device to capture with tshark?
            // https://osqa-ask.wireshark.org/questions/53919/how-can-i-precisely-specify-a-usb-device-to-capture-with-tshark

            // Tracking only one USB Port in Filter using USBPcap
            // https://osqa-ask.wireshark.org/questions/63837/tracking-only-one-usb-port-in-filter-using-usbpcap
            int targetDeviceId = 0;
            int filterDeviceId = 1;

            if (args.Length >= 1)
            {
                targetDeviceId = int.Parse(args[0]);
            }

            if (args.Length >= 2)
            {
                filterDeviceId = int.Parse(args[1]);
            }

            Console.WriteLine("Following filter control devices are available:");
            string target = null;

            int i = 0;

            foreach (string filter in filters)
            {
                Console.WriteLine($"{i + 1} {filter}");

                string deviceList = USBPcapClient.enumerate_print_usbpcap_interactive(filter);
                Console.WriteLine(deviceList);

                if (targetDeviceId == i)
                {
                    target = filter;
                    break;
                }

                i++;
            }

            if (string.IsNullOrEmpty(target) == false)
            {
                using (USBPcapClient usbCapture = new USBPcapClient(target, filterDeviceId))
                {
                    usbCapture.HeaderRead += UsbCapture_HeaderRead;
                    usbCapture.DataRead += UsbCapture_DataRead;

                    usbCapture.start_capture();

                    usbCapture.wait_for_exit_signal();
                }
            }
        }

        private static void UsbCapture_HeaderRead(object sender, HeaderEventArgs e)
        {
            Console.WriteLine(e.Header.magic_number);
            Console.WriteLine(e.Header.version_major);
            Console.WriteLine(e.Header.version_minor);
            Console.WriteLine(e.Header.thiszone);
            Console.WriteLine(e.Header.sigfigs);
            Console.WriteLine(e.Header.snaplen);
            Console.WriteLine(e.Header.network);
        }

        static int _index = 1;

        private static void UsbCapture_DataRead(object sender, DataEventArgs e)
        {
            int pcapRecordDataSize = (int)e.Record.incl_len;

            //Console.WriteLine($"[{index}]");
            //Console.WriteLine(e.Record.ts_sec);
            //Console.WriteLine(e.Record.ts_usec);
            //Console.WriteLine(e.Record.incl_len);
            //Console.WriteLine(e.Record.orig_len);

            byte[] buf = e.Data;

            switch (e.Header.function)
            {
                case URB_FUNCTION.URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER:
                    if (e.Header.IrpDirection == IRPDierction.PDO_TO_FDO)
                    {
                        Console.Write($"[{_index}] {e.Header.bus}.{e.Header.device}.{e.Header.EndpointNumber}, {e.Header.transfer}, {e.Header.IrpDirection} : {buf.Length} - ");

                        if (e.Header.status != USBD_STATUS.USBD_STATUS_SUCCESS)
                        {
                            break;
                        }

                        if (e.Header.transfer != USBPCAP_TRANSFER_TYPE.INTERRUPT)
                        {
                            break;
                        }

                        if (buf.Length != 8)
                        {
                            break;
                        }

                        byte code = buf[2];
                        if (code == 0)
                        {
                            Console.WriteLine("key up");
                        }
                        else
                        {
                            // https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2
                            if (code >= 0x4 && code <= 0x1d)
                            {
                                char ch = (char)(code - 4 + (short)'a');
                                Console.WriteLine(ch + " pressed");
                            }
                            else
                            {
                                Console.WriteLine("Key pressed");
                            }
                        }
                    }
                    break;

                default:
                    break;
            }

            _index++;
        }
    }
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/1/2020]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2020-06-15 03시20분
본문에서 소개한 "Keyboard Hooking In Kernel"보다 더 간단한 소스 코드로,

HarsaroopDhillon2 / KL-Kernel-Project
; https://github.com/HarsaroopDhillon2/KL-Kernel-Project/blob/master/main.c
정성태
2022-04-25 11시22분
[anonymous] 감사히 잘 쓰고 있습니다
[guest]
2022-04-25 09시56분
@anonymous 반갑습니다. 이 코드의 가치를 아시는 분이군요. ^^
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
14000정성태8/18/202524오류 유형: 979. ERROR: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory
13999정성태8/15/2025705닷넷: 2355. C# 14 - (8) null 조건부 연산자 개선 - 대입문에도 사용 가능파일 다운로드1
13998정성태8/14/2025794닷넷: 2354. C# 14 - (7) 확장 메서드에 정적 메서드와 속성 지원을 위한 전용 구문 추가파일 다운로드1
13997정성태8/14/2025937Linux: 120. docker 컨테이너로 매핑된 볼륨에 컨테이너 측의 사용자 ID를 유지하면서 복사하는 방법
13996정성태8/13/2025564오류 유형: 978. Unable to find the requested .Net Framework Data Provider.
13995정성태8/13/2025613개발 환경 구성: 754. Visual C++ - 리눅스 빌드를 위한 Ubuntu 18 docker 컨테이너 설정
13994정성태8/12/2025640오류 유형: 977. SQL Server - User, group, or role '...' already exists in the current database. (Microsoft SQL Server, Error: 15023)
13993정성태8/11/2025962오류 유형: 976. Microsoft.ML.OnnxRuntimeGenAI 패키지 사용 시 "cublasLt64_12.dll" which is missing. (Error 126: "The specified module could not be found.") 오류
13992정성태8/11/20251141닷넷: 2353. C# - Foundry Local을 이용한 gpt-oss-20b 모델 사용파일 다운로드1
13991정성태8/9/20251070오류 유형: 975. winget - Foundry Local 패키지 업데이트가 안 되는 문제
13990정성태8/8/2025826Windows: 283. Time zone 설정이 없는 Windows Server 2025
13989정성태8/8/20251210닷넷: 2352. C# - Windows S-mode 환경인지 체크하는 방법파일 다운로드1
13988정성태8/8/20251350오류 유형: 974. 비주얼 스튜디오 업데이트 시 잠김 파일 경고 - Visual Studio Standard Collector Service 150 (VSStandardCollectorService150)
13987정성태8/7/20251014닷넷: 2351. C# 14 - (6) event와 생성자에도 partial 메서드 적용파일 다운로드1
13986정성태8/6/20251096닷넷: 2350. C# 14 - (5) 람다 매개 변수에 접근자가 있는 경우에도 타입 생략 가능파일 다운로드1
13985정성태8/6/20251452오류 유형: 973. "wsl --install" 명령어 수행 시 "The server name or address could not be resolved"
13984정성태8/6/20251233Windows: 282. 윈도우 운영체제에 추가된 ssh 서버(Win32-OpenSSH)
13983정성태8/4/20251316오류 유형: 972. Microsoft.Data.SqlClient 6.1.0 버전부터 .NET 8 이상만 지원
13982정성태8/2/20251722개발 환경 구성: 753. CentOS 7 컨테이너 내에서 openssh 서버 호스팅
13981정성태8/1/20251391오류 유형: 971. CentOS 7에서 yum 사용 시 "Could not resolve host: mirrorlist.centos.org; Unknown error"
13980정성태7/31/20251569Linux: 119. eBPF - BPF_PROG_TYPE_CGROUP_SOCK 유형에서 정상 동작하지 않는 BPF_CORE_READ (2)
13979정성태7/30/20251871Linux: 118. eBPF - BPF_PROG_TYPE_CGROUP_SOCK 유형에서 정상 동작하지 않는 BPF_CORE_READ
13978정성태7/29/20251703오류 유형: 970. 파일 복사 시 "Data error (cyclic redundancy check). (0x80070017)" 에러
13977정성태7/28/20252019닷넷: 2349. C# 14 - (4) 문자열 리터럴을 utf-8 인코딩으로 저장파일 다운로드1
13976정성태7/25/20251865닷넷: 2348. C# - 카카오 카나나 모델 + Microsoft.ML.OnnxRuntimeGenAI 예제파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...