Security - Real World/Reversing

Windows Kernel Hooking - II-1

PS리버싱마크해커박종휘TV 2024. 1. 10. 16:16

이전 글을 보고 오는 것이 좋다.

 

Preface

 

전에 글을 비밀번호를 입력하면 입장할 수 있게 설정한다고 선언했지만, 이 부분까지는 그렇게 가릴 게 못 돼서 그냥 공개로 전환하고 발행한다.

 

이번에 할 것은, 커널 모드와 유저 모드의 상호작용을 구현하는 것이다.

 

아니 커널 모드만 하면 되지 왜 굳이 고생을 더 하여 유저 모드 애플리케이션을 제작하는 것인가요? 유저 모드 애플리케이션을 만들면 오히려 발각 확률을 높이는 것이 아닌가요?

 

물론 커널 모드의 개발만 가지고 프로그램을 구현할 수 있지만, 사용자에게 보여지는 인터페이스 혹은 사용자와의 상호작용의 구현은 커널 모드보다 유저 모드에서가 훨씬 수월하다. 따라서 커널 모드를 단독으로 개발하는 것보다 유저 모드를 병행하여 하이브리드 식으로 개발하는 것이 바람직하다.

 

그리고 딱히 발각 확률을 높이지 않는다. 나의 드라이버랑 유저 모드 애플리케이션이 직접적으로 상호작용하는 것이 아니기에 잡힐 확률을 그다지 높이지 않는다. 이게 무슨 말인지는 곧 설명하겠다.

 

Calling Kernel Function in User Mode?

 

가능하지 않다. 이게 위의 말한 이유가 된다. 우리가 만든 커널 드라이버의 함수를 직접적으로 호출하는 것은 시스템 상으로 불가능하다. Ring 0과 Ring 3은 접근 권한이 매우 제한되어 있어, 유저 모드에서 커널 모드로 접근하는 것 자체가 불가능하다. 그러면 어떻게 상호작용 할까?

 

정답은 후킹이다. 이 대제목이 Windows Kernel Hooking인 것도 이 때문이다. 전에 소개한 NtOpenProcess / OpenProcess에서도, 유저모드에서 OpenProcess를 호출하면 커널 모드의 NtOpenProcess가 호출되는데, 여기서 조건을 걸어서 만약 조건에 부합하면 특정 코드를 실행하고 반환하는 코드를 짤 수 있다. 이때 이 함수를 정하는 것이 중요한데 나는 다음과 같은 기준을 정하여 하나의 함수를 선정했다.

 

  • 윈도우 내부, 혹은 응용 프로그램에서 잘 쓰이지 않는 함수
  • 인자가 간단한 함수
  • 커널 모드 함수와 유저 모드 함수가 일대일 대응이 되는 함수

 

이 조건을 만족하는 dxgkrnl.sys!NtQueryCompositionSurfaceStatistics (커널) / win32u.dll! NtQueryCompositionSurfaceStatistics 함수를 쓰기로 하였다.

 

우선 NtQueryCompositionSurfaceStatistics는 공식 문서에 정리되어 있지 않기에, 직접 분석해야 했다. 우선 dxgkrnl.sys의 함수를 보았다.

 

Figure II. dxgkrnl.sys' NtQueryCompositionSurfaceStatistics

 

보다시피 인자는 2개이다. 우선 a1를 보자. a1을 가지고 ResolveHandle를 호출하는데 이 함수 내에서 또 a1를 가지고 ObReferenceObjectByHandle를 호출한다. 이때 공식 문서를 보면, a1이 HANDLE이라는 것을 알 수 있다.

Stats는 NTSTATUS라고 추론할 수 있다.

a2는 일종의 포인터라고 볼 수 있다. QueryStats를 한 뒤의 정보를 저장하는 것 같다.

 

이제 win32u.dll의 함수를 보자.

 

Figure I. win32u.dll's NtQueryCompositionSurfaceStatistics

 

간단한 syscall이다. 우리는 HANDLE과 반환값을 저장할 포인터가 필요한데, 반환값은 필요없으니 그냥 주지 않아도 된다.

 

따라서 우리는 커널 모드의 NtQueryCompositionSurfaceStatistics에 훅을 걸고,

 

NTSTATUS(*origNQCSS)(HANDLE handle, unsigned long long a2);

 

유저 모드에서 NtQueryCompositionSurfaceStatistics를 호출하면,

 

NTSTATUS(*pfnNQCSS)(HANDLE handle);

 

우리가 커널 모드에 설정한 훅에 성공적으로 도달한다.

 

Implementation

 

여기까지만 봐서는 우리가 여태까지 한 함수 후킹과 다를 바가 없다. 우리는, 유저 모드와 커널 모드의 상호작용을 구현할 것이다.

 

Imp - Kernel

 

우선 후킹하기 위해 dxgkrnl.sys의 NtQueryCompositionSurfaceStatistics를 Export한다.

 

function = reinterpret_cast<PVOID*>(KeGetSystemModuleExport(
	"\\SystemRoot\\System32\\drivers\\dxgkrnl.sys",
	"NtQueryCompositionSurfaceStatistics"));
origNQCSS = (NTSTATUS(*)(HANDLE _1, unsigned long long _2))function;

 

그리고 우리는 handle에 원하는 포인터를 넣을 것이므로, 이에 대한 구조체를 정의한다.

 

#define MAGIC 0x57616E654C6F6C69

typedef enum _USERMODE_COMMAND_MODE {
	Read,
	Write
} USERMODE_COMMAND_MODE;

typedef struct _USERMODE_COMMAND {
	unsigned long long magic = MAGIC;
	USERMODE_COMMAND_MODE mode;
	void* arg;
	void* retrn;
} USERMODE_COMMAND, * PUSERMODE_COMMAND;

typedef struct _READ_CMD_RETURN {
	int code;
} READ_CMD_RETURN, * PREAD_CMD_RETURN;

 

magic은 우리가 상호작용을 위해 호출한 것인지, 일반적으로 다른 프로그램에서 호출한 것인지 판단한다.

mode는 추후에 있을 Read, Write, Patch, ... 등 Instruction을 대변한다.

arg는 mode에 따른 인자들을 넣는 포인터이다.

retrn은 반환값을 저장할 포인터이다.

 

이 규칙에 따라 훅을 작성하자.

 

NTSTATUS HookFunction(
	HANDLE handle,
	unsigned long long a2
) {
	
	DbgPrintEx(0, 0, "NtQueryCompositionSurfaceStatistics called!!!!!");
	auto cvrt = (PUSERMODE_COMMAND)handle;

	if (!MmIsAddressValid(handle) || cvrt->magic != MAGIC) {
		WriteMemoryRO(function, &original, sizeof(original));
		auto ret = origNQCSS(handle, a2);
		WriteMemoryRO(function, &payload, sizeof(payload));
		return ret;
	}
	
	DbgPrintEx(0, 0, "It is MY FUCTION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
	
	if (cvrt->mode == Read) { // Todo: make this to function
		PREAD_CMD_RETURN retrn = (PREAD_CMD_RETURN)cvrt->retrn;
		retrn->code = 6974;
	}
	else if (cvrt->mode == Write) {
		// todo: add write function
	}

	return STATUS_SUCCESS;
}

 

이렇게 하면 커널 모드에서의 핸들링은 끝났다.

 

Imp - User

 

우선 호출하기 위해 win32u.dll을 로드하고 NtQueryCompositionSurfaceStatistics를 Export한다.

 

auto libwin32u = LoadLibrary(L"win32u.dll");
auto func = GetProcAddress(libwin32u, "NtQueryCompositionSurfaceStatistics");
pfnNQCSS = (NTSTATUS(*)(HANDLE _1))func;

 

그리고, 구조체에 담아 호출한다.

 

READ_CMD_RETURN rcr = { 0 };
USERMODE_COMMAND ucmd = { MAGIC, Read, nullptr, &rcr };
if (STATUS_SUCCESS == pfnNQCSS((HANDLE)&ucmd)) {
	std::cout << (((PREAD_CMD_RETURN)(ucmd.retrn))->code) << '\n';
}

 

잘 나오는 것을 볼 수 있다.

 

Figure III. It works!

 

이제 커널 모드에서 핸들링하는 것을 구체화하면 되는데, 이는 다음 편에 다루어 보겠다.

'Security - Real World > Reversing' 카테고리의 다른 글

Windows Kernel Hooking - II  (1) 2024.01.08
Windows Kernel Hooking - I-1  (1) 2024.01.06
Windows Kernel Hooking - I  (2) 2024.01.04