IAT(Import Address Table) 이란, 사용된 함수와 이를 내보내는 DLL과 같은 PE 파일에 관한 정보가 포합되어 있는 표입니다.
이러한 정보는 signature가 될수 있고, 또한 detect될 수 있기 때문에 IAT 에서 사용된 함수를 숨기는 작업을 해보겠습니다.
IAT
PE(Portable Excutable)헤더란, 32-bit, 64-bit windows 운영체제에서 사용되는 file format의 헤더를 의미합니다.
ex) .exe, .dll
이 PE 헤더에는 IAT라는 table이 있는데 이 table에 앞서 설명드린 프로그램에서 사용되는 함수의 목록과 DLL의 정보가 있습니다.

PeParser로 뜯어본 IAT입니다. GetProcAddress 같은 함수들이 보입니다.
EDR은 이 IAT를 뜯어보면서 수상한 함수(기능은 정상적이지만 자주 악용되는 함수)들을 import하고 있나 살펴봅니다.
물론, 고도화된 EDR은 IAT를 믿지 않고 kernel callbacks 나 VAD 검사를 통해 바이러스를 잡아내기 때문에 의미가 없습니다.
하지만 공부/실습이 목표이므로 IAT Hiding 을 수행해봅시다.
IAT Hiding
typedef LPVOID (WINAPI* fnWriteProcessMemory)( ... );
fnWriteProcessMemory pWriteProcessMemory = GetProcAddress(GetModuleHandleA("KERNEL32.DLL"),
"WriteProcessMemory");
pWriteProcessMemory( ... );
이런 식으로 custom function pointer를 만들어 숨길 수 있습니다.
그런데 여기서 사용되는 GetProcAddress, GetModuleHandle을 숨길 방법이 필요한데, 아예 같은 기능을 하는 함수를 만들어 버리는 방법입니다.
GetProcAddress
먼저 GetProcAddress가 어떻게 작동하는지 알아야 합니다.
EAT(Export Address Table)
DLL은 외부에서 자신을 쓸 수 있도록 함수 목록을 공개해두는데, 이를 EAT라고 합니다.
GetProcAddress는 DLL 시작 주소부터 탐색을 시작해 이 EAT가 포함된 Export Directory 구조체를 찾아갑니다.
이 구조체 안에는 함수를 찾기 위한 핵심 배열 주소(RVA)가 들어 있습니다.
- AddressOfNames: DLL이 내보내는 함수들의 '이름 문자열'이 모여있는 배열
- AddressOfNameOrdinals: 이름과 실제 함수의 순서를 매핑해주는 번호판 배열
- AddressOfFunctions: 함수의 실제 '상대 메모리 주소(RVA)'가 모여있는 배열
이름을 주소로 바꾸는 과정
- 이름 찾기
- 인덱스로 Ordinal 번호 얻기
- 최종 주소 추출
- 최종 절대 주소 계산 및 반환
여기서 최종 절대 주소 계산공식은 아래와 같습니다.
최종 함수 주소 = DLL 시작 주소(Base Address) + 상대 주소(RVA)
FARPROC GetProcAddressR(IN HMODULE hModule, IN LPCSTR lpApiName) {
PBYTE pBase = (PBYTE)hModule;
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
printf("[i] pImgExportDir: %p \n", pImgExportDir);
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
printf("[i] FunctionNameArray: %p \n", FunctionNameArray);
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
printf("[i] FunctionAddressArray: %p \n", FunctionAddressArray);
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
printf("[i] FunctionOrdinalArray: %p \n", FunctionOrdinalArray);
printf("[i] Start Looping ...\n");
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
if (strcmp(lpApiName, pFunctionName) == 0) {
return (FARPROC)pFunctionAddress;
}
}
return NULL;
}
GetProcAddress 의 대체 버전 함수입니다.
여기서 중요한 부분은
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
으로,
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
에서 구한 OrdinalArray를 사용해서 최종 주소를 계산 및 반환 합니다.
GetModuleHandle
GetModuleHandle 이 어떻게 작동하는지 알아야 합니다.
먼저, PEB structure를 받아와야 합니다.
PEB
PEB 란, Process Environment Block 으로 Windows 에서 특정 프로세스의 대한 모든 환경정보를 담고 있는 핵심 데이터 구조입니다.
LDR(Loader data) 또한 그 필드중 하나로, 프로세스에 로드된 모든 모듈(DLL 및 실행파일)의 목록을 관리하는 구조체입니다.
HMODULE GetModuleHandleR(IN LPCWSTR szModuleName) {
PPEB pPeb = (PEB*)(__readgsqword(0x60));
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
while (pDte) {
if (pDte->FullDllName.Length != NULL) {
if (IsStringEqual(pDte->FullDllName.Buffer, szModuleName)) {
return (HMODULE)pDte->Reserved2[0];
}
}
else {
break;
}
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}
return NULL;
}
PEB 구조체의 시작주소는, Intrinsic 함수를 이용하여 얻어올 수 있습니다.
PPEB pPeb = (PEB*)(__readgsqword(0x60));
64bit에서는 gs register를 사용하므로 gs register에 0x60 오프셋에 저장되어있는 값을 읽어올 수 있습니다.
그 뒤, LDR 구조체를 불러와 InMemoryOrderModuleList.Flink 에 있는 LDR_DATA_TABLE_ENTRY 구초제의 포인터를 받아옵니다.
그 뒤, 구조체를 순회하며 szModuleName과 일치하는 모듈을 찾아 반환합니다.

이렇게, GetProcAddress, GetModuleHanlde 함수가 IAT에 없는 것을 확인해볼 수 있습니다.
마치며
이번에는 IAT hiding 기술을 알아봤습니다.
IAT 에서 수상(?)한 함수를 숨겨 EDR 탐지를 회피해보는 시간을 가졌는데요, 앞으로 Syscall을 활용해 함수를 숨기는 방법도 알아보겠습니다.
'IT' 카테고리의 다른 글
| Syscall - implement (1) | 2026.06.10 |
|---|---|
| Syscall - 커널의 소통 창구 (0) | 2026.06.03 |
| Classic APC Injection (0) | 2026.05.25 |
| Process, Thread Enumeration (0) | 2026.05.25 |
| API Hooking with Custom structure (0) | 2026.05.24 |