PE(Portable Executable) File Format (5) - PE Header
Section Header
각 Section 의 속성(property)을 정의한 것이 Section Header 입니다.
section header 구조체를 보기 전에 한번 생각을 해보겠습니다.
앞서 PE 파일은 code, data, resource 등을 각각의 section 으로 나눠서 저장한다고 설명드렸습니다.
분명 PE 파일 포멧을 설계한 사람들은 어떤 장점이 있기 때문에 그랬을 겁니다.
PE 파일을 여러개의 section 구조로 만들었을때 (제가 생각하는) 장점은 바로 프로그램의 안정성입니다.
code 와 data 가 하나의 섹션으로 되어 있고 서로 뒤죽박죽 섞여 있다면, (실제로 구현이 가능하긴 합니다.)
그 복잡한은 무시하고라도 안정성에 문제가 생길 수 있습니다.
가령 문자열 data 에 값을 쓰다가 어떤 이유로 overflow 가 발생(버퍼 크기를 초과해서 입력) 했을때
바로 다음의 code (명령어) 를 그대로 덮어써버릴 것입니다. 프로그램은 그대로 뻗어 버리겠죠.
즉, code/data/resource 마다 각각의 성격(특징, 엑세스 권한)이 틀리다는 것을 알게 된 것입니다.
- code - 실행, 읽기 권한
- data - 비실행, 읽기, 쓰기 권한
- resource - 비실행, 읽기 권한
그래서 PE 파일 포멧 설계자들은 비슷한 성격의 자료를 section 이라고 이름 붙인 곳에 모아두기로 결정하였고,
각각의 section 의 속성을 기술할 section header 가 필요하게 된 것입니다.
(section 의 속성에는 file/memory 에서의 시작위치, 크기, 엑세스 권한 등이 있어야 겠지요.)
이제 section header 가 무슨 역할을 하는지 이해 되셨나요?
IMAGE_SECTION_HEADER
section header 는 각 section 별 IMAGE_SECTION_HEADER 구조체의 배열로 되어있습니다.
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
* 출처 : Microsoft 의 Visual C++ 에서 제공하는 winnt.h
IMAGE_SECTION_HEADER 구조체에서 알아야 할 중요 멤버는 아래와 같습니다. (나머지는 사용되지 않습니다.)
- VirtualSize : 메모리에서 섹션이 차지하는 크기
- VirtualAddress : 메모리에서 섹션의 시작 주소 (RVA)
- SizeOfRawData : 파일에서 섹션이 차지하는 크기
- PointerToRawData : 파일에서 섹션의 시작 위치
- Characteristics : 섹션의 특징 (bit OR)
VirtualAddress 와 PointerToRawData 의 값은 아무 값이나 가질 수 없고,
각각 (IMAGE_OPTIONAL_HEADER32 에 정의된) SectionAlignment 와 FileAlignment 에 맞게 결정됩니다.
VirtualSize 와 SizeOfRawData 는 일반적으로 서로 틀린값을 가집니다.
즉, 파일에서의 섹션 크기와 메모리에 로딩된 섹션의 크기는 틀리다는 얘기가 되는 거죠.
Characteristics 는 아래 값들의 조합(bit OR)으로 이루어 집니다.
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
마지막으로 Name 항목에 대해서 얘기해보겠습니다.
Name 멤버는 C 언어의 문자열처럼 NULL 로 끝나지 않습니다. 또한 ASCII 값만 와야한다는 제한도 없습니다.
PE 스펙에는 섹션 Name 에 대한 어떠한 명시적인 규칙이 없기 때문에 어떠한 값을 넣어도 되고 심지어 NULL 로 채워도 됩니다.
또한 개발 도구에 따라서 섹션 이름/갯수 등이 달라집니다.
따라서 섹션의 Name 은 그냥 참고용 일뿐 어떤 정보로써 활용하기에는 100% 장담할 수 없습니다.
(데이타 섹션 이름을 ".code" 로 해도 되거든요.)
자 그러면 실제 notepad.exe 의 Section Header 배열을 살펴보죠. (총 3 개의 섹션이 있습니다.)
구조체 멤버별로 살펴보면 아래와 같습니다.
offset value description
-------------------------------------------------------------------------------
000001D8 2E746578 Name (.text)
000001DC 74000000
000001E0 00007748 virtual size
000001E4 00001000 RVA
000001E8 00007800 size of raw data
000001EC 00000400 offset to raw data
000001F0 00000000 offset to relocations
000001F4 00000000 offset to line numbers
000001F8 0000 number of relocations
000001FA 0000 number of line numbers
000001FC 60000020 characteristics
IMAGE_SCN_CNT_CODE
IMAGE_SCN_MEM_EXECUTE
IMAGE_SCN_MEM_READ
00000200 2E646174 Name (.data)
00000204 61000000
00000208 00001BA8 virtual size
0000020C 00009000 RVA
00000210 00000800 size of raw data
00000214 00007C00 offset to raw data
00000218 00000000 offset to relocations
0000021C 00000000 offset to line numbers
00000220 0000 number of relocations
00000222 0000 number of line numbers
00000224 C0000040 characteristics
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
IMAGE_SCN_MEM_WRITE
00000228 2E727372 Name (.rsrc)
0000022C 63000000
00000230 00008304 virtual size
00000234 0000B000 RVA
00000238 00008400 size of raw data
0000023C 00008400 offset to raw data
00000240 00000000 offset to relocations
00000244 00000000 offset to line numbers
00000248 0000 number of relocations
0000024A 0000 number of line numbers
0000024C 40000040 characteristics
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
RVA to RAW
Section Header 를 잘 이해하셨다면 이제부터는 PE 파일이 메모리에 로딩되었을때
각 섹션에서 메모리의 주소(RVA)와 파일 옵셋을 잘 매핑할 수 있어야 합니다.
이러한 매핑을 일반적으로 "RVA to RAW" 라고 부릅니다.
방법은 아래와 같습니다.
1) RVA 가 속해 있는 섹션을 찾습니다.
2) 간단한 비례식을 사용해서 파일 옵셋(RAW)을 계산합니다.
IMAGE_SECTION_HEADER 구조체에 의하면 비례식은 이렇습니다.
RAW = RVA - VirtualAddress + PointerToRawData
간단한 퀴즈를 내보겠습니다.
아래 그림은 notepad.exe 의 File 과 Memory 에서의 모습입니다.
각각 RVA 를 계산해 보세요. (계산기 calc.exe 를 Hex 모드로 세팅하시면 계산이 편합니다.)
Q1) RVA = 5000h 일때 File Offset = ?
A1) 먼저 해당 RVA 값이 속해 있는 섹션을 찾아야 합니다.
=> RVA 5000h 는 첫번째 섹션(".text")에 속해있습니다. (ImageBase 01000000h 를 고려하세요.)
비례식 사용
=> RAW = 5000h(RVA) - 1000h(VirtualAddress) + 400h(PointerToRawData) = 4400h
Q2) RVA = 13314h 일때 File Offset = ?
A2) 해당 RVA 값이 속해 있는 섹션을 찾습니다.
=> 세번째 섹션(".rsrc")에 속해있습니다.
비례식 사용
=> RAW = 13314h(RVA) - B000h(VA) + 8400h(PointerToRawData) = 10714h
Q3) RVA = ABA8h 일때 File Offset = ?
A2) 해당 RVA 값이 속해 있는 섹션을 찾습니다.
=> 두번째 섹션(".data")에 속해있습니다.
비례식 사용
=> RAW = ABA8h(RVA) - 9000h(VA) + 7C00h(PointerToRawData) =
=> 계산 결과로 RAW = 97A8h 가 나왔지만 이 옵셋은 세번째 섹션(".rsrc")에 속해 있습니다.
RVA 는 두번째 섹션이고, RAW 는 세번째 섹션이라면 말이 안되지요.
이 경우에 "해당 RVA(ABA8h)에 대한 RAW 값은 정의할 수 없다" 라고 해야 합니다.
이런 이상한 결과가 나온 이유는 위 경우에 두번째 섹션의 VirtualSize 값이 SizeOfRawData 값 보다 크기 때문입니다.
PE 파일의 섹션에는 Q3) 의 경우와 같이 VirtualSize 와 SizeOfRawData 값이 서로 틀려서 벌어지는
이상하고 재미있는(?) 일들이 많이 있습니다. (앞으로 살펴보게 될 것입니다.)
이것으로 PE Header 의 기본 구조체들에 대한 설명을 마쳤습니다.
다음에는 PE Header 의 핵심인 IAT(Import Address Table), EAT(Export Address Table) 에 대해서 공부해 보겠습니다.
(continue)