[조립하면서 배우는 PE] 여섯번째 이야기 : 임포트(import) ... Reverse Engineering
리버싱 2017. 1. 10. 16:36여섯번째 이야기입니다. 아마 많은 분들이 기대하시던 내용일 것이라 생각하는데요, 잘 설명할 수 있을지 걱정입니다.이번 이야기는 임포트(import)에 관한 것입니다. 이번 이야기가지금까지의 다른 것들보다 다소 복잡한 것 사실이지만 흔히 생각하는 것처럼 매우 어렵지는 않습니다. 어차피 사람이 만든건데 이해 못할 정도는 아니겠죠. 그럼 시작해볼까요?
[그림 1] 임포트 테이블, ILT, IAT
임포트 테이블(임포트 디렉토리)
임포트 테이블의 구성
[그림 1]에서 볼 수 있듯이 임포트 테이블은 IMAGE_IMPORT_DESCRIPTOR 타입의 엔트리로 구성된 배열로 임포트한 DLL에 대한 정보를 담고 있습니다. [그림 1]에서는 USER32.DLL과 KERNEL32.DLL을 임포트한 모습을 예로 들었습니다. [그림 1]을 살펴보면 임포트 테이블의 각 엔트리가 임포트한 DLL에 하나에 대한 정보를 담고 있음을 확인할 수 있습니다. 또한 마지막 엔트리는 임포트 테이블의 끝을 나타내기 위해 NULL 로 채워져 있습니다. 이는 PE 파일에서 임포트한 DLL 개수에 대한 정보를 따로 관리하지 않음을 의미합니다. 쉽게 이야기해서 PE 파일 내에 임포트한 DLL 개수를 저장하고 있는 필드는 없다는 것이죠. 어쨌든 이러한 이유로 임포트 테이블의 전체 엔트리 개수는 "임포트한 DLL + 1"개가 됩니다.
임포트 테이블을 구성하고 있는 IMAGE_IMPORT_DESCRIPTOR는 아래와 같이 선언되어 있습니다. 그림과 비교해서 살펴보세요.총 5개의 멤버로 구성되어 있습니다.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; /* 현재는 사용하지 않습니다. */
DWORD OriginalFirstThunk; /* 이 유니언은 항상 OriginalFirstThunk로만
사용됩니다.*/
} ;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk : ILT(Import Lookup Table)를 가르키는 RVA 값입니다. [그림 1]에서 볼 수 있듯이 ILT는 IMAGE_THUNK_DATA로 구성된 배열입니다. IMAGE_THUNK_DATA는 4bytes 타입의 유니언으로 상황에 따라 IMAGE_IMPORT_BY_NAME을 가르키기도 하고, 함수의 주소를 가르키기도 하며, 오디널 값으로 사용되기도 하며 포워더로 사용되기도 합니다. 복잡하죠? 일단 IMAGE_THUNK_DATA가 어떻게 선언되었는지부터 살펴보도록 하겠습니다. [그림 1]과 [그림 2], [그림 3]을 참고하여 아래의 주석을 반복해서 읽어보시면 IMAGE_THUNK_DATA에 대해서 이해하시는 데 도움이 될 것입니다.
typedef struct _IMAGE_THUNK_DATA32
{
union
{
DWORD ForwarderString;
DWORD Function; //[그림 1],[그림 2]를 보면 IAT가 바인딩되기 전에는
// ILT와 마찬가지로 IMAGE_IMPORT_BY_NAME 구조체
// 를 가르키고 있다가, 바인딩 후에는 실제 함수의 주소를
// 가르키고 있는 것을 볼 수 있습니다. 이처럼
// IMAGE_THUNK_DATA가 함수의 주소를 담고 있으면
// Function의 의미로 사용된 것입니다. 참고로 IAT를
// 구성하는 IMAGE_THUNK_DATA는 바인딩 되기 전후의
// 의미가 다른데, 바인딩 전에는 주로 AddressOfData
// 또는 Ordinal의 의미로 사용되다가 바인딩 후에는
// Function의 의미로 사용됩니다. ILT의 경우는 IAT와
// 달라서 바인딩 전후의 모습이 변경되지 않습니다.
// 또한 ILT를 구성하는 IMAGE_THUNK_DATA는
// Function의 의미로 사용되지 않습니다.
[그림 2] IMAGE_THUNK_DATA 사용(1)
DWORD Ordinal; // [그림 1]에는 ILT를 구성하는 IMAGE_THUNK_DATA
// 가 IMAGE_IMPORT_BY_NAME 구조체를 가르키는 모습만
// 나와있지만 실제로는 [그림 3]처럼 Ordinal 값을 저장
// 하고 있을 수도 있습니다. Ordinal에 대해서는
// 익스포트 섹션에 대해서 알아볼 때 다시 이야기하도록
// 하겠습니다. 다만 지금 기억해두어야 할 것은 대부분의
// 경우 ILT를 구성하는 IMAGE_THUNK_DATA는
// IMAGE_IMPORT_BY_NAME을 가르키는 RVA값
// (AddressOfData)을 저장하거나, Oridinal 값을
// 저장하고 있다는 사실입니다. (IAT를 구성하는
// IMAGE_THUNK_DATA도 바인딩되기 전에는 ILT와 동일한
// 모습을 가집니다) 좀 더 쉽게 이야기하면 ILT는 임포트
// 한 함수에 대한 Ordinal 값을 저장하고 있는 배열이거나
// 임포트한 함수에 대한 이름을 저장하고 있는
// IMAGE_IMPORT_BY_NAME 구조체의 RVA값으로 이루어진
// 배열이라는 이야기죠. IMAGE_THUNK_DATA가
// AddressOfData로 사용되었는지, 아니면 Ordinal로
// 사용되었는지는 MSB의 값으로 판단합니다. 최상위 비트
// 값이 1이면 ordinal로 사용된 것이며, 0이면
// AddressOfData로 사용된 것입니다.
[그림 3] IMAGE_THUNK_DATA(2)
DWORD AddressOfData; // [그림 1],[그림 2]를 보면 ILT를 구성하고 있는
// IMAGE_THUNK_DATA와 IAT를 구성하고 있는
// IMAGE_THUNK_DATA가 바인딩 전에는 모두
// IMAGE_IMPORT_BY_NAME 구조체를 가르키고 있는
// 것을 알 수 있습니다. 이처럼 IMAGE_THUNK_DATA
// 가 IMAGE_IMPORT_BY_NAME을 가르키면
// 바로 AddressOfData의 의미로 사용된 것입니다.
// IMAGE_IMPORT_BY_NAME은 임포트할 함수의 이름을
// 저장하고 있는 구조체입니다.
} u1;
} IMAGE_THUNK_DATA32
IMAGE_THUNK_DATA의 사용에 대해 정리하면 아래와 같습니다.
- OriginalFirstThunk가 가르키는 ILT의 구성 요소로 사용된 IMAGE_THUNK_DATA는 IMAGE_IMPORT_BY_NAME의 주소값을 저장하는 AddressOfData의 의미로 사용되거나, ordinal 값을 저장하는 용도로 사용된다.
- FirstThunk가 가르키는 IAT의 구성 요소로 사용된 IMAGE_THUNK_DATA는 바인딩 전에는 IMAGE_IMPORT_BY_NAME의 주소값을 저장하는 AddressOfData의 의미로 사용되거나, ordinal 값을 저장하는 용도로 사용된다. 바인딩 후에는 실제 함수의 주소를 나타내는 Function의 의미로 사용된다.
- IMAGE_THUNK_DATA가 ordinal 값으로 사용되는 경우 최상위 비트 즉 MSB의 값은 항상 1이다.
TimeDateStamp : 바인딩 전에는 0으로 설정되며 바인딩 후에는 -1로 설정됩니다.
ForwarderChain : 바인징 전에는 0으로 설정되면 바인딩 후에는 -1로 설정됩니다.
Name : 위의 [그림 1],[그림 2],[그림 3]에 나타난 것처럼 임포트한 DLL의 이름을 가르키는 포인터 값입니다.(RVA값)
FirstThunk : IAT(Import Address Table)의 주소(RVA)를 가지고 있습니다. IAT 역시 ILT처럼 IMAGE_THUNK_DATA 배열이며 바인딩 전에는 ILT와 완벽하게 동일한 모습을 가집니다. 하지만 일단 PE 파일이 메모리에 로드된 후에는 로더가 임포트 테이블의 각 엔트리의 네임 정보를 확인한 후 해당 DLL의 익스포트 테이블을 참조하여 함수의 실제 주소를 알아냅니다. 그리고 나서 IAT를 실제 함수 주소로 업데이트 하게 됩니다.
별거 없네요. ^^; 대충 임포트 테이블의 모습과 임포트 과정이 눈에 보이시나요? 임포트 과정에 대해서는 이번 이야기의 마지막에 다시 한번 정리해보겠습니다. 그 전에 임포트 테이블의 위치에 대해서 잠깐 알아보도록 하죠.
임포트 테이블의 위치
임포트 테이블은 보통 임포트 섹션의 시작점에 위치합니다. 또한 각 섹션에 메모리 상의 위치나 파일 상태에서의 위치는 나중에 알아볼 섹션 테이블에 기록되어 있습니다. 이러한 이유에서 인지 몇 몇 문서나 책에서는 파일 상태에서 임포트 테이블의 위치를 찾을 때 섹션 테이블에서 임포트 섹션의 시작 주소를 찾는 방법을 사용하곤 하더군요. 하지만 이 방법은 정확한 방법이 아닙니다. 임포트 테이블이 반드시 임포트 섹션에 위치하는 것이 아니기 때문입니다. 실제로 임포트 섹션을 생성하지 않고 데이터 섹션에 임포트 테이블을 두는 경우도 종종 볼 수 있습니다. 이러한 경우에는 섹션 헤더에 포함된 정보만으로는 임포트 테이블을 찾을 수가 없는 것은 당연하겠죠. 로더의 입장에서 섹션 헤더의 정보는(섹션의 파일상/메모리 상의 위치와 사이즈 정보) 섹션 헤더에 기록된 파일의 위치에서부터 지정된 사이즈 만큼의 데이터를 메모리 상의 지정된 위치로 복사하는데 필요할 뿐 입니다. 그 이상도 그 이하도 아니죠. 더구나 로더는 디스크 상에 임포트 테이블이 어디에 위치하는지 알 필요가 없습니다. 디스크 상에서 임포트 테이블을 찾아 따로 로딩하는게 아니기 때문입니다. 섹션을 로딩하는 과정에서 섹션 데이터의 일부인 임포트 테이블은 자연스럽게 메모리 상에 위치하게 되는 것이죠. 이러한 이유로 메모리에 로드되기 전 즉 파일 상태에서의 임포트 테이블의 위치를 직접적으로 가르키는 정보는 PE 파일 어디에도 저장되어 있지 않습니다. 그럼에도 불구하고 필요에 따라 파일 상에서 임포트 테이블을 찾아야 한다면 그 방법은 데이터 디렉토리에서 찾을 수 있습니다.좀 더 자세히 알아볼까요? 데이터 디렉토리의 두번째 엔트리인 IMAGE_DIRECTORY_ENTRY_IMPORT에는 임포트 테이블이 시작되는 가상 주소의 RVA값과 사이즈가 저장되어 있습니다. 파일 상태에서야 임포트 테이블의 주소를 알 필요가 없었지만, PE 파일이 메모리에 완전히 로드된 다음에는 임포트 테이블을 찾아 임포팅에 필요한 작업을 해주어야 하기 때문에 이러한 정보를 유지하고 있는 것이죠. 어쨌든 임포트 테이블의 RVA 값을 알 수 있다면 파일 상태에서의 임포트 테이블의 위치도 어렵지 않게 알 수 있습니다. 아래 [그림 4]를 봐주세요.
[그림 4] StudPE로 살펴본 Import Table 정보
[그림 4]의 빨간 박스 부분을 살펴보면 재미있게도 PE 파일에는 존재하지 않는 정보가 보입니다. "Raw"라는 항목인데요 PE 파일에서 "raw"라는 단어는 메모리에 로드되기 전의 상태를 의미합니다. [그림 4]에서의 "Raw"는 RawOffset 즉 파일 상에서의 위치를 의미하는 것이겠죠. 지난 이야기에서 알아본 바와 같이 데이터 디렉토리에는 VirtualAddress와 Size 정보 밖에는 없습니다. 그렇다면 StudPE는 어떠한 방법으로 파일 상에서의 임포트 테이블의 위치를 알 수 있었을까요? [그림 5]을 봐주세요.
[그림 5] StudPE로 살펴본 섹션 테이블 정보
[그림 5]에서 살펴본 임포트 테이블의 RVA값은 0x2030 입니다. 따라서 임포트 테이블은 [그림 5]에 나타난 섹션 중 .rdata 섹션에 위치하고 있음을 알 수 있습니다. .rdata 섹션(RVA 0x2000)의 RawOffset 즉 파일 상에서의 위치가 0xA00 이므로, 임포트 테이블(RVA 0x2030)의 파일 상의 위치는 당연히 0xA30이 되겠죠. 생각보다 쉽네요. 지금까지의 내용을 정리해 보겠습니다.
- 임포트 섹션이 존재하는 경우 임포트 테이블은 대부분 임포트 섹션의 시작점에 위치한다. 하지만 반드시 시작점에 있어야 하는 것은 아니며 임포트 섹션내 아무 곳에나 위치하는 것이 가능하다
- 임포트 테이블은 임포트 섹션에만 존재해야 하는 것은 아니다. 다시 말해 임포트 섹션을 생성하지 않고 데이터 섹션등에 임포트 테이블을 두는 것이 가능하다.
- 섹션 헤더에 포함된 임포트 섹션에 대한 정보는 로딩 과정 중 파일 상에서 임포트 섹션을 식별하고 메모리에 로딩하기 위해 사용할 뿐이다. 임포트 섹션에 대한 섹션 헤더 정보는 임포트 과정과는 무관하다.
- PE 파일이 메모리에 로딩되고 나면 임포트 어드레스 테이블(IAT)을 수정해 주어야 한다. 따라서 로더는 메모리 상에서 임포트 테이블(IAT의 주소 정보를 가지고 있음)의 위치를 알 수 있어야 하는데, 이 정보는 데이터 디렉토리의 두번째 엘리먼트인 IMAGE_DIRECTORY_ENTRY_IMPORT 에 저장되어 있다.
- 로더가 파일 상태에서의 임포트 테이블의 위치를 알 필요는 없기 때문에 PE 파일 포맷 내에 임포트 테이블의 RawOffset 값을 직접 저장하고 있는 필드는 존재하지 않는다.
- 파일 상태에서 임포트 테이블의 주소는 섹션 헤더 정보와 데이터 디렉토리에 기록된 정보를 비교하여 확인할 수 있다.
임포트 테이블을 메모리상에서 또는 파일 상에서 어떻게 찾아야 하는지 감이 좀 잡히셨나요? [그림 1]의 그림이 메모리상에서 임포트 테이블의 위치를 나타내고 있다는 사실도 이해되시죠? 이제 임포트에 대한 이야기를 마무리 할 시간이 된 것 같습니다.
임포트 과정
지금부터는 [그림 1]을 봐주시면 됩니다. 임포트와 관련되어 알아두어야 할 구조체는 임포트 테이블을 구성하는 IMAGE_IMPORT_DESCRIPTOR와 ILT와 IAT를 구성하는 IMAGE_THUNK_DATA 그리고 임포트할 함수의 이름을 저장하고 있는 IMAGE_IMPORT_BY_NAME 뿐입니다. 사실 그 다지 복잡할 것이 없는 구조이죠. 각 구조체간의 관계는 [그림 1]을 통해 정리하시면 되겠습니다.
지금까지의 내용만으로도 충분히 임포트 과정을 이해할 수 있을 것이라 생각합니다만, 그래도 한 번 더 정리해 보도록 하겠습니다.
1. PE 파일을 메모리에 로드한 후 데이터 디렉토리의 두번째 엔트리인 IMAGE_DIRECTORY_ENTRY_IMPORT로 부터 임포트 테이블의 주소를 구한다.
2. 임포트 테이블을 구성하는 각각의 IMAGE_IMPORT_DESCRIPTOR로 부터 임포트 할 DLL의 이름을 알아낸다.
3. 해당 DLL을 위한 공간을 확보하고 DLL을 메모리에 맵핑시킨다.
4. ILT(Import Lookup Table)로 부터 임포트할 함수의 이름 또는 ordinal 값을 알아낸다.
5. 위의 정보를 이용하여 임포트할 DLL의 익스포트 테이블로 부터 실제 함수의 주소를 알아낸다.
6. 알아낸 함수의 주소를 IAT에 기록한다.
-.-; 비교적 간단하지 않나요? IAT를 업데이트 하는 것은 로더의 몫이므로 우리는 바인딩 전의 모습만 정확하게 만들어 주면 되겠습니다. 그런데 작업하기 전에 한가지를 더 알아야 하겠군요. 바로 익스포트에 관련된 정보입니다.
To be Continue ...
출처 - http://zesrever.tistory.com/54