02 PE 파일 분석-Import 분석
원문 : http://kkamagui.springnote.com/pages/404563
들어가기 전에...
- 이 글은 kkamagui에 의해 작성된 글입니다.
- 마음껏 인용하시거나 사용하셔도 됩니다. 단 출처(http://kkamagui.tistory.com, http://kkamagui.springnote.com)는 밝혀 주십시오.
- 기타 사항은 mint64os at gmail.com 이나 http://kkamagui.tistory.com으로 보내주시면 반영하겠습니다.
- OS 제작에 대한 상세한 내용은 책 "64비트 멀티코어 OS 구조와 원리"를 참고하기 바랍니다.
개요
지난번에 PE 파일의 헤더구조에 대해서 알아보았다. 잘 기억이 안나면 01 PE 파일 분석-헤더분석 문서를 다시 보자. 이번에 분석할 부분은 PE 파일에 포함된 Import 영역이다.
Import 영역은 PE 파일이 실행될 때 외부로부터 당겨와서 사용하는 함수들의 목록이 포함되어있으며, Export 영역은 다른 모듈이 사용할 수 있게 노출해 놓은 함수들이 들어있다.
일단 Import 영역부터 살펴보자.
Import 영역 분석
Import 영역은 PE 파일내의 특정 섹션에 자리잡고 있으며, IMAGE_OPTIONAL_HEADER의 DataDirectory에서 위치를 찾을 수 있다. DataDirectory내의 인덱스는 0x01이며 IMAGE_DIRECTORY_ENTRY_IMPORT 매크로로 WinNT.h에 정의되어있다. Data Directory가 포함하는 정보는 해당 위치의 RVA 값과 크기 정보 이다.
분석하는 데이터는 실제 파일의 형태로 되어있으므로, 여기서 부터는 섹션의 RVA와 PointerOfRawData의 값을 이용해서 찾아야 한다. RVA 값을 이용해서 어떻게 실제 섹션 데이터가 있는 파일 내의 위치를 찾을까? 답은 섹션 정보에있는 RVA값과 Virtual Size 그리고 PointerOfRawData의 값을 이용하는 것이다.
그럼 함수를 하나 만들자. 함수의 원형은 아래와 같이 RVA를 넣어주면 실제 파일에서 존재하는 위치를 계산해 주는 것이다.
- DWORD GetPointerOfRawDataFromRVA( DWORD dwRVA );
저 함수를 이용해서 실제 섹션내에 존재하는 Import 섹션의 위치를 찾아보자. 어떻게 해야 할까? Import 섹션은 IMAGE_OPTIONAL_HEADER의 Data Directory[ IMAGE_DIRECTORY_ENTRY_IMPORT ]에서 찾을 수 있다고 했다. Virtual Address를 위 함수에 넣으면 실제 파일내의 Offset을 구할 수 있다.
Import 섹션은 IMAGE_IMPORT_DESCRIPTOR 구조체로 시작한다. 해당 구조체의 배열 형태로 구성되며 마지막은 NULL 구조체로 되어있어 끝을 표시한다. 아래는 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)
};
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 : IMAGE_THUNK_DATA 구조체 RVA 주소를 가리킴. 이때 IMAGE_THUNK_DATA는 Import하는 함수 이름이나 서수(Ordinal)를 포함하는 구조체 IMAGE_IMPORT_BY_NAME을 가리킴
- Name : Import한 DLL의 이름을 담고 있는 ASCII 문자열의 RVA 주소
- FirstThunk : OriginalFirstThunk와 같이 IMAGE_THUNK_DATA 구조체의 RVA 주소를 가리키지만 PE 파일이 메모리에 맵핑되고 나면 Import 한 DLL 내의 함수 주소가지고 있는 IMAGE_THUNK_DATA를 가리킴
위의 항목을 보면 Import 섹션에 외부로 부터 당겨쓰는 DLL이나 함수 정보를 모두 포함하고 있음을 알 수 있다. 그럼 OriginalFirstThunk 및 FirstThunk가 가리키는 구조체인 IMAGE_THUNK_DATA의 구조는 어떻게 되어있을까?
WinNT.h에서 IMAGE_THUNK_DATA에 대한 구조체를 보면 아래와 같이 단순한 공용체임을 알 수 있다.
- typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64; - typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
공용체이기 때문에 상황에 따라 쓰임새가 다르다는 것을 알 수 있는데, 각 상황에 따라 아래와 같이 해석된다.
- ForwarderString : 실제 Import 한 함수가 Forwarding된 함수일 경우. Forwarding에 대해서는 Export에서 설명
- Function : FirstThunk의 경우 메모리에 로딩이 되면 실제 함수 주소를 가리키는 IMAGE_THUNK_DATA의 RVA값을 표시한다고 했는데, 이때 실제 Function의 주소가 포함된 부분
- Ordinal : Import를 함수 명이 아니라 서수(Ordinal)로 한 경우. 0x80000000 값으로 마스크하면 최 상위 비트를 구할 수 있는데 이것이 1로 셋팅된 경우 서수로 판단
- AddressOfData : 실제 Import 된 함수의 이름이 포함된 IMAGE_IMPORT_BY_NAME 구조체에 대한 RVA 주소 포함
위의 공용체를 보면 크게 함수 이름과 서수를 통한 Import가 가능하다는 것을 알 수 있다. 이 말은 곧 export 시에 서수와 함수 이름 모두를 외부로 노출한다는 말인데, 후에 export 섹션을 분석하면서 차차 알아보자.
이제 살펴볼 마지막은 실제 함수 이름을 포함하는 IMAGE_IMPORT_BY_NAME 구조체이다. 이 구조체는 WinNT.h에 아래와 같이 표시되어있다.
- typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
각 항목은 아래와 같다.
- Hint : Import시 서수를 사용하지 않으면 임의로 설정되는 0 base의 숫자, Import 한 함수가 추가될때마다 1씩 증가. 별로 중요한 값은 아닌듯...
- Name : NULL로 끝나는 Ascii 문자열. 실제 함수 이름을 포함
굉장히 복잡한 구조체들이 얼기설기 엮여있는데, 이것을 메모리 로딩 전의 PE 파일과 메모리 로딩 후의 PE 파일을 비교해서 그림으로 보면 좀 간단하다.
<메모리에 로딩 전 PE 파일 구조>
<메모리 로딩 후 PE 파일의 구조>
Import 섹션을 찾는 순서는 아래와 같다(위 그림을 참조하면서 같이보자).
- Data Directory 또는 섹션 헤더에서 Import 섹션의 시작을 찾는다.
- 시작부분은 IMAGE_IMPORT_DESCRIPTOR의 구조체로 시작하고 n개가 존재한다. 끝은 구조체의 모든 데이터가 0이면 끝이다.
- Name의 RVA를 따라가서 DLL 이름을 뽑는다.
- Original First Thunk 또는 First Thunk의 RVA를 따라가서 IMAGE_THUNK_DATA의 구조체 배열에 접근한다. IMAGE_IMPORT_DESCRIPTOR와 마찬가지로 n개가 존재하며 마지막은 구조체의 모든 데이터가 0인지로 판단한다.
- IMAGE_THUNK_DATA의 필드 값에 따라 파일명으로 함수를 Import 했는지 서수(Ordinal)로 Import 했는지 판단하여 해당 정보를 뽑는다.
실제 구현
자 이로써 Import 섹션을 분석할 수 있는 모든 조건이 갖추어졌다. 실제 위에서 나온 구조체를 바탕으로 Import 섹션을 한번 분석해 보자.
주의 할 것은 Borland 사의 링커의 경우 Original First Thunk를 아예 사용하지 않는 다는 것이다. 다행이도 First Thunk 항목은 다 연결되어있으니 First Thunk를 이용해서 덤프하는 것으로 하자. 사실 정확하게 구현을 하려면 Forwarder String까지 해야 하나 예가 드물어서 서수 아니면 함수명이라 가정하고 프로그램을 작성하였다. 혹 문제가 발생하면 ForwarderString 쪽을 처리하면 될 것 같다.
- PEAnalyzer.h : PE 파일을 분석하는 클래스의 헤더 파일
- PEAnalyzer.cpp : PE 파일을 분석하는 클래스의 소스 파일
- main.cpp : 실제 사용하는 예제
아래는 실행 결과이다.
<서수(Ordinal)로 Import 한 경우>
<이름으로 Import 한 경우>
마치면서
지금까지 PE 파일에서 Import 한 함수들을 분석하는 방법을 알아보았다. Import 섹션은 보안 분야에서 상당히 중요하게 다뤄지고 있는 부분이다. 고전적인 루트킷 같은 경우, 실행 중인 프로세스에 DLL을 삽입하여 Import 섹션의 함수를 후킹해 정보를 빼내는 기능을 하였고, 실행 중인 프로세스에 Import 섹션의 함수를 모니터링 함으로써 후킹을 감시하는 기능도 할 수 있다.
물론 명시적 로딩(Explicit Loading) 즉 LoadLibrary() 같은 함수를 이용하여 DLL을 로딩하고 함수를 사용하는 경우는 Import 섹션에 데이터가 그리 많이 존재하지 않겠지만 명시적 로딩을 사용하더라도 반드시 2개의 함수 즉 LoadLibrary(), GetProcAddress() 와 같은 함수는 필요하기 때문에 Import 섹션을 분석하는 것은 의미가 있다.
다음은 Export 섹션에 대해서 알아보도록 하자.
첨부
이 글은 스프링노트에서 작성되었습니다.
'Windows System Application' 카테고리의 다른 글
04 PE 파일 분석-Relocation 분석 (4) | 2007.11.14 |
---|---|
03 PE 파일 분석-Export 분석 (0) | 2007.11.14 |
00 윈도우 DLL 분석 (1) | 2007.11.14 |
01 FAT 파일 시스템(File System) (4) | 2007.11.14 |
00 Window I/O 관련 (2) | 2007.11.14 |