tmxklab

IAT(Import Address Table) 본문

Security/02 Reversing

IAT(Import Address Table)

tmxk4221 2021. 8. 4. 13:41

IAT(Import Address Table)의 설명에 앞서 먼저 DLL에 간략히 알아보자.

 

 

0. DLL(Dynamic Link Library)


1) DLL

DLL은 실행 파일에서 해당 라이브러리의 기능을 사용 시에만 참조하여 호출하는 방식의 라이브러리를 뜻한다. Static Link Library와 다르게 컴파일 시점에 실행 파일에 함수를 복사하지 않고 함수의 위치 정보만 가지고 해당 함수를 호출할 수 있게 도와준다.

 

즉, 프로그램에 라이브러리를 포함시키지 않고 별도의 파일(DLL)로 구성하여 필요한 함수만 불러쓴다.

 

이러한 방식의 장점으로는 여러 프로그램에서 동시에 사용할 수 있으며, 라이브러리가 업데이트되었을 때 해당 DLL 파일만 교체하면되므로 쉽고 편하다. 다만 실행 파일이 DLL에 있는 특정 함수를 Import하기 위해서는 DLL에서 함수나 변수와 같은 Export정보가 필요하다.

 

 

2) DLL 로딩 방식

Explicit Linking(명시적 연결)

  • 프로그램에서 사용되는 순간에 로딩하고 사용이 끝나면 메모리에서 해제되는 방법
  • DLL이 메모리에 로드되는 시점 : LoadLibrary() 함수 호출 시
  • DLL이 메모리에 해제되는 시점 : FreeLibrary() 함수 호출 시
  • 익스포트 함수 사용 시점 : GetProcAddress() 함수 호출 시
  • 장점 : DLL이 필요할 때마다 로드할 수 있어 메모리 소모 적음, DLL 로딩이 실패하더라도 대응 코드 만들어서 회피
  • 단점 : 함수 호출이 다소 복잡

Implicit Linking (암시적 연결)

  • 프로그램 시작할 때 같이 로딩되어 프로그램 종료할 때 메모리에서 해제되는 방법
  • DLL이 메모리에 로드되는 시점 - 프로그램이 실행되면 DLL도 함께 로드
  • DLL이 메모리에 해제되는 시점 - 프로그램이 종료되면 DLL도 해제
  • 익스포트 함수 사용 시점 - 프로그램이 실행되는 동안 어디서나
  • 장점 : 함수 호출이 간편
  • 단점 : 프로그램이 실행되는 동안 DLL이 계속 존재하기 때문에 메모리 낭비 발생, DLL 로딩 실패 시 프로그램 실행되지 않음

 

참고자료 :

https://blog.daum.net/art_code/678127

 

위에서 DLL 로딩 방식 중에서 IAT가 Implicit Linking에 대한 메커니즘을 제공한다.

 

IAT를 확인하기 위해 32bit 프로그램을 디버깅하여 kerenel32.dll의 API함수를 호출하는 부분을 확인해보자.

위 그림을 살펴보면 call할 때 직접 호출하지 않고 특정 주소(0x414328)에 있는 값을 가져와서 호출하게 된다. 그 특정 주소(0x414328)는 Text섹션의 메모리 영역이며 더 정확히는 IAT메모리 영역이다.

 

이렇게 Kernel32.dll의 API함수 주소를 바로 직접 call하지 않는 이유는 다음과 같다.

 

① 모든 환경에서 API함수 호출을 보장하기 위해서

어떠한 프로그램을 컴파일하는 순간에 어떤 Windows(9X, 2K, XP, Vista, 7), 어떤 언어(KOR, ENG, JPN 등), 어떤 Service Pack에서 실행될지 알 수 없다. 위에서 열거한 모든 환경에서 kernel32.dll의 버전이 달라지고 API 함수의 위치(주소)가 달라진다. 따라서, 모든 환경에서 API함수 호출을 하기 위해서는 실제 주소가 저장될 위치를 준비하고 위 그림처럼 PTR DS:[실제 주소가 저장될 위치]형식의 명령어를 적어두기만 한다.

 

② DLL Relocation

예를 들어 a.dll과 b.dll의 ImageBase값이 동일한 경우를 생각해보자. PE로더는 먼저 a.dll을 성공적으로 로드하였고, 다음 b.dll을 로딩하려고 봤더니 이미 해당 ImageBase값에 a.dll이 로드되어있다. 그래서 PE로더는 다른 비어있는 메모리 공간을 찾아서 b.dll을 로딩시켜줘야 한다. 이것이 DLL Relocation이며, 실제 주소를 하드코딩할 수 없는 이유이다. 또한, PE헤더에 주소를 나타낼 때 VA(Virtual Address)를 쓰지 못하고, RVA(Relative Virtual Address)를 써야 하는 이유이다.

 

+) 참고

참고로 일반적인 DLL 파일은 ImageBase가 0x10000000으로 되어 있어서 보통 DLL Relocation이 발생하는 경우가 많지만, Windows 시스템 DLL 파일들(kernel32.dll, uwer32.dll, gdi32 등)은 자신만의 고유한 ImageBase가 있어서 DLL Relocation이 발생하지 않는다.

 

3) DLL Binding

IAT는 디스크 상의 PE파일로 존재할 때와 실제로 프로세스 주소 공간 내로 매핑되었을 때의 내용이 달라진다. 즉, DLL이 로드될 때마다 로더는 이러한 IAT 변환 작업, 다시 말해 DLL 바인딩 작업을 수행해야 한다. 바인드된 실행파일의 IAT는 실제 함수의 주소를 가리키고 있게 된다.

추가적으로 이러한 작업은 프로그램의 초기화 시간을 지연시키는 문제를 발생한다. MS에서는 IAT에 함수의 주소를 기록하는 작업을 미리 수행하여 지연시간 문제를 해결하여 로딩 시의 속도 향상을 도모하도록 한다.

 

 

1. IAT(Import Address Table)


IAT(Import Address Table)란 Windows 프로그램이 어떤 Library에서 어떤 함수를 사용하고 있는지를 기술한 Table이다.

앞서 말했듯이 Implicit Linking에 대한 메커니즘을 제공하며, 프로그램이 시작할 때 같이 로딩되어 종료할 때 메모리에서 해제된다.

먼저, 알아볼 것은 IMAGE_IMPORT_DESCRIPTOR 구조체이다. IAT는 PE파일이 어떤 Library를 import하고 있는지에 대해 해당 구조체에 명시한다.

 

1) IMAGE_IMPORT_DESCRIPTOR

일반적인 프로그램에서 보통 여러 개의 Library를 import하기 때문에 Library의 갯수만큼 해당 구조체의 배열 형식으로 존재하며, 구조체 배열의 마지막은 NULL 구조체로 끝난다.

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
	union { 
		DWORD Characteristics; 
		DWORD OriginalFirstThunk; // INT(Import Name Table) address(RVA)
	}; 
	DWORD TimeDateStamp; 
	DWORD ForwarderChain; 
	DWORD Name;                 // library name string address(RVA)
	DWORD FirstThunk;           // IAT(Import Address Table) address (RVA)
} lMAGE_IMPORT_DESCRIPTOR;

[ 주요 멤버 설명 ]

  • OriginalFirstThunk : INT(Import Name Table)의 주소(RVA)
  • Name : Library 이름 문자열의 주소(RVA)
  • FirstThunk : IAT(Import Address Table)의 주소(RVA)

이 때, INT와 IAT는 동일하게 IMAGE_THUNK_DATA32구조체 배열로 구성되어 있으므로 OrigianlFirstThunk멤버 변수와 FirstThunk멤버 변수는 INT와 IAT의 IMAGE_THUNK_DATA32 구조체 배열의 주소(RVA)가 담겨져 있다.

 

2) IMAGE_THUNK_DATA32

typdef struct _IMAGE_THUNK_DATA32{
	union {
		DWORD ForwarderString; // PBYTE
		DWORD Function; // PDWORD
		DWORD Ordinal;
		DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
	} u1;
} IMAGE_THUNK_DATA32;

INT와 IAT는 바인딩(Binding)이라는 과정을 거치기 전에는 동일하게 IMAGE_THUNK_DATA32 구조체 배열로 구성되어 있다. 바인딩 과정을 거치게 되면 기존의 IAT의 IMAGE_THUNK_DATA32 구조체의 값들이 실제 함수의 주소로 변경하게 된다.여기서 바인딩이란, PE 로더가 라이브러리의 함수 주소를 EAT(Export Address Table)에서 찾아 그 함수 주소를 IAT의 IMAGE_THUNK_DATA32 구조체에 기록하는 것을 의미한다.

 

[ INT의 IMAGE_THUNK_DATA32 주요 멤버 변수]

(ForwarderString, Function 멤버 변수는 사용되지 않음)

  • AddressOfData : IMAGE_IMPORT_BY_NAME 구조체의 주소(RVA)
  • Ordinal : 임포트한 함수에 대한 Ordinal 값을 명시(단, IMAGE_THUNK_DATA32 구조체의 값이 MSB가 1이면 Ordinal 변수 사용)

 

[ IAT의 IMAGE_THUNK_DATA32 주요 멤버 변수 ]

  • AddressOfData : IMAGE_IMPORT_BY_NAME 구조체의 주소(RVA)
  • Ordinal : 임포트한 함수에 대한 Ordinal 값을 명시
  • ForwaderString, Function : 바인딩을 거친 후 실제 함수의 주소를 명시할 때 사용하는 변수

OriginalFirstThunk는 실행 전,후의 값이 같지만, FirstThunk는 실행 시 PE 로더에 의해 실제 함수가 있는 가상 주소로 바뀌어진다.

 

3) IMAGE_IMPORT_BY_NAME

typedef struct _IMAGE_IMPORT_BY_NAME { 
	WORD Hint;     // ordinal 
	BYTE Name[l]; // function name string 
} lMAGE_IMPORT_BY_NAME, *PlMAGE_IMPORT_BY_NAME;

[ 주요 멤버변수 ]

  • Hint : PE 로더가 임포트하려는 함수를 빠르게 찾을 수 있도록 도와주기 위해 사용하는 멤버 변수, 임포트하려는 Library의 EAT(Export Address Table)이 인덱스 값을 가지고 있는 멤버 변수(필수적인 것이 아니기 때문에 0으로 채워져 있는 경우가 많음)
  • Name : 임포트하고자 하는 함수의 이름을 명시

 

4) PE Loader가 임포트 함수 주소를 IAT에 입력하는 기본적인 순서

① IMAGE_IMPORT_DESCRIPTOR의 Name멤버를 읽어서 라이브러리의 이름 문자열("kernel32.dll")을 얻는다.

② 해당 라이브러리를 로딩 → LoadLibrary("kernel32.dll")

③ IMAGE_IMPORT_DESCRIPTOR의 OriginalFirstThunk 멤버를 읽어서 INT 주소를 얻는다.

④ INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME 주소(RVA)를 얻는다.

⑤ IMAGE_IMPORT_BY_NAME의 Hint(Ordinal) 또는 Name 항목을 이용하여 해당 함수의 시작 주소를 얻는다. → GetProcAddress("GetCurrentThreadId")

⑥ IMAGE_IMPORT_DESCRIPTOR의 FirstThunk(IAT)멤버를 읽어서 IAT 주소를 얻는다.

⑦ 해당 IAT 배열 값에 위에서 구한 함수 주소를 입력한다.

⑧ INT가 끝날 때까지(Null을 만날 때까지)위 4) ~ 7) 과정을 반복한다.

 

 

2. Debugging


이제 직접 PE파일을 대상으로 IAT를 확인해보자.

 

IAT를 확인하기 위해서 IMAGE_IMPORT_DESCRIPTOR 구조체를 확인해야 하는데 해당 구조체 배열은 PE 헤더가 아닌 PE 바디에 존재하는 것을 명심하자. 그리고 그곳을 찾아가기 위한 정보는 PE 헤더에 존재하는데 IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress값이 실제 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 주소(RVA)이다.

 

1) File에서 INT, IAT확인

notepad.exe를 대상으로 진행하고 CFF Explorer라는 툴을 사용하여 확인해보자.

DataDirectory[1].VirtualAddress값은 0x2F908(RVA)이며, .rdata섹션에 존재하는 것을 확인

 

 

.rdata섹션의 Raw Address는 0x26C00이며, Virtual Address는 0x28000이다.

 

이제 RVA to RAW공식을 사용하여 변환해주면

RAW = RVA - VirtualAddress + PointerToRawData

RAW = 0x2F908 - 0x28000 + 0x26C00 = 0x2E508

0x2E508부터 20byte만큼이 IMAGE_IMPORT_DESCRIPTOR 구조체이다.

 

사실 굳이 저렇게 변환 과정없이 CFF Explorer에서 왼쪽 탭에 Import Directory를 누르면 IAT의 File Offset과 값을 확인할 수 있다(ㅋㅋㅋ) 어쨋든 실제로 PE Body에 존재하고 kernel32.dll의 IMAGE_IMPORT_DESCRIPTOR인 것을 확인할 수 있다. 또한, 현재 OFTs(OriginalFirtstThunk, INT)값과 FTs(FirstThunk, IAT)값이 동일한 것을 확인할 수 있다.

 

이는 앞서 말했듯 프로그램을 실행하기 전이므로 바인딩을 거치지 않아서 IAT에 실제 주소 값이 Overwritten되지 않았기 때문이다.

 

2) Process에서 INT, IAT확인

그럼 이제 x64dbg 디버거를 통해 kernel32.dll에 대한 INT와 IAT를 확인해보자.

 

① INT 확인

먼저, INT를 확인해보자. 앞서 INT의 RVA(OFTs)값은 0x2FC08이므로 ImageBase값에서 0x2FC08을 더해주면 된다.

ImageBase값은 0x7FF620D20000이므로 INT RVA값(0x2FC08)은 0x7FF620D4FC08이다.

확인 결과 0x3068C이며 CFF Explorer에서 확인했던 것과 동일하다. 또한 이 값은 IMAGE_IMPORT_BY_NAME을 가리키는 RVA값이므로 다시 ImageBase값에 0x3068C를 더하고 덤프떠서 확인해보자. (0x7FF620D20000 + 0x3068C = 0x7FF620D5068C)

확인결과 2Byte로 Hint값인 0x02B8이 사용되었으며, Name멤버 변수에는 "GetProcAddress"문자열을 확인할 수 있다. → IMAGE_IMPORT_BY_NAME

즉, 앞서 파일형태에서 CFF Explorer로 확인했던 값과 완전히 일치하며 값이 달라지지 않았다.

 

② IAT 확인

IAT의 RVA(FTs)값은 0x28920이므로, 다시 ImageBase 값에 더해서 확인해보자.

(0x7FF620D20000 + 0x28920= 0x7FF620D48920)

CFF Explorer에서 확인했던 것과 달리 INT와 동일하게 IMAGE_IMPORT_BY_NAME을 가리키는 RVA값이 아니라 0x7FFE0A1CAEC0값이 저장되어 있다.

해당 주소를 따라 가보자

해당 주소는 kernel32.dll의 text section에 위치하며, GetProcAddress()를 호출하는 실제 주소 값인 것을 확인할 수 있다.

 

이를 통해 프로그램이 실행되기 이전에는 INT와 IAT는 동일한 값(IMAGE_THUNK_DATA32)이지만, 프로그램이 실행된 후에는 PE Loader에 의해 IAT에 실제 함수의 주소 값이 덮어씌어지는 것을 확인할 수 있다.

 

3. References


+) 책 : "리버싱 핵심원리"

https://yokang90.tistory.com/26

https://dhhd-goldmilk777.tistory.com/151

https://bsodtutorials.blogspot.com/2014/03/import-address-tables-and-export.html

https://kali-km.tistory.com/entry/DLL%EC%9D%B4%EB%9E%80

 

'Security > 02 Reversing' 카테고리의 다른 글

EAT(Export Address Table)  (0) 2021.08.06
RVA to RAW  (0) 2021.08.04
Window API 관련  (0) 2021.04.18
ARM Assembly 정리 (기초)  (0) 2020.12.18
[리버싱] 어셈블리어 설명 및 종류  (0) 2020.02.03
Comments