DLL 후킹 - API 코드 패치
가장 널리 사용됨.
모든 API(5바이트 이상)에 대해 후킹 가능
dynamic, process memory, code, DLL injection
방법
dll injection 될때 후킹할 코드의API 시작 코드 5바이트를 백업해두고 jmp로 패치시킴
프로세스가 실행될때 API를 호출하면 jmp해서 내 코드로 옴
내 코드에서 5바이트를 다시 원상복구 시키고 API call 함 return 되면 내 코드로 오니까
내 코드 실행시키고 ret하면 프로세스 코드로 리턴 됨.
책에서는 스텔스 프로세스를 만드는 것으로 하지만 나는 저번과 마찬가지로 소문자 저장 시 대문자로 바꾸는 걸 구현했다.
main
#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "tchar.h"
#include "winbase.h"
#define DEF_PROC_NAME (L"notepad.exe")
#define DEF_DLL_NAME (L"C:\\Users\\a0102\\source\\repos\\Code_Patch_Dll\\x64\\Release\\Code_Patch_Dll.dll")
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
DWORD FindProcessID(LPCTSTR szProcessName)
{
DWORD dwPID = 0xFFFFFFFF;
HANDLE hSnapShot = INVALID_HANDLE_VALUE;
PROCESSENTRY32 pe;
// Get the snapshot of the system
pe.dwSize = sizeof(PROCESSENTRY32);
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
// find process
Process32First(hSnapShot, &pe);
do
{
if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile))
{
dwPID = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnapShot, &pe));
CloseHandle(hSnapShot);
return dwPID;
}
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllName)
{
HANDLE hProcess, hThread;
LPVOID pRemoteBuf;
DWORD dwBufSize = (DWORD)(_tcslen(szDllName) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
DWORD dwErr = GetLastError();
return FALSE;
}
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL);
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName)
{
BOOL bMore = FALSE, bFound = FALSE;
HANDLE hSnapshot, hProcess, hThread;
MODULEENTRY32 me = { sizeof(me) };
LPTHREAD_START_ROUTINE pThreadProc;
if (INVALID_HANDLE_VALUE == (hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))
return FALSE;
bMore = Module32First(hSnapshot, &me);
for (;bMore;bMore = Module32Next(hSnapshot, &me))
{
if (!_tcsicmp(me.szModule, szDllName) || !_tcsicmp(me.szExePath, szDllName))
{
bFound = TRUE;
break;
}
}
if (!bFound)
{
CloseHandle(hSnapshot);
return FALSE;
}
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
CloseHandle(hSnapshot);
return FALSE;
}
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "FreeLibrary");
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return TRUE;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwPID = (DWORD)FindProcessID(DEF_PROC_NAME);
InjectDll(dwPID, DEF_DLL_NAME);
//printf("injec");
//EjectDll(dwPID, DEF_DLL_NAME);
return 0;
}
injection 하는 파일은 저번 파일과 다른게 없다. loadlibrary로 injection 할 dll을 원격 스레드로 로딩한다.
DLL
#include "windows.h"
#include "tchar.h"
#include "pch.h"
#include <stdlib.h>
#define DEF_NTDLL ("kernel32.dll")
#define DEF_WRITEFILE ("WriteFile")
typedef unsigned __int64 QWORD;
// global variable
BYTE g_pOrgBytes[5] = { 0, };
typedef BOOL(WINAPI* WRITEFILE)(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)
{
FARPROC pFunc;
DWORD dwOldProtect;
PBYTE pByte;
pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pFunc;
if (pByte[0] != 0xE9)
return FALSE;
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// Unhook
memcpy(pFunc, pOrgBytes, 5);
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
FARPROC pfnOrg;
DWORD dwOldProtect, dwAddress;
BYTE pBuf[5] = { 0xE9, 0, };
PBYTE pByte;
pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pfnOrg;
if (pByte[0] == 0xE9)
return FALSE;
VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
pOrgBytes[0] = ((BYTE*)pfnOrg)[0];
pOrgBytes[1] = ((BYTE*)pfnOrg)[1];
pOrgBytes[2] = ((BYTE*)pfnOrg)[2];
pOrgBytes[3] = ((BYTE*)pfnOrg)[3];
pOrgBytes[4] = ((BYTE*)pfnOrg)[4];
dwAddress = (QWORD)pfnNew - (QWORD)pfnOrg - 5;
memcpy(&pBuf[1], &dwAddress, 4);
memcpy(pfnOrg, pBuf, 5);
VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
BOOL WINAPI MyWriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)
{
int nLen = nNumberOfBytesToWrite;
unhook_by_code(DEF_NTDLL, DEF_WRITEFILE, g_pOrgBytes);
PBYTE lpBuffer_ = NULL;
lpBuffer_ = (PBYTE)malloc(nNumberOfBytesToWrite + 1);
memset(lpBuffer_, 0, nNumberOfBytesToWrite + 1);
memcpy(lpBuffer_, lpBuffer, nNumberOfBytesToWrite);
for (int i = 0; i < nLen; i++)
{
if (0x61 <= lpBuffer_[i] && lpBuffer_[i] <= 0x7A)
lpBuffer_[i] -= 0x20;
}
memcpy((void*)lpBuffer, (void*)lpBuffer_, nNumberOfBytesToWrite);
free(lpBuffer_);
FARPROC pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL),
DEF_WRITEFILE);
BOOL status = ((WRITEFILE)pFunc)(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
hook_by_code(DEF_NTDLL, DEF_WRITEFILE, (PROC)MyWriteFile, g_pOrgBytes);
return status;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
// #2. API Hooking
case DLL_PROCESS_ATTACH:
hook_by_code(DEF_NTDLL, DEF_WRITEFILE,
(PROC)MyWriteFile, g_pOrgBytes);
break;
// #3. API Unhooking
case DLL_PROCESS_DETACH:
unhook_by_code(DEF_NTDLL, DEF_WRITEFILE, g_pOrgBytes);
break;
}
return TRUE;
}
DLL이 프로세스에 로딩 되면 hook_by_code 함수가 호출 되는데, 대치할 DLL 이름과, 함수 이름, 후킹으로 실행할 함수 주소와 백업용 메모리를 넘긴다.
hook by code 함수는 바꿀 함수를 GetProcAddress로 주소를 찾고 그 함수의 첫번째 문자가 0xE9 즉 jump면 이미 수정된 것이니까 return하고 그게 아니라면 오리지널 바이트를 저장한다. 그 후 나의 함수 거리를 계산해서 'jmp 거리'로 덮어쓴다.
이제 후킹된 함수를 호출하면 mywritefile함수가 호출되고 mywritefile함수는 unhook시킨 다음 소문자를 대문자로 바꾸고 writefile로 저장을 한 뒤 다시 hook 하고 return 해준다.
unhook함수는 hooking된 함수를 찾아서 오리지널 함수로 바꾸고 return한다.
후킹전 writefile 모습
후킹 후 writefile 모습
jmp mywritefile로 바뀜.
후킹 후 파일을 저장해서 mywritefile이 실행되고 있음
약간 하다가 실수 한것이 오리지널 함수 5바이트를 저장할때 이상한게 저장돼서 보니 내가 디버거로 writefile함수에 bp를 걸어놔서 자꾸 0xcc가 저장되어 오류가 났었다. 5바이트엔 bp를 안걸고 해야된다.
jmp 명령어가 0xE9와 0xEB가 있는데 0xEB는 명령어 뒤에 1바이트 거리만큼 점프하고 0xE9는 해당 명령어 끝 4바이트의 거리만큼 점프를 한다.