반응형

IAT에는 Windows 운영체제의 핵심 개념인 Process, Memory, DLL구조 등에 대한 내용이 함축되어 담겨있다.

IAT는 한마디로 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지를 적어놓은 테이블이다.


1. DLL (Dynamic Linked Library)

DLL은 직역하면 "동적 연결 라이브러리" 라고 한다.

DOS운영체제 시절에는 DLL개념이 없어서 프로그램에서 printf라는 함수를 사용하려고 한다면 printf의 바이너리 코드를 프로그램 자체에 삽입시키는 방식을 사용하였다. (하드코딩과 비슷한 방식을 사용)

 

윈도우에서 멀티 태스킹을 지원하면서 프로그램이 실행 중에 다른 곳에 접근 하는 등 다른 동작들을 할 수 있게 되버렸기 때문에 직접 삽입하는 방식은 정말 비효율적이였다.

게다가 이렇게 사용하게 되면, 유지 보수는 물론이고 심각한 메모리 낭비가 생기게 된다.

 

멀티 태스킹을 지원함과 동시에 하드코딩 방식을 다른 방식으로 바꾸게 되었는데, 특징은 이러하다.

  • 프로그램에 라이브러리를 직접 삽입하지 말고 별도의 파일(DLL)로 구성하여 필요할 때마다 불러서 사용하자.
  • DLL이 메모리에 한번 올라가면 Memory Mapping으로 여러 Process에서 공유해서 사용하자.
  • 유지 보수를 위해 DLL하나만 고쳐주고 배포하면 되니 더 간단하고 쉽다.

 

실제 DLL로딩 방식은 2가지인데,

  1. 프그램에서 사용되는 순간 로딩하고 사용이 끝나면 메모리에서 해제되는 방법(Explicit Linking)
  2. 프로그램이 시작할 때 같이 로딩되어 프로그램이 종료할 때 메모리에서 해제되는 방법(Implicit Linking)
  •  

IAT는 여기서 2번째에 해당하는 Implicit Linking에 대한 로직을 제공하는 역할을 한다.

 

x64dbg로 메모장을 열어서 확인해보면

 

x64dbg에서는 기호 탭을 통해 dll이 어디에 로딩 되는지 dll함수는 어디에 로딩 되는지 알 수 있다.

 

x64dbg으로 메모장을 열어 CreateFileW함수를 불러오는 구문을 찾은 모습이다.

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

 

특정 주소에 있는 값, 즉 0x00007FF8D2A54B60 주소에 무엇이 있는지 찾아보면,

아까 기호 탭에서 봤던 kernel32.dll안에 CreateFileW 함수를 가르키게 된다.

 

0x00007FF8D2A54B60 주소에 직접 접근하여 kernel.dll안에 CreateFileW 함수를 호출하면 되지 않느냐 할 수 있지만,

DLL함수 주소를 직접 호출하면 안되는 2가지 이유는 다음과 같다.

 

1. 프로그램은 실행 환경에 따라 메모리 길이, 메모리 주소 등, 많은 것이 바뀌기 때문이다.

프로그램 제작자가 프로그램을 컴파일(생성)하는 순간에는 어느 환경에서 실행 되는지 알 수 없다.

모든 환경마다 dll 의 버전이 틀려지고, DLL안 함수의 위치(주소)도 달라진다.

모든 환경에서 동일한 함수 호출을 보장하기 위해서 컴파일러는 DLL안 함수의 실제 주소가 저장될 위치를 준비하고 CALL DWORD PTR DS:[1001104] 형식의 명령어만 적어둔다.

그리고 파일이 실행되는 순간 PE Loader가 1001104 의 위치에 시스템마다 다른 함수의 주소를 입력하는것이다.

 

2. 위에서 RVA 때 개념만 잠깐 나왔던 DLL Relocation 때문이다.

간단하게 말하면 a.dll, b.dll을 사용하는데 a.dll은 0001주소를 사용하고 b.dll도 0001메모리를 사용하려고 한다면, 첫번째로 부른 a.dll이 이미 메모리 주소를 사용하고 있으므로 PE Loader가 다른 빈 메모리 공간에 b.dll을 로딩시켜준다.

 

결론)

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

 

+) 참고

Windows 시스템 DLL 파일들(kernel32.dll, uwer32.dll, gdi32 등)은 자기만의 고유한 ImageBase가 있어서 DLL Relocation이 발생하지 않는다.


2. IMAGE_IMPORT_DESCRIPTOR

프로그램이 어떤 라이브러리를 불러오고 있는지 IMAGE_IMPORT_DESCRIPTOR 구조체에 담겨져 있다.

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)
} IMAGE_IMPORT_DESCRIPTOR;

일반적인 프로그램에서는 여러 개의 Library 를 Import 하기 때문에 
Library 의 갯수 만큼 위 구조체의 배열 형식으로 존재하게 되며, 구조체 배열의 마지막은 NULL 구조체로 끝나게 된다.

 

주요 멤버

  • OriginalFirstThunk : INT(Import Name Table) 의 주소(RVA)
  • Name : Library 이름 문자열의 주소(RVA)
  • FirstThunk : IAT(Import Address Table) 의 주소(RVA)
    • 참고------ 
    • PE 헤더에서 Table이라고 하면 배열을 뜻함
    • INT와 IAT type(4바이트 자료형) 배열이고 NULL로 끝난다.(크기가 따로 명시되어있지 않다.)
    • INT에서 각 원소의 값은  IMAGE_IMPORT_BY_NAME 구조체 포인터이다.(IAT도 같은 값을 가지는 경우가 있다.)
    • INT와 IAT의 크기는 같아야 한다.

 

  1.  IID(IMAGE_IMPORT_DESCRIPTOR) 의 Name 맴버를 읽어서 라이브러리의 이름 문자열(dll의 이름)을 얻는다.
  2.  해당 라이브러리를 로딩한다 ->LoadLibrary(dll)
  3.  IID의 OriginalFirstThunk 맴버를 읽어서 INT주소를 얻는다.
  4.  INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME 주소(RVA)를 얻는다.
  5.  IMAGE_IMPORT_BY_NAME의 Hint(Ordinal) 또는 Name 항목을 이용하여 해당 함수의 시작 주소를 얻는다.-> GetProcAddress("GetCurrentThreadld")
  6.  IID의 FirstThunk(IAT) 맴버를 읽어서 IAT 주소를 얻는다.
  7.  해당 IAT 배열값에 위에서 구한 주소를 입력한다.
  8.  INT가 끝날 때까지(NULL을 만날때까지) 위 4~7과정을 반복한다.

 

Notepad를 통해 직접 확인해보기.

 

IMAGE_IMPORT_DESCRIPTOR 구조체는 PE바디에 있다.

NT header의 IMAGE_OPTIONAL_HEADER.DataDirectory[1].VirtualAddress값이 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 주소(RVA)이다.

 

실제로 확인해보자.

1. CFF Explorer로 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 시작 주소를 찾는다.

IMAGE_OPTIONAL_HEADER.DataDirectory[1].VirtualAddress값, 즉 IMAGE_IMPORT_DESCRIPTOR 의 시작 주소는 "0002D0A8" 이며, .rdata섹션에 존재한다.

 

2. .rdata섹션의 Virtual address와 Raw address를 찾는다.

정리해보면, 

0x0002D0A8 = 구조체 배열의 시작 주소
0x00024E00 = .rdata의 Raw address
0x00026000 = .rdata의 Virtual address

이렇게 되는데 이것을 바탕으로 RAW를 구하는 공식을 사용해 offset을 구해본다.

RAW = RVA - VirtualAddress + PointerToRawData(Raw address)

RAW = 0x0002D0A8 - 0x00026000 + 0x00024E00 = 0x0002BEA8

 

실제로 맞는지 CFF로 확인해볼 수 있다. 

import directory에 맨 처음 dll인 KERNEL32.dll부터 주소가 계산한 것과 같은 0002BEA8으로 보인다. 계산이 맞다.

 

HXD로 0002BEA8에 접근해보면,

IMAGE_IMPORT_DESCRIPTOR 구조체 하나의 크기는 20byte = 0x14바이트이다 이 만큼 읽어주면 이것이 첫번째로 불러오는 dll의 주소정보이다.

 

RAW 계산식으로 확인해보면

0x00024E00 = .rdata의 Raw address
0x00026000 = .rdata의 Virtual address

 Offset  Member  RVA  RAW
0002BEA8  OriginalFirstThunk (INT) 0002D3C0 0002D3C0 - 00026000 + 00024E00 = 0002C1C0
0002BEB4  Name 0002E1D8 0002E1D8 - 00026000 + 00024E00 = 0002CFD8
0002BEB8  FirstThunk (IAT) 000268B8 000268B8 - 00026000 + 00024E00 = 000256B8

 

1. OriginalFirstThunk (INT)

INT는 임포트하는 함수의 정보가 담긴 구조체 포인터 배열이다. 이를 통해서 함수의 시작 주소를 정확히 구할 수 있다.

주소 값이 각각 IMAGE_IMPROT_BY_NAME 구조체를 가리킨다.

 

1.1 IMAGE_IMPROT_BY_NAME

1번에서 배열의 첫 번째 값 0002DE1C를 RAW로 바꾸어(0002DE1C - 00026000 + 00024E00) "0002CC1C"로 가면 임포트하는 API 함수 이름 문자열이 있다.

Name: GetProcAddress

 

 

2. Name(CFF에서 봤을 때 처럼 맨 처음으로 불러오는 DLL은 KERNEL32.DLL이다.)

 

3. FirstThunk - IAT(Import Address Table)

IAT의 첫 번째 원소값인 '0002DE1C'은 의미 없는 값이고 메모리에 로딩될 시 이 자리에 정확한 맨 첫번째로 불러왔던 dll의 첫번째 함수인 GetProcAddress의 주소값으로 대체된다.

 

실제로 확인해보면,

0x7FF795690000(Imagebase) + 0x268B8(IAT의 RVA) = 0x7ff7956b68b8

0x7ff7956b68b8에 담겨있는 00007FFD5B7EAEC0에 접근하면

실제 dll안에 GetProcAddress함수가 들어있는 모습

 

 

반응형

'윈도우 > PE 구조' 카테고리의 다른 글

PE 구조 (7) - EAT  (0) 2022.05.02
PE 구조 (5) - (RVA to RAW) 예제 포함  (0) 2022.04.05
PE 구조 (4) - (Section Header)  (0) 2022.04.02
PE 구조 (3) - (NT Header)  (0) 2022.03.31
PE구조 (2) - (DOS Header, DOS Stub)  (0) 2022.03.30