security/리버싱핵심원리

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

민사민서 2023. 3. 15. 23:33

챕터 마지막의 질문글을 보고

SetWindowsHookEx()를 KeyHook.dll이 아니라 HookMain.exe에서 호출해보면 어떨까 싶어서 짜보았다.

 

[계획]

SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0); 를 dll이 아닌 HookMain.exe에서 호출하여 훅 체인에 새로운 훅 프로시져를 등록하기

1) SetWindowsHookEx() 함수와 WH_KEYBOARD 상수는 "windows.h" 헤더에 정의되어있음.

=> OK

2) dll에 정의된 KeyboardProc 함수 포인터(HOOKPROC 타입)를 받아와야 함

=> export 함수 GetHookProc() 정의

__declspec(dllexport) HOOKPROC GetHookProc() {
	return (HOOKPROC) KeyboardProc;
}

3) 훅 프로시져가 정의된 dll 핸들(HINSTANCE 타입)을 받아와야 함

=> export 함수 GetDllHandle() 정의

__declspec(dllexport) HINSTANCE GetDllHandle() {
	return g_hInstance;
}

4) SetWindowsHookEx() 리턴값을 dll의 g_hHook 변수에 업데이트시켜야 함

// KeyboardProc()에서 return CallNextHookEx(g_hHook, nCode, wParam, lParam); 에서 쓰이므로

=> export 함수 SetHookHandle() 정의

__declspec(dllexport) void SetHookHandle(HHOOK hHook) {
    g_hHook = hHook;
}

 

그래서 KeyHook2.dll 코드는 export 함수만 바뀐채로 아래와 같이 구현하면 됨

// KeyHook2.dll

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

#define DEF_PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
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) {
		if(!(lParam&0x80000000)) {
			GetModuleFileNameA(NULL, szPath, MAX_PATH);
			p = strrchr(szPath, '\\');

			if(!_stricmp(p+1, DEF_PROCESS_NAME))
				return 1;
		}
	}
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus 
extern "C" {
#endif
__declspec(dllexport) HOOKPROC GetHookProc() {
	return (HOOKPROC) KeyboardProc;
}
__declspec(dllexport) HINSTANCE GetDllHandle() {
	return g_hInstance;
}
__declspec(dllexport) void SetHookHandle(HHOOK hHook) {
    g_hHook = hHook;
}
#ifdef __cplusplus
}
#endif

HookMain2.exe에서는 export 함수 3개를 받아와서

HookMain.exe의 HookStart() & HookStop() 함수가 하던 작업을 main 함수에서 하면 됨

// HookMain2.exe

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

#define DEF_DLL_NAME "KeyHook2.dll"
#define DEF_HDLL_NAME "GetDllHandle"
#define DEF_HHOOK_NAME "SetHookHandle"
#define DEF_HPROC_NAME "GetHookProc"

typedef HINSTANCE(*PFN_GETDLLHANDLE)();
typedef void(*PFN_SETHOOKHANDLE)(HHOOK);
typedef HOOKPROC(*PFN_GETHOOKPROC)();

void main() {
	HMODULE hDll = NULL;
	HHOOK g_hHook = NULL;
	PFN_GETDLLHANDLE GetDllHandle = NULL;
	PFN_SETHOOKHANDLE SetHookHandle = NULL;
	PFN_GETHOOKPROC GetHookProc = NULL;

	hDll = LoadLibraryA(DEF_DLL_NAME);

	GetDllHandle = (PFN_GETDLLHANDLE)GetProcAddress(hDll, DEF_HDLL_NAME);
	SetHookHandle = (PFN_SETHOOKHANDLE)GetProcAddress(hDll, DEF_HHOOK_NAME);
	GetHookProc = (PFN_GETHOOKPROC)GetProcAddress(hDll, DEF_HPROC_NAME);
	
	// SetWindowsHookEx() 통해 키보드 훅 체인에 KeyBoardProc() 추가
	// GetDllHandle() 대신 hDll 넣어도 됨
	g_hHook = SetWindowsHookEx(WH_KEYBOARD, GetHookProc(), GetDllHandle(), 0);

	if (g_hHook == NULL) {
		DWORD dwError = GetLastError();
		printf("Failed to set hook, error code: %d\n", dwError);
	} else {
		printf("Hook successfully set!\n");
	}
	// 생략해도 잘 동작
	SetHookHandle(g_hHook);

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

	// 후킹 종료
	if(g_hHook) {
		UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}

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

 

 

1.  콜백함수를 directly export 했을 때 실패한 이유

// in KeyHook2.dll
__declspec(dllexport) LRESULT CALLBACK KeyboardProc(int code,WPARAM wParam,LPARAM lParam) {
	// function body
}

// in HookMain2.exe
#define DFN_PROC_NAME "KeyboardProc"
typedef LRESULT(CALLBACK *PFN_KEYBOARDPROC)(int, WPARAM, LPARAM);

int main() {
	~~~
    PFN_KEYBOARDPROC KeyboardProc = (PFN_KEYBOARDPROC)GetProcAddress(hDll, DFN_PROC_NAME);
    g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, getDllHandle(), 0);
    ~~~
}

[문제점]

- HookMain2.exe를 실행해도 notepad.exe에 키보드 입력이 정상 반영됨

- Process Explorer로 find DLL 해서 모니터링 해보니까 KeyHook2.dll이 HookMain2.exe에만 로드되어있음

- 즉 WH_KEYBOARD 후크에 대한 훅 프로시져가 제대로 등록되지 않아 dll Injection이 안일어난다고 유추 가능

- SetWindowsHookEx()의 세번째 인자에 문제가 생겼다고 결론 내림 (실제로 NULL 들어감)

- GetProcAddress(hDll, "KeyboardProc")에서 해당 이름을 가진 export function을 발견하지 못한 것이다.

 

[왜 안되는가?]

- 컴파일러가 C++ export function들에게 부여한 actual names들을 Dependency Walker로 확인해보자

KeyHook2.dll
KeyHook3.dll

- extern "C" 덕분에 C++ compiler가 C-style function name mangling 을 사용하여 컴파일을 하였다

- GetDllHandle, SetHookHandle, GetHookProc은 내부적으로 저장되는 이름이 그대로인 반면

- KeyboardProc은 '_KeyboardProc@12'로 내부적으로 저장된다 (@12 suffix: 함수 인자의 total bytes)

// C naming에서 stdcall 함수들(CALLBACK도 여기 포함)은 네이밍 방식이 좀 다름

 

[해결 방법]

#define DEF_PROC_NAME "_KeyboardProc@12"

- GetProcAddress의 두번째 인자(찾고자 하는 함수의 이름) 재설정한다.

1-1. dll export시 extern "C" 있고 없고의 차이

// KeyHook3.dll
extern "C" __declspec(dllexport) LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
	// body
}
extern "C" __declspec(dllexport) HINSTANCE GetDllHandle() {
	return g_hInstance;
}
extern "C" __declspec(dllexport) void SetHookHandle(HHOOK hHook) {
    g_hHook = hHook;
}

Dependency Walker로 분석한 결과, C name decoration

=> name decorate/undecorate 결과가 동일하다

// KeyHook3.dll
__declspec(dllexport) LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
	// body
}
__declspec(dllexport) HINSTANCE GetDllHandle() {
	return g_hInstance;
}
__declspec(dllexport) void SetHookHandle(HHOOK hHook) {
    g_hHook = hHook;
}

Dependency Walker로 분석한 결과, C++ name decoration
Undecorate C++ function 한 결과

=> name decorate/undecorate 결과가 달라진다

=> export하는 함수들은 C name decoration을 거치도록 해야한다 (추후 이름으로 주소 가져오려면)

1-2. Name Mangling

- 프로그램에서 함수를 선언하거나 전역 변수 등의 선언 했을때, 실제 생성된 함수는 컴파일러의 특징에 따라서 일정한 규칙을 통하여 함수, 변수명이 변경 된다

- Name Mangling을 적용 / 미 적용 방법 : extern "C"를 붙이게 되면 C Type으로 함수명을 Naming을 하겠다는 선언이다. C Type의 Naming은 코드 상에서 사용하는 함수명 그대로 사용하는 것을 의미 

Name Mangling 미적용    extern "C" __declspec(dllexport) void h(void)   void h()
Name Mangling 적용   __declspec(dllexport)   ?h@@YAXXZ

* 단 C Naming을 이용해도 이름이 약간 달라지기도 함 (ex. 매개변수 복잡한 KeyboardProc() => _KeyboardProc@12 )

 

why?

- 기존 C 스타일 이름의 함수로는 C++에서 함수 오버로딩(다른 매개 변수, 리턴타입 등을 가진 같은 이름의 여러 함수를 만들 수 있음)을 지원할 수 없다

- 따라서 함수 이름에 대한 decoration/mangling을 한다 (컴파일러마다 다른 mangling 규칙 가짐)

https://spikez.tistory.com/19

 

C++ 상에서 발생하는 name mangling 에 관한 내용

C++상에서 발생하는 name mangling에 관한 내용 1. name mangling 이란? 간단히 말하면 compiler 가 임의로 함수나 변수의 이름을 변경하는 것을 의미합니다. 그렇다면 왜 함수나 변수의 이름을 변경하는 것

spikez.tistory.com

 

cf) C에서의 name decoration(매우 단순)

* C언어 (디폴트) 네임 맹글링 = 콜링 컨벤션에 따른 차이밖에 없음

 - __cdecl

디폴트 콜링 컨벤션이다. 함수 앞에 '_'만 추가 된다.

   ex> void TestFunc(int a)         <->     _TestFunc

 - stdcall

 ; 주로 윈도우즈에서 사용되는 형식이다. (매크로 'WINAPI', 'CALLBACK'이 바로 __stdcall을

   #define 한 것이다) 함수 이름앞에 '_'를 붙이고 마지막에 @과 인자의 바이트 합 숫자를 붙인다.

   ex> void TestFunc(int a, char *b)    <->   _TestFunc@8    //int 4바이트, char 포인터형 4바이트

 

 

2. 에러 처리 루틴

if (g_hHook == NULL) {
	DWORD dwError = GetLastError();
	printf("Failed to set hook, error code: %d\n", dwError);
} else {
	printf("Hook successfully set!\n");
}

1번에서 훅 프로시져가 문제인 것을 해당 에러처리 루틴을 통해 확신함. 코드 실행 결과 아래와 같음

SetWindowsHookEx Call Failed, Error number: 1427

=> ERROR_INVALID_FILTER_PROC, Invalid hook procedure.

 

(추가 - Ollydbg 분석)

Ollydbg로 분석해보니 SetWindowsHookEx()의 세번째 인자가 NULL로 셋팅되어있길래

GetProcAddress 호출 직후 EAX=0, 당연히 SetWindowsHookEx 세번째 인자도 NULL

KeyboardProc = (PFN_KEYBOARDPROC)GetProcAddress(hDll, DEF_PROC_NAME); 이 부분에도 에러처리 추가

if (KeyboardProc == NULL) {
    DWORD error = GetLastError();
    printf("Failed to get function address: %d\n", error);
}

코드 실행 결과

GetProcAddress Call Failed, Error number: 127

=> ERROR_PROC_NOT_FOUND, The specified procedure could not be found.

 

3. other ways

- SetWindowsHookEx()의 세 번째 인자로 getDllHandle() 대신 HookMain2.exe에 LoadLibraryA()에 의해 로드된 KeyHook2.dll의 핸들 hDll을 넣어도 잘 동작

- SetHookHandle(g_hHook) 생략해도 잘 동작

- KeyboardProc 후킹 프로시져를 직접 export 해줘도 됨

3-1. KeyHook2.dll에서 g_hHook 업데이트 안해줘도 잘 동작하네?

- SetWindowsHookEx() (여기 참고)

https://yokang90.tistory.com/48

 

- SetWindowsHookEx Function

# Windows 메시지 후킹 부분을 공부하다가 보게 된 Windows API 함수이다. 공부하면서 모르는 함수 나올때 마다 블로깅하면서 정리해두면 나중에 도움이 될 것 같다. # Parameters (1) idHook- Type : int- Descript

yokang90.tistory.com

리턴 타입: HHOOK

리턴 값: 함수 호출 성공 시 hook procedure handle 리턴, 실패 시 NULL 리턴

(실패한 경우 GetLastError 함수를 통해 에러 정보를 확인할 수 있다)

 

- CallNextHookEx()

LRESULT CallNextHookEx(
  [in, optional] HHOOK  hhk,
  [in]           int    nCode,
  [in]           WPARAM wParam,
  [in]           LPARAM lParam
);

Passes the hook information to the next hook procedure in the current hook chain.

A hook procedure can call this function either before or after processing the hook information.

첫번째 인자 hhk: This parameter is ignored (!!), optional

=> 첫 번째 인자 무시되고 (int nCode, WPARAM wParam, LPARAM lParam) 인자로 하여 훅 체인의 다음 프로시져 호출

=> KeyHook2.dll의 g_hHook 변수 굳이 업데이트 할 필요 없네

 

4. HINSTANCE vs HMODULE

// HookMain.exe
HMODULE hDll = LoadLibraryA("KeyHook.dll");

// KeyHook.dll
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpvReserved) {
	// body
}

16비트 윈도우 환경에서 HMODULE과 HINSTANCE는

1. HMODULE : 코드 영역에 대한 처리

2. HINSTANCE : 데이터 영역에 대한 처리

하지만 32bit 윈도우는 프로세스별로 메모리 공간이 독립적으로 할당되기 때문에 코드영역, 데이터영역 모두 독립된 프로세스 메모리 영역에 할당됩니다. 따라서 HMODULE과 HINSTANCE 모듈의 구분이 의미가 없어졌죠.

32bit 환경에서는 HMODULE == HINSTANCE 같은 의미