Operating System/이해하면 인생이 바뀌는 Windows API hook

Inline hook

Tony Lim 2024. 12. 2. 18:20

inline hook 기술 개요 및 작동 원리

  • 함수의 진입점(function prologue)에 jmp code(0xE9)를 삽입해서 흐름을 변경하는 방법
  • 32bit 시스템에서 jmp 코드 피연산자는 점프할 위치에 대한 절대값이 아니라 현재 명령어가 저장된 위치를 기준으로 떨어진 거리를 상대적으로 계산한다.
  • jmp code는 명령어와 피연산자를 포함 모두 5바이트 (x86기준)


inline hook 사용시 주의사항

  • jmp 코드로 흐름을 조작하더라도 정상적인 호출관계에 의한 stack frame 구조를 손상시키지 말아야한다.
    (function prologue , epilogue가 정상적으로 동작해야함)
  • 조작된 흐름이 끝나고 본래 위치로 되돌아 가는 과정에서 문제가 발생하지 않도록 강제화 해야한다.

 


32bit inline hook

HANDLE MyOpenProcess(
    DWORD dwDesiredAccess, BOOL  bInheritHandle, DWORD dwProcessId)
{
    unhookOpenProcess();

    HANDLE(WINAPI * pfOpenProcess)(DWORD, BOOL, DWORD) = NULL;
    pfOpenProcess = (HANDLE(WINAPI*)(DWORD, BOOL, DWORD))g_pOpenProcess;

    dwDesiredAccess |= PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
    HANDLE hProcess = pfOpenProcess(dwDesiredAccess,
        bInheritHandle, dwProcessId);

    TCHAR name[MAX_PATH + _MAX_FNAME] = { 0 };
    ::GetModuleFileNameEx(hProcess, NULL, name, size(name));
    //_putws(name);
    if (wcsstr(name, L"Notepad.exe") != NULL)
    {
        cout << "\tMyOpenProcess()" << endl;
        cout << "\t************************************" << endl;
        cout << "\t***** Block notepad process!!! *****" << endl;
        cout << "\t************************************" << endl;

        hookOpenProcess();
        return NULL;
    }

    hookOpenProcess();
    return hProcess;
}

기존의 OpenProcess signature 랑 똑같이 가져간다.

내부에서 pfOPenProcess 를 호출하기 때문에 unhookOpenProcess를 시작할 때 호출해준다.
안그러면 자기자신을 호출하는 경우가 되기 때문이다.

unhook 에서 hook까지는 본래의 openprocess가 유지되는것이다. 이 찰나의 시간에 다른 스레드에서 openProcess를 호출하면 api hook이 동작하지 않게 되는 것이다.
이때 보통 lock을 사용하기 때문에 성능이 감소하게 된다.

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

using namespace std;

#pragma pack(push, 1)
typedef struct JUMP_CODE {
    BYTE opCode;
    LPVOID targetAddr;
} JUMP_CODE;
#pragma pack(pop)


void* g_pOpenProcess;
BYTE g_codesBeforeHook[5];

BOOL hookOpenProcess(void);
void unhookOpenProcess(void);


HANDLE MyOpenProcess(
    DWORD dwDesiredAccess, BOOL  bInheritHandle, DWORD dwProcessId)
{
    unhookOpenProcess();

    HANDLE(WINAPI * pfOpenProcess)(DWORD, BOOL, DWORD) = NULL;
    pfOpenProcess = (HANDLE(WINAPI*)(DWORD, BOOL, DWORD))g_pOpenProcess;

    dwDesiredAccess |= PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
    HANDLE hProcess = pfOpenProcess(dwDesiredAccess,
        bInheritHandle, dwProcessId);

    TCHAR name[MAX_PATH + _MAX_FNAME] = { 0 };
    ::GetModuleFileNameEx(hProcess, NULL, name, size(name));
    //_putws(name);
    if (wcsstr(name, L"Notepad.exe") != NULL)
    {
        cout << "\tMyOpenProcess()" << endl;
        cout << "\t************************************" << endl;
        cout << "\t***** Block notepad process!!! *****" << endl;
        cout << "\t************************************" << endl;

        hookOpenProcess();
        return NULL;
    }

    hookOpenProcess();
    return hProcess;
}


void printProcessList(void)
{
    HMODULE hK32 = ::LoadLibrary(L"Kernel32.dll");
    HANDLE(WINAPI * pfOpenProcess)(DWORD, BOOL, DWORD) = NULL;

    pfOpenProcess = (HANDLE(WINAPI*)(DWORD, BOOL, DWORD))
        ::GetProcAddress(hK32, "OpenProcess");

    DWORD aPid[1024] = { 0 };
    DWORD dwNeeded = 0;
    if (::EnumProcesses(aPid, sizeof(aPid), &dwNeeded))
    {
        DWORD count = dwNeeded / sizeof(DWORD);
        HANDLE hProcess;
        for (DWORD i = 0; i <= count; ++i)
        {
            hProcess = pfOpenProcess(
                PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
                FALSE, aPid[i]);
            if (hProcess != NULL)
            {
                TCHAR name[MAX_PATH + _MAX_FNAME] = { 0 };
                ::GetModuleFileNameEx(hProcess, NULL, name, sizeof(name));
                ::wprintf(L"%s [PID: %d]\n", name, aPid[i]);

                ::CloseHandle(hProcess);
            }
            else
                ::wprintf(L"ERROR [PID: %d]\n", aPid[i]);
        }
    }

    ::FreeLibrary(hK32);
}



BOOL hookOpenProcess(void)
{
    HMODULE hK32 = ::GetModuleHandle(L"Kernel32.dll");
    HANDLE(WINAPI * pfOpenProcess)(DWORD, BOOL, DWORD) = NULL;

    pfOpenProcess = (HANDLE(WINAPI*)(DWORD, BOOL, DWORD))
        ::GetProcAddress(hK32, "OpenProcess");
    g_pOpenProcess = pfOpenProcess;

    DWORD dwOldProtect = 0;
    BOOL bResult = ::VirtualProtect(
        (LPVOID)pfOpenProcess, 5,
        PAGE_EXECUTE_READWRITE, &dwOldProtect);

    memcpy(g_codesBeforeHook, pfOpenProcess, 5);

    //목적지주소 - 현재명령어주소 - 5
    //5(Byte)는 JMP, CALL 명령어의 크기 만큼 빼주는 것.
    JUMP_CODE jmpCode = { 0 };
    jmpCode.opCode = 0xE9;  //jmp
    jmpCode.targetAddr =
        (void*)((DWORD)MyOpenProcess - (DWORD)pfOpenProcess - 5);

    memcpy(pfOpenProcess, &jmpCode, 5);
    //::FlushInstructionCache(::GetCurrentProcess(), g_pOpenProcess, 5);

    return TRUE;
}

void unhookOpenProcess(void)
{
    if (g_pOpenProcess == NULL)
        return;

    memcpy(g_pOpenProcess, g_codesBeforeHook, 5);
    //::FlushInstructionCache(::GetCurrentProcess(), g_pOpenProcess, 5);
}


//32bit inline hook
int main()
{
    setlocale(LC_ALL, "");

    hookOpenProcess();
    printProcessList();

    return 0;
}

https://www.inflearn.com/community/questions/1454158/32bit-inline-hook-release-debug-x86-%EC%A7%88%EB%AC%B8#337622

 

32bit inline hook (release/ debug x8... - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

 


32bit inline hook 기반 함수 대체

IAT에서 메모리를 기존 A함수 에서 B함수로 바꾸면 A호출 시 B가 실행되게 하듯이 inline hook에서도 가능하다.

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

using namespace std;

#pragma pack(push, 1)
typedef struct JUMP_CODE {
    BYTE opCode;
    LPVOID targetAddr;
} JUMP_CODE;
#pragma pack(pop)


BYTE g_codesBeforeHook[5];

//최적화 하지 않음


int targetFunc(int a, int b)
{
    puts("targetFunc() - Begin");
    int result = a + b;
    puts("targetFunc() - End");
    return result;
}

int newTargetFunc(int a, int b)
{
    puts("newTargetFunc() - Begin");
    int result = a * b;
    puts("newTargetFunc() - End");
    return result;
}

__declspec(naked) int replacedTargetFunc(int a, int b)
{
    __asm
    {
        push        ebp
        mov         ebp, esp
        push        ecx
    }

    int result;
    result = newTargetFunc(a, b);

    __asm
    {
        mov         eax, dword ptr[result]
        mov         esp, ebp
        pop         ebp
        ret
    }
}

void hookTargetFunc(void *pfFunc)
{
    DWORD dwOldProtect = 0;
    BOOL bResult = ::VirtualProtect(
        (LPVOID)targetFunc, 5,
        PAGE_EXECUTE_READWRITE, &dwOldProtect);

    memcpy(g_codesBeforeHook, targetFunc, 5);

    JUMP_CODE jmpCode = { 0 };
    jmpCode.opCode = 0xE9;  //jmp
    jmpCode.targetAddr =
        (void*)((DWORD)pfFunc - (DWORD)targetFunc - 5);

    memcpy(targetFunc, &jmpCode, 5);
}

void unhookTargetFunc(void)
{
    memcpy(targetFunc, g_codesBeforeHook, 5);
}

//32bit inline hook
int main()
{
    printf("Return: %d (Before hook)\n\n", targetFunc(3, 4));

    hookTargetFunc(replacedTargetFunc);
    printf("Return: %d (After hook)\n\n", targetFunc(3, 4));
    unhookTargetFunc();

    printf("Return: %d (main() - End)\n\n", targetFunc(3, 4));
    return 0;
}

 jmp를 통해서 targetFunc 호출시 naked 함수로 가게 한다.

이후 naked 함수에서는 function prologue,epilouge 를 만들어 놓고 그사이에 newTargetFunc를 호출하게 하는 방식이다.

 function signature가 같을 필요가 없어서 좀 더 자유롭다.


inlineHook x64

64bit에서는 inline assembly가 안된다. 실제로 .asm 파일을 작성해서 같이 컴파일해서 돌리는게 필요하다.

assembly는 name mangling이 존재하지 않는다. c++에서도 이를 막아야한다.

 

newTargetFunc		PROTO C

.code

replacedTargetFunc  PROC
	;targetFunc() 프롤로그
	mov         dword ptr [rsp+10h],edx  
	mov         dword ptr [rsp+8],ecx  
	sub         rsp,38h 

	call newTargetFunc

	;targetFunc() 프롤로그 다음
	mov rax, 00000001400010ECh
	jmp rax

replacedTargetFunc  ENDP

END

assembly 는 naked를 사용하지 못하기 때문에 , assmbley 에 function prologue 를 작성해준다.

newTargetFunc를 호출하고 prologue 다음 메모리로 호출이된다.

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

using namespace std;

extern "C" void replacedTargetFunc(int, int);


//임의기준주소 Off
int targetFunc(int a, int b)
{
    puts("targetFunc() - Begin");
    int result = a + b;
    puts("targetFunc() - End");
    return result;
}

extern "C" int newTargetFunc(int a, int b)
{
    puts("newTargetFunc() - Begin");
    int result = a * b;
    printf("result: %d\n", result);
    puts("newTargetFunc() - End\n");
    return result;
}

void hookTargetFunc(void* pfOrg, void* pfNew, BYTE* pOrgBytes)
{
    DWORD dwOldProtect = 0;
    BYTE pBuf[12] = { 0x48, 0xB8, };

    ::VirtualProtect((LPVOID)pfOrg, 12,
        PAGE_EXECUTE_READWRITE, &dwOldProtect);

    memcpy(pOrgBytes, pfOrg, 12);

    // MOV RAX, 절대주소(64-bit).
    memcpy(&pBuf[2], &pfNew, 8);
    //JMP RAX
    pBuf[10] = 0xFF;
    pBuf[11] = 0xE0;

    // Hook - 12 byte 패치(JMP XXXX)
    memcpy(pfOrg, pBuf, 12);
}

int main()
{
    printf("Return: %d (Before hook)\n\n", targetFunc(3, 4));

    BYTE originalCodes[12];
    hookTargetFunc(targetFunc, replacedTargetFunc, originalCodes);

    int result = targetFunc(3, 4);
    printf("Return: %d (After hook)\n\n", result);
    return 0;
}

extern c를 통해서 c++ name mangling을 방지한다.

빌드 할때도 매번 실행할때마다 메모리 주소가 똑같해야하니 ASLR (address space layout randomization)을 사용안하게 해야한다.

 

 

 

 

 

'Operating System > 이해하면 인생이 바뀌는 Windows API hook' 카테고리의 다른 글

DLL injection  (0) 2024.12.13
IAT hook  (0) 2024.12.02
사전지식 - 두 번째  (0) 2024.11.27
사전 지식 - 첫 번째  (0) 2024.11.22