PE 포맷이란 윈도우 OS가 파일을 실행시키기 위해서 Portable Executable 포맷
(확장자 : .exe와 .dll)을 동적 라이브러리를 링킹하기 위한 참조 값과 API export and import tables, 리소스 데이터와 TLS 데이터를 캡슐화한 것이다.
소스 코드를 컴파일하고 링크를 하여 PE 구조의 실행 파일이 생성되는 과정을 순서도로 보면
실행 파일에는 어떤 내용들이 들어있는지 윈도우 Notepad.exe를 메모장으로 열어보겠다.
MZ는 PE를 만든 Mark Zbikowski의 이니셜로써, MS-DOS 헤더의 시작을 알리는 문자이다.
"This program cannot be run in DOS mod" 문자열은 DOS 에서 윈도우 프로그램이 실행되면 출력하는 문자열이다. PE 문자열 전까지가 도스 헤더이다.
이번에는 Notepad.exe를 Hex 코드로 열어보겠다.
hex에디터중에는 HexWorkshop(http://www.Hexworkshop.com)이 최고 좋다고
생각되지만 상용프로그램이라 여기서는 프리웨어인 XVI32로 분석을 하겠다.
PE 구조
DOS Header
DOS Stub
PE File Header
Optional Header
Section Table
Sectiions
PE 포맷은 규칙에 의해서 프로그램 실행 파일의 제일 처음 부분부터 배열되있다. 여기에는 DOS 시절에만 사용되었던 DOS Header와 DOS Stub, 현재 Win32 환경에서 사용되는 PE 헤더, 그리고 코드나 데이터 등의 정보가 들어가 있는 Section table과 그 섹션 테이블에 정의된 섹션의 개수만큼 Section들이 나열되 있다.
도스 헤더 부분은 IMAGE_DOS_HEADER라는 구조체로 구성되어 있으며, 이 구조체에서는
두 가지 정도만 알면 된다.
IMAGE_DOS_HEADER 구조체
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
word e_magic; // Magic number
word e_cbip; // Bytes on last page of file
word e_cp; // Pages in file
word e_crlc; // Relocations
word e_cparhdr; // Size of header in paragraphs
word e_minalloc; // Minimum extra paragraphs needed
word e_maxalloc; // Maximum extra paragraphs needed
word e_ss; // Initial (relative) SS value
word e_sp; // Initial SP value
word e_csum; // Checksum
word e_ip; // Initial IP value
word e_cs; // Initial (relative) CS value
word e_lfarlc; // File address of relocation table
word e_ovno; // Overlay number
word e_res[4]; // Reserved words
word e_oemid; // OEM identifier (for e_oeminfo)
word e_oeminfo; // OEM information; e_oemid specific
word e_res2[10]; // Reserved words
word e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
e_magic은 DOS 헤더를 구별하는 식별로써 이 값을 체크해서 이 파일이 올바른 MZ 파일인지 검사하게 된다. 모든 실행 파일은 파일 가장 첫 부분에 'MZ'라는 2바이트의 아스키 코드값을 가지고 있는데 PE 로더는 이 값을 체크해서 맞는다면 실행 파일을 메모리에 로드한다.
e_magic의 상수
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
#define IMAGE_OS2_SIGNATURE 0x454E // NE
#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE -> 도스
#define IMAGE_VXD_SIGNATURE 0x454C // LE -> wkdcl
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00 -> 윈도우 nt 계열
e_ifanew는 PE 헤더가 있는 곳의 offset 값(RVA 값)이다.
offset :
어셈블리에서 사용되는 용어로 '특정 위치로부터 상대적으로 떨어진 값'을 나타내는 말이다.
ex. 0x60이 특정위치이고, 현재 0x20에 있다면 offset = 0x60 - 0x20 = 0x40 이다.
VA :
Virtual Address의 약자로 윈도우즈는 메모리를 '가상 주소'라는 개념으로 관리하는데 실제의 물리적인 주소를 가상 주소로 사용한다. 메모리는 원래 일렬로 늘어선 형태인데 여러 프로그램에서 데이터를 저장하다 보면 중간 중간 공백이 생기게 된다.
ex. 0x1번지에서 데이터를 4바이트만큼 쓰고 있고 0x5, 0x6번지는 쓰이지 않으며 0x7번지부터 또 4바이트만큼 쓰이고 있다고 가정해보자.
특정 프로그램이 메모리로부터 4바이트를 요구하면 0x5, 0x6번지가 남아있는데도 메모리의 특성상 연속적으로 메모리를 제공해야 하기 때문에 0xB부터 4바이트를 제공해 준다. 이런 행동들이 쌓이면 메모리에는 사용되지 않는 공백이 생겨나게 되고 메모리가 남아있지만 사용할 수 없는 영역이 생겨나게 된다.
이런 영역들을 사용하기 위해 생겨난 개념이 가상 메모리로, 프로그램이 새로 실행되면 가상으로 메모리를 만들고, 물리적 메모리를 가상 메모리에 맵핑한다. 쉽게 말해 가상 메모리에서는 0x00000000부터 0xFFFFFFFF까지의 메모리를 가상으로 만들고 이 프로그램에서 4바이트가 필요하면 물리적 메모리를 연속으로 주는 것이 아니라 0x5, 0x6, 0xB, 0xC를 가상적으로 0x0, 0x1, 0x2, 0x3처럼 연속적인 것처럼 간주하고 메모리를 할당해주는 방식이다.
따라서 프로그램에서 메모리에 접근할 때 가상 메모리인 0x0부터 4바이트를 접근하기만 하면 되며 운영체제가 알아서 물리적 메모리를 찾아가서 값을 읽어온다. 실제 물리적 메모리 0x5, 0x6, 0xB, 0xC를 말이다.
RVA :
Relative Virtual Address의 약자로서 offset과 같은 개념이다. 그 대상이 가상 메모리라는 점만 다르다.
IMAGE_DOS_HEADER 다음은 PE\0\0 문자열이고, 그 다음부터는 IMAGE_NT_HEADER 부분이다.
IMAGE_NT_HEADER 구조체
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADER, *PIMAGE_NT_HEADERS;
Signature는 이 파일이 올바른 PE 포맷으로 구성되어 있는지 확인하기 위한 일종의 플래그 값으로 올바른 PE 포맷 파일이라면 항상 값은 4바이트인 PE\0\0 값을 갖는다. 이 값을 체크해서 PE 포맷 파일인지 아닌지 구별할 수 있다.
FileHeader은 IMAGE_FILE_HEADER 구조체 멤버로 현재 파일이 exe인지 dll인지, 어느 플랫폼에서 실행되는지, 섹션의 개수가 몇 개인지의 정보를 담고 있다.
OptionalHeader는 IMAGE_OPTIONAL_HEADER 구조체 멤버로 크기가 상당히 큰 구조체이다.
IMAGE_FILE_HEADER 구조체
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 호환되는 머신
WORD NumberOfSections; // 세션의 개수
DWORD TimeDateStamp; // 오브젝트 생성일자(1970년 1월 1일 09시(
GMT시간 기준)초 단위 표현
DWORD PointerToSymbolTable; // COFF 심벌 테이블의 주소
DWORD NumberOfSymbols; // COFF 심벌 테이블의 심벌의 개수
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER의 크기
WORD Characteristics; // 파일의 대한 정보 OR 연산되어 표현됨
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine : 현재 파일이 어느 플랫폼(CPU)에서 실행되는지에 대한 정보를 가지고 있다.
NumberOfSections : 섹션을 분석하기 위해 사용되는 값이다. 파일을 Hex 파일 등으로 열어 하드 코딩시에 이 값을 변경시켜 섹션 수를 늘리고 코드를 추가할 수 있다.
TimeDataStamp : 파일이 생성된 날짜와 시간이다.
SizeOfOptionalHeader : IMAGE_FILE_HEADER 바로 다음에 위치한
IMAGE_OPTIONAL_HEADER 구조체의 크기이다. 32비트 윈도우에서는 0xE0의 크기를 갖는다.
Characteristics : 현재 파일이 exe인지 dll 파일인지의 플래그를 가지고 있는 변수이다.
Machine의 상수
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian,
0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x169 // MIPS little-endian
WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x1F0 // IBM PowerPC little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x1a2 // SH3 ittle-endian
#define IMAGE_FILE_MACHINE_SH3E 0x1a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x1a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x1c0 // ARM little-endian
#define IMAGE_FILE_MACHINE_THUMB 0x1c2
#define IMAGE_FILE_MACHINE_IA64 0x200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x284 // ALPHA64
Characteristics의 상수
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info
stripped from file
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable
(i.e. no unresolved
externel references)
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line numbers stripped
from file
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols
stripped from file
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim
working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb
addresses
#define IMAGE_FILE_BYTES_REVERSED_L0 0x0080 // Bytes of machine word
are reversed
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info
stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x400 // If Image is on
removable media, copy
and run from the swap file
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net,
copy and run from the swap file
#define IMAGE_FILE_SYSTEM 0x1000 // System file
#define IMAGE_FILE_DLL 0x2000 // File is a DLL
#define IMAGE_FILE_UP_SYSTEM 0x4000 // File should only be
run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word
are reversed.
IMAGE_OPTIONAL_HEADER 구조체
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion; // 컴파일러 버전 정보 비주얼 C라면 6.0이 나온다.
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // 기계어 코드의 전체 크기
DWORD SizeOfInitializedData; // 초기화되는 데이터 크기
DWORD SizeOfUninitializedData; // 초기화되지 않는 데이터 크기
DWORD AddressOfEntryPoint; // 실행 코드의 시작 위치 main()
DWORD BaseOfCode; // 코드의 시작 위치
DWORD BaseOfData; // .data 섹션의 시작 주소(RVA 값)
//
// NT additional fields
//
DWORD ImaeBase; // PE 파일이 메모리에 맵핑될 메모리 실제 시작 주소
DWORD SectionAlignment; // 메모리상에 올려진 후의 섹션의 배치 간격,
SectionHeader의 VirtualAddress에 적용
DWORD FileAlignment;
DWORD MajorOperatingSystemVersion;
DWORD MinorOperatingSystemVersion;
DWORD MajorImageVersion;
DWORD MinorImageVersion;
DWORD MajorSubsystemVersion;
DWORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 파일의 전체 크기
DWORD SizeOfHeader;
DWORD CheckSum;
WORD Subsystem; // 개발될 때의 파일 환경(OR 연산하여 표시)
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;
SizeOfCode : 코드의 전체 크기이다. 대개 .text 섹션에 CPU가 실행하는 기계어 코드가 들어있는데 이 코드의 전체 크기이다.
SizeOfInitializedData : 초기화되어 있는 섹션 영역들의 총합이다.
(참고 : 섹션들 중에서는 읽기 가능하고 쓰기가 가능하거나 가능하지 않은 프로그램 내에서 쓰이는 변수들이 저장되어 있는 섹션 영역들이 있다.(.data나 .idata 같은 영역들) 이 섹션 영역은 섹션의 속성에 따라 데이터가 초기화되어 있는 것들이 있고 그렇지 않은 것들이 있다)
SizeOfUninitializedData : 초기화되어 있지 않은 섹션 영역들의 총합이다.
AddressOfEntryPoint : 프로그램은 main이나 WinMain, 혹은 DllMain에서 시작하는데 위치가 RVA로 저장되어 있다.
(참고 : 주의해야 할 점은 프로세스가 가장 먼저 시작하는 위치라고 생각해서는 안 된다. 모든 프로그램은 메인 함수가 실행되기 전에 프로그램 실행을 준비하기 위해 Start up 코드를 실행하게 되므로 가장 먼저 시작되는 코드의 위치가 이 RVA가 아니다)
BaseOfCode : 코드 영역의 첫 번째 바이트 주소로 즉, 코드 영역의 시작 주소이다.
(참고 : 이 값을 AddressOfEntryPoint와 혼동하지 않도록 한다. 이 둘은 전혀 다르며 항상 AddressOfEntryPoint보다 BaseOfCode의 주소가 앞에 있다. RVA 값이라는 것을 주의한다)
BaseOfData : 데이터 영역(보통 .data 섹션)의 시작 주소이다. 이 영역은 읽고 쓰기가 가능한 경우가 많으며 RVA 값이다.
ImageBase : PE 파일이 메모리에 매핑될 시작 주소이다. 이는 RVA가 아니라 RVA의 기준이 되는 주소이다.
(참고 : 보통 exe 파일의 경우에는 0x00400000의 값을 갖고 dll은 0x10000000의 값을 갖는다. 단, 항상 그런것은 아니다)
ex. PE 포맷에서 RVA의 기준은 ImageBase의 멤버이며 만약 RVA의 값을 가지고 있는baseOfCode가 0x00001000h 값이고 ImageBase가 0x00400000h라고 한다면 PE 로더는 코드의 시작 주소를 0x00401000h에 올리게 된다. 즉 해당 파일이 메모리에 올려진 후에 코드의 시작 주소는 ImageBase + BaseOfCode이다. RVA 값에 ImageBase를 더해주는 것은 RVA를 실제 가상 주소로 변환하는 방법이다.
(참고 : RVA는 PE 로더에 의해 메모리에 올려진 후에 계산되는 것이지 헥스에디터 같은 툴로 분석을 할 때 RVA 계산을 해서는 안 된다. 헥스에디터로 바로 수정할 때는 순수 BaseOfCode 값이 가지고 있는 주소 그대로 이동해야 한다)
SectionAlignment : 메모리상에 올려진 후의 섹션의 배치 간격이다.
ex. .text 섹션의 정보를 가지고 있는 섹션 헤더가 있다고 했을 때에 이 섹션 헤더에는 .text 섹션이 위치할 두 가지 주소의 정보를 가지고 있다.
첫 번째 주소 정보는 PE 로더가 참조해서 올려야 할 가상 주소(VirtualAddress)를 위한 주소
두 번째 주소 정보는 메모리에 올려지지 않은 파일(RawAddress)을 위한 주소이다.
.text 섹션의 헤더 중 VirtualAddress 멤버는 0x2000의 값을 가지고 있고, PointerToRawData(Raw Address) 멤버는 0x1000의 값을 가지고 있다고 했을때 PE 파일을 실행하면 PE 로더는 섹션 헤더에 담겨있는 정보들을 참고해 .text 섹션을 메모리 주소 어딘가에 올려야 한다. 일단 ImageBase가 0x00400000이므로 DOS 헤더를 0x00400000에 올린다. 그리고 .text 섹션의 헤더 중 VirtualAddress 값을 조사하니 0x2000이라는 값이 나왔다. 그러면 PE 로더는 0x00400000에 0x2000을 더해서 0x00402000 메모리 주소에 .text 섹션을 올린다. 파일상에서 0x1000번지에 저장을 시키기 귀해 PE 포맷을 생성하고 PointerToRawData값을 0x1000으로 세팅한다음 .text 섹션 영역을 파일상의 0x1000에 저장시킨다. 그래서 파일 상태로 분석할 때 .text 영역의 위치를 알아내기 위해서 PointerToRawData 값만 참조해 그 주소로 이동하면 되는 것이다.
SectionAlignment는 섹션이 PE 로더에 의해 메모리에 올려질 때 항상 이 멤버의 배수 값으로 위치한다.
ex. 이 값이 0x1000이라고 한다면 항상 섹션 헤더의 VirtualAddress값은 이 값의 배수가 된다. 0x1000, 0x2000, 0x3000은 가능하나 0x1500, 0x2004, 0x3600 등과 같은 값은 불가능하다는 말이다.
FileAlignment : SectionAlignment와 개념은 같지만, 그 기준이 PointerToRawData에 영향을 준다는 것만 다르다. 파일상의 섹션 위치 간격이다.
SizeOfImage : 이미지 파일이 전체 크기, 즉 파일의 전체 크기이다.
SizeOfHeaders : 도스 헤더, 도스 스텁, PE 헤더, 섹션 헤더 모두를 더한 값, PE 포맷의 모든 헤더를 더한 값이다.
SizeOfStackReserve : 프로그램에서 사용될 스택을 얼마만큼 예약할지의 바이트 기준값이다.
SizeOfStackCommit : 프로그램에서 사용될 스택을 얼마만큼 commit할지의 바이트 기준값이다.
SizeOfHeapReserve : 프로그램에서 사용될 힙을 얼마만큼 예약할지의 바이트 기준값이다.
SizeOfHeapCommit : 프로그램에서 사용될 힙을 얼마만큼 commit할지의 바이트 기준값이다.
DataDirectory : IMAGE_DATA_DIRECTORY 구조체인 이 멤버는 Export table, Import table, Resource 영역, Exception 영역, 보안 영역, 디버그 영역 등을 접근할 수 있는 주소를 가지고 있는 배열이다.
IMAGE_DATA_DIRECTORY 구조체
typedef _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD VirtualSize;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
14개의 배열 구성
#define_IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define_IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define_IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define_IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define_IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define_IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define_IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define_IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define_IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define_IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define_IMAGE_DIRECTORY_ENTRY_LOAD_GONFIG 10 // Load Configuration Directory
#define_IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory
in headers
#define_IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define_IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import
Descriptors
#define_IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
Import 함수들을 조사할 때 DataDirectory의 두 번째 배열을 참조한다.
이 배열에는 각각 해당 영역으로 갈 수 있는 RVA 주소와 그 영역의 크기 정보를 담고 있다.
import table로 이동하면 IMAGE_IMPORT_DESCRIPTOR 구조체를 얻을 수 있는데 이 구조체는 해당 파일이 import하고 있는 dll과 함수 정보들을 가지고 있다. API 후킹시에 사용되는 중요한 테이블이다.
섹션 헤더는 섹션 테이블이라고도 하며 PE 포맷의 옵셔널헤더 바로 뒤에 구조체 배열 형식으로 위치해 있다. 이 구조체는 IMAGE_SECTION_HEADER 으로 윈도우즈에서 정의해 두었으며 섹션의 개수가 세 개면 섹션 정보가 담겨 있는 섹션 헤더 배열 세 개가 있고 마지막에 null로 채워진 섹션 헤더 구조체가 위치하게 된다. 섹션 개수 정보는 IMAGE_FILE_HEADER의 NumberOfSections 멤버가 가지고 있다.
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;
Name : 섹션의 이름을 나타내는 멤버로써 섹션 이름을 나타내는 문자열을 저장하게 되는데 IMAGE_SIZEOF_SHORT_NAME은 8바이트 크기를 나타내는 상수이다.
(참고 : .text나 .data 같은 이름을 갖지만 섹션의 이름으로 섹션의 성격을 파악해서는 안된다. 이 멤버의 값은 null일 수도 있으며 섹션 이름이 같다고 하여 항상 같은 속성을 지니지는 않는다. 섹션의 속성을 파악하기 위해서는 Characteristics 멤버를 참조하여야 한다.
다음은 일반적인 섹션의 이름별 용도이다
.text : 실행되는 코드들
.data : 초기화된 전역변수를 담고 있는 읽고 쓰기 가능한 섹션
.rdata : 읽기 전용 데이터 섹션, 문자열 표현이나 c++/com 가상 함수 테이블
.bss : 초기화되지 않은 전역 변수들을 위한 섹션
.idata : 다른 DLL로부터 가져다 쓰는 함수들의 정보 IMAGE_IMPORT_DESCRIPTOR의 배열로 이루어져 있으며 하나당 DLL 한 개의 정보를 담고 있다.
.edata : 다른 모듈이 이 파일을 사용할 때 사용하도록 해놓은 함수 리스트)
PhysicalAddress/VirtualSize : PE 로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 얼마만큼의 크기를 가지고 있게 되는지의 정보이다.
(참고 : 이 멤버는 Misc 유니온 구조체의 멤버이며 이는 물리적 주소를 의미하는 것이 아니라 가상 주소 상에서 해당 섹션의 크기를 나타내는 멤버라는 점을 주의하기 바란다)
(이유 : .EXE 파일이 PE 포맷이지만, .OBJ 파일도 PE 포맷이다. .EXE 파일은 VirtualSize의 의미로 사용되지만, 과거의 .OBJ 파일은 PhysicalAddress의 의미로 섹션의 번지수를 지정하였다. 하지만 이젠 .OBJ의 경우 이 필드는 0으로 세팅된다. 따라서 PhysicalAddress라는 것은 더이상 의미가 없어졌다)
VirtualAddress : PE 로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 어느 주소에 위치하는지의 RVA 주소를 값으로 가지고 있다.
(참고 : 이 멤버는 항상 IMAGE_OPTIONAL_HEADER멤버인 SectionAlignment의 배수 값을 가진다. PE 로더가 메모리에 섹션을 올릴 때 이 멤버의 값을 참조하게 된다. 이 멤버의 값이 0x1000h이고 IMAGE_OPTONAL_HEADER의 ImageBase값이 0x00400000h라면 PE 로더는 해당 섹션을 0x00401000h 번지에 올린다. 항상 이 값은 ImageBase를 기준으로 하는 RVA 값이라는 것에 주의하기 바란다)
SizeOfRawData : 이 멤버는 Raw data상에서 해당 섹션에 대한 실제 사용된 크기 정보를 담고 있다. 해당 섹션의 빈 공간이 얼마나 있는가를 알아내기 위해서 반드시 필요하다.
ex. 섹션 중에서는 CPU가 실행할 수 있는 기계어 코드가 담겨 있는 코드 섹션 부분이 있다. 이 코드 섹션이 Raw data상의 0x00001000h 주소에 위치해 있고 코드의 크기는 0x00000F00h이며, FileAlignment 값은 0x00001000h라고 하자.
코드 섹션 위치 : 0x00001000h
코드 섹션에서 사용되는 크기 : 0xF00h
FileAlignment의 값 : 0x1000h
그렇다면 코드 섹션에 대한 사이즈는 0xF00h이면 되므로 코드 섹션의 크기는 0x00001000h에서 0x00000F00h까지인 0xF00h만 할당하면 될 것이다. 하지만 윈도우즈는 Raw data상에서 반드시 FileAlignment의 배수로 섹션 공간을 할당하기 때문에 0x00001000h에서 0x00002000h까지인 0x1000h 크기로 코드 섹션을 할당하게 된다. 이러한 윈도우즈의 섹션 관리법으로 인해 각 섹션들은 대개 사용하지 않는 빈 공간을 가지고 있게 된다. 우리는 이 빈 공간에 우리의 코드를 작성함으로써 남이 만든 프로그램을 내 뜻대로 조작할 수 있다. 물론 섹션을 직접 추가하거나 섹션 테이블의 값을 조정해서 현재 존재하는 섹션의 크기를 늘릴 수도 있다. 섹션을 분석함으로써 다른 프로그램의 영역에 내가 원하는 영역을 만들 수도 있다.
PointerToRawData : raw data가 파일상의 어느 주소에 위치해 있는지 나타내는 변수이다.
ex. PointerToRawData가 0x1000h이고 FileAlignment가 0x1000h, SizeOfRawData가 0x500h이라면 이 섹션은 0x00001000h에서 0x00000fffh까지이며, 실제 사용되고 있는 영역은 0x00001000h~0x000004ffh이고, 0x00000500h~0x00000fffh의 공간은 할당된 채로 사용되지 않는 영역(빈 영역)이다. 1
Characteristics : 섹션에 대한 속성 정보를 플래그로 가지고 있다.
그중 몇가지를 보면
IMAGE_SCN_CTN_CODE 0x00000020 // 코드로 채워진 섹션
IMAGE_SCN_CTN_INITIALIZED_DATA 0x00000040 // 데이터가 초기화된 섹션
IMAGE_SCN_CTN_UNINITIALIZED_DATA 0x00000080 // 데이터가 비초기회된 섹션
IMAGE_SCN_CTN_EXECUTE 0x20000000 // 코드로서 실행될 수있는 섹션
IMAGE_SCN_CTN_READ 0x40000000 // 읽기 가능 영역 섹션
IMAGE_SCN_CTN_WRITE 0x80000000 // 쓰기 가능 영역 섹션
해당 섹션의 속성값을 알아내기 위해서 AND 연산을 이용한다. 간단히 해당 섹션이 읽기 속성을 가지고 있는지 검사하기 위해서는
if(SectionHeader.Characteristics & IMAGE_SCN_MEM_READ) 또는
if(SectionHeader.Characteristics & 0x40000000)코드가 참이면 읽기 속성을 가지고 있는 것이다. 하지만 앞의 매크로 상수는 특정 헤더 파일에 정의되어 있어서 그 파일을 include해주어야 사용할 수 있다.
이 속성값을 변경함으로써 쓰기가 금지된 섹션에 write할 수도 읽고 쓰기 가능한 섹션에 write를 금지시킬 수도 있다.
PE 구조는 어려울 뿐만 아니라 양도 방대하므로 기본적인 것만 익힌 후에 필요할 때마다 참조하는식으로 공부하는것이 좋다.
(확장자 : .exe와 .dll)을 동적 라이브러리를 링킹하기 위한 참조 값과 API export and import tables, 리소스 데이터와 TLS 데이터를 캡슐화한 것이다.
소스 코드를 컴파일하고 링크를 하여 PE 구조의 실행 파일이 생성되는 과정을 순서도로 보면
실행 파일에는 어떤 내용들이 들어있는지 윈도우 Notepad.exe를 메모장으로 열어보겠다.
MZ는 PE를 만든 Mark Zbikowski의 이니셜로써, MS-DOS 헤더의 시작을 알리는 문자이다.
"This program cannot be run in DOS mod" 문자열은 DOS 에서 윈도우 프로그램이 실행되면 출력하는 문자열이다. PE 문자열 전까지가 도스 헤더이다.
이번에는 Notepad.exe를 Hex 코드로 열어보겠다.
hex에디터중에는 HexWorkshop(http://www.Hexworkshop.com)이 최고 좋다고
생각되지만 상용프로그램이라 여기서는 프리웨어인 XVI32로 분석을 하겠다.
다음은 Hex 에디터로 열었을 때의 화면이다.
PE 로더는 실행 파일의 내용 중 가장 먼저 만나게 되는 DOS MZ Heade를 분석한다. 그리고 0x3C에 위치한 내용을 읽어 들여서, PE Header로 jump를 한다.
0x3C 위치를 자세히 보면 "E0 00 00 00"로 표시되어있는데, 실행 파일에서 주소를 확인할 때는
리틀엔디안 방식이어야 한다. 그래서 "00 00 00 E0" 주소지부터가 PE 헤더의 시작이다.
바이트 순서에는 Little Endian(리틀엔디안)과 Big Endian(빅엔디안)이 있다.
PE 로더는 실행 파일의 내용 중 가장 먼저 만나게 되는 DOS MZ Heade를 분석한다. 그리고 0x3C에 위치한 내용을 읽어 들여서, PE Header로 jump를 한다.
0x3C 위치를 자세히 보면 "E0 00 00 00"로 표시되어있는데, 실행 파일에서 주소를 확인할 때는
리틀엔디안 방식이어야 한다. 그래서 "00 00 00 E0" 주소지부터가 PE 헤더의 시작이다.
바이트 순서에는 Little Endian(리틀엔디안)과 Big Endian(빅엔디안)이 있다.
리틀엔디안 : 주소는 낮은 주소에 낮은 자릿수를 기록하고, 주소 값이 증가할수록 높은 자릿수에 기록하는 방식. 산술 유닛에서 산술 연산의 순서가 낮은 자릿수에서 높은 쪽으로 가면서 처리되기 때문에 프로세서의 산술 연산이 더 쉬워진다. 보통 많ㄴ이 사용하는 인텔, X86계열에서 사용한다. |
빅엔디안 : 정수로 정렬된 큰 수에 대한 비교를 메모리의 작은 주소부터 큰 주소 방향으로 읽으면서 바로 비교할 수 있어 더 빨리 처리할 수 있으며, 모든 정수와 문자열을 같은 순서 방향으로 읽을 수 있다는 장점이 있다. |
PE 구조
DOS Header
DOS Stub
PE File Header
Optional Header
Section Table
Sectiions
PE 포맷은 규칙에 의해서 프로그램 실행 파일의 제일 처음 부분부터 배열되있다. 여기에는 DOS 시절에만 사용되었던 DOS Header와 DOS Stub, 현재 Win32 환경에서 사용되는 PE 헤더, 그리고 코드나 데이터 등의 정보가 들어가 있는 Section table과 그 섹션 테이블에 정의된 섹션의 개수만큼 Section들이 나열되 있다.
도스 헤더 부분은 IMAGE_DOS_HEADER라는 구조체로 구성되어 있으며, 이 구조체에서는
두 가지 정도만 알면 된다.
IMAGE_DOS_HEADER 구조체
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
word e_magic; // Magic number
word e_cbip; // Bytes on last page of file
word e_cp; // Pages in file
word e_crlc; // Relocations
word e_cparhdr; // Size of header in paragraphs
word e_minalloc; // Minimum extra paragraphs needed
word e_maxalloc; // Maximum extra paragraphs needed
word e_ss; // Initial (relative) SS value
word e_sp; // Initial SP value
word e_csum; // Checksum
word e_ip; // Initial IP value
word e_cs; // Initial (relative) CS value
word e_lfarlc; // File address of relocation table
word e_ovno; // Overlay number
word e_res[4]; // Reserved words
word e_oemid; // OEM identifier (for e_oeminfo)
word e_oeminfo; // OEM information; e_oemid specific
word e_res2[10]; // Reserved words
word e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
e_magic은 DOS 헤더를 구별하는 식별로써 이 값을 체크해서 이 파일이 올바른 MZ 파일인지 검사하게 된다. 모든 실행 파일은 파일 가장 첫 부분에 'MZ'라는 2바이트의 아스키 코드값을 가지고 있는데 PE 로더는 이 값을 체크해서 맞는다면 실행 파일을 메모리에 로드한다.
e_magic의 상수
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
#define IMAGE_OS2_SIGNATURE 0x454E // NE
#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE -> 도스
#define IMAGE_VXD_SIGNATURE 0x454C // LE -> wkdcl
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00 -> 윈도우 nt 계열
e_ifanew는 PE 헤더가 있는 곳의 offset 값(RVA 값)이다.
offset :
어셈블리에서 사용되는 용어로 '특정 위치로부터 상대적으로 떨어진 값'을 나타내는 말이다.
ex. 0x60이 특정위치이고, 현재 0x20에 있다면 offset = 0x60 - 0x20 = 0x40 이다.
VA :
Virtual Address의 약자로 윈도우즈는 메모리를 '가상 주소'라는 개념으로 관리하는데 실제의 물리적인 주소를 가상 주소로 사용한다. 메모리는 원래 일렬로 늘어선 형태인데 여러 프로그램에서 데이터를 저장하다 보면 중간 중간 공백이 생기게 된다.
ex. 0x1번지에서 데이터를 4바이트만큼 쓰고 있고 0x5, 0x6번지는 쓰이지 않으며 0x7번지부터 또 4바이트만큼 쓰이고 있다고 가정해보자.
특정 프로그램이 메모리로부터 4바이트를 요구하면 0x5, 0x6번지가 남아있는데도 메모리의 특성상 연속적으로 메모리를 제공해야 하기 때문에 0xB부터 4바이트를 제공해 준다. 이런 행동들이 쌓이면 메모리에는 사용되지 않는 공백이 생겨나게 되고 메모리가 남아있지만 사용할 수 없는 영역이 생겨나게 된다.
이런 영역들을 사용하기 위해 생겨난 개념이 가상 메모리로, 프로그램이 새로 실행되면 가상으로 메모리를 만들고, 물리적 메모리를 가상 메모리에 맵핑한다. 쉽게 말해 가상 메모리에서는 0x00000000부터 0xFFFFFFFF까지의 메모리를 가상으로 만들고 이 프로그램에서 4바이트가 필요하면 물리적 메모리를 연속으로 주는 것이 아니라 0x5, 0x6, 0xB, 0xC를 가상적으로 0x0, 0x1, 0x2, 0x3처럼 연속적인 것처럼 간주하고 메모리를 할당해주는 방식이다.
따라서 프로그램에서 메모리에 접근할 때 가상 메모리인 0x0부터 4바이트를 접근하기만 하면 되며 운영체제가 알아서 물리적 메모리를 찾아가서 값을 읽어온다. 실제 물리적 메모리 0x5, 0x6, 0xB, 0xC를 말이다.
RVA :
Relative Virtual Address의 약자로서 offset과 같은 개념이다. 그 대상이 가상 메모리라는 점만 다르다.
IMAGE_DOS_HEADER 다음은 PE\0\0 문자열이고, 그 다음부터는 IMAGE_NT_HEADER 부분이다.
IMAGE_NT_HEADER 구조체
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADER, *PIMAGE_NT_HEADERS;
Signature는 이 파일이 올바른 PE 포맷으로 구성되어 있는지 확인하기 위한 일종의 플래그 값으로 올바른 PE 포맷 파일이라면 항상 값은 4바이트인 PE\0\0 값을 갖는다. 이 값을 체크해서 PE 포맷 파일인지 아닌지 구별할 수 있다.
FileHeader은 IMAGE_FILE_HEADER 구조체 멤버로 현재 파일이 exe인지 dll인지, 어느 플랫폼에서 실행되는지, 섹션의 개수가 몇 개인지의 정보를 담고 있다.
OptionalHeader는 IMAGE_OPTIONAL_HEADER 구조체 멤버로 크기가 상당히 큰 구조체이다.
IMAGE_FILE_HEADER 구조체
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 호환되는 머신
WORD NumberOfSections; // 세션의 개수
DWORD TimeDateStamp; // 오브젝트 생성일자(1970년 1월 1일 09시(
GMT시간 기준)초 단위 표현
DWORD PointerToSymbolTable; // COFF 심벌 테이블의 주소
DWORD NumberOfSymbols; // COFF 심벌 테이블의 심벌의 개수
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER의 크기
WORD Characteristics; // 파일의 대한 정보 OR 연산되어 표현됨
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine : 현재 파일이 어느 플랫폼(CPU)에서 실행되는지에 대한 정보를 가지고 있다.
NumberOfSections : 섹션을 분석하기 위해 사용되는 값이다. 파일을 Hex 파일 등으로 열어 하드 코딩시에 이 값을 변경시켜 섹션 수를 늘리고 코드를 추가할 수 있다.
TimeDataStamp : 파일이 생성된 날짜와 시간이다.
SizeOfOptionalHeader : IMAGE_FILE_HEADER 바로 다음에 위치한
IMAGE_OPTIONAL_HEADER 구조체의 크기이다. 32비트 윈도우에서는 0xE0의 크기를 갖는다.
Characteristics : 현재 파일이 exe인지 dll 파일인지의 플래그를 가지고 있는 변수이다.
Machine의 상수
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian,
0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x169 // MIPS little-endian
WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x1F0 // IBM PowerPC little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x1a2 // SH3 ittle-endian
#define IMAGE_FILE_MACHINE_SH3E 0x1a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x1a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x1c0 // ARM little-endian
#define IMAGE_FILE_MACHINE_THUMB 0x1c2
#define IMAGE_FILE_MACHINE_IA64 0x200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x284 // ALPHA64
Characteristics의 상수
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info
stripped from file
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable
(i.e. no unresolved
externel references)
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line numbers stripped
from file
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols
stripped from file
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim
working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb
addresses
#define IMAGE_FILE_BYTES_REVERSED_L0 0x0080 // Bytes of machine word
are reversed
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info
stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x400 // If Image is on
removable media, copy
and run from the swap file
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net,
copy and run from the swap file
#define IMAGE_FILE_SYSTEM 0x1000 // System file
#define IMAGE_FILE_DLL 0x2000 // File is a DLL
#define IMAGE_FILE_UP_SYSTEM 0x4000 // File should only be
run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word
are reversed.
IMAGE_OPTIONAL_HEADER 구조체
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion; // 컴파일러 버전 정보 비주얼 C라면 6.0이 나온다.
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // 기계어 코드의 전체 크기
DWORD SizeOfInitializedData; // 초기화되는 데이터 크기
DWORD SizeOfUninitializedData; // 초기화되지 않는 데이터 크기
DWORD AddressOfEntryPoint; // 실행 코드의 시작 위치 main()
DWORD BaseOfCode; // 코드의 시작 위치
DWORD BaseOfData; // .data 섹션의 시작 주소(RVA 값)
//
// NT additional fields
//
DWORD ImaeBase; // PE 파일이 메모리에 맵핑될 메모리 실제 시작 주소
DWORD SectionAlignment; // 메모리상에 올려진 후의 섹션의 배치 간격,
SectionHeader의 VirtualAddress에 적용
DWORD FileAlignment;
DWORD MajorOperatingSystemVersion;
DWORD MinorOperatingSystemVersion;
DWORD MajorImageVersion;
DWORD MinorImageVersion;
DWORD MajorSubsystemVersion;
DWORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 파일의 전체 크기
DWORD SizeOfHeader;
DWORD CheckSum;
WORD Subsystem; // 개발될 때의 파일 환경(OR 연산하여 표시)
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;
SizeOfCode : 코드의 전체 크기이다. 대개 .text 섹션에 CPU가 실행하는 기계어 코드가 들어있는데 이 코드의 전체 크기이다.
SizeOfInitializedData : 초기화되어 있는 섹션 영역들의 총합이다.
(참고 : 섹션들 중에서는 읽기 가능하고 쓰기가 가능하거나 가능하지 않은 프로그램 내에서 쓰이는 변수들이 저장되어 있는 섹션 영역들이 있다.(.data나 .idata 같은 영역들) 이 섹션 영역은 섹션의 속성에 따라 데이터가 초기화되어 있는 것들이 있고 그렇지 않은 것들이 있다)
SizeOfUninitializedData : 초기화되어 있지 않은 섹션 영역들의 총합이다.
AddressOfEntryPoint : 프로그램은 main이나 WinMain, 혹은 DllMain에서 시작하는데 위치가 RVA로 저장되어 있다.
(참고 : 주의해야 할 점은 프로세스가 가장 먼저 시작하는 위치라고 생각해서는 안 된다. 모든 프로그램은 메인 함수가 실행되기 전에 프로그램 실행을 준비하기 위해 Start up 코드를 실행하게 되므로 가장 먼저 시작되는 코드의 위치가 이 RVA가 아니다)
BaseOfCode : 코드 영역의 첫 번째 바이트 주소로 즉, 코드 영역의 시작 주소이다.
(참고 : 이 값을 AddressOfEntryPoint와 혼동하지 않도록 한다. 이 둘은 전혀 다르며 항상 AddressOfEntryPoint보다 BaseOfCode의 주소가 앞에 있다. RVA 값이라는 것을 주의한다)
BaseOfData : 데이터 영역(보통 .data 섹션)의 시작 주소이다. 이 영역은 읽고 쓰기가 가능한 경우가 많으며 RVA 값이다.
ImageBase : PE 파일이 메모리에 매핑될 시작 주소이다. 이는 RVA가 아니라 RVA의 기준이 되는 주소이다.
(참고 : 보통 exe 파일의 경우에는 0x00400000의 값을 갖고 dll은 0x10000000의 값을 갖는다. 단, 항상 그런것은 아니다)
ex. PE 포맷에서 RVA의 기준은 ImageBase의 멤버이며 만약 RVA의 값을 가지고 있는baseOfCode가 0x00001000h 값이고 ImageBase가 0x00400000h라고 한다면 PE 로더는 코드의 시작 주소를 0x00401000h에 올리게 된다. 즉 해당 파일이 메모리에 올려진 후에 코드의 시작 주소는 ImageBase + BaseOfCode이다. RVA 값에 ImageBase를 더해주는 것은 RVA를 실제 가상 주소로 변환하는 방법이다.
(참고 : RVA는 PE 로더에 의해 메모리에 올려진 후에 계산되는 것이지 헥스에디터 같은 툴로 분석을 할 때 RVA 계산을 해서는 안 된다. 헥스에디터로 바로 수정할 때는 순수 BaseOfCode 값이 가지고 있는 주소 그대로 이동해야 한다)
SectionAlignment : 메모리상에 올려진 후의 섹션의 배치 간격이다.
ex. .text 섹션의 정보를 가지고 있는 섹션 헤더가 있다고 했을 때에 이 섹션 헤더에는 .text 섹션이 위치할 두 가지 주소의 정보를 가지고 있다.
첫 번째 주소 정보는 PE 로더가 참조해서 올려야 할 가상 주소(VirtualAddress)를 위한 주소
두 번째 주소 정보는 메모리에 올려지지 않은 파일(RawAddress)을 위한 주소이다.
.text 섹션의 헤더 중 VirtualAddress 멤버는 0x2000의 값을 가지고 있고, PointerToRawData(Raw Address) 멤버는 0x1000의 값을 가지고 있다고 했을때 PE 파일을 실행하면 PE 로더는 섹션 헤더에 담겨있는 정보들을 참고해 .text 섹션을 메모리 주소 어딘가에 올려야 한다. 일단 ImageBase가 0x00400000이므로 DOS 헤더를 0x00400000에 올린다. 그리고 .text 섹션의 헤더 중 VirtualAddress 값을 조사하니 0x2000이라는 값이 나왔다. 그러면 PE 로더는 0x00400000에 0x2000을 더해서 0x00402000 메모리 주소에 .text 섹션을 올린다. 파일상에서 0x1000번지에 저장을 시키기 귀해 PE 포맷을 생성하고 PointerToRawData값을 0x1000으로 세팅한다음 .text 섹션 영역을 파일상의 0x1000에 저장시킨다. 그래서 파일 상태로 분석할 때 .text 영역의 위치를 알아내기 위해서 PointerToRawData 값만 참조해 그 주소로 이동하면 되는 것이다.
SectionAlignment는 섹션이 PE 로더에 의해 메모리에 올려질 때 항상 이 멤버의 배수 값으로 위치한다.
ex. 이 값이 0x1000이라고 한다면 항상 섹션 헤더의 VirtualAddress값은 이 값의 배수가 된다. 0x1000, 0x2000, 0x3000은 가능하나 0x1500, 0x2004, 0x3600 등과 같은 값은 불가능하다는 말이다.
FileAlignment : SectionAlignment와 개념은 같지만, 그 기준이 PointerToRawData에 영향을 준다는 것만 다르다. 파일상의 섹션 위치 간격이다.
SizeOfImage : 이미지 파일이 전체 크기, 즉 파일의 전체 크기이다.
SizeOfHeaders : 도스 헤더, 도스 스텁, PE 헤더, 섹션 헤더 모두를 더한 값, PE 포맷의 모든 헤더를 더한 값이다.
SizeOfStackReserve : 프로그램에서 사용될 스택을 얼마만큼 예약할지의 바이트 기준값이다.
SizeOfStackCommit : 프로그램에서 사용될 스택을 얼마만큼 commit할지의 바이트 기준값이다.
SizeOfHeapReserve : 프로그램에서 사용될 힙을 얼마만큼 예약할지의 바이트 기준값이다.
SizeOfHeapCommit : 프로그램에서 사용될 힙을 얼마만큼 commit할지의 바이트 기준값이다.
DataDirectory : IMAGE_DATA_DIRECTORY 구조체인 이 멤버는 Export table, Import table, Resource 영역, Exception 영역, 보안 영역, 디버그 영역 등을 접근할 수 있는 주소를 가지고 있는 배열이다.
IMAGE_DATA_DIRECTORY 구조체
typedef _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD VirtualSize;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
14개의 배열 구성
#define_IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define_IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define_IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define_IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define_IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define_IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define_IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define_IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define_IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define_IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define_IMAGE_DIRECTORY_ENTRY_LOAD_GONFIG 10 // Load Configuration Directory
#define_IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory
in headers
#define_IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define_IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import
Descriptors
#define_IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
Import 함수들을 조사할 때 DataDirectory의 두 번째 배열을 참조한다.
이 배열에는 각각 해당 영역으로 갈 수 있는 RVA 주소와 그 영역의 크기 정보를 담고 있다.
import table로 이동하면 IMAGE_IMPORT_DESCRIPTOR 구조체를 얻을 수 있는데 이 구조체는 해당 파일이 import하고 있는 dll과 함수 정보들을 가지고 있다. API 후킹시에 사용되는 중요한 테이블이다.
섹션 헤더는 섹션 테이블이라고도 하며 PE 포맷의 옵셔널헤더 바로 뒤에 구조체 배열 형식으로 위치해 있다. 이 구조체는 IMAGE_SECTION_HEADER 으로 윈도우즈에서 정의해 두었으며 섹션의 개수가 세 개면 섹션 정보가 담겨 있는 섹션 헤더 배열 세 개가 있고 마지막에 null로 채워진 섹션 헤더 구조체가 위치하게 된다. 섹션 개수 정보는 IMAGE_FILE_HEADER의 NumberOfSections 멤버가 가지고 있다.
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;
Name : 섹션의 이름을 나타내는 멤버로써 섹션 이름을 나타내는 문자열을 저장하게 되는데 IMAGE_SIZEOF_SHORT_NAME은 8바이트 크기를 나타내는 상수이다.
(참고 : .text나 .data 같은 이름을 갖지만 섹션의 이름으로 섹션의 성격을 파악해서는 안된다. 이 멤버의 값은 null일 수도 있으며 섹션 이름이 같다고 하여 항상 같은 속성을 지니지는 않는다. 섹션의 속성을 파악하기 위해서는 Characteristics 멤버를 참조하여야 한다.
다음은 일반적인 섹션의 이름별 용도이다
.text : 실행되는 코드들
.data : 초기화된 전역변수를 담고 있는 읽고 쓰기 가능한 섹션
.rdata : 읽기 전용 데이터 섹션, 문자열 표현이나 c++/com 가상 함수 테이블
.bss : 초기화되지 않은 전역 변수들을 위한 섹션
.idata : 다른 DLL로부터 가져다 쓰는 함수들의 정보 IMAGE_IMPORT_DESCRIPTOR의 배열로 이루어져 있으며 하나당 DLL 한 개의 정보를 담고 있다.
.edata : 다른 모듈이 이 파일을 사용할 때 사용하도록 해놓은 함수 리스트)
PhysicalAddress/VirtualSize : PE 로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 얼마만큼의 크기를 가지고 있게 되는지의 정보이다.
(참고 : 이 멤버는 Misc 유니온 구조체의 멤버이며 이는 물리적 주소를 의미하는 것이 아니라 가상 주소 상에서 해당 섹션의 크기를 나타내는 멤버라는 점을 주의하기 바란다)
(이유 : .EXE 파일이 PE 포맷이지만, .OBJ 파일도 PE 포맷이다. .EXE 파일은 VirtualSize의 의미로 사용되지만, 과거의 .OBJ 파일은 PhysicalAddress의 의미로 섹션의 번지수를 지정하였다. 하지만 이젠 .OBJ의 경우 이 필드는 0으로 세팅된다. 따라서 PhysicalAddress라는 것은 더이상 의미가 없어졌다)
VirtualAddress : PE 로더에 의해 이미지가 메모리에 올려진 후에 해당 섹션이 어느 주소에 위치하는지의 RVA 주소를 값으로 가지고 있다.
(참고 : 이 멤버는 항상 IMAGE_OPTIONAL_HEADER멤버인 SectionAlignment의 배수 값을 가진다. PE 로더가 메모리에 섹션을 올릴 때 이 멤버의 값을 참조하게 된다. 이 멤버의 값이 0x1000h이고 IMAGE_OPTONAL_HEADER의 ImageBase값이 0x00400000h라면 PE 로더는 해당 섹션을 0x00401000h 번지에 올린다. 항상 이 값은 ImageBase를 기준으로 하는 RVA 값이라는 것에 주의하기 바란다)
SizeOfRawData : 이 멤버는 Raw data상에서 해당 섹션에 대한 실제 사용된 크기 정보를 담고 있다. 해당 섹션의 빈 공간이 얼마나 있는가를 알아내기 위해서 반드시 필요하다.
ex. 섹션 중에서는 CPU가 실행할 수 있는 기계어 코드가 담겨 있는 코드 섹션 부분이 있다. 이 코드 섹션이 Raw data상의 0x00001000h 주소에 위치해 있고 코드의 크기는 0x00000F00h이며, FileAlignment 값은 0x00001000h라고 하자.
코드 섹션 위치 : 0x00001000h
코드 섹션에서 사용되는 크기 : 0xF00h
FileAlignment의 값 : 0x1000h
그렇다면 코드 섹션에 대한 사이즈는 0xF00h이면 되므로 코드 섹션의 크기는 0x00001000h에서 0x00000F00h까지인 0xF00h만 할당하면 될 것이다. 하지만 윈도우즈는 Raw data상에서 반드시 FileAlignment의 배수로 섹션 공간을 할당하기 때문에 0x00001000h에서 0x00002000h까지인 0x1000h 크기로 코드 섹션을 할당하게 된다. 이러한 윈도우즈의 섹션 관리법으로 인해 각 섹션들은 대개 사용하지 않는 빈 공간을 가지고 있게 된다. 우리는 이 빈 공간에 우리의 코드를 작성함으로써 남이 만든 프로그램을 내 뜻대로 조작할 수 있다. 물론 섹션을 직접 추가하거나 섹션 테이블의 값을 조정해서 현재 존재하는 섹션의 크기를 늘릴 수도 있다. 섹션을 분석함으로써 다른 프로그램의 영역에 내가 원하는 영역을 만들 수도 있다.
PointerToRawData : raw data가 파일상의 어느 주소에 위치해 있는지 나타내는 변수이다.
ex. PointerToRawData가 0x1000h이고 FileAlignment가 0x1000h, SizeOfRawData가 0x500h이라면 이 섹션은 0x00001000h에서 0x00000fffh까지이며, 실제 사용되고 있는 영역은 0x00001000h~0x000004ffh이고, 0x00000500h~0x00000fffh의 공간은 할당된 채로 사용되지 않는 영역(빈 영역)이다. 1
Characteristics : 섹션에 대한 속성 정보를 플래그로 가지고 있다.
그중 몇가지를 보면
IMAGE_SCN_CTN_CODE 0x00000020 // 코드로 채워진 섹션
IMAGE_SCN_CTN_INITIALIZED_DATA 0x00000040 // 데이터가 초기화된 섹션
IMAGE_SCN_CTN_UNINITIALIZED_DATA 0x00000080 // 데이터가 비초기회된 섹션
IMAGE_SCN_CTN_EXECUTE 0x20000000 // 코드로서 실행될 수있는 섹션
IMAGE_SCN_CTN_READ 0x40000000 // 읽기 가능 영역 섹션
IMAGE_SCN_CTN_WRITE 0x80000000 // 쓰기 가능 영역 섹션
해당 섹션의 속성값을 알아내기 위해서 AND 연산을 이용한다. 간단히 해당 섹션이 읽기 속성을 가지고 있는지 검사하기 위해서는
if(SectionHeader.Characteristics & IMAGE_SCN_MEM_READ) 또는
if(SectionHeader.Characteristics & 0x40000000)코드가 참이면 읽기 속성을 가지고 있는 것이다. 하지만 앞의 매크로 상수는 특정 헤더 파일에 정의되어 있어서 그 파일을 include해주어야 사용할 수 있다.
이 속성값을 변경함으로써 쓰기가 금지된 섹션에 write할 수도 읽고 쓰기 가능한 섹션에 write를 금지시킬 수도 있다.
PE 구조는 어려울 뿐만 아니라 양도 방대하므로 기본적인 것만 익힌 후에 필요할 때마다 참조하는식으로 공부하는것이 좋다.
'Windows > _System Programming' 카테고리의 다른 글
64비트 기반 프로그래밍 (0) | 2010.02.08 |
---|---|
Windows에서의 문자셋(Character Sets) (0) | 2010.02.08 |
시스템 프로그래밍(System Programming)의 시작 (2) | 2010.02.08 |
윈도우 파일시스템(File System) (2) | 2010.02.05 |
파티션(Partition)의 개념 (2) | 2010.02.05 |