PE 포맷: 바인드된 파일의 임포트 정보

@codemaru · August 21, 2007 · 9 min read

지난 시간(마소 8월호 "실행 파일 속으로")에 소개하지 못했던 PE 포맷의 바인드 정보를 분석해 보도록 합시다. 간단하게 바인드 됐다라는 말의 의미부터 짚고 넘어가도록 하죠. 바이드 됐다라 함은 DLL의 IAT가 이미 완성 되었다는 의미입니다. 더 어려운가요?

임포트 정보를 분석할 때 우리는 PE 포맷의 IAT에 기록된 내용을 바탕으로 로딩할 때 그곳에 실제 함수 번지를 채운다고 했었죠. 그런데 로딩할 때마다 함수 정보를 구해서 채우는 작업이 물론 눈 깜짝할사이 벌어지긴 하지만 시간이 걸리긴 하죠. DLL이 수십개 되면 무시못할 지연 시간이 생깁니다. 똑똑한 MS 아이들 이를 가만히 지켜보고만 있진 않았습니다. 그렇다면 로딩하기 전에 이미 로딩됐을때의 번지를 기록해 두면 어떻게 될까? 라는 생각을 한거죠. 그리고 그렇게 해보니까 효율이 좋아지더라는 겁니다. 결론은 바인드 됐다라는 말은 IAT가 이미 완성되었다 라는 말이라고 생각하면 됩니다.

그렇다면 바인드 정보를 포함하고 있는 notepad.exe를 한번 분석해 보도록 합시다. 윈도우에 포함된 기본 노트패드 입니다. 우선 바인드된 PE 포맷은 섹션 헤더 이후에 BOUND_IMOPRT_TABLE(이하 BIT)이라는 놈이 따라 나옵니다. 이 놈의 위치는 NT헤더 OptionalHeader에 있는 DataDirectory에서 IMAGE_DIRECTORY_ENTRY_ BOUND_IMPORT를 따라 가면 됩니다. PE Viewer로 슈욱 따라가보면 아래와 같은 화면이 나옵니다.

PE                    md 0

미칠것 같이 간단한 구조체의 연속입니다. 기본적으로 아래에 있는 IMAGE_BOUND_IMPORT_DESCRIPTOR가 쭈욱 옵니다. NumberOfModuleForwardRefs가 0이 아니면 그 개수만큼 뒤에 IMAGE_BOUND_FORWARD_REF가 따라 옵니다. 그런데, 결국 두 구조체는 그놈이 그놈입니다. 그냥 쉽게 IMAGE_BOUND_IMPORT_DESCRIPTOR가 쭈욱 온다고 생각하시면 편합니다. OffsetModuleName은 BIT의 시작 번지 부터 문자열이 있는 곳까지의 오프셋을 의미합니다.

typedef struct \_IMAGE\_BOUND\_IMPORT\_DESCRIPTOR {  
    DWORD   TimeDateStamp;  
    WORD    OffsetModuleName;  
    WORD    NumberOfModuleForwarderRefs;  
// Array of zero or more IMAGE\_BOUND\_FORWARDER\_REF follows  
} IMAGE\_BOUND\_IMPORT\_DESCRIPTOR,  \*PIMAGE\_BOUND\_IMPORT\_DESCRIPTOR;  
  
typedef struct \_IMAGE\_BOUND\_FORWARDER\_REF {  
    DWORD   TimeDateStamp;  
    WORD    OffsetModuleName;  
    WORD    Reserved;  
} IMAGE\_BOUND\_FORWARDER\_REF, \*PIMAGE\_BOUND\_FORWARDER\_REF;
```이제 바운드된 이미지의 IAT를 살펴봅시다. 아래는 노트패드의 iAT 화면입니다. 머 일반적인 IAT와 다른 점이라면 IAT에 들어있는 값들입니다. 원래라면 IMAGE\_IMPORT\_NAME을 가리키는 RVA가 들어 있어야 하죠. 그런데 여기는 이미 완성된 IAT가 들어있습니다. 익스포트 테이블 뒤지고 할 필요가 없어진 것이죠.  
  
![](PE_포맷_바인드된_파일의_임포트_정보.md_1.png)

다 끝난 것 같아 보이지만 바인드 정보의 가장 핵심적인 부분을 밝히지 않았습니다. 바로 이미 기록해 둔 주소가 바뀌는 경우 입니다. notepad.exe는 gdi32.dll을 땡겨 씁니다. 와 좋습니다. 바인드 다 돼있죠. 그런데 gdi32.dll이 업데이트가 되었습니다. 원래는 EndPage의 주소가 0x77E35923이었는데, 바뀐 DLL에서는 0x77777777이 되었습니다. notepad.exe를 실행합니다. 바로 크래시 될까요? 그러면 완전 야매겠죠. 그렇진 않습니다.  
  
어떻게 그렇지 않을까요? 바로 임포트 룩업 테이블(ILT)가 있기 때문입니다. ILT에는 기존 IAT와 동일한 정보가 들어있습니다. 어떤 DLL의 어떤 함수를 땡겨 쓰는가에 대한 것이죠. 따라서 DLL이 업데이트 되면 완성된 IAT를 이용하지 않고 ILT를 통해서 바인드 되지 않은 이미지와 동일하게 처리됩니다.  
  
신기하죠? 그렇다면 gdi32.dll이 업데이트 됐는지는 어떻게 알아낼까요? 앞서 살펴본 IMAGE\_IMPORT\_BOUND\_ DESCRIPTOR에 그 비밀이 있습니다. 거기에 기록된 TimeStamp값을 이용하는 것이죠. 그 값과 해당 DLL의 NT헤더에 있는 TimeStamp 값을 비교합니다. 일치하면 업데이트 되지 않은 것이고, 일치하지 않으면 업데이트 된 것으로 간주하는 것입니다.   
  
아래는 DLL 로더에서 바인드된 DLL 인지, 바인드된 DLL이 바인드될 때와 동일한 것인지를 체크하는 부분입니다. 1을 리턴 하면 IAT를 다시 작성할 필요가 없는 경우입니다. 0을 리턴하면 DLL이 업데이트 되었기 때문에 ILT를 통해서 다시 IAT를 만들어야 하는 경우입니다. -1은 로딩하려는 DLL 자체가 존재하지 않는 경우입니다.  
```cpp
DWORD CDllLoader::IsBound(CDllInfo \* dll)  
{  
    IMAGE\_NT\_HEADERS \*nt = dll->NTHeader();  
    IMAGE\_DATA\_DIRECTORY ibd;  
    ibd = dll->DataDirectory(IMAGE\_DIRECTORY\_ENTRY\_BOUND\_IMPORT);  
  
    // 바인드 정보가 없는 경우 리턴  
    if(ibd.VirtualAddress == 0 || ibd.Size == 0)  
        return 0;  
  
    DWORD ret = 1;  
    HMODULE module;  
    list<HMODULE> modules;  
    char \*dllName;  
    PIMAGE\_BOUND\_IMPORT\_DESCRIPTOR bidBase;  
    PIMAGE\_BOUND\_IMPORT\_DESCRIPTOR bid;  
    bidBase = bid = (PIMAGE\_BOUND\_IMPORT\_DESCRIPTOR)   
                           GetPtr(dll->baseMem, ibd.VirtualAddress);  
  
    while(bid->TimeDateStamp)  
    {  
        // 모듈 로딩  
        dllName = (char \*) GetPtr(bidBase, bid->OffsetModuleName);  
        module = ::LoadLibraryA(dllName);  
        if(!module)  
        {  
            ret = -1;  
            break;  
        }  
  
        // 타임 스탬프 비교  
        dll->modules.push\_back(module);  
        if(bid->TimeDateStamp != GetModuleTimeStamp(module))  
        {  
            ret = 0;  
            break;  
        }  
  
        // 포워딩 DLL 스킵  
        for(WORD i=0; i<bid->NumberOfModuleForwarderRefs; ++i)  
            ++bid;  
          
        ++bid;  
    }  
  
    // 실패한 경우 로드한 모듈 프리  
    if(ret <= 0)  
    {  
        for\_each(dll->modules.begin(), dll->modules.end(), ::FreeLibrary);  
        dll->modules.clear();  
    }  
  
    return ret;  
}
```심오한 바인드 정보의 세계 잘 보셨나요? 끝으로 남은 마지막 궁금증을 해결해 보도록 합시다. 다 좋습니다. 그렇다면 바인드된 PE 파일은 어떻게 만드는 겁니다. Visual C++에 포함된 bind 유틸리티를 사용하면 됩니다. 아래 화면을 보고 사용해 보세요.   
  
![](PE_포맷_바인드된_파일의_임포트_정보.md_2.png)

그런데 사실 선 바인딩을 하는 것은 큰 효과가 없습니다 . 왜냐하면 설치된 시스템의 DLL들과 바인딩을 수행한 시스템의 DLL들이 다를 수 있기 때문입니다. 바인딩은 XP에서 했는데 설치되는 컴퓨터는 죄다 2000이라면 쓸데없는 짓을 한게 되겠죠. 그래서 바인딩을 수행하는 가장 이상적인 타이밍은 대상 시스템에 설치되는 시점입니다. 인스톨러가 설치 파일을 다 설치한다음 바인딩을 해버리는 거죠.  
  

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중