security/리버싱핵심원리

SetWindowsHookEx()을 이용한 Windows 메시지 후킹

민사민서 2023. 3. 15. 14:37
// KeyHook.dll

#include "stdio.h"
#include "windows.h"

#define DEF_PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL; // hook procedure가 속한 DLL 핸들
HHOOK g_hHook = NULL; // Hook 핸들
HWND g_hWnd = NULL;

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpvReserved) {
	switch(dwReason) {
		case DLL_PROCESS_ATTACH:
			g_hInstance = hinstDll;
			break;
		case DLL_PROCESS_DETACH:
			break;
	}
	return TRUE;
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
	char szPath[MAX_PATH] = {0, };
	char *p = NULL;

	if(nCode>=0) {
		// 31번째 bit: 0=key press, 1=key release
		if(!(lParam&0x80000000)) {
			GetModuleFileNameA(NULL, szPath, MAX_PATH);
			p = strrchr(szPath, '\\');

			// 현재 프로세스 이름 비교해서 notepad.exe라면 메시지 무시
			if(!_stricmp(p+1, DEF_PROCESS_NAME))
				return 1;
		}
	}
	// 일반적인 경우 CallNextHookEx()를 호출하여 다음 훅(혹은 운영체제)로 메시지 전달
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

// C++ 컴파일러에게 C 문법을 적용해 컴파일하라고 알려줌
#ifdef __cplusplus 
extern "C" {
#endif
// dll에서 export하는 함수 정의
__declspec(dllexport) void HookStart() {
	// WH_KEYBOARD 후크 사용, 콜백함수, dll 핸들, global hook
	g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}
__declspec(dllexport) void HookStop() {
	if(g_hHook) {
		UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}
}
#ifdef __cplusplus
}
#endif​
// HookMain.exe

#include "stdio.h"
#include "conio.h"
#include "windows.h"

#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"

typedef void(*PFN_HOOKSTART)(); // 반환값 자료형 void인 함수 포인터에게 별칭 붙임
typedef void(*PFN_HOOKSTOP)(); // 반환값 자료형 void인 함수 포인터에게 별칭 붙임

void main() {
	HMODULE hDll = NULL;
	PFN_HOOKSTART HookStart = NULL;
	PFN_HOOKSTOP HookStop = NULL;
	char ch = 0;

	// KeyHook.dll 로딩
	hDll = LoadLibraryA(DEF_DLL_NAME);

	// dll의 export 함수 주소 얻기
	HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
	HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
	
	// hooking 시작
	HookStart();

	printf("press 'q' to quit!\n");
	while( _getch() != 'q' );

	// hooking 종료
	HookStop();

	// KeyHook.dll 언로딩
	FreeLibrary(hDll);
}

 

 

0. dll 빌드 방법

1) 새 프로젝트 - Win32 - Win32 콘솔 응용 프로그램 - 이름/솔루션 이름 입력 - 마법사 - DLL, 빈프로젝트 체크 - 마침

2) 프로젝트 - 새 항목 추가 - 코드 - C++ 파일(.cpp) - 이름 입력

3) 솔루션 구성 Release & 솔루션 플랫폼 Win32

4) 프로젝트 - Keyboard.dll 속성 - 구성 속성 - C/C++ - 코드 생성 - 런타임 라이브러리 - 다중 스레드(/MT)

// exe랑 dll이랑 컴파일 옵션 통일하자 

// /MD로 해도 잘 동작함. 크기는 46KB에서 7KB로 확 준다!!

// standard c library function(stdio.h 등) 사용하고 있고 MFC library(afx.h 등) 사용 안하므로 MFC 옵션 신경 x

// 마찬가지로 ATL 옵션 신경 x

5) 컴파일(Ctrl+F7 혹은 소스코드 우클릭 - 컴파일)

// obj 파일 생성

6) 솔루션 빌드(F7 혹은 디버그 - 솔루션 빌드)

// 링크 수행되면서 실행 가능한 파일 생성됨 (Release 폴더)

// 컴파일도 자동으로 수행됨

 

1. exe 빌드 방법

1) 새 프로젝트 - Win32 - Win32 콘솔 응용 프로그램 - 이름/솔루션 이름 입력 - 마법사 - 콘솔응용프로그램, 빈프로젝트 체크 - 마침

2) 프로젝트 - 새 항목 추가 - 코드 - C++ 파일(.cpp) - 이름 입력

3) 솔루션 구성 Release & 솔루션 플랫폼 Win32

4) 프로젝트 - Keyboard.dll 속성 - 구성 속성 - C/C++ - 코드 생성 - 런타임 라이브러리 - 다중 스레드(/MT)

5) 솔루션 빌드(F7 혹은 디버그 - 솔루션 빌드)

 

2. Process Explorer 호출 => Ctrl+Shift+Esc

 

3. 키보드 후킹 도움글

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=kmymirae&logNo=150046035481 

 

키보드 후킹에 대한 짧은 도움글

HHOOK SetWindowsHookEx( int idHook, // type of hook to install HOOKPROC lpfn, // a...

blog.naver.com

 

4. SetWindowsHookEx, KeyBoardProc

HHOOK SetWindowsHookEx(
    int idHook, 		// hook type
    HOOKPROC lpfn,		// hook procedure
    HINSTANCE hMod,		// hook procedure가 속해있는 dll 핸들
    DWORD dwThreadID		// hook을 걸고 싶은 thread의 ID (0은 global hook)
);

- 동작: 프로세스에서 특정 메시지가 발생했을 때 DLL 파일을 프로세스에 강제로 인젝션 후 hook procedure 호출

- 인자 설명

1) idHook: hook type, 여기선 WH_KEYBOARD 후크를 사용

2) lpfn: 콜백함수, 키보드 입력에 대한 default 포맷은 아래와 같다

3) 4) hMod와 dwThreadID는 후킹할 목적지를 정하는 곳, 전자는 다른 프로세스를 후킹할 때 dll 핸들을 받아서 사용하고, 후자는 GetCurrentThreadID()로 받아서 자신을 후킹할 때 주로 사용

LRESULT CALLBACK KeyboardProc(
int code, // hook code
WPARAM wParam, // virtual-key code
LPARAM lParam // keystroke-message information 
){ ... }

- 인자 설명

1) code: HC_ACTION(0), HC_NOREMOVE(3) , code>=0으로 해서 해당 메시지가 들어오는지 파악

2) wParam: 사용자가 누른 키보드의 virtual key code

3) lParam: 각 비트별로 다양한 의미 나타냄 (31번째 bit - key press/release 관련)

 

5. c언어에선 큰따옴표 작은따옴표 차이 존재

' ' : 문자

" " : 문자열

 

6. /MT 옵션 vs /MD 옵션

- C 런타임 라이브러리의 연결 방식과 관련된 컴파일 옵션이다

- MD 옵션으로 배포 시 'xxx.dll이 없어 코드 실행을 진행할 수 없습니다' 에러 발생하기도 함

- 런타임옵션은 메인 프로그램과 아래 라이브러리 모두 통일하자!

- 이유는, /MT 는 각 모듈별로 메모리를 할당/관리 하는 반면 /MD는 메모리관리를 공유하기 때문인데, 혼용해서 쓰게되면 다른 메모리쪽을 침범할 수 있기 때문

/MD (Multi-Threaded DLL)

  • /MD 옵션은 C 런타임 라이브러리를 별도의 DLL로 동적으로 연결(dynamic link)하는 방식
  • MSVCRT.lib가 .obj 파일에 배치되어 빌드된다. (디버그 모드의 경우 MSVCRTD.lib)
  • 여러 실행 파일이 라이브러리를 공유하여 사용할 수 있어 실행 파일 크기 및 메모리 사용량 감소
  • 실행 파일 배포 시 반드시 재배포 패키지를 함께 배포해야 함

/MT (Multi-Threaded)

  • /MT 옵션은 C 런타임 라이브러리를 실행 파일 내에 포함하여 정적으로 연결(static link)하는 방식이다.
  • LIBCMT.lib가 .obj 파일에 배치되어 빌드된다. (디버그 모드의 경우 LIBCMTD.lib)
  • 대상 시스템에 설치된 DLL에 의존하지 않는다.
  • 실행 파일의 크기가 커진다.

7. DllMain

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpvReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;
        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
            break;
        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
            break;
        case DLL_PROCESS_DETACH:
            if (lpvReserved != nullptr)
            {
                break; // do not do cleanup if process termination scenario
            }
         // Perform any necessary cleanup.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

- DLL이 가지는 진입점 함수

- DllMain은 프로세스나 쓰레드가 시작 또는 종료할 때 호출된다

- 구현 안할 시 C/C++ 런타임 라이브러리 내에 자체적으로 구현되어 있는 DllMain 함수가 사용됨

- 인자 설명

1) hModule: Dll의 인스턴스 핸들, Dll 파일 이미지가 매핑된 가상 메모리 주솟값

2) fdwReason: 4가지 값 가짐 (Process 시작&종료, Thread 시작&종료)

3) lpReserved:

fdwReason=DLL_PROCESS_ATTACH: DLL이 명시적 런타임 링크 방식으로 로드되면 NULL,  DLL이 암시적 로드타임 링크 방식으로 로드되면 not NULL

fdwReason=DLL_PROCESS_DETACH: FreeLibrary가 호출되었거나 DLL 로드가 실패한 경우라면 NULL, 프로세스가 종료중이라면 not NULL.

 

8. LRESULT CALLBACK KeybaordProc(int, WPARAM, LPARAM)

리턴 타입: LRESULT

호출 규약: CALLBACK // __stdcall, 'CALLBACK' macro is defined in the Windows Header file 

#define CALLBACK __stdcall

즉, 이 함수를 import한다고 하면 아래와 같이 function pointer를 정의하고 GetProcAddress 해야한다

typedef LRESULT (CALLBACK* KeyboardProcPtr)(int, WPARAM, LPARAM);

 

9. DLL에서 함수 export하는 법

-  DLL (동적 라이브러리)에 구현한 함수를 외부로 노출시키려면 __declspec( dllexport ) 키워드 붙여야 됨

- 해당 키워드가 붙지 않은 함수는 외부에서 호출 불가

- export 하는 함수들은  ' extern "C" {  } ' 로 둘러쌓여있어야 함

#ifdef __cplusplus
extern "C" { // 해당 함수가 C로 컴파일된 함수임을 컴파일러에게 명시적으로 알려줌
#endif
__declspec(dllexport) void HookStart() { }
__declspec(dllexport) void HookStop() { }
#ifdef __cplusplus
}
#endif

why extern "C" ??

extern "C" 라는 키워드는 C언어 형식으로 함수 이름을 짓고 extern "C" 를 제거하면 C++ 규칙대로 함수 이름을 짓는다

extern "C" 방식은 모든 컴파일러에서 알아볼수 있는 형식이므로 extern "C" 방식으로 함수를 작성하면 다른 컴퓨터에서 무리 없이 돌아가지만

extern "C"를 제거한 C++ 방식으로 함수를 작성하면 함수이름이 이상하게 변하는데 그 이상하게 변환하는 규칙이 각각 컴파일러 마다 제각각이여서 다른 컴퓨터와 호환이 되지 않는다 (Name Mangling?)