Malware Analysis/PE

#5 PE - IAT

geunyeong 2021. 8. 1. 18:23

Abstract

IAT는 PE 프로그램이 사용할 외부 라이브러리(DLL 등)의 파일명과 해당 외부 라이브러리에서 가져올 함수의 이름을 명시하는 영역이다. 파일에서의 IAT 영역은 외부 라이브러리 파일명과 함수의 이름을 가리키는 오프셋 값이 들어있지만, PE 로더에 의해 메모리로 로드된 후에는 외부 라이브러리의 가상 메모리 영역에 존재하는 함수의 시작 주소 값으로 변환된다.

Import Address Table

Overview

IAT는 Import Address Table로 PE 파일이 사용하는 외부 API 함수의 주소들을 배열 형태로 저장해놓는 영역을 의미한다. IAT의 위치는 Optional 헤더의 DataDirectory 배열중 13번째 원소(DataDirectory[12])에 저장되어 있다.

 

IAT를 나타내는 OptionalHeader.DataDirectories[12]

 

DataDirectory가 나타내는 주소 값은 RVA이므로 ImageBase를 더한 값이 메모리 상에서의 IAT가 된다. 아래 그림은 메모리 상에서의 IAT 영역과 IAT 중 첫번째 주소값이 가리키는 위치의 코드를 나타낸 그림이다. IAT 중 첫번째 주소값인 76354F20h로 가보면 UnhandledExceptionFilter API 함수의 시작 코드가 나타나는 것을 볼 수 있다. 이처럼 IAT란 외부 API 함수들의 시작 주소가 배열 형태로 저장된 영역을 말한다.

 

메모리 상에서의 IAT 영역

 

프로그램은 IAT 영역의 값을 가져와 그 주소로 점프한다. 아래 그림에서 MessageBoxA 함수를 호출하는데 MessageBoxA 함수 주소로 바로 점프하는 것이 아니라 MessageBoxA 주소가 저장된 402034h를 참조해 그 주소로 call하는 모습을 볼 수 있다. 이러한 방식을 사용하면 DLL 재배치 등으로 인해 API 함수의 시작 주소가 달라져도 유연하게 대처할 수 있다는 장점이 있다.

 

IAT에 저장된 MessageBoxA 함수 주소를 가져와 해당 주소로 call

 

IAT 복원 방법

PE 파일이 메모리에 로드되면 IAT가 자동으로 구성된다. 메모리가 아닌 파일 상에서 IAT를 복원하는 방법은 다음과 같다.

  1. Optional 헤더의 Import Directory를 찾는다.
  2. Import Directory 영역에서 IID를 분석한다.
  3. import 하는 API 함수 이름과 DLL 이름을 찾는다.

RVA to Raw

PE 파일은 섹션 헤더의 PointerToRawData를 제외하면 모든 주소 값이 RVA(Relative Virtual Address)로 작성되어 있다.  RVA는 ImageBase로부터의 거리를 의미한다. 이러한 상대거리를 이용하는 이유는 대부분의 주소 값들이 메모리에 로드되고 나서 사용되기 때문이기도 하고, DLL의 ImageBase 재배치나 ASLR 같이 ImageBase 값은 변동이 가능하기 때문에 ImageBase로부터 얼마나 떨어져 있는지를 사용하는 것이다. 만약 어느 문자열의 주소 값이 40203Ch이고, ImageBase가 400000h라면 문자열의 RVA 값은 203Ch가 된다.

  • VA(Virtual Address) = OptionalHeader.ImageBase + RVA
  • 예) OptionalHeader.ImageBase(400000h) + RVA(203Ch) = 40203Ch(VA)

때문에 파일에서 RVA 주소값을 참조하려면 Raw 주소로 변환해야 한다. RVA 값을 Raw 주소로 변환하려면 우선 RVA 값이 어느 섹션에 속하는지를 봐야 한다. 

 

섹션 헤더

 

각 섹션이 저장되는 영역의 RVA 범위는 아래와 같다.

  • .text 섹션: 1000h ~ 1D7Eh
  • .rdata 섹션: 2000h ~ 2A98h
  • .data 섹션: 3000h ~ 33A4h
  • .rsrc 섹션: 4000h ~ 41E0h

만약 RVA가 203Ch라면 .rdata 섹션에 속할 것이다. RVA 값을 Raw 주소 값으로 변환하는 식은 아래와 같다.

  • Raw 주소 값 = RVA - RVA가 속한 섹션의 VA + RVA가 속한 섹션의 PointerToRawData
  • 예) RVA(203Ch) - RVA가 속한 섹션의 VA(2000h) + RVA가 속한 섹션의 PointerToRawData(1200h) = 123Ch

IID

IID란 Image Import Descriptor라는 의미다. Optional 헤더의 Import Directory(DataDirectory[1])에 IID가 위치해있는 영역의 주소와 크기가 저장되어 있다.

 

OptionalHeader.DataDirectories[1]

 

Import Directory가 가리키고 있는 주소로 가면 IID 구조체의 배열이 나타난다. Import Directory의 RVA 값을 Raw 주소 값으로 변환한 후 파일 데이터에서 찾아가보면 아래 그림과 같다. 붉은 박스로 표시된 곳이 IID 구조체 배열이다. 하나의 IID는 20바이트 크기이며, IID의 모든 값이 널 바이트로 채워진 영역을 만나면 IID 배열의 끝으로 인식한다. 즉 IID의 개수를 별도로 저장하는 영역이 없다. 붉은 박스 영역을 20바이트 씩 나누어보면 총 8개의 IID가 존재한다(NULL IID까지 합하면 9개).

  • RVA(250Ch) - RVA가 속한 섹션의 VA(2000h) + RVA가 속한 섹션의 PointerToRawData(1200) = 170Ch

 

파일 상에서의 IID 배열

 

IID는 어떤 DLL로부터 어떤 API를 사용할 것이다라는 것에 대한 정보를 가지고 있다. IID는 IMAGE_IMPORT_DESCRIPTOR라는 구조체를 사용하며 winnt.h에 정의되어 있다.

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

주요 멤버는 다음과 같다.

  • OriginalFirstThunk: INT(Import Name Table)로 임포트 하려는 함수의 이름이 나열되어 있다.
  • Name: 임포트 하려는 함수가 어느 DLL에 속해있는지 알려주기 위한 DLL 이름이 저장된 영역의 RVA 값이다.
  • FirstThunk: IAT의 위치다. 메모리 상에서는 API 함수의 시작 주소가 저장되지만 파일에서는 INT처럼 함수의 이름이 나열되어 있다.

앞서 본 IID 배열 중 첫번째 IID를 통해 설명을 이어가겠다.

 

파일 상에서의 IID 배열 중 첫번째 IID 요소

 

위 그림 속 IID를 IMAGE_IMPORT_DESCRIPTOR 구조체로 분석해보면 아래와 같다.

OriginalFirstThunk 25F4h
TImeDateStamp 0
ForwarderChain 0
Name 2692h
FirstThunk 2034h

IID - Name

앞서 말했듯 임포트 하려는 함수가 속해있는 DLL의 이름이 저장되어 있다. Name 멤버의 값인 2692h를 Raw 주소 값으로 변환하고 파일 내에서 해당 오프셋으로 가보면 USER32.dll이라는 DLL 이름이 저장되어 있다. 즉 첫번째 IID는 USER32.dll이 Export하는 API를 가져오는 IID임을 알 수 있다.

  • RVA(2692h) - Section's VA(2000h) + Section's PointerToRawData(1200h) = 1892h

 

첫번째 IID의 Name이 가리키는 위치로 가보면 USER32.dll이라는 문자열이 나타남

 

IID - OriginalFirstThunk

해당 DLL에서 Import 하려는 API 함수의 Order(위수)와 이름이 담겨있다. 단 OriginalFirstThunk 값이 가리키는 위치에 바로 나타나는 것은 아니고 한번 더 참조해야 한다. 왜냐하면 하나의 DLL에서 2개 이상의 API를 Import 하는 경우도 있기 때문이다. 앞서 IID 배열과 마찬가지로 OriginalFirstThunk가 가리키는 RVA 배열도 NULL 바이트로 배열의 끝을 나타낸다. 앞서 본 IID에서 OriginalFirstThunk 값을 Raw 주소 값으로 변환하면 아래와 같다.

  • RVA(25F4h) - Section's VA(2000h) + Section's PointerToRawData(1200h) = 17F4h

 

첫번째 IID의 OriginalFirstThunk가 가리키는 위치로 가보면 또다른 RVA 값이 나타남

 

OriginalFirstThunk 값이 가리키는 위치로 가면 또다시 RVA 값이 나타난다. 이 RVA 값을 Raw로 변환하고 참조하면 IMAGE_IMPORT_BY_NAME 구조체가 나타난다.

  • RVA(2684h) - Section's VA(2000h) + Section's PointerToRawData(1200h) = 1884h

 

IID의 OriginalFirstThunk가 가리키는 위치에 저장된 값을 다시 참조하면 함수 이름이 나타남

 

1884h 오프셋으로 가면 2바이트 크기의 Order와 그 뒤로 Import 하려는 함수의 이름이 나타난다. 즉 USER32.dll의 MessageBoxA 함수를 Import 한다는 것이다. 1884h 오프셋에 있는 구조가 IMAGE_IMPORT_BY_NAME 구조체다.

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

 

IID - FirstThunk

IAT를 구성하기 위해 IAT 영역에 쓸 API의 이름을 나타낸다. 앞서 OriginalFirstThunk와 마찬가지로 이 값을 참조하면 IMAGE_IMPORT_BY_NAME 을 가리키며 NULL 바이트로 그 끝을 나타내는 RVA 배열이 나타난다 .

  • RVA(2034h) - Section's VA(2000h) + Section's PointerToRawData(1200h) = 1234h

 

첫번째 IID의 FirstThunk가 가리키는 위치로 가보면 또다른 RVA 값이 나타남

 

FirstThunk가 가리키던 값 2034h(Raw 1234h)에 저장된 값 2684h를 다시 Raw 주소로 변환해 참조하면 IMAGE_IMPORT_BY_NAME 구조체를 볼 수 있다.

 

첫번째 IID가 가리키는 위치에 저장된 값을 다시 참조하면 함수 이름이 나타남

 

OriginalFirstThunk를 통해 갔던 1884h다. 이처럼 INT와 IAT는 서로 같은 IMAGE_IMPORT_BY_NAME을 가리키는데 INT와 IAT를 나눈 이유는 항상 INT와 IAT가 같지 않을 수 있기 때문이다. 즉 INT에는 있는데 IAT는 없는 API가 있을 수 있다는 의미다. 또 한가지 차이점은 INT는 메모리에 로드되어도 아무런 변화가 없지만 IAT는 메모리에 로드되면 PE 로더에 의해 FirstThunk가 가리키던 위치에 API 주소가 쓰여진다.

FirstThunk가 가리키던 값인 2034h에 ImageBase를 더해 VA로 변환한 후 메모리를 들여다보면 2684h가 아닌 MessageBoxA 함수의 시작 주소 7693ED60h로 변경된 것을 볼 수 있다.

 

메모리 상에서의 첫번째 IID의 FirstThunk 값이 USER32.dll의 MessageBoxA 함수 시작 주소로 변환된 모습

IATViewer

Python의 pefile 모듈을 사용하면 IAT를 쉽게 복원할 수 있다. 아래는 IAT를 복원하는 스크립트다.

import sys
import pefile

def view_iat(filepath):
    pe = pefile.PE(filepath)

    for iid in pe.DIRECTORY_ENTRY_IMPORT:
        print(iid.dll.decode('utf-8'))

        for api in iid.imports:
            print('\t+ [{:06X}] {}'.format(api.address, api.name.decode('utf-8')))
        
        print()


if __name__ == '__main__':
    if len(sys.argv) != 2:
        print('Usage: {} <PE file>'.format(sys.argv[0]))
        exit(1)

    view_iat(sys.argv[1])

Github

git: https://github.com/geun-yeong/IATViewer

'Malware Analysis > PE' 카테고리의 다른 글

#7 PE - Rich 헤더  (0) 2021.08.01
#6 PE - .reloc 섹션  (0) 2021.08.01
#4 PE - Section Header  (0) 2021.08.01
#3 PE - Optional Header  (1) 2021.08.01
#2 PE - File Header  (0) 2021.08.01