일명 나뭇잎 책이라고 불리는 리버싱 핵심 원리 책을 보면서, 배운 내용을 정리하는 포스팅입니다.
해당 포스팅에서는 API 후킹 방법중 하나인 코드 패치 내용 정리입니다.
API 코드 패치
API 코드 패치 기법은 프로세스 메모리에 로딩된 라이브러리 이미지에서 후킹을 원하는 API 코드 자체를 수정하는 방법을 말합니다. 이 기법은 API 후킹에서 널리 사용된다고 하는데, 그 이유가 대부분의 API를 후킹할 수 있기 때문이라고 합니다.
하지만 후킹하려면 API의 코드의 길이가 최소 5바이트보다 커야 한다고 합니다. 하지만 대부분의 모든 API의 코드 크기는 5바이트보다 크기 때문에 사실상 제한이 없다고 보면 됩니다.
API 코드 패치 동작 원리
IAT 후킹 방식이 프로세스의 특정 IAT 값을 조작해서 후킹하는 방식이라면, 코드 패치 방식은 실제 API 코드 시작 바이트가 JMP 명령어로 패치합니다. 후킹된 API가 호출되면 JMP 명령어가 실행되어 후킹 함수로 제어가 넘어오는 방식입니다.
이때, 사용되는 JMP 명령어의 Instruction을 살펴보면, “E9 XXXXXXXX” 형태라는 것을 알 수 있습니다. 해당 기법을 통해서는 32bit에서는 통하지만, 64bit에서는 통하지가 않습니다.
그 이유는 32bit에서는 가상메모리 공간이 4GB로서, 2GB는 커널 영역이 사용하고, 2GB는 유저 영역이 사용합니다. 이때, 32비트의 크기로는 2GB정도되는 영역을 왔다갔다 할 수 있습니다. 따라서 “E9 XXXXXXXX” 명령어로는 32bit에서는 충분히 사용을 할 수가 있습니다.
하지만 64bit에서는 가상메모리 공간이 일반적으로 16TB를 사용하고 있으며 최대 16EB 까지 사용이 가능합니다. 따라서 JMP 명령어의 Instruction “E9 XXXXXXXX” 으로는 커버를 할 수가 없습니다.
64bit에서는 이를 해결 하기 위해서 Instruction을 섞어서 사용하는 요령이 필요합니다. 해당 부분은 따로 포스팅 해서 업데이트를 할 예정입니다. 해당 부분은 x64 hooking - code patch 에서 보시면 됩니다.
분석
기본적으로 DLL 인젝션을 통해 API 후킹을 하기 위해서는 DLL Injector는 필요합니다. 어떤 DLL Injector를 사용해도 상관없습니다. 여기서 사용한 DLL Injector는 나뭇잎 책에서 제공하는 것을 사용했습니다.
여기서 후킹할 API는 MessageBox 입니다. MyMessageBox 를 만들어서 MeesageBox 후킹해서 MyMeesageBox로 실행흐름을 바꿔서 원하는 메시지 박스를 표시하도록 할 것입니다.
소스 코드를 보면서, 실습을 진행해봅시다.
DllMain()
실행 파일에서 늘 작성하던 main() 함수처럼 DLL 파일에서도 마찬가지로 늘 작성해야하는 DllMain() 함수가 있습니다. DllMain() 함수에서 늘 시작한다고 보면 됩니다.
후킹 하고자 하는 함수는 ExitProcess 입니다. 모든 프로세스에서 종료할 때, 이 함수를 이용합니다. 프로그램 종료만으로도 후킹 성공 여부를 확실하게 알 수 있기 때문에 해당 API를 선택했습니다.
제가 제작한 후킹 함수는 MyExitProcess 입니다. 해당 함수에서는 후킹 되었다는 메시지 박스를 표시하고 ExitProcess API가 정상적으로 호출되도록 작성을 했습니다.
DllMain() 함수에서는 DLL이 인젝션이 되면, Code Patch 기법을 이용해서 API의 실행흐름을 MyExitProcess 로 바꿉니다. DLL이 이젝션되면 ExitProcess의 본래의 실행흐름으로 복원합니다.
1 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) |
코드를 보시면, DLL이 로딩될 때, kernel32.dll에서 ExitProcess API를 후킹 한다는 것을 볼 수 있고, DLL이 언로딩될 때 본래의 실행흐름으로 바꾸는 것을 볼 수 있습니다.
MyExitProcess 후킹 함수 구현
ExitProcess API를 대신 사용할 MyExitProcess 함수를 구현을 해야 합니다. 본래 ExitProcess API 대신 호출이 되는 것이기 때문에 함수 인자 같은 경우에는 동일하게 해줘야 합니다. 그래야지 MyExitProcess 함수를 호출하고 나서 정상적으로 ExitProcess API를 호출할 수 있습니다.
MyExitProcess 후킹 함수는 다음과 같습니다.
1 | void WINAPI MyExitProcess(UINT uExitCode) |
후킹 로직 구현
다음 부분은 코드 패치 기법을 이용하여 API를 후킹하는 hook() 함수입니다.
1 | BOOL hook(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes) |
hook() 함수의 기능은 원본 코드 시작부분의 5바이트를 “JMP XXXXXXXX”명령어로 변경시켜서 코드의 실행 흐름을 바꿔서 후킹하는 것입니다. 후킹은 다음의 순서로 동작이 됩니다.
후킹 대상 API 주소(ExitProcess)를 구한다.
만약 이미 후킹이되었다면, 후킹을 하지 않는다.
후킹 대상 API 메모리에 5바이트 패치를 위하여 메모리에 WRITE 속성을 추가한다.
언훅을 할 때, 원본 코드를 복원하기 위해서 5바이트를 백업한다.
JMP 주소를 계산한다.
후킹 대상 API 메모리에 5바이트 패치를 한다.(JMP XXXXXXXX)
메모리 속성을 원래대로 복원한다.
여기서 중요한 부분은 JMP 명령어에 사용할 상대 주소 값을 어떻게 구하는 것 입니다. 점프할 위치 까지의 상대 거리 주소 값은 다음같은 방식으로 구할 수 있습니다.
XXXXXXXX = 다음 명령어 주소 - 현재 명령어 주소 - 현재 명령어 길이
여기서 명령어의 길이를 빼주는 이유는 CPU가 명령어 주소를 계산할 떄 다음같은 방식으로 구하기 때문입니다.
다음 명령어주소(IP) = 현재 명령어주소(IP) + relative_offset + 현재 명령어 길이
따라서 점프할 위치 까지의 상대 거리 주소 값을 구하기 위해서는 다음 명령어 주소 값에서 현재 명령어 주소 값을 뺴주고 현재 명령어 길이(5바이트)를 빼줘야합니다.
이러한 방식 말고도 절대주소로도 점프할 수 있습니다. (ex. “PUSH+RET”, “MOV+JMP”) 하지만 이러한 방식을 이용할 경우 명령어 길이가 늘어납니다. 64비트에서는 상대 거리 주소값을 이용해서 점프할 수 없기 때문에 어쩔 수 없이 절대주소로 점프하는 기법을 이용합니다.
언훅 동작 원리
다음 부분은 언훅을 하는 unhook() 함수입니다. 해당 함수에서는 원래 코드의 5바이트를 복원하는 것입니다. 아까전에 원본 코드의 5바이트를 저장한 것을 이용해서 후킹 대상 API의 주소에서 패치한 5바이트를 복원을 하면 됩니다.
1 | BOOL unhook(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes) |
실행
전체 소스 코드
#include "Windows.h"
#include "stdio.h"
BYTE g_pOrgBytes[5] = { 0, };
BOOL hook(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
FARPROC pfnOrg;
DWORD dwOldProtect, dwAddress;
BYTE pBuf[5] = { 0xE9, 0x00, 0x00, 0x00, 0x00 };
PBYTE pByte;
// 후킹 대상 API 주소를 구한다
pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pfnOrg;
// 만약 이미 후킹되어 있다면 return FALSE
if (pByte[0] == 0xE9)
return FALSE;
// 5바이트 패치를 위하여 메모리에 WRITE 속성 추가
VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 원본코드(5바이트) 백업
memcpy(pOrgBytes, pfnOrg, 5);
// JMP 주소 계산(E9 XXXXXXXX)
// XXXXXXXX = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
memcpy(&pBuf[1], &dwAddress, 4);
// Hook: 5바이트 패치(JMP XXXXXXXX)
memcpy(pfnOrg, pBuf, 5);
// 메모리 속성 복원
VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
BOOL unhook(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)
{
FARPROC pFunc;
DWORD dwOldProtect;
PBYTE pByte;
// API 주소를 구한다
pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pFunc;
// 만약 이미 언후킹 되어있다면 return FALSE
if (pByte[0] != 0xE9)
return FALSE;
// 원래 코드(5바이트) 패치를 하기 위해서 메모리에 Write 속성 추가
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// unhook
memcpy(pFunc, g_pOrgBytes, 5);
// 메모리 속성 복원
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
}
void WINAPI MyExitProcess(UINT uExitCode)
{
MessageBoxA(NULL, "API hooking - code patch", "hook", MB_OK);
unhook("kernel32.dll", "ExitProcess", g_pOrgBytes);
ExitProcess(uExitCode);
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
hook("kernel32.dll", "ExitProcess", (PROC)MyExitProcess, g_pOrgBytes);
break;
case DLL_PROCESS_DETACH:
unhook("kernel32.dll", "ExitProcess", g_pOrgBytes);
break;
}
return TRUE;
}
## 참고자료
[CALL - Call Procedure](https://www.felixcloutier.com/x86/call)
[JMP - Jump](https://www.felixcloutier.com/x86/jmp)
[API Hooking x86 x64](https://eram.tistory.com/entry/API-Hooking-x86?category=564239)
[x64 Hooking](https://shhoya.github.io/2019/03/27/64bitHook.html)
[x86, x64 API HOOKING](https://5kyc1ad.tistory.com/354)
[Win32 API 후킹 - Trampoline API Hooking](https://www.sysnet.pe.kr/2/0/1231)
[Windows x64 binary 모듈 단위 이동](https://ezbeat.tistory.com/453)
[x64 Hooks](http://sandsprite.com/blogs/index.php?uid=7&pid=235)
[Assembly Challenge : Jump to a non-relative address without using registers](https://rayanfam.com/topics/assembly-challenge-jump-to-a-non-relative-address-without-using-registers/)