본문 바로가기

리버싱/기초 지식

Study03 - 기초 지식(IAT)

IAT, Import Address Table은 실행 파일이 어떤 라이브러리의 어떤 함수를 가져다 쓰는지 기록해놓은 테이블이다. PE Loader가 PE 파일을 메모리로 로딩할 때 IAT에 기재된 API 이름을 참조하여 실제 주소를 찾아 IAT에 API를 가리키는 주소를 적어놓으면 코드에서 라이브러리를 참조할 때 이 주소를 사용하는 방식이다.

 

프로그램에 라이브러리를 포함하지 않는 DLL 방식을 사용하게 되면서 라이브러리를 프로그램 시작 시 로딩하고 종료 시 메모리에서 해제하는 Implicit Linking 방식과 프로그램 내부에서 사용되는 순간에 로딩하고 사용이 끝나면 해제하는 Explicit Linking 방식 두 가지로 구분되었는데 이 IAT는 전자의 방식에 사용된다.

 

Import Address Table에 있는 내용(entry)들은 Import Lookup Table의 내용들과 동일한데 파일이 실행(binding)되는 순간 PE 로더에 의해 운영체제 버전에 따라 32bit 또는 64bit 주소값으로 덮어씌워진다. 위에서 말했듯이 IAT에 API를 가리키는 주소를 적어놓는 것으로 임포트(import)된 심볼들의 주소를 테이블에 기재함으로써 프로그램에서 사용할 수 있는 것이다. 이 주소들은 명목상으로는 '가상 주소(virtual address)'라 불리지만 심볼들의 실제 주소를 가리키고 있다.

 

실제 주소가 IAT에 적혀있지만 프로그램에서는 이 실제(hardcoded) 주소를 직접 호출하지 않는다. 현대 운영체제는 계속 개발되면서 새로운 버전, 서비스 팩이 출시되는데 이때마다 DLL 파일 자체의 버전이 달라지고 내부에 정의된 함수의 위치가 달라진다. 윈도우 10에서 호출하던 함수가 윈도우 7에서도 똑같은 주소에 자리잡고 있으리란 보장이 없기 때문에 이 함수의 실제 위치를 저장할 공간(IAT)만 미리 만들어두고 나중에 DLL을 로드할 때 PE 로더가 이 공간에 실제 함수의 주소를 채워서 사용하게 된다. 즉 간접적으로(indirect) 호출하게 되는 함수 포인터같은 방식이라 생각할 수 있을 것이다.

 

아래와 같은 프로그램 코드를 보자.

지난 포스팅에서 다뤘던 레나의 17번째 튜토리얼이다. 해당 튜토리얼에서는 GetDlgItemTextA라는 함수를 사용하여 텍스트 필드에 적힌 문자열 값을 얻어왔는데 이 함수는 Win32 API로 user32.dll 파일에 저장되어 있다. 프로그램에서 이 함수를 사용하려면 위에서 말했듯이 IAT를 참조하여 함수의 실제 주소를 찾아내어 사용해야 할 텐데 이를 확인하기 위해 프로그램의 흐름을 따라가보면 다음과 같다.

  • 0x004012BD: 프로그램 코드에서 함수 호출

  • 0x004013CA: IAT entry에서 함수 실제 위치 확인

  • 0x76D42320: 함수의 실제 위치에서 코드 실행

0x004013CA 부근에는 여러 함수들의 이름이 기재되어 있는데 이곳이 IAT 구역으로 현재 확인하고 있는 함수의 엔트리가 가리키고 있는 곳(0x00402048)의 데이터가 함수의 실제 주소가 될 것이다. 데이터를 확인해보면 리틀 인디언 방식으로 0x76D42320이 저장되어 있으며 이곳으로 이동해보면 GetDlgItemTextA 함수가 적재된 영역을 확인할 수 있다.

 

이렇듯 IAT를 활용함으로써 프로그램은 DLL 파일이 변경될때마다 프로그램 코드를 수정하지 않아도 되며 해당 함수를 사용하는 코드에 고정된 주소로 하드코딩할 필요도 없기 때문에 유연하게 프로그램을 실행할 수 있다.

 

프로그램이 패커에 의해 패킹된 경우 거의 모든 패커는 프로그램의 크기를 줄이기 위해, 언패킹을 어렵게 만들기 위해 임포트 테이블을 망가뜨린다. 그렇지만 결국 PE 로더에 의해 실행되어야 하는 PE 프로그램인만큼 패커는 이 임포트 테이블을 실행 시 복구하여 프로그램이 어떤 DLL과 함수를 사용하는지 어디에 함수의 주소를 저장할지 파악하여 프로그램이 정상적으로 작동할 수 있도록 한다.

 

 

관련 PE 구조로 임포트 테이블(Import Table)이 있는데 대부분 Import Directory Table을 의미한다. 이는 임포트 테이블의 맨 앞에 존재하며 IMAGE_OPTIONAL_HEADER에서 IMPORT Table 구조체로 위치를 확인할 수 있다. Import Directory Table에서는 프로그램이 사용하는 모든 DLL 정보들을 아래와 같은 엔트리(entry)로 저장하고 있다.

IMPORT Directory Table

위의 이미지는 abex의 첫 번째 크랙미 파일로 KERNEL32.dll, USER32.dll을 사용하고 있기 때문에 두 개의 엔트리가 등록되어 있으며 마지막 엔트리는 DLL 목록의 끝을 표시하기 위해 아무런 값도 저장되지 않은 널 엔트리가 등록된 것을 볼 수 있다. 엔트리는 아래와 같은 IMAGE_IMPORT_DESCRIPTOR 구조체로 이루어져 있으며 사용하는 DLL 갯수 + 1개(빈 엔트리)만큼 Import Directory Table에 등록되어있다.

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
 union {
 DWORD Characteristics;
 DWORD OriginalFirstThunk;
 };
 DWORD TimeDateStamp;
 DWORD ForwarderChain;
 DWORD Name;
 DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;

MSDN에서도 해당 구조체의 정보를 찾을 수 없었기에 이곳을 참조했다. PEview에서 분석한 것과 비교하면 Characteristics 필드가 존재하지 않는데 이는 현재 사용되지 않는 멤버 변수라고 한다. 공용체(union)인 만큼 OriginalFirstThunk가 혼자 존재한다고 봐도 무방할 것이다.

아무튼 필드 OriginalFirstThunk, FirstThunk는 각각 Import Name Table RVA, Import Address Table RVA에 해당하는데 MSDN에 따른 각 필드의 용도는 다음과 같다.

  • Import Name Table RVA: Import Lookup Table에 대한 RVA값으로 이 테이블에서는 각 임포트에 대한 이름이나 순서(ordinal)를 담고 있다.

  • Time Date Stamp: 이미지가 바운드되기 전까지는 0값으로 채워져 있으며 실제로 프로그램이 실행되어 이미지가 바운드된다면 DLL의 타임스탬프 값으로 채워진다.

  • Forwarder Chain: 첫번째 forwarder 참조에 대한 인덱스 값이다.

  • Name RVA: DLL 파일 이름의 아스키 문자열의 주소값이다. Image Base에 상대적인 값이다.

  • Import Address Table RVA: Import Address Table에 대한 RVA 값이다. 이 테이블은 이미지가 바운드되기 전까지는 Import Lookup Table과 동일한 값을 가진다.

이 Import Name Table, Import Address Table의 RVA는 어디에 사용하는 걸까? 이는 Image Base와 결합하여 INT, IAT의 위치를 알 수 있다. Import Name Table, Import Address Table은 IMAGE_THUNK_DATA32 구조체의 배열로 구성되어 있는데 PE 파일 상태에서는 다음과 같은 동일한 데이터가 들어가 있는 것을 확인할 수 있다. Import Directory Table에서는 사용하는 DLL 정보를 저장하고 있었다면 이곳에서는 사용하는 함수 정보를 저장하고 있다.

IMAGE_THUNK_DATA32 구조체는 다음과 같은 필드로 이루어져 있다.

  public struct IMAGE_THUNK_DATA32
  {
    union {
      public uint ForwarderString;
      public uint Function;
      public uint Ordinal;
      public uint AddressOfData;
    }
  }

이 중 AddressOfData 멤버를 살펴볼 필요가 있는데 이는 IMAGE_IMPORT_BY_NAME 구조체를 가리키는 포인터로 다음과 같은 필드로 이루어져 있다.

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint 필드는 PE 로더에게 이 함수를 ordinal(고유 번호)로 임포트할지, 이름으로 임포트할지 알려주는 역할을 한다. 최상위 비트가 1이라면 이 IMAGE_IMPORT_BY_NAME 구조체 전체가 ordinal 값으로 취급되며 함수 이름을 다루지 않는다. 그렇지 않다면 Name 필드에 임포트하고자 하는 함수의 이름을 명시한다.

 

INT, IAT에서 Data 필드에 해당하는 0x0000307C, 0x0000308C, 0x0000309A를 확인해보면 실제로 해당 함수 이름이 저장된 null-termination 문자열을 확인할 수 있다. RVA 값이므로 PEview에서 확인 시 RVA 모드로 확인해야 한다.

IMPORT Hint/Names 섹션

PE 로더가 프로그램을 메모리에 로딩할 때 이 함수 이름 문자열을 가지고 해당하는 함수를 찾아 함수의 실제 주소를 Import Address Table에 채우는 것이다. 하지만 PE 파일 상태에서는 프로그램이 메모리에 로딩되지 않았기 때문에 INT, IAT 모두 PE 파일 내부에 있는 함수 이름을 가리키고 있다.

 

IMAGE_OPTIONAL_HEADER의 Image Base 필드값과 결합(VA = Image Base + RVA)하면 디버거에서도 어느 메모리 영역에 해당 함수가 저장되어 있는지 확인할 수 있다. 이를 이용하여 실제로 프로그램이 PE 로더에 의해 메모리에 로드됐을 때 IAT가 실제 함수 위치를 가리키는 주소값으로 채워지는지 확인해보기 위해 abex의 첫 번째 크랙미를 디버거로 열어본 후 실제 위치를 직접 확인해보면 다음과 같다.

INT에는 PEview로 열어본 값이 그대로 저장되어 있지만 IAT에는 함수의 실제 주소가 리틀 인디언으로 저장된 것을 확인할 수 있었다. 

현재는 여러개의 DLL을 임포트하고 있기 때문에 IMAGE_OPTIONAL_HEADER에서 Import Address Table의 IMAGE_DATA_DIRECTORY 구조체는 0값을 가지고 있다. 만약 단 하나의 DLL을 사용한다면 시작 주소와 크기가 저장된다.

 

 

[출처 | docs.microsoft.com/ko-kr/windows/win32/debug/pe-format?redirectedfrom=MSDN#import-address-table]

[출처 | tech-zealots.com/malware-analysis/journey-towards-import-address-table-of-an-executable-file/]

[출처 | en.wikipedia.org/wiki/Portable_Executable#Import_table]

[출처 | www.yes24.com/Product/Goods/27628413]

[출처 | reversecore.com/23]

[출처 | reverseengineering.stackexchange.com/questions/16870/import-table-vs-import-address-table]

[출처 | sandsprite.com/CodeStuff/Understanding_imports.html]