Mafia의 진실
Mafia의 진실 2010.02.10

낚시성 제목에 방문하신 분께는 죄송합니다. 이 글은 오래전부터 twitter 사용자들을 대상으로 퍼져나가고 있는 'Mobster world'라는 온라인 MMORPG 게임과 관련된 글입니다. 트위터 사용자 분들 중에는 위와 유..

iPhoto 슬라이드 쇼

트위터에 질문이 하나 올라와 짧게 작성해봤습니다. iPhoto에서 슬라이드 쇼 생성하는 방법입니다. 1. 먼저 사진 메뉴에서 슬라이드 쇼에 추가할 사진을 선택합니다. 사진 선택은 Command 키와 마우스를 이용합니다. 2...

노키아에서 동작하는 Mac OS X 10.3

핀란드에 사는 Toni Nikkanen이라는 분이 자신의 Nokia N900 모델에 Mac OS 10.3 Panther를 설치하고 실행시키는데 성공했다고 합니다. Toni의 블로그에 가보니 Mac OS X 말고도 Windows..

사용자 삽입 이미지
섯번째 이야기입니다. 아마 많은 분들이 기대하시던 내용일 것이라 생각하는데요, 잘 설명할 수 있을지 걱정입니다.이번 이야기는 임포트(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 ...

Posted by zesrever

다섯번째 이야기입니다. 이번 이야기는 좀 짧을 것 같네요. ^^; Data Directory 자체보다는 Data Directory가 가르키는 자료 구조들이 중요하죠. 어쨌든, 자~ 빠.아져 볼.까요? 

Data Directory
데이터 디렉토리는 PE 헤더의 마지막에 위치한 128bytes 사이즈의 배열입니다.(여기에 대해서는 사실 확인 작업이 좀 필요할 것 같습니다. 누차 밝혀온 것 처럼 데이터 디렉토리는 옵션이어서 존재하지 않을 수도 있고 존재하는 경우 16개의 엔트리를 가져야 한다고 했습니다. 이호동님께서 저술하신 "Windows 시스템 실행 파일의 구조와 원리"의 76page 표2-4와 80page의 설명을보면 데이터 디렉토리의 엘리먼트 개수는 무조건 16이라고 못박아 놨더군요. 하지만 최근의 인터넷 상의 논의들을 살펴보면 반드시 16개일 필요는 없는 것 같습니다. 제가 직접 확인해보지 못한 관계로 사실로 규정하기는 어렵지만, 사실 실험 자체가 어려운 것은 아니라 곧 확인하고 연재 마치기 전에 결과를 말씀드리도록 하겠습니다. ) 각 배열 구성 요소(이하 엘리먼트)들은 IMAGE_DATA_DIRECTORY 타입을 가지는데 아래와 같이 정의 되어 있습니다.

  typedef struct _IMAGE_DATA_DIRECTORY {
      DWORD   VirtualAddress;
      DWORD   Size;
  } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Optional Header의 뒷부분에 있던 NumberOfRvaAndSize 라는 이름의 필드가 데이터 디렉토리의 엘리먼트 수를 저장하고 있다고 했던 것 기억나시죠? 위 구조체를 보면 필드의 이름에 이유가 있음을 알 수 있을 것입니다.
 데이터 디렉토리의 각 엘리먼트는 익스포트 테이블, 임포트 테이블 등 PE 파일에서 중요한 역할을 담당하는 개체들의 위치(VirtualAddress)와 크기(Size)에 대한 정보를 가지고 있습니다. 아래 데이터 디렉토리의 전체적인 모양을 그려두었습니다.
 

사용자 삽입 이미지


위 그림에 나타나있는 데이터 디렉토리내의 엘리먼트의 데이터 타입은 이름만 다르고 실제 모양은 모두 같습니다. 예를 들어 IMAGE_DIRECTORY_ENTRY_EXPORT도 IMAGE_DATA_DIRECTORY  로 정의되어 있으며 IMAGE_DIRETORY_ENTRY_IMPORT도 IMAGE_DATA_DIRECTORY로 정의되어 있습니다.  또한 0-15번까지의 엔트리만 유효한 값을 가지고 있으며 맨 마지막 엘리먼트는 데이터 디렉토리의 끝을 나타내기 위해 8byte 전체가 0x00으로 채워져 있습니다. (이것만 봐도 데이터 디렉토리의 엘리먼트가 꼭 16개가 아니라도 될 것 같다는 생각이 듭니다. 16개로 고정되어 있는 것이라면 끝을 나타내는 엘리먼트가 필요없겠죠)

15개의 엘리먼트 모두를 알아두어야 하는 것은 아닙니다. 일단 색칠되어 있는 부분들은 알아야 하는데 처음 공부하는 분들은 첫번째 엘리먼트(EXPORT), 두번째 엘리먼트(IMPORT), 열번째 엘리먼트(TLS) 정도만 공부하면 되겠습니다. 좀 더 익숙해지면 지연로딩에 관련된 항목들도 공부해야 하는데 처음부터 완벽할 필요는 없죠. ^^; 

IMAGE_DIRECTORY_ENTRY_EXPORT : 익스포트 테이블의 메모리 상에서의 시작점과 크기에 대한 정보를 가지고 있습니다. 익스포트 테이블은 대부분 DLL에 존재합니다. 익스포트 테이블에 대한 자세한 이야기는 뒤에서 다루도록 하겠습니다.

IMAGE_DIRECTORY_ENTRY_IMPORT : 임포트 테이블의 메모리 상에서의 시작점과 크기에 대한 정보를 가지고 있습니다. 아마 가장 먼저 공부해야 하고 가장 잘 알아야 하는 부분일 것 같습니다. 리버싱을 하다보면 메뉴얼 언패킹을 하는 경우가 있는데, 이 때 이 엘리먼트가 가르키는 임포트 테이블에 대한 지식이 매우 유용하게 사용되기 때문이죠. 여섯번째 이야기가 바로 임포트 테이블에 관한 것입니다. ^^;

IMAGE_DIRECTORY_ENTRY_BASERELOC : 재배치와 관련된 데이터 구조에 대한 시작점과 크기 정보를 가지고 있습니다. 재배치는 일반 EXE 파일과는 무관하며 DLL과 연관되어 있습니다. 역시 자세한 이야기는 뒤에서 살펴보도록 하죠 ^^;

IMAGE_DIRECTORY_ENTRY_TLS : 리버서의 입장에서 TLS는 TLS callback 함수를 이용한  안티 리버싱 테크닉 때문에 알아두어야 합니다. 자세한 이야기는 여기를 참고하세요.


PE 제작하기  4 : 데이터 디렉토리 채우기

Step 1: 데이터 디렉토리를 위한 공간 128bytes를 추가합니다. MyFirstPE를 열어 Ctrl+0(숫자)을 누른후 128bytes를 추가하면 되겠습니다. 완성된 모습은 아래와 같습니다.

사용자 삽입 이미지


끝났습니다. ㅠ.ㅠ; 어이없죠?  사실 우리는 MessageBox를 임포트해서 사용할 것이기 때문에 두번째 데이터 디렉토리와 열세번째 데이터 디렉토리를 채워줘야 하는데요... 이 작업은 여섯번째 이야기에서 하려고 합니다. 아무래도 임포트를 이해해야 하기 때문에...

맺음말
^^; 지금은 오후 9시입니다. 허무해 하신 분들을 위해 빨리 작업해서 이 밤이 다가기 전에 여섯번째 글을 올리도록 하겠습니다. 즐핵하세요~



Posted by zesrever

사용자 삽입 이미지

번째 이야기입니다.점점 복잡해지기 시작하네요. 그렇다고 매우 어렵거나 이해하기 어려운 정도의 수준은 아니니까 편한 마음으로 읽어보시기 바랍니다. 지난 세번째 이야기에서는 PE Header 중 FileHeader 부분까지 알아보았습니다. 이번 이야기는 PE 파일의 구성 요소 중에서도 가장 중요한 OptionalHeader에 대해서 알아보려고 합니다.

   [그림 1] Optional Header


Optional Header
한번쯤 구글 신에서 PE 파일에 대한 기도를 올려보신 분들은 익히 들어서 알고 계시겠지만 Optional Header는 그 이름과는 다르게 절대로 옵션이 아닙니다. Optional Header는 PE 파일의 논리적 구조에 대한 매우 중요한 정보를 담고 필수 적인 구성 요소입니다. [그림 1]에서 볼 수 있듯이 PE 헤더의 마지막 구성 요소인 Optional Header는 30개의 필드와 1개의 데이터 디렉토리로 구성되어 있습니다. 필드 수부터 압박을 느끼게 하는 군요. 하지만 매우 다행스럽게도 지금까지 알아본 다른 헤더와 마찬가지로 전체 필드를 모두 알아야 하는 것은 아닙니다. Optional Header를 구성하는 30개의 필드 중 알아두어야 하는 필드는 대략 열 몇개 정도입니다. 먼저 알아두어야 할 필드들을 하나씩 살펴보도록 하겠습니다.

  • Magic(2bytes) : Optional Header의 시작 위치에 존재하는 필드로 Optional Header를 구분하는 시그너춰로 사용됩니다. 이 값은 0x10B로 고정되어 있습니다.
  • AddressOfEntrypoint(4bytes) : 흔히 엔트리 포인트라고 부르는 필드로 PE 파일이 메모리에 로드된 후 맨 처음 실행되어야 하는 코드의 주소를 담고 있습니다. 주소값이므로 당연히 4bytes 사이즈를 가지겠죠. 주의할 점은 이 필드에는 Virtual Address가 아닌 RVA 값 즉 ImageBase로 부터의 offset 이 기록된다는 사실입니다. 일반적으로 엔트리 포인트는 .text 섹션(실행 코드를 담고 있는 메모리 영역)의 시작점인 경우가 대부분이기 때문에 이 값은 후에 알아볼 .text 섹션 헤더의 VirtualAddress 값과 일치하는 경우가 많습니다. (섹션 헤더에 있는 VirtualAddress는 해당 섹션의 메모리 상의 시작점을 가르킵니다.) 이 필드에 대해서는 이번 이야기의 후반부에 PE를 제작할 때 좀 더 자세히 알아보게 될 것입니다.

  • ImageBase(4bytes) : 여러  차례 언급되었던 내용이죠. 로더는 PE 파일을 로드할 때 ImageBase값을 참조하여 가급적이면 ImageBase부터 로드하려고 시도합니다. EXE 파일의 경우 가상 메모리 공간에 가장 처음 로드되므로 항상 ImageBase에 로드됩니다. 하지만 DLL의 경우 ImageBase로 지정된 주소 공간이 다른 모듈에 의해서 이미 사용 중인 상황이 발생할 수 있습니다. 이러한 경우 로더는 해당 DLL을 다른 곳에 로드하고 재배치를 작업을 수행하게 됩니다. 대부분의 링커는 이 값을 0x00400000(EXE의 경우), 0x10000000(DLL의 경우) 로 설정합니다. 이 값은 링커 옵션 중 BASE 옵션을 이용하여 수정이 가능합니다.
  • SectionAlignment(4bytes) : Alignment(정렬)는 아키텍쳐와 깊은 연관 관계를 가지고 있는 개념으로 퍼포먼스에 많은 영향을 끼칩니다. 자세한 내용은 구글신에게 기도를 드려보시고 응답이 없으시면 따로 질문해주세요. 네번째 이야기는 좀 내용이 많아 alignment에 대한 개념은 생략하겠습니다. 어쨌든 Section Alignment는 각 섹션이 메모리 상에서 차지해야 하는 최소의 단위로 이해하시는 것이 정신 건강에 좋습니다. 예를들어 Section Alignment의 값이 4096이고 .text 섹션의 크기가 100bytes라면 실제로 메모리 상에서 .text 섹션은 4096bytes를 차지하게 된다는 것이죠.  만약 .text 섹션이 5000bytes라면 어떻게 될까요? Section Alignment는 단위라 했으므로 총 2개 단위 즉 8192 bytes만큼을 차지하게 될 것입니다. 더불어 이러한 이유때문에 각 섹션은 Section Alignment x n의 위치에서 시작하게 됩니다.[그림 2]를 참고하세요. PE에 대한 공식/비공식적인 문서를 살펴보면 Section Alignment 먼트는 page 사이즈 즉 4096보다 작을 수 없다고 되어 있습니다. 재미있는 사실은 이러한 진술과는 무관하게 linker 옵션 중 ALIGN 옵션을 이용하면 4096보다 작은 값을 지정할 수 있다는 것이구요. 실제로 제한적인 상황에서는 Section Alignment 값이 4096보다 작아도 실행하는데는 지장이 없다는 것입니다. 여기까지의 내용이 복잡하면 지금은 이렇게만 알아두면 되겠습니다.
     
      - 메모리 상에서 각 섹션은 Section Alignment x n 번지에서 시작한다.
      - 메모리 상에서 하나의 섹션은 Section Alignment x m 사이즈를 가진다.
      - 일반적으로 Section Alignment의 값은 페이지 사이즈와 동일한 4096 값을 사용한다.
  • FileAlignment : SectionAlignment가 메모리 상에서의 섹션 정렬과 관련있었다면 FileAlignment는 디스크 상에서의 섹션 정렬과 관련있는 필드입니다. 개념은 SectionAligment와 동일합니다. 이 값은 512부터 65535사이의 2의 n승 형태의 값을 사용하도록 약속되어 있습니다. 512, 1024, 2048 ... 뭐 이런식이죠. 우리는 512를 사용할 것입니다. (만약 이 값이 SectionAlignment와 동일하다면 디스크 상의 PE 파일의 모습이나 메모리 상의 PE 파일 모습은 예외적인 상황을 제외하면 100% 같습니다.
  • SizeOfImage  : 메모리 상에 로드된 PE 파일의 총 사이즈를 의미합니다.  이 값은 SectionAlignment x n의 형태가 됩니다. 자세한 계산 방법은 직접 PE 제작을 하면서 알아보도록 하죠.
  • SizeOfHeader : 디스크 상에서의 헤더의 총 사이즈를 의미합니다. 이 부분은 그림을 보고 이해하는 것이 더 좋을 것 같군요. ^^;  아래 [그림 2]를 봐주세요.

    사용자 삽입 이미지

                     [그림 2] PE 파일 각 구성 요소의 사이즈

     [그림 2]는 잘 이해해 두는 것이 좋을 것 같습니다. 실제 PE 파일을 제작하기 위해서 꼭 필요한 지식입니다. 위 그림을 보면 SizeOfHeader는 DOS header에서 패딩을 포함한 section header의 끝까지의 사이즈를 의미함을 알 수 있습니다. 이 값은 파일 상태에서 계산한 것으로 항상 FileAlignment x n 값을 가집니다. 실제 PE 파일이 메모리에 로드되면 SizeOfHeaders의 값이 SectionAlignment x n 형태로 변경되어야 하겠지만 이 값 자체는 로더에 의해서만 사용되는 값이라서 메모리에 로드된 후에도 변경되지 않고 그대로 유지됩니다.
  • MajorSubsystemVersion, MinorSubsystemVersion : Win32 애플리케이션의 경우 버전을 4.0으로 해야 합니다. 따라서 대부분의 경우 MajorSubsystem 값은 4, MinorSubsystem 값은 0이 됩니다.
  • SizeOfStackReserve, SizeOfStackCommit : 이 값은 Stack 영역으로 예약된 메모리의 사이즈와 할당된 메모리의 사이즈 값을 가집니다. 보통 스택 영역으로는 1page를 할당하며 16page를 예약해 둡니다. 따라서 대부분의 경우 SizeOfStackReserve 값은 0x10000, SizeOfStackCommit 값은 0x1000이 됩니다.
  • SizeOfHeapReserve, SizeOfHeapCommit : Heap 사이즈에 대한 정보라는 점만 빼고 위와 같습니다.
  • Subsystem : Console용 애플리케이션인 경우 Windows CUI(0x3)을 GUI용 애플리케이션인 경우 Windows GUI(0x2)값을 가져야 합니다.

PE 제작하기 3 : Optional Header 만들기(Data Directory 제외, 다음글에서 다룹니다)

중요한 필드들의 정보를 알아보았습니다. 이제 OptionalHeader를 직접 만들어 보도록 하겠습니다. 먼저 OptionalHeader가 어떤 모양으로 선언되어 있는지 살펴보도록 하겠습니다.

  typedef struct _IMAGE_OPTIONAL_HEADER {
      //
      // Standard fields.
      //
      WORD    Magic;
      BYTE    MajorLinkerVersion;
      BYTE    MinorLinkerVersion;
      DWORD   SizeOfCode;
      DWORD   SizeOfInitializedData;
      DWORD   SizeOfUninitializedData;
      DWORD   AddressOfEntryPoint;
      DWORD   BaseOfCode;
      DWORD   BaseOfData;
      //
      // NT additional fields.
      //
      DWORD   ImageBase;
      DWORD   SectionAlignment;
      DWORD   FileAlignment;
      WORD    MajorOperatingSystemVersion;
      WORD    MinorOperatingSystemVersion;
      WORD    MajorImageVersion;
      WORD    MinorImageVersion;
      WORD    MajorSubsystemVersion;
      WORD    MinorSubsystemVersion;
      DWORD   Win32VersionValue;
      DWORD   SizeOfImage;
      DWORD   SizeOfHeaders;
      DWORD   CheckSum;
      WORD    Subsystem;
      WORD    DllCharacteristics;
      DWORD   SizeOfStackReserve;
      DWORD   SizeOfStackCommit;
      DWORD   SizeOfHeapReserve;
      DWORD   SizeOfHeapCommit;
      DWORD   LoaderFlags;
      DWORD   NumberOfRvaAndSizes;
      IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;


컥~ 정말 많습니다. 뭐 안될 것 있겠습니까? 일단 부딪혀보도록 하죠.

Step 1:  먼저 데이터 디렉토리를 제외한 나머지 필드들을 채워나갈 것이므로 96bytes 사이즈의 빈 공간을 생성합니다. 그 다음 Magic 넘버(2bytes)를 기록해야 하겠습니다. 앞에 설명한 대로 0x10B로 채우면 됩니다. 리틀엔디언 잊지 마시구요. 그 다음에는 MajorLinkerVersion과 MinorLinkerVersion을 채워야 하겠습니다. 위 구조체 선언에서 알 수 있는 것처럼 각 각 1byte를 차지하고 있습니다. 고맙게도 이 필드는 0으로 채워도 실행에는 아무런 지장이 없습니다. 0으로 채우도록 합니다.

사용자 삽입 이미지



Step 2: SizeOfCode와 SizeOfInitializedData, SizeOfUninitializedData를 채울 차례군요. 먼저 이 필드들의 값은 Windowx XP에서 실험해 본 결과 실행과는 별 상관없어 보입니다. 따라서 이 값을 0으로 채워도 무방하겠습니다.(물론 이 필드로부터 Code 사이즈등을 읽어내는 프로그램이 있다거나 ntdll.dll에 구현된 로더가 이 값을 참조하도록 수정된다면 이야기가 달라지겠죠) SizeOfCode는 IMAGE_SCN_CNT_CODE 속성을 가진 섹션들의 총 사이즈를 담고 있는 값입니다. FileAlignment x n 형태의 값을 가지게 됩니다. 이미 언급한대로 0으로 채워도 무방하지만 조금더 성의를 보여 0x200(512bytes, 나중에 FileAlignment 값으로 512를 사용할 것입니다. 또한 우리가 사용할 코드는 메시지 박스 하나 띄우는 코드라 512bytes 안에 충분히 들어갈 것 입니다.)  SizeOfInitializedData와 SizeOfUninitizedData는 각각 IMAGE_SCN_CNT_INITIALIZED_DATA  속성과 IMAGE_SCN_CNT_UNINITIALIZED_DATA 속성을 가지는 섹션의 총 사이즈입니다. 실행과 별 관계없으므로 설명을 간략하게 하기 위해 일단 0으로 채우도록 하겠습니다.


Step 3: 으흠.. 첫번째 고비를 만났습니다. ^^; AddressOfEntryPoint 값을 채워넣어야 하겠습니다. 반드시 그럴 필요는 없지만 일반적으로 AddressEntryOfEntryPoint는 코드 섹션(.text)의  시작점을 가르키는 경우가 대부분입니다. MyFirstPE.exe 역시 .text 섹션의 시작점을 AddressOfEntryPoint로 삼을 것입니다. 그렇다면 이미지 상태의 PE 파일(메모리에 로드된 PE파일)에서 첫번째 섹션의 시작점을 계산해보면 되겠군요.(코드 섹션인 .text를 MyFirstPE.exe의 첫번째 섹션으로 등록할 것입니다.
사용자 삽입 이미지
물론 .text 섹션이 반드시 첫번째 섹션일 필요는 없습니다.) 옆의 그림을 봐주세요.옆의 그림을 이해하는데 가장 중요한 사실은 이미지 상태의 PE 파일에서 각 섹션은 SectionAlignment x n 번지에서 시작해야 한다는 것입니다. ImageBase값을 0x00400000 설정할 것이므로 그 다음 alignment 지점은 0x00401000이 되겠군요. 또한 DOS header부터 section table까지의 총 사이즈가 0x1000 bytes를 초과하지 않으므로 0x00401000이 이미지 상태에서 .text 섹션의 시작 주소가 됩니다. AddressOfEntryPoint는 RVA 값이므로 0x00401000 - 0x00400000 = 0x1000이 됩니다. Alignment에 대한 개념만 확실하면 그리 어렵지 않은 문제죠. 이제 AddressOfEntryPoint의 값을  0x1000으로 설정해 주세요.

사용자 삽입 이미지


Step 4: BaseOfCode와 BaseOfData를 채워나갈 차례입니다. PE를 공부하다보면 가끔 의아할 때가 있는데요 이 필들들도 그러한 느낌을 받게 하는군요. 이 필드들은 매우 중요해 보이지만 실제로 ntdll.dll에 구현되어 있는 로더는 이 필드를 사용하지 않는 것 같습니다. 모두 0x0으로 채워도 실행에는 아무런 문제가 없습니다. 그래도 표준을 따른다는 의미로 정상적인 값으로 채워보도록 하죠. 이 필드는  코드 섹션의 시작점과 데이터 섹션의 시작점을 가르키는 RVA 값입니다. 우리의 경우 BaseOfCode는 0x1000, BaseOfData는 0x2000 으로 하면 되겠습니다. (이해되지 않으면 Step 8의 그림을 참조하세요.)

사용자 삽입 이미지

Step 5: ImageBase는 0x00400000 으로 설정하겠습니다.
사용자 삽입 이미지


Step 6: SectionAlignment는 0x1000, FileAlignment는 0x200으로 채우도록 하겠습니다.
사용자 삽입 이미지


Step 7: MajorOperatingSystemVersion 부터 Win32VersionValue는 아래 그림과 같이 채워주세요. 자세한 내용은 구글신에게 기도롤... (Windowx XP에서 테스트해 본 결과 MajorSubsystemVersion 외에는 실행에 영향을 끼치지 않습니다. )

사용자 삽입 이미지


Step 8: SizeOfImage는 아래의 그림 하나로 설명이 될 것 같군요. ^^ 0x4000으로 채웁니다.

사용자 삽입 이미지

















사용자 삽입 이미지

Step 9: SizeOfHeaer값을 채울 차례입니다. 설명은 앞에서 했습니다. 0x200(512)으로 채웁니다.

사용자 삽입 이미지


Step 10: 이번에는 Checksum 값이네요. 다행스럽게도 이 값은 사용되지 않습니다. 0x0으로 채웁니다.
사용자 삽입 이미지


Step 11: Subsystem입니다. 우리는 메시지 박스를 띄울 것이므로 Win32 GUI로 해야 겠습니다. 이 값을 0x2로 채웁니다.

사용자 삽입 이미지


Step 12: DllCharacteristics 입니다. 이 값은 DLL 초기화 함수(DllMain)를 언제 호출해야 하는지를 나타내는 flag 값입니다. 이 값은 아래와 같이 정의되어 있습니다만, 실제로 0x0으로 설정하여도 정상적으로 잘 동작합니다.(DllMain 함수는 실제로 아래의 값과 관계없이 항상 호출되는 것 같습니다.)
1 Call when DLL is first loaded into a process's address space
2 Call when a thread terminates
4 Call when a thread starts up
8 Call when DLL exits
일단 0x0으로 채우겠습니다.

사용자 삽입 이미지

Step 13: SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit은 앞에서 설명한 대로 각각 16page(0x10000), 1page(0x1000) 값으로 채웁니다.

사용자 삽입 이미지

Step 14: LoaderFlags : 지금은 사용하지 않는 필드입니다. Thanks.. 0x0 으로 채웁니다.
사용자 삽입 이미지

Step 15: 드디어 마지막입니다. NumberOfRvaAndSizes 차례군요. Windowx XP에서 실행한 결과 실행과는 별 관계없는 값이기는 합니다만, 마지막이고 하니 즐거운 마음으로 알아보죠. 이 필드는 쉽게 이야기하면 데이터 디렉토리 엔트리 개수라고 보시면 됩니다. 데이터 디렉토리가 옵션이기 때문에 필요한 필드죠. 우리는 데이터 디렉토리가 필요하므로 이 값은 0x10 즉 16으로 설정하면 되겠습니다.
(나중에 알아볼 기회가 있겠지만 LoaderFlags와 NumberOfRvaAndSizes는 안티 리버싱에 사용됩니다.)

사용자 삽입 이미지

맺음말
좀 긴 글이었던 것 같습니다. 그림도 많고. ^^; 포기하지 마시고 끝까지 고고싱~ 입니다. 다음글은 Data Directory에 관한 것입니다. 그럼 즐핵~ 하세요.
Posted by zesrever
사용자 삽입 이미지
번째 이야기입니다. 이번에는 PE 헤더에 대해서 알아보도록 하겠습니다. PE 헤더는 좌측의 [그림 1]과 같이 크게 3부분으로 이루어져 있습니다. 첫번째 부분은 PE signature 로 이 값은 "50 45 00 00" 으로 고정되어 있습니다. 두번째 부분은 20bytes 고정 사이즈를 가지는 파일 헤더(IMAGE_FILE_HEADER) 입니다. 세번째 부분은  옵셔널 헤더(IMAGE_OPTIONAL_HEADER)로 보통은 224bytes 사이즈를 가지지만 원칙적으로 사이즈는 가변적입니다.

        [그림 1] PE 헤더의 구조

보다 자세하게 알아보도록 하겠습니다. 다음은 PE 헤더(IMAGE_NT_HEADERS)의 정의입니다.

  typedef struct _IMAGE_NT_HEADERS {
      DWORD Signature;
      IMAGE_FILE_HEADER FileHeader;
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;
  } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;


PE Signature(Signature)
이미 언급한 대로 4byte 사이즈의 데이터로 이 값은 항상 "50 45 00 00(PE\0\0)"으로 고정되어 있습니다.

FileHeader(IMAGE_FILE_HEADER)
20bytes 사이즈의 구조체입니다. 파일 헤더는 주로 PE 파일의 물리적 모양에 대한 정보를 가지고 있습니다. 파일 헤더는 모두 7개의 필드로 구성되어 있는데 이 중에서 알아두어야 할 필드는 모두 4개 입니다. 구조체 선언을 먼저 살펴보도록 하겠습니다.

  typedef struct _IMAGE_FILE_HEADER {
      WORD    Machine;
      WORD    NumberOfSections;
      DWORD   TimeDateStamp;
      DWORD   PointerToSymbolTable;
      DWORD   NumberOfSymbols;
      WORD    SizeOfOptionalHeader;
      WORD    Characteristics;
  } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;


위 필드 중 알아두어야 할 것은 [그림 1]에 명시되어 있는 Machine, NumberOfSections, SizeOfOptionalHeader, Characteristics 뿐입니다. 나머지 필드는 무시하여도 좋으며 직접 생성 시 모두 0으로 채울 것입니다.
  • Machine은 CPU ID를 나타내는 데 IA32의 경우 이 값은 0x14c가 되어야 하며 IA64인 경우 0x200이 되어야 합니다.
  • NumberOfSections은 말 그대로 섹션의 개수를 의미합니다. 리버싱 중 섹션을 생성하거나 제거할 때는 반드시 이 값을 변경해 주어야 합니다.
  • SizeOfOptionalHeader는 파일 헤더 뒤에 오는 옵셔널 헤더의 사이즈를 의미합니다. 이 필드의 존재 자체만으로도 옵셔널 헤더의 사이즈가 고정되어 있지 않다는 사실을 알 수 있습니다. 옵셔널 헤더의 사이즈에 영향을 끼치는 것은 옵셔널 헤더에 포함되어 있는 데이터 디렉토리입니다. 일반적인 경우 데이터 디렉토리가 포함된 옵셔널 헤더는 224bytes를 차지하지만 데이터 디렉토리는 필요없는 경우 생략이 가능하기 때문에 데이터 디렉토리를 포함하지 않는 옵셔널 헤더는  96bytes(데이터 디렉토리의 사이즈는 128bytes 입니다.) 사이즈를 가지게 됩니다.
  • Characteristics는 말 그대로 PE 파일의 속성을 의미합니다. 이 필드는 PE 파일이 EXE 파일인지 DLL 파일인지, 재배치가 가능한가와 같은 정보를 담게 됩니다. 일반적인 실행 파일의 경우 0x10F 값을 가집니다. 이는 PE 파일이 EXE 파일이며 재배치 정보를 가지고 있지 않음을 의미합니다. 여기서 잠깐 재배치란 옵셔널 헤더에 지정되어 있는 ImageBase에 PE 파일을 로드할 수 없는 경우 로드 가능한 주소에 PE 파일을 로드하고 실행 코드 내에서 절대 주소 값 등을 변경하는 작업입니다. EXE 파일의 경우 가상 메모리 공간에 가장 먼저 로드되므로 항상 ImageBase에 로드가 가능하여 재배치가 절대로 발생하지 않습니다. 반면 DLL은 상황에 따라 ImageBase에 로드될 수 없는 경우가 발생하기 때문에 (ImageBase에 이미 다른 DLL 등이 로드된 경우) 재배치가 발생할 수 있습니다. MS의 재배치로 인한 성능 저하를 막기 위해 윈도우의 주요 DLL의 ImageBase 값을 세밀하게 조정해 놓았다는 것도 기억해 두면 좋을 것 같습니다.

PE 조립하기 2: PE Signature + FileHeader 생성
옵셔널 헤더는 다음 글에서 알아보도록 하겠습니다. 이번 글에서는 파일 헤더까지만 생성해 보도록 하죠. 먼저 두번째 이야기에서 생성한 MyFirstPE.bin을 WinHex로 열어야 하겠죠.

Step 1. PE 시그너춰와 FileHeader를 위한 공간을 추가합니다. 각각 4bytes와 20bytes이므로 총 24bytes 공간을 추가하면 되겠습니다. WinHex의 경우 Ctrl+0을 누르면 추가 공간을 할당할 수 있습니다. 그림은 생략합니다.

Step 2. [그림 2]에서 처럼 PE 시그너춰를 입력합니다.

사용자 삽입 이미지

   [그림 2] PE 시그너춰 입력 (클릭 후 확대해서 보세요)

Step 3. FileHeader를 완성해야 합니다. 제일 먼저 Machine 값을 입력합니다. Machine 값은 IA32일 경우 0x14c로 입력하면 되겠습니다. 입력할 때는 리틀 엔디언임을 고려해서 입력해야 하며 Machine은 2bytes 사이즈를 가지므로 차례대로 4c 01을 입력하면 되겠습니다.
사용자 삽입 이미지

    [그림 3] Machine 필드 채움(클릭 후 확대해서 보세요)

Step 4. NumberOfSections 필드를 채울 차례군요. 이 필드는 총 2bytes입니다. 우리가 최종적으로 생성할 실행 파일은 코드를 저장하기 위한 .text 섹션과 상수 데이터를 저장하기 위한 .rdata 섹션 그리고 임포트 한 API에 대한 정보를 담고 있는 .idata 섹션 이렇게 3개의 섹션을 가지게 될 것입니다. 그러므로 이 필드는 리틀 엔디언 방식임을 고려해서 차례대로 03 00 으로 채우면 되겠습니다.

사용자 삽입 이미지

  [그림 4] NumberOfSections 필드 채움(클릭 후 확대해서 보세요)

Step 5. TimeDateStamp와 PointerToSymbolTable, NumberOfSymbols 필드를 채웁니다. 이 필드는 각각 4bytes 사이즈를 가집니다. 일단 심볼과 관련된 두 개의 필드(PointerToSymbolTable, NumberOfSymbols)는 거의 사용되지 않는 필드이며 0으로 채워도 무방합니다. TimeDateStamp는 PE 파일이 생성된 시간을 1970년 1월 1일 00시를 기준으로하여 초단위로 기록해야 하는데 Windows XP에서 실험한 결과로는 역시 사용되지 않는 필드인 것 같습니다. 즉 0으로 채워도 실행하는 데는 아무런 지장이 없습니다. 이 필드 역시 0으로 채우겠습니다.

사용자 삽입 이미지
 [그림 5] TimeDateStamp, PointerToSymbolTable, NumberOfSymbols 필드 채움

Step 6. SizeOfOptionalHeader를 채울 차례입니다. Optional header는 데이터 디렉토리가 있는 경우 224 bytes, 데이터 디렉토리가 필요없는 경우 96bytes라고 이미 말한 바 있습니다. 우리는 임포트 섹션을 사용해야 하는데 이 경우 IAT 테이블에 대한 정보가 데이터 디렉토리에 기록되어야 하므로 (이에 대해서는 다음 글에서 알아볼 것입니다) 데이터 디렉토리가 필요합니다. 따라서 우리가 만드는 PE 파일의 Optional header 사이즈는 224bytes(0xE0)가 됩니다. 이 필드의 사이즈는 2bytes 이므로 차례대로 "E0 00"으로 채우면 되겠습니다.

사용자 삽입 이미지

  [그림 6] SizeOfOptionalHeader 필드 채움


Step 7.  이제 마지막입니다. ^^; Characteristics 필드를 채울 차례군요. 2bytes 사이즈를 가지구요, 대체로 일반적인 애플리케이션은 0x10F 값을 가진다는 사실을 앞에서 알아보았습니다. 0x10F는
Relocation Stripped, Executable Image, Line number stipped, 32bit machine expected 속성을 체크한 결과입니다. ^^; 자세한 내용은 Google 신에게 기도를 드려보세요~ (책임회피성발언...^^)
그렇다면 차례대로 "0F 01"을 채워나가면 되겠군요.

사용자 삽입 이미지

  [그림 7] Characteristics 필드 채움


맺음말
그럭저럭 해볼만하지 않은가요? ^^; 다음 이야기는 PE 헤더 중 OptionalHeader에 관한 것입니다. 그럼 즐핵~하세요.

Posted by zesrever

사용자 삽입 이미지

번째 이야기입니다. 오늘은 그림을 종이에 못그렸습니다. 주변에 휴지는 많은데  종이는 없네요. ㅠ.ㅠ; 제 소중한 노트도 잃어버리고. 대신에 화이트 보드에 쓱쓱그려 올려봅니다.(한밤중에 찍은 사진이라 불빛은 도저히 어떻게 할 수가 없습니다)  이번에는 정말로 정말로 별거 없습니다. 그저 [그림 1]만 쓱쓰~윽 그려 낼 수 있으면 그걸로 땡입니다. 게다가 첫번째 이야기 때 이미 어느 정도 언급되었던 내용이라 크게 어려운 점은 없을 것입니다. 그래도 연재 후반부에 실제 PE 파일을 맹글때 필요한 내용들이므로 천천히 읽어보시기 바랍니다. 그럼 시작하죠.

   [그림 1] DOS 헤더와 DOS 스텁 코드


DOS 헤더
이미 첫번째 이야기에서 알아본 사실이지만 복습도 할겸 한 번더 설명드립니다. [그림 1]이 다 이해되시는 분들은 스킵하세요. DOS 헤더는 DOS와의 호환을 위해 유지되고 있는 헤더라고 보시면 됩니다. 실제로는 별 쓸모가 없습니다만, 어쨌든 PE 파일은 DOS 헤더로 시작합니다. DOS 헤더는 항상 64bytes 사이즈를 가지며 윈도우 내에서 IMAGE_DOS_HEADER 라는 구조체로 정의되어 있습니다. IMAGE_DOS_HEADER에 대한 정의는 머리만 아프게 하므로 생략하겠습니다만, 나중에라도 대충 살펴보면 DOS 헤더는  다 외울 수 없는 그리고 외울 필요도 없는 약 20개 가량의 멤버로 구성되어 있다는 사실을 알 수 있을 것입니다. 다행스럽게도 DOS 헤더의 멤버 중에서 기억해야 할 녀석들은 DOS 헤더의 처음 2bytes를 차지하는 e_magic과 마지막 4bytes를 차지하는 e_lfanew 뿐입니다. 나머지는 거의 필요없는 부분으로 나중에 PE 파일을 수작업으로 생성할 때 보겠지만 전부 0으로 채워도 아무 문제가 되지 않습니다.  DOS 헤더의 처음 2bytes를 차지하는 e_magic은 DOS 헤더의 signature로 [그림 1]에 보이는 것과 같이 항상 '4D 5A(MZ)' 값을 가집니다. DOS 헤더의 마지막 4bytes를 차지하는 e_lfanew는 PE 헤더의 시작점을 가르키는 오프셋 값입니다. 더불어 알아두어야 할 사실은 DOS 헤더의 시작 위치 즉 PE 파일의 시작 위치에 관한 것입니다. 디스크 상에서의 시작 위치야 당연히 파일의 시작점이 될테구요 메모리 상에서는 PE 헤더(그림 1에서 IMAGE_NT_HEADER)에 기록되어 있는 ImageBase가 시작 위치가 된다는 사실 다시 한번 기억해 두시기 바랍니다.  

DOS 스텁 코드 
별로 관심을 가지지 않아도 될 만한 부분입니다. 실제로 DOS 스텁 코드는 필수 구성 요소는 아니어서 DOS 스텁 코드가 없더라도 프로그램이 실행되는 데는 아무런 지장이 없습니다.그래도 알아두어야 할 내용이 있다면 DOS 스텁 코드는 프로그램을 도스 모드에서 실행시켰을 때 실행 되는 코드이며 보통은 "This program must be run under Microsoft Windows"라는 메시지를 출력하고 종료되는 코드가 삽입된다는 점과  오브젝트 파일을 링킹할 때 STUB 옵션을 이용하여 원하는 스텁 코드를 삽입할 수 있다는 점 정도만 알아두면 되겠습니다. 물론 그림을 보면 알겠지만 스텁 코드는 DOS 헤더 바로 다음에 위치한다는 점도 알아두시고요 더불어 사이즈는 고정되어 있지 않다는 점도 알아두세요. 

PE를 조립하자 1 : DOS Header 만들기
이제 PE의 전체 구조와 DOS Header에 대해서 알았으니 PE 파일 제작의 첫번째 단계로 DOS Header를 만들어 보도록 하겠습니다.

준비물 : Hex 에디터(본 글에서는 WinHex를 사용했습니다.)

제작과정

사용자 삽입 이미지
Step 1 : [그림 2]와 같이 WinHex를 실행시켜 64bytes 사이즈를 가지는 새로운 파일을 생성합니다.








[그림 2] 64bytes 사이즈의 새로운 파일을 생성


Step 2: [그림 3]과 같이 처음 2bytes 부분의 값을 "4D 5A"로 수정합니다. DOS 헤더의 시그너춰인건 아시죠?

사용자 삽입 이미지


[그림 3] DOS 시그너춰(e_magic) 입력 (클릭 후 확대해서 보세요)

Step 3: [그림 4]에서 처럼 마지막 4bytes (e_lfanew)의 값을 0x40(64)로 설정합니다. 우리는 DOS stub 코드를 사용하지 않을 것입니다.!!! 우리가 만들 PE 파일은 DOS 헤더 다음에 바로 PE 헤더가 오는 것이죠. 리틀엔디언임을 고려하여 40 00 00 00 으로 입력해야 합니다.

사용자 삽입 이미지
[그림 4] PE 헤더의 시작 지점을 지정

Step 4: DOS Header가 완성되었습니다. 매우 쉽네요. 이 파일은 계속 사용해야 하므로 MyFirstPE.bin이라는 이름으로 저장하겠습니다.

맺음말

거의 첫번째 이야기의 복습판이군요. 정말로 별다른 내용이 없습니다. ^^; 세번째 이야기에서는 PE 헤더에 대해서 알아보도록 하겠습니다.

Posted by zesrever

사용자 삽입 이미지


번째 이야기입니다. PE 파일을 수작업으로 만들어 가려면 당연히 전체적인 모습부터 알아야 겠습니다. 어떻게 생겨먹은 놈(공식적으로 PE의 성별에 대해서 확인된 바는 없습니다. ^^;) 인지를 알아야 조립을 할 수 있겠죠. PE 파일의 전체적인 구조는 [그림 1]과 같습니다.  PE 파일의 전체적인 구조를 공부할 때는 "PE 파일은 디스크 상의 모습과 메모리 상의 모습이 거의 같다"라는 것과 PE 파일의 구성 요소, 그리고 각 구성 요소의 시작점을 찾는 방법을 잘 알아두어야 합니다.



    [그림 1] PE File의 전체적인 모양 (클릭 후 확대해서 보세요)


PE 구성 요소
PE는 DOS header, DOS stub code, PE header, Section table 및 1개 이상의 섹션으로 구성됩니다.  각 구성 요소에 대해서는 차근차근 공부해 나갈 것입니다. 지금은 자세한 내용보다는 전체적인 구조에 대해서 파악하는 것이 중요합니다. 이에 맞춰 각 구성 요소의 명칭과 구성 형태를 [그림 1]을 참고하여 스스로 그림으로 그려낼 수 있도록 노력하면 되겠습니다. 한가지만 더 언급하자면 DOS stub 코드는 PE 파일의 필수 구성요소가 아니라서 생략이 가능합니다.


각 구성 요소의 시작 위치 계산 방법

DOS header : PE 파일은 DOS header로 시작합니다. 따라서 디스크 상에서는 파일의 첫부분이 DOS header가 됩니다. 메모리 상에서 PE 파일은 ImageBase에서 시작하므로 DOS header는 ImageBase에서 찾을 수 있습니다.간단하게 확인해 보도록 하죠. 먼저 calc.exe를 StubPE를 이용하여 열어보겠습니다. [그림 2]는 StudPE를 통해 살펴 본 PE 헤더 일부입니다.  ImageBase 값이

사용자 삽입 이미지
01000000이군요. 메모리에 로드된 PE 파일의 시작점이 01000000 번지라는 이야기입니다. 디버거를 하면 간단하게 확인이 가능합니다. calc.exe를 ollydbg로 오픈해 보죠. 오픈 후에는 [그림 3]처럼 우측 하단의 [데이터 윈도우]를 클릭하고 Ctrl+G(Goto)를 눌러 01000000을 입력합니다

[그림 2] calc.exe의 ImageBase 값


사용자 삽입 이미지
[그림 3]  메모리상 0x01000000 번지로 이동(확대해서보세요)

이동하여 살펴보면 [그림 4]처럼 데이터값 4D 5A(MZ)를 발견할 수 있습니다.
사용자 삽입 이미지











[그림 4] calc.exe가 로드된 후 0x01000000번지의 내용

MZ은 DOS 개발자 중 한명인 Mark Zbikowski라는 분의 이니셜로 DOS header의 시그너춰로 사용됩니다. 이로써 0x01000000번지 즉 ImageBase에 DOS header가 로드되어 있음을 확인할 수 있습니다.

DOS Stub Code :  윈도우용 애플리케이션을 도스 모드에서 실행시킨 경우 애플리케이션이 정상적으로 동작되지 않습니다. 이러한 경우 윈도우는 애플리케이션 대신에 스텁 코드를 실행시킵니다. 스텁 코드에는 DOS 모드에서 실행 가능한 프로그램이면 어느 것이나 삽입이 가능합니다. 스텁 코드는 DOS header 바로 뒤에 위치합니다. DOS header의 사이즈는 64bytes로 고정되어 있기 때문에 디스크 상에서나 메모리 상에서 DOS 스텁 코드는 시작점으로 부터 64bytes만큼 떨어진 곳에서 찾을 수 있습니다.  

PE Header : 디스크 상이나 메모리 상에서 PE 헤더의 위치는 DOS header에 있는 e_lfanew 값을 이용하여 계산할 수 있습니다. e_lfanew는 4byte 크기의 데이터로 DOS header의 마지막에 위치합니다. e_lfanew에는 파일의 시작점에서 부터 PE 헤더까지의 오프셋 값이 저장되어 있습니다. 간단하게 확인해 보도록 하겠습니다. Hex 에디터로 calc.exe를 열어 e_lfanew 값을 확인합니다. e_lfanew 는 DOS header의 마지막 멤버로 DOS header 시작점으로 부터 60byte 만큼 떨어진 곳에 위치합니다.
확인된 e_lfanew 값을 DOS header 시작 주소(PE 파일 시작 주소)에 더해주면 PE 헤더(PE헤더는 "50 45 00 00"으로 시작합니다)를 찾을 수 있습니다.  이는 디스크상에서나 메모리상에서 모두 동일합니다. [그림 5]를 참고하세요.

사용자 삽입 이미지

  [그림 5] PE 헤더 찾기 (확대해서 보세요)

섹션 테이블 : 섹션 테이블은 PE Header 바로 뒤에 위치합니다. 따라서 섹션 테이블을 찾으려면 PE 헤더 시작 주소에서 PE header 사이즈를 더해 주면 될 것입니다. 이는 메모리 상 PE나 디스크 상의 PE가 모두 동일합니다. 나중에 좀 더 자세히 알아보겠지만 PE 헤더는 PE signature(4 bytes 고정)와 File Header(20bytes 고정) 그리고 Optional Header(기본 : 224bytes, 변할 수 있음) 로 구성되어 있습니다. 따라서 PE 헤더의 사이즈는 24bytes + Optional Hedaer 사이즈가 됩니다. Optional Header의 사이즈는 File Header에 저장되어 있으므로 어렵지 않게 알아낼 수 있습니다.
 
섹션의 위치 : 각 섹션의 위치는 섹션 테이블에 저장된 섹션 헤더을 통해서 확인 가능합니다. 섹션 헤더에는 해당 섹션의 위치와  관련하여 VirtualAddress라는 값과 PointerToRawData라는 값이 저장되어 있습니다. 둘 다 offset 값인데 PointerToRawData는 디스크 상의 섹션의 위치를 가르키는 offset 값이고 VirtualAddress는 메모리 상에서의 section의 위치를 가르키는 offset 값입니다. 이러한 사실로 부터 우리는 [그림 1]에 나타난 것처럼 디스크 상에서의 섹션의 위치와 메모리 상에서의 섹션의 위치가 같지 않을 수도 있다는 사실을 유추해 볼 수 있겠습니다. 이 부분은 섹션에 대해서 좀 더 자세히 다룰 때 함께 알아보도록 하겠습니다. 여기서는 간단히 확인만 하도록 하겠습니다. 먼저 디스크 상에서의 섹션의 위치입니다. calc.exe를 계속 사용하도록 하죠. [그림 6]에서 처럼 StudPE로 calc.exe를 열어 section header를 살펴보겠습니다.
사용자 삽입 이미지
    [그림 6] calc.exe의 섹션 헤더

.text 섹션의 RawOffset 값이 0x400임을 알 수 있습니다. 이는 디스크 상의 .text 섹션이 파일의 시작점으로 부터 0x400 떨어진 곳에서 시작한다는 것을 의미합니다. 이를 확인하기 위해 [그림 7]에서 처럼 마우스 오른쪽 버튼을 클릭한 후 "GoTo Section Start"를 선택하여 .text 섹션의 시작 위치로 이동해보겠습니다.

사용자 삽입 이미지

















[그림 7] calc.exe의 .text 시작점으로 이동

시작 위치로 이동하였으면 [그림 8]을 봐주세요.

사용자 삽입 이미지


[그림 8] calc.exe의 .text 섹션(on disk, 클릭해서 확대 후 보세요)

.text 섹션으로 이동 후 StudPE의 HexViewer의 하단 정보를 확인하면 파일 상의 위치(in File)가 0x400임을 확인할 수 있습니다. 길을 잃어버리시면 안되죠~ 지금까지의 실험은 섹션 테이블에 위치한 섹션 헤더의 PointerToRawData 값은 해당 섹션의 파일 내 시작 위치를 알아내는 데 사용한다는 사실을 확인해 보기 위한 것이었습니다. 이제는 메모리에서 찾아볼까요? Ollydbg를 이용하여 calc.exe를 오픈해 보도록 하죠. 그 다음 ImageBase로 이동해 보겠습니다.(앞에서 해보셨죠? 헛갈리는 분들은 그림 2,3,4를 참고하세요) 이동 후에서는 [그림 9]에서와 같이 마우스 오른쪽 버튼을 눌러 메뉴를 띄운 후 Special->PE header를 선택합니다. 이 메뉴는 데이터를 PE 헤더로 파싱할 때 사용합니다.

사용자 삽입 이미지





















[그림 9] 메모리에 위치한 calc.exe의 PE 헤더 분석

PE 헤더로 파싱 후 [그림 10]에 나와있는 위치를 살펴보면 .text 섹션 헤더를 찾을 수 있습니다. 우리는 메모리 상의 .text 섹션의 위치를 찾으려고 하는 것이므로 이번에는 VirtualAddress 항목을 봐야 겠습니다. 예에서는 VirtualAddress 항목의 값이 0x1000인 것을 확인할 수 있는데 이는 ImageBase로 부터의 offset 값(이를 상대 가상 주소, Relative Virtual Address 줄여서 RVA라고 부릅니다.)입니다. 따라서 .text 섹션의 메모리 상 위치는 ImageBase값인 0x01000000에 오프셋 값 0x1000을 더하면 됩니다. 그럼 0x01001000 번지로 이동해 볼까요?

사용자 삽입 이미지

 [그림 10] 메모리 상의 .text 섹션 헤더 (클릭 후 확대해서 보세요)

아래 [그림 11]은 .text 섹션으로 이동하여 살펴본 결과입니다.
사용자 삽입 이미지







[그림 11] calc.exe의 .text 섹션의 메모리 상 시작점

주의 깊게 살펴보면 0x01001000번지에 있는 데이터는 9A22D877인데 [그림 8]에서 살펴본 .text 섹션의 시작 번지에 있는 데이터는 EA22D877로 서로 다른 값을 가지고 있음을 알 수 있습니다.  ^^; 이는 calc.exe의 경우 .text 섹션의 시작점에 실제 코드가 아닌 IAT이 위치해 있기 때문에 발생하는 상황으로 로드시 DLL 바인딩되면서 IAT에 있는 각 항목의 값들이 변경된 결과입니다. 지금은 이 부분에 신경쓰지 말고 메모리 상 섹션의 위치는 섹션 헤더의 VirtualAddress를 통해서 알 수 있다는 사실만 기억하세요.


PE 파일은 디스크 상의 모습과 메모리 상의 모습이 거의 같다
지금까지 살펴본 결과로 DOS header에서 Section table까지의 구성은 디스크 상의 PE 파일이나 메모리 상의 PE파일 모두 동일하다는 사실을 알 수 있습니다. 이는 디스크 상의 PE 파일이 메모리로 로드 될 때 DOS header 부터 Section table까지는 그대로 로드됨을 의미합니다. 차이가 발생하는 부분은 각 섹션인데요(섹션 헤더에 디스크 상의 위치와 메모리 상의 위치를 구분하는 데이터가 있는 것만 봐도 각 경우의 위치가 다를 것이라는 것을 짐작할 수 있겠죠) 이는 alignment와 관련있습니다. Alignment와 관련하여 기억해 둘 사실은 아래와 같습니다.
 - 섹션들의 디스크 상의 정렬 단위와 메모리 상의 정렬 단위가 다를 수 있다.
 - 디스크 상의 정렬 단위와 메모리 상의 정렬 단위는 각각 Optional Header의 FileAlignment와
   SectionAlignment에 저장되어 있다.
 - 디스크 상의 섹션은 FileAlignment의 배수가 되는 주소에서 시작한다.
 - 메모리 상의 섹션은 SectionAlignment의 배수가 되는 주소에서 시작한다.
 - 이러한 이유로 FileAlignment와 SectionAlignment 값이 다른 경우 디스크 상의 PE 파일의 모습
    과 메모리 상의 PE 파일의 모습은 DOS header부터 Section table까지는 일치하나 섹션이 배치되는 모습은 약간씩 차이가 발생한다.

Ollydbg에서 분석 한 PE 헤더의 내용을 뒤져보면 [그림 12]에서 처럼 SectionAlignment와 FileAlignment 값을 확인할 수 있습니다.

사용자 삽입 이미지

[그림 12] SectionAlignment와 FileAlignment

[그림 6]에서 확인할 수 있었던 것처럼 calc.exe의 .text 섹션은 디스크 상에서 0x400번지에서 시작하였으며 [그림 10]에서 확인한 .text 섹션의 시작 주소는 0x01001000 이었습니다. 0x400은 0x200의 배수이며 0x1001000은 0x1000의 배수가 맞군요. ^^;

정리해 보겠습니다. PE 파일은 메모리에 로드될 때 DOS header부터 Section table까지는 그대로 로드되며 각 section의 경우 지정된 alignment 값에 따라 약간의 차이를 가지고 로드된다는 사실을 기억하시면 됩니다. 덧붙이자면 음... 사실 로드되지 않는 섹션도 더러 있습니다. ^^ 그냥 그럴 수도 있다고 알아두시면 될 것 같습니다.

맺음말
지금까지 PE 파일의 전체 구조를 살펴보았습니다. 지금까지의 모든 설명을 종합 한 것이 [그림 1]입니다.  [그림 1]을 혼자서 그려 내실 수 있을 때까지 반복하시면 도움이 될 것입니다.  두번째 글부터는 각 구성 요소에 대해서 하나씩 알아보도록 하겠습니다.

Posted by zesrever

BLOG main image
Slow but Steady, Broad and Deep ... by zesrever

공지사항

카테고리

분류 전체보기 (44)
Digital Forensics (4)
Reverse Engineering (21)
Vulnerability (2)
Secure Coding (0)
Book Story (1)
Digital Life (7)
My Life (7)
세미나자료 (1)
개인용 (0)
Musics (0)
Total : 263,016
Today : 2 Yesterday : 15