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;
}
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 |