오늘은 IAT 후킹에 대해 알아보겠습니다.
PE구조가 정말!! 중요하기 때문에 잘 모르거나 기억이 안나시면 꼭 링크에서 PE구조에 대해(특히 import table 구조체 부분) 보고 와주시길 바랍니다.
[Reversing] PE 헤더란? (PE Header)
*해당 포스팅은 Dreamhack의 CaptainHook문제의 PE구조를 분석하며 작성되었습니다. 포스팅 내용을 직접 확인하고 싶으신 분들은 해당 문제와 PE bear, IDA로 함께 확인하시면 될 것 같습니다! PE는 Portable
poiri3r.tistory.com
먼저 저번시간에 간략하게 설명했었는데 후킹의 범주에 대해 알아보겠습니다.
| 후킹 분류 | 방식 | 예시 |
| API 후킹 | 사용자 프로그램이 호출하는 Windows API/DLL 함수 후킹 | IAT,EAT,Inline 등 .. |
| 윈도우 후킹(메세지 후킹) | Windows GUI 이벤트(키보드, 마우스 등)을 가로채는 후킹 | WH_KEYBOARD 후킹 |
| 시스템 콜 후킹 | 커널 호출 전 거치는 syscall 후킹 | SSDT후킹, win32u.dll 후킹 |
| 라이브러리 레벨 후킹 | 특정 라이브러리 함수나 클래스 호출 후킹 | C++ vtable 후킹 |
| 하드웨어 후킹 | 드라이버, I/O 처리 등을 후킹 | DirectX,IRP후킹(윈도우 드라이버) |
엄청 많은 종류의 후킹이 있는데요, 저도 처음 들어보는것들이 넘 많은데, 나중에 천천히 다 알아볼 수 있으면 좋겠네요.
그 중 API 후킹에 대해서도 표로 정리해보겠습니다.
API 후킹이란 프로그램이 호출하는 Windows API나 DDL 함수를 중간에 가로채고 원하는 동작을 실행하게 만드는 기술로, 함수 원형은 그대로 유지되지만, 실제 실행 흐름을 내 코드로 꺾는 방식입니다.
| 후킹 방식 | 설명 | 특징 |
| IAT 후킹 | Import Table의 함수 주소를 덮어씀 | 안정적이나 정적 호출에만 사용 가능 |
| Inline 후킹 | 함수 시작 부분에 jmp 덮어씀 | 모든 호출 차단 가능 |
| EAT 후킹 | Export Table 수정 | DLL 자체를 후킹함 |
| 트램펄린 후킹 | 함수의 시작 부분을 덮어써서 내 코드로 jmp한 뒤, 원래 코드 일부를 저장해두고 복귀 | inline 후킹의 확장 버전, 난이도가 높음 |
그 중 오늘 우리가 알아볼 것은 IAT 후킹입니다. 저번 포스팅에서 알아봤듯이 컴파일시 주소가 고정되기 때문에 발생하는 취약점으로 import table의 함수 주소를 덮어쓰는 방식입니다.
실습을 위해 테스트용 EXE를 작성해보겠습니다.
#include <windows.h>
int main() {
MessageBoxA(NULL, "Hello", "Original", MB_OK);
return 0;
}
저번 포스팅에서 썼었던 코드고, 해당 코드를 메모장에 적어서 저장해주고 MS에서 제공해주는 파워셸(x64 natice tools ...)로 컴파일해주었습니다.
cl victim.c /Fe:victim.exe user32.lib

코드가 있는 디렉토리로 이동한 뒤 다음과 같은 명령어로 컴파일해주었습니다. user32.lib에 연결해서 컴파일하는 방법입니다.
사실 요즘 vs나 visual studio를 안쓰고 항상 쉘이나 리눅스로만 컴파일을 하는데 visual studio로 컴파일해도 똑같을 것 같습니다.
지피티의 가이드라인을 따라가다보니 컴파일러 사용법이 잘 기억이 안나네요...

실행하면 다음과 같이 윈도우 창이 뜨게 됩니다.
이 다음부터 좀 많이 어려운데요. 3가지 과정을 거치게 됩니다.
- c언어로 hook.c 작성
- hook.c을 dll형식으로 컴파일
- victim.exe를 실행하면서 우리가 만든 dll을 주입해주는 인젝터 프로그램 작성(DLL 인젝션)
이러한 방식은 DLL Injection + IAT 후킹 구조로, IAT 후킹 자체는 DLL 안에서 이루어지고 실행 중 프로세스에 넣는 역할은 injector가 담당하는 방식입니다.
조금 실전성은 떨어지는 것 같지만 그래도 흠.. 어쩌겠습니까 처음 해보면 맞으면서 배워야죠 ,,
다음은 iathook.c 코드입니다
먼저 visual studio -> 빈프로젝트 생성 ->dll로 생성합니다.

이제 후킹해주는 dll을 작성해보도록 하겠습니다.
위에 방식으로 dll을 만들었더니 한시간 넘게 돌아봐도 안돼서 메모장에다가 적고 ms 파워쉘로 컴파일했습니다.
#include <windows.h>
#include <imagehlp.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "imagehlp.lib")
typedef int (WINAPI *MSGBOXA)(HWND, LPCSTR, LPCSTR, UINT);
MSGBOXA g_Original = NULL;
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
return g_Original(hWnd, "poirier", lpCaption, uType);
}
void HookIAT() {
HMODULE hMod = GetModuleHandle(NULL);
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(hMod, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
for (; pImportDesc->Name; pImportDesc++) {
LPCSTR pszModName = (LPCSTR)((PBYTE)hMod + pImportDesc->Name);
if (_stricmp(pszModName, "user32.dll") != 0)
continue;
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hMod + pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++) {
PROC* ppfn = (PROC*)&pThunk->u1.Function;
if (*ppfn == (PROC)MessageBoxA) {
DWORD dwOldProtect;
VirtualProtect(ppfn, sizeof(PROC), PAGE_READWRITE, &dwOldProtect);
g_Original = (MSGBOXA)*ppfn;
*ppfn = (PROC)MyMessageBoxA;
VirtualProtect(ppfn, sizeof(PROC), dwOldProtect, &dwOldProtect);
return;
}
}
}
}
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
HookIAT();
}
return TRUE;
}
코드는 다음과 같습니다. 굉장히 어렵고 당황스러우시죠.. 저도 그랬습니다.. 하나하나씩 천천히 분석해보도록 하겠습니다.
사실 위처럼 코드를 짜는 방식은 좀 비효율적이고 실전성이 떨어져서 그냥 훑어만 보셔도 괜찮을 것 같습니다.
include <windows.h>
include <imagehlp.h>
pragma comment(lib, "user32.lib")
pragma comment(lib, "imagehlp.lib")
헤더+라이브러리 부분입니다.
include <imagehlp.h>는 PE구조를 다룰 때 필요한 함수와 구조체가 정의되어 있습니다. 저희가 IAT 위치를 찾아야 되므로 필수적입니다.
#pragma comment 는 컴파일시 해당 라이브러리를 자동으로 링크하도록 지정하는 명령어입니다.
이 코드에서는 user32.dll과 imagehlp.dll을 로드하였습니다.
typedef int (WINAPI *MSGBOXA)(HWND, LPCSTR, LPCSTR, UINT);
MSGBOXA g_Original = NULL;
벌써 어질어질한데요. MSGBOXA라는 함수 포인터 타입을 새로 만들었습니다.
이 타입은 HWND, LPCSTR, LPCSTR, UINT 4개의 인자를 받습니다(위의 3종류는 변수명이 아니라 자료형입니다)
위의 4개의 인자는 MessageBoxA랑 인자가 같습니다.

또 우리가 후킹하려고 하는 함수는 Windows함수이기 때문에 콜링컨벤션을 fastcall으로 맞추기 위해 WINAPI로 지정해주었습니다.
다음과 같이 포인터 타입을 만들었고 g_Original이라는 변수를 생성해서 NULL을 입력해 주었습니다.
g_Original은 IAT에서 빼낸 실제 MessageBoxA의 주소를 저장해두는 백업 포인터입니다.
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCpation, UINT utype){
return g_Original(hWnd, "Poiri3r", lpCaption, uType);
}
MyMessageBoxA라는 포인터를 새로 만들었고, 방금 만든 g_Original의 내용에서 우리가 수정하고싶은 문자열만 그대로 받지 않고 Poiri3r로 바꿔서 return합니다.
void HookIAT(){
HMODULE hMod = GetModuleHandle(NULL);
ULONG ulSize;
HookIAT 함수의 시작 부분입니다. 실행중인 프로그램의 IAT를 찾기 위한 핵심 부분입니다.
HMODULE hMod = GetModuleHandle(NULL)은 HMODULE 자료형의 hMod 변수에 현재 실행중인 프로세스 모듈의 베이스 주소를 가져옵니다. 결과적으로 hMod에 victim.exe의 메모리 시작 주소가 담기며, 이걸 기준으로 import header를 찾습니다.
(*GetModuleHandle은 WinAPI함수로 NULL을 넘기면 현재 실행중인 EXE파일의 핸들을 가져옵니다)
ULONG ulSize : 나중에 함수 반환을 받기 위해 필요합니다.
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(hMod, TRUE, IMAGE_DIRECTORY_ENTRY_POINT, &ulSize);

PIMAGE_IMPORT_DESCRIPTOR는 위의 IMAGE_IMPORT_DESCRIPTOR의 구조체 하나를 가리키는 포인터자료형입니다
ImageDirectoryEntryToData라는 함수는 우리가 PE구조를 읽을때, import table,export table, 등 필요한 데이터 디렉토리를 쉽게 찾아주는 함수입니다.

설명하기가 좀 어렵지만, ImageDirectoryEntryToData는 PVOID형식으로 구조체의 시작주소를 반환하기때문에 PIMAGE_IMPORT_DESCRIPTOR로 형변환해서 pImportDesc에 저장합니다.
for (; pImportDesc->Name; pImportDesc++){
LPCSTR pszModName = (LPCSTR)((PBYTE)hMod + pImportDesc->Name);
if(_stricmp(pszModName, "user32.dll")!=0)
continue;
PE구조에서 살펴봤듯이 프로그램에서 로드하는 dll의 개수당 IMAGE_IMPORT_DESCRIPTOR 구조체가 가동되고, 그 구조체 안에서도 개별 함수들의 주소 정보가 IMAGE_THUNK_DATA에 들어갑니다. 또 그 주소안에 함수들의 이름이 존재합니다. 즉
- 실행시 OriginalFirstThunk를 읽어서
- IMAGE_IMPORT_BY_NAME 구조체에서 함수 이름을 찾아
- user.dll에서 MessageBoxA의 실제 주소를 찾고
- 그 주소를 firstThunk에 써서 IAT가 완성됩니다.
이 과정에서 위의 for 문은 Import Table을 하나씩 확인하면서 각 DLL이름을 읽은 뒤, user32.dll인지 확인하고, 맞으면 후킹할 함수(MessageBoxA)를 찾으러 들어갑니다.
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hMod + pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++){
PROC* ppfn = (PROC*)&pThunk->u1.Function;
FirstThunk는 IAT의 RVA(상대주소)이고, 우리가 실제로 후킹해야 하는 함수 주소들이 있는 곳입니다.
pImportDesc의 FirstThunk값과 hMod(베이스주소)를 더하면 IAT 배열의 시작 주소가 나오고 이걸 PIMAGE_THINK_DATA 자료구조로 형변환을 해서 저장하는 pThunk변수를 생성해줍니다. (이러한 자료구조로 저장해야 .Function으로 함수 이름에 접근 가능)

다음 For문에서 pthunk->u1.Function으로 접근해서 함수의 이름을 ppfn에 저장한 뒤 ppfn의 이름이 MessageBoxA인지 확인하고 있습니다.
if (*ppfn == (PROC)MessageBoxA){
DWORD dwOldProtect;
VirtualProtect(ppfn,sizeof(PROC), PAGE_READWRITE, &dwOldProtect);
g_Original = (MSGBOXA)* ppfn;
*ppfn = (PROC)MyMessageBoxA;
VirtualProtect(ppfn, sizeof(PROC), dwOldProtect, &dwOldProtect);
return;
가장 중요한 코드입니다
ppfn으로 MessageBoxA를 찾았다면 후킹을 진행해야합니다. 하지만 IAT엔트리는 보통 읽기 전용으로 보호가 되어있기 때문에 먼저 메모리 보호를 읽고쓰기가 가능하게 변경해야합니다.

시작주소는 ppfn, 변경할 크기는 PROC의 사이즈, 보호옵션은 READ&WRITE고 마지막 포인트에 새로 만든 DWORD 변수인 dwOldProtect를 넣어서 기존 속성을 저장해둡니다.
그 뒤 원래의 MessageBoxA(후킹대상)을 맨 처음 만들어뒀던 g_Original에 백업을 해둔 뒤, ppfn에 우리가 후킹해서 바꿔둔 함수인 MyMessageboxA를 넣습니다. 그 뒤에 다시 VirtualProtect를 다시 읽기전용으로 바꾸면 됩니다.
마지막 부분입니다.
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
HookIAT();
}
return TRUE;
}
DLL이 로드되거나 언로드될때 자동으로 호출되는 Dllmain함수를 지정해주고 작성한 IAT후킹을 프로세스에 DLL이 붙는 순간 자동으로 시작하게 만드는 부분입니다.
reason : DllMain이 호출된 이유가 Process_Attach(dll이 프로세스에 처음 로드될 때) HookIAT함수가 실행되게 만듭니다.
제가 공부하면서 주석을 다 붙였는데 주석 붙인 코드는 다음과 같습니다. 이해가 안되는 부분 있으면 확인해보시면 될것같습니다.
#include <windows.h>
#include <imagehlp.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "imagehlp.lib")
typedef int (WINAPI *MSGBOXA)(HWND, LPCSTR, LPCSTR, UINT); //포인터타입 생성
MSGBOXA g_Original = NULL; //원본 포인터 저장
//후킹시작
//원본 함수에서 문자열만 바꿔서 저장하는 MyMessageBoxA
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType){
return g_Original(hWnd, "Poiri3r", lpCaption, uType);
}
void HookIAT(){
HMODULE hMod = GetModuleHandle(NULL); // 현재 실행중인 프로세스 모듈의 베이스주소를 가져옴
ULONG ulSize; //변수 선언
//IMAGE_IMPORT_DESCIPTOR의 포인터 자료구조로 ImageDirectoryEntryToData 리턴값을 저장
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(hMod, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
//Import table을 순회하면서 각 DLL이름(->Name)을 읽은 뒤 user32.dll이면 후킹하러 들어감
for (; pImportDesc->Name; pImportDesc++){
LPCSTR pszModName = (LPCSTR)((PBYTE)hMod + pImportDesc->Name);
//stricmp로 문자열이 user32.dll을 찾음
if(_stricmp(pszModName, "user32.dll")!=0)
continue;
/*"PIMAGE_THUNK_DATA를 찾음(위와 같은 방식)
pThunk는 hMod(base주소)에 pImportDesc의 주소를 더해 상대주소를 구함
pThunk구조체 안의 Function정보를 읽어서 MessageBox이면
*/
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hMod + pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++){
PROC* ppfn = (PROC*)&pThunk->u1.Function;
if (*ppfn == (PROC)MessageBoxA){
DWORD dwOldProtect;
//메모리 속성을 읽기 전용에서 읽고쓰기 전용으로 바꿈
VirtualProtect(ppfn,sizeof(PROC), PAGE_READWRITE, &dwOldProtect);
//기존의 함수 실행정보는 g_Original에 저장한 뒤 우리가 만든 MyMessageBoxA를 저장
g_Original = (MSGBOXA)* ppfn;
*ppfn = (PROC)MyMessageBoxA;
//다시 메모리 보호를 읽기전용으로 변경
VirtualProtect(ppfn, sizeof(PROC), dwOldProtect, &dwOldProtect);
return;
}
}
}
}
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) {
//DllMain함수가 호출된 이유가 Process attach 즉 DLL이 프로세스에 처음 로드될때 HookIAT함수를 실행
if (reason == DLL_PROCESS_ATTACH) {
HookIAT();
}
return TRUE;
}
이제 이 hook.c를 컴파일해줘야합니다.
hook.c /LD /nologo /MD /Zi /Od /GS- /Fe:hook.dll user32.lib imagehlp.lib

다음과 같은 명령어로 VS 개발자 프롬프트에서 컴파일 해주시면 됩니다.
이중 /LD는 dll로 컴파일하는 부분입니다.

이제 마지막 단계로 victim.exe를 실행하면서 우리가 만든 dll을 주입해주는 인젝터 프로그램이 필요합니다.
c언어로 작성해주겠습니다. 코드는 다음과 같습니다.
#include <windows.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// victim.exe 실행
if (!CreateProcess("victim.exe", NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("CreateProcess 실패\n");
return 1;
}
// DLL 주입
LPVOID pRemoteStr = VirtualAllocEx(pi.hProcess, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(pi.hProcess, pRemoteStr, "hook.dll", strlen("hook.dll") + 1, NULL);
HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
LPVOID pLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
CreateRemoteThread(pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibrary, pRemoteStr, 0, NULL);
// 실행 재개
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
너무 읽기에 피로할 것 같아서 해당 코드는 진행구조만 설명하고 자세한 설명은 생략하겠습니다(사실 저도 벅차서 더 못하겠습니다..)
순서는 다음과 같습니다.
- victim.exe를 일시정지된 상태로 실행
- hook.dll을 원격 프로세스에 쓰기
- LoadLibraryA("hook.dll")을 원격 스레드로 실행
- victim.ece 실행 재개
어쨌든 해당코드를 컴파일해주면 되는데 명령어는 다음과 같습니다.
cl injector.c /nologo
하면 injector.exe가 완성되고 실행해보면 다음과 같이 실행됩니다.

한 6시간 정도 컴퓨터 앞에 앉아서 챗지피티한테 물어보고 찾아가면서 포스팅을 했는데 제가 공부한 내용중 제일 어려운 내용이 아니었을까 싶습니다. 이게 사실 frida를 사용하면 이렇게 안돌아가고 쉽게 작성이 가능하지만 frida를 사용하는건 IAT랑은 결이 조금 달라서 확실하게 공부해보자 먼길을 돌아왔습니다.
PE구조에 대해 얼마정도 이해했다고는 생각했어도 실제 코드를 작성할 때 어떻게 활용할지 잘 몰랐던 것 같은데 이번에 후킹을 공부해보면서 구조체의 동작에 대해 잘 이해하게 된 것 같아 그래도 득이 있는 것 같습니다.
후킹 스크립트도 챗지피티가 다 짜줬고 AI에 많이 의존을 한 것 같지만 .. 그래도 이해했다는거에 의의를 두고 싶습니다.,
혹시나 궁금한 점이 있으시다면 댓글로 달아주시면 성심성의껏 대답해드리겠습니다./
다음 포스팅할때는 오늘 포기했던 injector.c 설명과 함께 frida스크립트를 통해 동일한 동작을 하는 후킹 스크립트에 대해 작성해보도록 하겠습니다.
글이 많이 길고 어려운데 읽어주셔서 감사합니다~

'취약점분석 > Reversing' 카테고리의 다른 글
| [Reversing] Hooking - 4 (Frida Hooking ②) (3) | 2025.07.12 |
|---|---|
| [Reversing] Hooking - 3 (Frida Hooking ①) (2) | 2025.07.07 |
| [Reversing] Hooking - 1 (DLL,IAT 이론) (2) | 2025.07.05 |
| [Reversing]콜링 컨벤션(Calling convention) -2 (64 bits) (1) | 2025.06.27 |
| [Reversing]콜링 컨벤션(Calling convention) - 1 (32 bits) (1) | 2025.06.25 |