개요
지난번에 PE 파일의 헤더구조 및 Import/Export 섹션에 대해서 알아보았다. 잘 기억이 안나면 01 PE 파일 분석-헤더분석 문서 및 02 PE 파일 분석-Import 분석, 그리고 03 PE 파일 분석-Export 분석 문서를 다시 보자.
일단 들어가기 전에 재배치(Relocation)이 무엇인지 알아보자. 재배치는 코드에 특정 값을 더해줘서 다른 메모리 주소에서 실행 가능하게 해주는 것을 말한다. 말 그대로 코드를 다시 배치하는 과정인데, 왜 이런걸 해야 하는 걸까?
EXE 파일의 경우 윈도우에서는 굳이 재배치를 할 필요가 없다. 왜냐하면 로더가 프로그램을 로딩할때 제일 먼저 EXE 파일을 위한 메모리를 할당해주기 때문이다. IMAGE_OPTIONAL_HEADER에 ImageBase라는 필드를 기억할지 모르겠다. 이것이 바로 PE 파일이 로딩될 Base 주소를 의미한다. EXE 파일의 경우 가장 먼저 메모리를 할당 받으므로 ImageBase(일반적으로 0x400000)에 로딩 가능하다.
DLL의 경우는 어떨까? 실행파일이 사용하는 DLL이 어디 한두개일까? 여러개가 로딩이 되면 당연히 그중에 몇몇 DLL은 ImageBase에 로딩하지 못하는 경우도 발생한다. 이때 어쩔 수 없이 다른 메모리 주소에서 실행해야하는데 이 과정을 재배치 과정이라고 하고 재배치 섹션의 정보가 사용되는 것이다.
이제 세부 구조에 대해서 알아보자
재배치 섹션의 시작
재배치 정보는 다른 정보와 마찬가지로 IMAGE_OPTIONAL_HEADER의 Data Directory에서 찾을 수 있고 0x05 인덱스(IMAGE_DIRECTORY_ENTRY_BASERELOC)에서 그 RVA를 구할 수 있다.
재배치 섹션의 처음 시작은 IMAGE_BASE_RELOCATION 구조체로 시작하며 WinNT.h에 아래와 같이 정의되어있다.
- typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
각 항목은 위와 같이 정의되어있고 아래와 같은 의미를 가진다.
- VirtualAddress : 재배치가 수행될 메모리 상의 RVA.
- SizeOfBlock : 재배치 영역의 크기. IMAGE_BASE_RELOCATION 자신 크기를 포함한 전체 크기
Export 섹션보다 더 간단한 구조를 가진다. 이후에 보면 알겠지만 재배치 데이터 같은 경우 IMAGE_BASE_RELOCATION 구조체 다음에 n개의 WORD 형태로 반복해서 나타나고 그중 하위 0xFFF는 Offset으로 사용된다.
따라서 재배치할 영역이 크고 넓은 경우 IMAGE_BASE_RELOCATION + n개의 WORD의 형태가 반복되어서 나타나게 된다. 마지막은 역시 데이터가 0인가를 이용하여 판단한다.
재배치 정보
그럼 IMAGE_BASE_RELOCATION 이후에 존재하는 n개의 WORD는 어떤식으로 구성될까?
상위 4Bit는 재배치 Type으로 사용되며 하위 12Bit는 Offset으로 사용된다. 따라서 코드를 작성한다면 아래와 같이 쓸 수 있을 것이다.
- WORD wData;
- printf( "Type %X\n", ( wData & 0xF000 ) >> 12 );
- printf( "Offset %X\n", wData & 0xFFF );
Type과 Offset은 아래와 같은 의미를 가진다.
- Type : 재배치 정보의 타입. 사실 거의 의미가 없고 0일 경우 패딩 데이터
- Offset : 실제 코드가 재배치 될 영역. VirtualAddress와 더해져서 수정해야할 RVA 값이 됨
위에서 보듯 실제 코드가 수정되어야 하는 위치는 IMAGE_BASE_RELOCATION의 VirtualAddress와 Offset을 더한 값이 된다. 정말 간단하다.
여기서 알아두어야 할 점은 Offset이 최대 0xFFF라는 것이다. 즉 4Kbyte까지 커버가 가능하므로 4Kbyte 이상이 되면 다시 IMAGE_BASE_RELOCATION을 만들어서 접근해야 한다.
이것을 그림으로 보면 아래와 같다.
재배치 수행
위에서 메모리에 어디를 재배치 해야하는가에 대한 정보를 알아보았다. 그럼 과연 그 위치에서 무엇을 해야 정상적으로 동작될까? 개요에서 재배치를 수행하는 이유가 모듈의 시작 위치가 이동되기 때문이라고 했다. 그럼 당연히 이동한 거리만큼 값을 더해줘야 정상적으로 수행이 될 것이라는 것을 알 수 있다.
재배치를 수행하는 과정은 아주 간단하다. 아래의 순서대로 수행하면 된다.
- 재배치가 수행되야 할 곳의 DWORD 값을 읽는다.
- 읽은 DWORD 값에서 현재 IMAGE_OPTIONAL_HEADER의 ImageBase를 뺀다. 빼는 순간 코드는 0을 기본 Base 주소로 하는 코드로 변한다.
- 뺀 값에 실제로 모듈이 로딩된 메모리 주소를 더한다. 더하는 순간 코드는 실제 모듈이 로딩된 위치에서 정상적으로 수행가능한 코드로 변한다.
단순한 뺄셈과 덧셈만으로 재배치가 가능하다. 자료 구조가 간단한 만큼 처리 또한 간단하다.
실제 구현
구조가 아주 간단한 만큼 코드도 아주 간단하다. IMAGE_BASE_RELOCATION의 정보가 0일때까지 모든 재배치 정보를 표시한다.
- PEAnalyzer.h : PE 파일을 분석하는 클래스의 헤더 파일
- PEAnalyzer.cpp : PE 파일을 분석하는 클래스의 소스 파일
- main.cpp : 실제 사용하는 예제
- PEAnalyzer.exe : 실행 파일
아래는 실행 결과이다.
마치면서...
간단히 재배치 섹션에 대해서 알아보았다. 아주 심플한 자료구조와 반복된 작업을 수행하면 코드를 어디서든 실행가능하게 할 수 있다는 놀라운 사실을 알게 됬으니, 이제 실행 코드를 임의의 영역에 메모리를 할당 시키고 실행 시키는 것도 가능 할 것이다(왜 자꾸 말만하면 어둠(??)의 세계 이야기가 나오는지 모르겠다... ㅡ_ㅡ;;;; 이러면 안되는데...).
다음은 기회가 되면 실행파일을 조작해서 특정한 일을 수행하는 내용을 하려 하는데, 약간 민감한 부분이 잇어서 언제가 될지는 모르겠다.