[조립하면서 배우는 PE] 네번째 이야기 : PE Header(2) Reverse Engineering
리버싱 2017. 1. 10. 16:31
네번째 이야기입니다.점점 복잡해지기 시작하네요. 그렇다고 매우 어렵거나 이해하기 어려운 정도의 수준은 아니니까 편한 마음으로 읽어보시기 바랍니다. 지난 세번째 이야기에서는 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에 관한 것입니다. 그럼 즐핵~ 하세요.
출처 - http://zesrever.tistory.com/58