2023년 4월 5일 수요일

How Image Loader Bind DLL To Process

개요

PE 파일을 프로세스 형태로 메모리에 올리는 과정에 흔히들 "이미지 로더"라는 이름을 들어보셨을 겁니다 근데 막상 이미지 로더의 동작이 어디서 부터 어디까지냐 ? 하는 물음에 대한 답을 찾기는 굉장히 애매하고 그 과정이 매우 외롭다고 생각합니다.. 이미지 로더는 유저모드 시스템 DLL인 Ntdll.dll 내에 존재하며, DLL의 일부분인 표준 코드처럼 동작합니다 이 코드가 특별한 점은 실행 프로세스 내에 항상 존재한다는 보장성(Ntdll.dll은 항상 로드되어 있음)과 새로운 애플리케이션의 일부로서 유저 모드에서 실행하는 최초의 코드라는 점입니다

주제 

앞서 살짝 언급드렸듯이 실행 파일 (PE)이 실행되면 메모리에 프로세스 형태로 안착하는 과정을 거치게 됩니다 일명 "프로세스 초기화" 과정을 거치게 되는데 오늘은 그 중의 하나인 DLL 바인딩 동작에 관한 몇가지를 소개드리겠습니다

개념

  • DLL 이란 ?  DLL의 개념은 너무나 잘 아시겠지만... 사전적인 설명보다 개념적으로 풀어 말하자면, 실행 파일이 동작하는 데 있어서 모든 함수셋을 직접 만들어 쓸 필요는 없습니다 누군가 미리 만들어놓은 함수 셋을 갖다쓰면 참 좋겠는데 그 개념이 바로 DLL 이져
  • DLL 바인딩이란 ? 누군가 미리 만들어 놓은 함수 셋이라도 갖다쓰려면 규칙이 필요하져 개발자는 LoadLibrary로 갖다 쓰고 싶은 DLL을 불러올 수 있지만 실질적으로 그것이 가능하려면 DLL 바인딩 과정이 있어야 합니다 그것을 해주는 주체가 Windows OS 환경에서는 이미지 로더이고 시점은 실행 파일이 실행되는 시점이 됩니다
    • 그리고 실행파일만 DLL을 갖다 쓰는게 아닙니다 다른 DLL 파일도 DLL 파일을 갖다 쓸 수 있습니다(경고) 이부분은 실습으로 자세히 알아보겠습니다
    • 이미지 로더의의 바인딩 과정이 가능하려면 DLL을 비롯한 모든 PE 파일은 아래와 같은 "포맷"을 갖춰야 합니다

<그림 1 - PE 구성도 >


<그림 2 - PE 헤더 구성도>

 

되게 복잡해 보이고 다 알아야 할 것 같지만 제가 설명하고 싶은 부분만 말씀드리겠습니다 (왜냐면 그 외의 것은 사실 바인딩 과정에 꼭 알아야 할 필요는 없기 때문입니다)

Dos Header와 Dos Stub는 Widnows OS의 시초가 되는 DOS 운영체제를 위해 존재하는 부분으로 도스에서 실행되도록, 하위 호환을 위한 부분이라 중요하지 않습니다 다만, Dos Header의 마지막 4바이트에 적힌 값 여기서는 0x3C 에 해당하는 것은 NT 헤더 오프셋으로 그 위치로 이동하게 되면 NT 헤더가 시작됩니다 

NT 헤더는 3개의 멤버로 구성됩니다 

NT 헤더 멤버

  • 시그니처
  • 파일 헤더 (머신, 섹션 개수, 옵셔널 헤더 사이즈, 파일 특성)
  • 옵셔널 헤더 (매직, 엔트리 포인트 주소, 이미지 베이스, 섹션/파일 얼라인먼트, 메이저 os 버전, 이미지 크기, 헤더 크기, 서브시스템, 데이터 디렉토리 개수, 데이터 디렉토리)

이 중, 파일 헤더와 옵셔널 헤더는 또 각각이 하나의 구조체입니다

각각이 의미하는 ... 뭐 PE 시그니처를 나타내는 0x00004550, 파일이 실행될 CPU 타입 정보를 나타내는 Machine 값(여기서는 0x014c(Inter386)) 등의 기본적인 이야기는 여기서 하지 않겠습니다 다만, 옵셔널 헤더 구조체의 마지막, 데이터 디렉토리 멤버는 16개 (0x10) 배열로, 그 중 2번째 배열이 담고있는 정보가 Import Table이라는 것은 강조할만한 내용입니다

그만 머뭇거리고 본론으로 들어가죵

본론



옵셔널 헤더의 맨 마지막에는 IMAGE_DATA_DIRECTORY 구조체 배열의 DataDirectory 멤버가 존재합니다 이 배열의 각 요소는 순서대로 아래와 같은 값을 갖습니다

  • IMAGE_DIRECTORY_ENTRY_EXPORT (0)
  • IMAGE_DIRECTORY_ENTRY_IMPORT (1)
  • IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
  • IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
  • IMAGE_DIRECTORY_ENTRY_SECURITY (4)
  • IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
  • IMAGE_DIRECTORY_ENTRY_DEBUG (6) 
  • IMAGE_DIRECTORY_ENTRY_COPYRIGHT x86 usage / IMAGE_DIRECTORY_ENTRY_ARCHITECTURE (7)
  • IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
  • IMAGE_DIRECTORY_ENTRY_TLS (9)
  • IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG(10)
  • IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
  • IMAGE_DIRECTORY_ENTRY_IAT (12)
  • IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT (13)
  • IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR (14)

배열의 총 개수는 16개인데 요소는 15개 뿐인 것은 배열의 마지막 요소는 필드를 0으로 채워서 배열이 끝남을 표시하도록 했기 때문입니다

이 중, 바인딩과 관련된 곳은 배열의 1번 요소, IMAGE_DIRECTORY_ENTRY_IMPORT 입니다

해당 구조체는 VirtualAddress 와 Size 멤버로 구성되어 있는데 VirtualAddress 멤버에 담긴 주소를 따라가면, IMAGE_IMPORT_DESCRIPTOR 라는 이름의 구조체가 존재합니다

이 구조체는 아래와 같은 멤버변수를 담고 있습니다 임포트하는 DLL과 그 안에 있는 함수들 중에 사용하는 것(임포트)들에 대한 정보의 위치를 기록해놓은 저장소라고 보시면 됩니다

_IMAGE_IMPORT_DESCRIPTOR 원본 접기
typedef struct _IMAGE_IMPORT_DESCRIPTOR {   
    union {        DWORD   Characteristics;                // 0 for terminating null import descriptor       
    PIMAGE_THUNK_DATA OriginalFirstThunk;   // RVA to original unbound IAT   
};   
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;   
PIMAGE_THUNK_DATA FirstThunk;           // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

여기서 제일 중요한 3가지 필드를 살펴보자면

  • OriginalFirstThunk    Import Lookup Table(ILT)의 RVA(상대주소)
  • Name                        DLL명을 담고 있는 Null - 종료 아스키 문자열테이블의 RVA
  • FirstThunk                 IAT(이미지 로더에 의해 구성되는 함수 주소의 선형 배열)

OriginalFirstThunk와 FirstThunk는 모두 IMAGE_THUNK_DATA 구조체 배열을 가리킵니다 이 구조체는 특별히 서로다른 멤버를 갖는 하나의 큰 union 입니다 무슨말인고 하면, OriginalFirstThunk가 가리키는 구조체에서는 AddressOfData가, FirstThunk가 가리키는 구조체에서는 Function이 멤버가 됩니다

IMAGE_TUHNK_DATA 원본 접기
typedef struct _IMAGE_THUNK_DATA {
    union {
        PBYTE  ForwarderString;
        PDWORD Function;
        DWORD Ordinal;
        PIMAGE_IMPORT_BY_NAME AddressOfData;
    } u1;
} IMAGE_THUNK_DATA;
typedef IMAGE_THUNK_DATA * PIMAGE_THUNK_DATA;

모듈에 의해 임포트 되는 각 함수들은 모두 IMAGE_THUNK_DATA 구조체를 통해 표현됩니다

근데 왜 우리가 배열을 두개나 알아야 할까여? 그 이유는, 하낭나의 배열은 임포트된 루틴(함수)들의 이름을 저장하기 위해(ILT) 쓰이고, 다른 하나는 임포트된 함수들의 주소(IAT)를 담고 있기 때문입니다

앞서 말씀드렸다시피, FirstThunk가 담고있는 u1.Function이 임포트된 함수의 주소를, OriginalFirstThunk 구조체는 IMAGE_IMPORT_BY_NAME 구조체를 가리키는 AddressOfData를 사용하며 이 필드는 임포트된 함수의 첫번째 문자를 가리키는 Name 필드를 담고 있습니다

DLL에서 함수를 찾아갈 떄는 임포트된 함수의 이름을 이용하거나 ordinal number를 이용하는 두가지 방법이 있습니다 함수가 ordinal 임포트 방식을 취학하고 있는지 아닌지를 구분하려면 ILT 배열의 IMAGE_THUNK_DATA 구조체의 ordinal 플래그가 세팅되어있는지를 확인하면 됩니다

Tell if a routine is an ordinal import 원본 접기
#define IMAGE_ORDINAL_FLAG 0x80000000
if( (*thunkILT).u1.Ordinal & IMAGE_ORDINAL_FLAG )
{
  //ordinal import
}


개발자 관점에서 보자면, PE 파일은 단순히 nested 구조를 띄는 구조체 일 뿐입니다


이처럼 바인딩 전에 IAT와 ILT가 담고 있는 값은  보시는 것처럼 동일하게 IMPORT_BY_NAME (Ordinal + 함수명로 구성된) 구조체 RVA 입니다

바인딩 과정을 거치면 ILT는 그대로지만, IAT는 실제 함수의 주소로 채워지게 됩니다


그렇다면, 두구두구 바인딩은 누가? 언제? 어떻게? 하는 걸까여

(1) 이미지 로더가 (2) 프로세스가 실행되었을 때 (3) EAT(Export Address Table)에서 함수 주소를 찾아, IAT에 기록 

이라고 간결하게 말씀드릴 수 있겠습니다

PEB 구조체의 LdrInitState 변수값이 0이면 아직 프로세스 초기화가 진행되기 전임을, 1로 설정하면 바인딩(임포트 로딩)이 진행중임을 마지막으로 2이면 임포트 로드가 완료됐음을 의미합니다

(3)번의 어떻게에 대해서는 실습을 통해 자세히 말씀드리겠습니다

실습



그림은 PEView를 통해 본 jscript9.dll의 IMPORT Directory Table 입니다 구조체로는 IMAGE_IMPORT_DESCRIPTOR에 해당합니다 jscript9.dll 이라는 DLL도 DLL이지만 다른 DLL을 임포트하는 모습입니다

각 멤버는 IMAGE_IMPORT_DESCRIPTOR 구조체에서 각각 다음에 해당합니다

Import Name Table RVA - OriginalFirstThunk

Time Date Stamp - TimeDateStamp

Forwarder Chain - ForwarderChain

Name RVA - Name

Import Address Table RVA - FirstThunk

프로세스가 실행되면 모듈 베이스(모듈의 시작 위치)에 PE헤더부터 올라가게 됩니다


 실습에서 모듈의 시작위치는 0x62520000으로 잡혔습니다

이미지 로더는 바인딩 작업을 위해 첫 번째로, DLL 이름 문자열이 기록된 위치 정보가 담긴 Name 필드를 통해 DLL 이름을 확인하고 메모리에 로드합니다


모듈 베이스에서 해당 RVA 값을 더하니 kernel32.dll라는 문자열이 기록된 메모리를 확인할 수 있습니다


그 다음으로 이미지 로더는 IID의 OriginalFirstThunk (ILT의 RVT)를 이용해 ImageThunkData 구조체에 담아 AddressOfData 멤버를 통해 DLL 내 임포트하고 있는 함수명을 하나하나 확인 합니다

말씀드렸다시피 AddressOfData(Import Name Table)에는 kernel32.dll에서 사용하는 함수 이름 문자열이 있는 RVA 정보가 Table 형태로 기록되어 있습니다



INT(Import Name Table)


7번째 RVA값(00094c38)값을 가지고 실제 메모리 주소(모듈 베이스를 더한 곳)로 이동해보면 "VirtualProtect"문자열을 확인할 수 있습니다


이제 이미지로더는 "VirtualProtect"문자열을 가지고 kernel32.dll의 EAT(Export Address Table)로 가서 실제 호출 주소 정보를 얻은 뒤에 Import Address Table에 기록하게 됩니다

이 과정이 일어나기 전에는 ILT(Import Lookup Table)과 IAT(Import Address Table)이 아래와 같이 서로 같은 값을 바라보게 됩니다






위 : ILT, 아래 IAT 테이블의 배열의 각 요소가 같은 값을 가리키고 있습니다


바인딩이 일어나고 나면 아래와 같이 변하게 됩니다(실습 시도를 여러번 하다보니 바인딩이 일어난 뒤 시점을 찍은 실습은 앞과 다른 실습이라 모듈베이스 0x60b80000로 달라졌습니다)


위 : IAT 테이블이 담고 있는 값이 달라진 것을 확인할 수 있습니다

7번째 요소가 가리키는 값의 의미는


실제 kernel32.dll 의 VirtualProtectStub 루틴의 시작 주소입니다


즉, kernel32.dll모듈 내 함수의의 주소를 의미합니다


결론

바인딩을 거치면서 달라지는 모습을 도식화하면 아래와 같습니다



이렇듯 바인딩 이후엔 IAT가 실제 함수 주소로 재작성되기 때문에 원본 정보를 얻을 방법이 남아있어야 하고 그것이 바로 INT입니다


출처

댓글 없음:

댓글 쓰기