Development

InfinityHook

안녕하세요. 독자 여러분. 지난 DIFF-2019-004에서 “패치가드를 우회할 수 있는 커널후킹 라이브러리 InfinityHook”에 대한 내용을 얘기했었습니다. 이번 내용은 InfinityHook을 빌드 및 실행하는 방법과 이것을 활용하는 방법에 대해 알아보고자 합니다.

기본 개발 환경

아래와 같은 환경을 구축해서 테스트해보시길 권합니다.

Visual Studio, SDK, WDK 순으로 설치합니다.

빌드

설치를 마쳤으면, 커널 드라이버 빌드 준비가 끝났습니다.

빌드 절차는 다음과 같습니다.

  1. InfinityHook 소스코드를 다운로드 받습니다.
  2. 다운로드 받은 압축 파일을 적당한 폴더(예: C:\InfHook)에 풉니다.
  3. 프로젝트 파일(C:\InfHook\src\infinityhook.sln)을 더블클릭하여 비주얼스튜디오로 프로젝트를 오픈해주세요.
  4. Ctrl+Shift+B 를 눌러 빌드합니다. 빌드가 성공하였다면 다음 그림과 같은 빌드 메시지를 볼 수 있을 것입니다.

빌드 간단하네요. 빌드된 커널 드라이버 경로는 C:\InfHook\src\x64\Debug\kinfinityhook.sys 입니다.

테스트

빌드된 커널드라이버는 아래와 같은 후킹 코드 샘플을 포함하고 있습니다.

// NtCreateFile 후킹 함수
NTSTATUS DetourNtCreateFile(...)
{
    //
    // We're going to filter for our "magic" file name.
    //
    if (ObjectAttributes &&
        ObjectAttributes->ObjectName &&
        ObjectAttributes->ObjectName->Buffer)
    {
        ...

        //
        // 파일명이 "ifh--"을 포함한다면?
        //
        if (wcsstr(ObjectName, IfhMagicFileName))
        {
            kprintf("[+] infinityhook: Denying access to file: %wZ.\n", ObjectAttributes->ObjectName);

            ExFreePool(ObjectName);

            //
            // 접근 차단
            //
            return STATUS_ACCESS_DENIED;
        }
	}

    //
    // 원본 NtCreateFile 함수 호출
    //
    return OriginalNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
}

NtCreateFile을 후킹하여, 특정 파일명(ifh--)을 포함한 파일은 접근 차단되도록 작성되어 있음을 알 수 있습니다. 커널드라이버(kinfinityhook.sys)를 로드하여 ifh-- 파일을 정말 차단할 수 있는지 확인해 보겠습니다.

지금 빌드된 드라이버는 테스트용 인증서로 서명된 드라이버(test-signed driver)라서, 커널드라이버가 로드될 수 있도록 윈도우즈 부트 설정을 바꿔줘야 합니다. 관리자 권한의 cmd.exe 를 열고 다음 2줄을 실행합니다.

bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe /set TESTSIGNING ON

그리고 재부팅합니다. 재부팅 후, 테스트용 파일(ifh--xxxx.txt)을 바탕화면에 만들어주세요. 내용은 어떤 내용이든 상관없습니다. 메모장으로 해당 파일이 열리는 것을 확인해주세요.

이제 드라이버를 로드해보겠습니다. 관리자 권한의 cmd.exe 창을 띄우고 다음 명령을 입력해주세요. (binpath= type= 사이에 공백이 있다는 점 주의하세요.)

C:\InfHook\src\x64\Debug>sc create infhook binpath= C:\InfHook\src\x64\Debug\kinfinityhook.sys type= kernel
[SC] CreateService 성공

C:\InfHook\src\x64\Debug>sc start infhook

SERVICE_NAME: infhook
        종류               : 1  KERNEL_DRIVER
        상태               : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        검사점             : 0x0
        WAIT_HINT          : 0x0
        PID                : 0
        플래그             :

C:\InfHook\src\x64\Debug>

상태가 RUNNING으로 출력되면 infinityhook 드라이버가 제대로 로드가 된 것입니다. 지금부터는 ifh--xxxx.txt가 차단되어야 하겠죠?

ifh--xxxx.txt을 열어보면 아래 그림처럼 파일이 열리지 않는 것을 확인해볼 수 있습니다.

잘되는군요! 테스트를 마쳤으니, 다음과 같이 드라이버를 언로드하겠습니다.

C:\InfHook\src\x64\Debug>sc stop infhook

SERVICE_NAME: infhook
        종류               : 1  KERNEL_DRIVER
        상태              : 1  STOPPED
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        검사점         : 0x0
        WAIT_HINT          : 0x0

C:\InfHook\src\x64\Debug>

이제 infinityhook을 수정해서 다른 시스템콜을 후킹하는 방법을 알아보겠습니다.

네버다이 메모장

네버다이 메모장을 구현해보면서 infinityhook을 이용해 시스템콜을 후킹하는 방법을 살펴보겠습니다. 네버다이 메모장은 이름 그대로 절대 죽지 않는 메모장입니다.(스스로 종료하는 것은 허용합니다.) 이것을 구현하기 위해선 프로세스의 종료를 담당하는 nt!NtTerminateProcess 시스템콜을 후킹해야 합니다.

기존 샘플 코드에서 SyscallStub 함수 부분만 다음과 같이 수정해주면 시스템콜 후킹을 추가로 구현해볼 수 있습니다.

void __fastcall SyscallStub(_In_ unsigned int SystemCallIndex, _Inout_ void** SystemCallFunction) {
    if (SystemCallIndex == /*후킹하길 원하는 시스템콜 인덱스*/) {
        *SystemCallFunction = /*후킹 함수*/;
    }
}

후킹하려는 시스템콜의 서비스테이블 인덱스을 알아봐야 하겠죠? 윈도우즈 시스템 콜 인덱스 리스트는 Google P0의 j00ru가 여기에 잘 정리해놓았습니다.

NtTerminateProcess의 인덱스는 0x2c 군요. SyscallStub 함수는 다음과 같은 형태가 될 것입니다.

void __fastcall SyscallStub(_In_ unsigned int SystemCallIndex, _Inout_ void** SystemCallFunction) {
    if (SystemCallIndex == 0x2c/*NtTerminateProcess 인덱스*/) {
        // 원본 NtTerminateProcess 주소를 백업
        if (OrigNtTerminateProcess == NULL)
            OrigNtTerminateProcess = (NtTerminateProcess_t)*SystemCallFunction;

        // NtTerminateProcess 함수 콜을 후킹 함수(DetourNtTerminateProcess)로 바꿔준다.
        *SystemCallFunction = DetourNtTerminateProcess;
    }

그럼, 소스코드(C:\InfHook\src\kinfinityhook\entry.cpp) 전체를 다음과 같이 재작성해주세요. 코드설명은 주석으로 대신하겠습니다.

#include "stdafx.h"
#include "entry.h"
#include "infinityhook.h"

typedef NTSTATUS (NTAPI *NtTerminateProcess_t)(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus);
static NtTerminateProcess_t OrigNtTerminateProcess = NULL;
NTSTATUS DetourNtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus);

typedef PCHAR (*PsGetProcessImageFileName_t)(IN PEPROCESS);
static PsGetProcessImageFileName_t PsGetProcessImageFileName = NULL;

// 드라이버 메인
extern "C" 
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    kprintf("[+] infinityhook: Loaded.\n");

    // 드라이버 언로드 핸들러 등록
    DriverObject->DriverUnload = DriverUnload;

    // 후킹 시작(시스템콜이 발생하면, SyscallStub 함수로 제어권이 넘어감)
    NTSTATUS Status = IfhInitialize(SyscallStub);
    if (!NT_SUCCESS(Status)) kprintf("[-] infinityhook: Failed to initialize with status: 0x%lx.\n", Status);

    return Status;
}

// 드라이버 언로드할 때
void DriverUnload(_In_ PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);

    // 후킹 종료
    IfhRelease();

    kprintf("\n[!] infinityhook: Unloading... BYE!\n");
}

void __fastcall SyscallStub(_In_ unsigned int SystemCallIndex, _Inout_ void** SystemCallFunction) {
    if (SystemCallIndex == 0x2c/*NtTerminateProcess 인덱스*/) {
        // 원본 NtTerminateProcess 주소를 백업
        if (OrigNtTerminateProcess == NULL)
            OrigNtTerminateProcess = (NtTerminateProcess_t)*SystemCallFunction;

        // NtTerminateProcess 함수 콜을 후킹 함수(DetourNtTerminateProcess)로 바꿔준다.
        *SystemCallFunction = DetourNtTerminateProcess; 
    }
}

// NtTerminateProcess 후킹 함수
NTSTATUS DetourNtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus) {

    kprintf("HookNtTerminateProcess Entry\n");

    // 원본 NtTerminateProcess 호출
    return OrigNtTerminateProcess(ProcessHandle, ExitStatus);
}

프로세스가 종료될 때(NtTerminateProcess가 호출될 때), DetourNtTerminateProcess 후킹 함수가 제어권을 가로챌 수 있도록 작성된 코드입니다. 지금은 커널로그만 출력하고 어떤 행위도 하지 않도록 되어 있죠.

후킹이 제대로 되는지 확인해 볼까요? 우선 DbgView를 관리자 권한으로 실행하고 Ctrl+K를 눌러 커널 로그를 덤프뜰 수 있도록 해줍니다. 다음, 새로 작성한 코드를 빌드해봅시다. 비주얼스튜디오에서 Ctrl+Shift+B를 눌러주세요. 빌드 완료 후, 관리자 cmd 에서 net start infhook 을 실행하여 새로 작성한 커널드라이버를 다시 로드해줍니다. 프로세스를 몇 개 종료해보면, 아래와 같이 DbgView에 로그메시지가 제대로 출력되는 것을 볼 수 있습니다.

제대로 후킹이 걸려진 것 같군요. 걸려진 것을 확인했으니 net stop infhook 명령으로 커널 드라이버를 언로드시켜주세요.

이제 후킹 함수(DetourNtTerminateProcess)를 다음과 같이 재작성 해보겠습니다. 소스코드 설명은 주석으로 대신하겠습니다.

// NtTerminateProcess 후킹 함수
NTSTATUS DetourNtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus) {

    NTSTATUS  rtStatus = STATUS_SUCCESS;
    PEPROCESS pEProcess = NULL;
    PCHAR pStrProcName = NULL;

    // 프로세스가 스스로 종료되는 것은 허용한다.
    if (NtCurrentProcess() == ProcessHandle) 
        return OrigNtTerminateProcess(ProcessHandle, ExitStatus);

    // 프로세스 핸들로부터 EPROCESS 구조체의 주소를 구한다.
    rtStatus = ObReferenceObjectByHandle(ProcessHandle, FILE_READ_DATA, NULL, KernelMode, (PVOID*)&pEProcess, NULL);
    if (!NT_SUCCESS(rtStatus) || pEProcess == NULL)
        return OrigNtTerminateProcess(ProcessHandle, ExitStatus);

    // EPROCESS 로부터 프로세스명을 구한다.
    if (PsGetProcessImageFileName == NULL) {
        UNICODE_STRING StringPsGetProcessImageFileName = RTL_CONSTANT_STRING(L"PsGetProcessImageFileName");
        PsGetProcessImageFileName = (PsGetProcessImageFileName_t)MmGetSystemRoutineAddress(&StringPsGetProcessImageFileName);
    }

    pStrProcName = (PCHAR)PsGetProcessImageFileName(pEProcess);
    bool block = pStrProcName != NULL && !strcmp(pStrProcName, "notepad.exe");

    // EPROCESS 구조체 참조 해제
    ObReferenceObject(pEProcess);

    if (block)	// 메모장(notepad.exe)이 종료되는 것을 막는다.
        return STATUS_SUCCESS;

    // 원본 NtTerminateProcess 호출
    return OrigNtTerminateProcess(ProcessHandle, ExitStatus);
}

코드를 다시 빌드(Ctrl+Shif+B)하고, net start infhook 으로 드라이버를 로드해보세요.

다음처럼 불멸의 메모장을 보실 수 있습니다!

참고자료