PE Viewer 개발 (2) - IMAGE_NT_HEADERS
- 리버싱/리버싱
- 2020. 2. 9. 16:03
C언어로 개발하는 PE Viewer(PE 뷰어) 두번째 포스팅에서는 NT 헤더(IMAGE_NT_HEADERS) 구조체를 파싱해볼 것이다. 찾는 부분이 아니라면 PE Viewer 관련 다른 포스팅을 확인해보길 바란다.
이번 포스팅은 상당히 길다. 나누기가 애매해서 그냥 한꺼번에 올렸다.
NT Header (IMAGE_NT_HEADER)
IMAGE_NT_HEADER 구조체의 시작 위치는 IMAGE_DOS_HEADER 구조체의 e_lfanew 변수를 참조해 구할 수 있다.
- 시작 위치: IMAGE_DOS_HEADER.e_lfanew
- 크기: 248바이트(0xF8)
IMAGE_NT_HEADERS 정의
IMAGE_NT_HEADER 구조체는 아래 사진에서 구분지어있는 순서대로 ‘Signature’, ‘FileHeader’, ‘OptionalHeader’ 3개의 멤버로 이루어져 있는 248바이트(0xF8) 크기의 구조체이다.
IMAGE_NT_HEADR 구조체는 다음과 같다 (출처: MSDN).
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEADERS 변수 선언
IMAGE_NT_HEADERS의 시작은 IMAGE_DOS_HEADER의 elfanew이므로 IMAGE_NT_HEADERS 구조체의 포인터 변수 inh의 위치는 'buffer + (idh->e_lfanew)'이 된다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // Signature
// IMAGE_NT_HEADERS의 시작주소는 IMAGE_DOS_HEADER의 e_lfanew에 저장되어 있음(=IMAGE_NT_HEADERS.Signature의 시작주소)
PIMAGE_NT_HEADERS inh = (PBYTE)buffer + (idh->e_lfanew);
// 코드...
}
IMAGE_NT_HEADERS.Signature
IMAGE_NT_HEADERS의 첫 번째 멤버 변수인 Signtature는 PE 파일임을 의미하는 시그니처로 ‘PE’ 값을 가지며 이 값은 변경될 수 없다.
IMAGE_NT_HEADERS.Signature 파싱 및 출력
Signature는 DWORD 값으로 그냥 'inh->Signature'로 파싱할 수 있다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // Structure
printf("***************************** [IMAGE_NT_HEADER.Signature] *******************************\n\n");
printf("- Signature: %08X\n\n", inh->Signature);
// 코드...
}
IMAGE_NT_HEADERS.Signature 출력 결과는 아래와 같다. PE 파일 시그니쳐인 'PE'가 정상적으로 확인된다.
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER
IMAGE_NT_HEAEDERS의 두 번째 멤버인 IMAGE_FILE_HEADER 구조체는 Signature 뒤부터 시작되며 20바이트 (0x14) 크기를 갖는다.
- 시작위치: IMAGE_FILE_HEADER.Signature 뒤
- 크기: 20바이트(0x14)
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER 정의
파일의 개략적인 속성을 나타내는 구조체이다. 중요히 여겨지는 멤버로는 아래와 같이 ‘Machine’, ‘NumberOfSections’, ‘SizeOfOptionalHeader’, ‘Characteristics’ 등이 있다.
- Machine : CPU ID를 나타내는 시그니쳐
- NumberOfSections : 섹션의 갯수
- SizeOfOptionalHeader : IMAGE_OPTIONAL_HEADER 구조체의 크기
- Characteristics : 파일의 속성
보통 Machine이 나타내는 CPU ID로 x32, x64를 구분하기도 한다. 또한 NumberOfSections가 나타내는 섹션의 갯수는 가변적이기 때문에 IMAGE_SECTION_HEADER를 파싱할 때 꼭 필요한 정보이다. 정의된 섹션의 갯수가 실제 섹션의 수보다 많다면 파싱하다가 에러가 나지만, 정의된 섹션의 갯수가 실제 섹션의 갯수보다 작다면 뒤에 섹션은 파싱되지 못하고 정의된 갯수만큼만 섹션이 인식된다. Characteristics를 보고는 이게 dll파일인지 실행가능한 exe 파일인지 등에 대한 정보를 얻을 수 있다.
IMAGE_FILE_HEADR 구조체는 다음과 같다 (출처: MSDN).
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // CPU ID를 나타내는 시그니쳐
WORD NumberOfSections; // 섹션의 갯수
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER 구조체의 크기
WORD Characteristics; // 파일의 속성
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER 변수 선언
IMAGE_FILE_HEADER 구조체의 포인터 변수가 가리켜야 할 위치는 ‘buffer + (idh->e_lfanew) + (Signature 크기(4))’가 된다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // IMAGE_FILE_HEADER
// IMAGE_FILE_EHADER는 Signature가 끝나고 시작됨
PIMAGE_FILE_HEADER ifh = (PBYTE)buffer + idh->e_lfanew + 4;
// 코드...
}
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER 파싱 및 출력
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER 의 파싱 및 출력은 아래와 같이 이루어진다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // IMAGE_FILE_HEADER
printf("************************* [IMAGE_NT_HEADER.IMAGE_FILE_HEADER] ***************************\n\n");
printf("- Machine: %02X\n", ifh->Machine);
printf("- NumberOfSections: %02X\n", ifh->NumberOfSections);
printf("- TimeDateStamp: %04X\n", ifh->TimeDateStamp);
printf("- PointerToSymbolTables: %04X\n", ifh->PointerToSymbolTable);
printf("- NumberOfSymbols: %04X\n", ifh->NumberOfSymbols);
printf("- SizeOfOptionalHeader: %02X\n", ifh->SizeOfOptionalHeader);
printf("- Characteristics: %02X\n\n", ifh->Characteristics);
// 코드...
}
IMAGE_NT_HEADERS.IMAGE_FILE_HEADER 의 출력 결과는 아래와 같다. Machine 값이 14C인 것을 보니 x32 이고 섹션 갯수는 총 7개임을 알 수 있다.
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER32
IMAGE_NT_HEAEDERS의 세번째 멤버인 IMAGE_OPTIONAL_HEADER 구조체는 IMAGE _FILE_HEAEDERS 뒤부터 시작되며 구조체 크기는 240바이트로 정해져 있지만 PE 로더는 IMAGE_FILE_HEADER의 SizeOfOptionalHeader 멤버의 값을 보고 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 인식하게 된다.
- 시작위치 : IMAGE_FILE_HEADER 뒤
- 크기 : IMAGE_FILE_HEADER.SizeOfOptinalHeader 참조 (224바이트 (0xE0))
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER32 정의
35개의 기본 필드와 IMAGE_DATA_DIRECTORY 구조체 배열로 이루어져 있다. IMAGE_OPTIONAL_HEADER32에는 아래와 같이 중요한 멤버들이 굉장히 많다.
특히, PE파일이 로딩되는 시작 주소인 ImageBase가 있고 어떻게 보면 PE 파일 분석의 핵심이라고 할 수 있는 IAT와 같은 테이블이 포함된 DataDirectory도 존재한다.
- Magic : IMAGE_OPTIONAL_HEADER를 나타내는 시그니쳐
- AddressOfEntryPoint : EP의 RVA 값
- ImageBase : PE 파일이 로딩되는 시작 주소
- SectionAlignment : 메모리에서의 섹션의 최소단위
- FileAlignment : 파일에서의 섹션의 최소단위
- SizeOfImage : 가상 메모리에서 PE Image가 차지하는 크기(로딩된 크기)
- SizeOfHeader : PE 헤더의 전체 크기(FileAlignment의 배수)
- Subsystem : 서브시스템 종류 정의
- NumberOfRvaAndSizes : DataDirectory 배열의 갯수
- DataDirectory : IMAGE_DATA_DIRECTORY 구조체의 배열
IMAGE_OPTIONAL_HEADER 구조체 정의는 아래와 같다 (출처: MSDN).
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // IMAGE_OPTIONAL_HEADER를 나타내는 시그니쳐
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase; // PE 파일이 로딩되는 시작 주소
DWORD SectionAlignment; // 메모리에서의 섹션의 최소단위
DWORD FileAlignment; // 파일에서의 섹션의 최소단위
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 가상 메모리에서 PE Image가 차지하는 크기(로딩된 크기)
DWORD SizeOfHeaders; // PE 헤더의 전체 크기(FileAlignment의 배수)
DWORD CheckSum;
WORD Subsystem; // 서브시스템 종류 정의
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; // DataDirectory 배열의 갯수
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER 변수 선언
IMAGE_OPTIONAL_HEAEDERS 구조체의 포인터 변수인 ioh가 가리켜야 할 위치는 ‘buffer + (idh->e_lfanew) + (SIgnature 크기) + (IMAGE_FILE_HEADER 크기)’가 된다. IMAGE_OPTIONAL_HEADER에는 구조체 배열이 포함되어 있는데 이 다음 포스팅인 IMAGE_SECTION_HEADER를 분석하고 파싱할 때 필요한 arr_num도 같이 계산되어 있다. 이건 추후에 다른 포스팅에서 설명하도록 하겠다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // IMAGE_OPTIONAL_HEADER
// IMAGE_OPTIONAL_HEADER는 IMAGE_FILE_HEADER가 끝나고 시작됨
PIMAGE_OPTIONAL_HEADER ioh = (PBYTE)buffer + (idh->e_lfanew) + 24;
// 코드...
}
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER 파싱 및 출력
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER 의 파싱 결과 및 파일 출력은 아래와 같이 이루어진다. NumberOfRvaAndSizes는 DataDirectory 배열의 갯수를 나타내므로 DataDirectory는 해당 갯수만큼 반복적으로 돌며 출력해야 한다.
int pe_print_32(char* file, char* option)
{
// 코드...
// IMAGE_NT_HEADERS // IMAGE_OPTIONAL_HEADER
printf("*********************** [IMAGE_NT_HEADER.IMAGE_OPTIONAL_HEADER] *************************\n\n");
printf("- Magic: %02X\n", ioh->Magic);
printf("- MajorLinkerVersion: %01X\n", ioh->MajorLinkerVersion);
printf("- MinorLinkerVersion: %01X\n", ioh->MinorLinkerVersion);
printf("- SizeOfCode: %04X\n", ioh->SizeOfCode);
printf("- SizeOfInitializedData: %04X\n", ioh->SizeOfInitializedData);
printf("- SizeOfUninitializedData: %04X\n", ioh->SizeOfUninitializedData);
printf("- AddressOfEntryPoint: %04X\n", ioh->AddressOfEntryPoint);
printf("- BaseOfCode: %04X\n", ioh->BaseOfCode);
printf("- BaseOfData: %04X\n", ioh->BaseOfData);
printf("- ImageBase: %04X\n", ioh->ImageBase);
printf("- SectionAlignment: %04X\n", ioh->SectionAlignment);
printf("- FileAlignment: %04X\n", ioh->FileAlignment);
printf("- MajorOperatingSystemVersion: %02X\n", ioh->MajorOperatingSystemVersion);
printf("- MinorOperatingSystemVersion: %02X\n", ioh->MinorOperatingSystemVersion);
printf("- MajorImageVersion: %02X\n", ioh->MajorImageVersion);
printf("- MinorImageVersion: %02X\n", ioh->MinorImageVersion);
printf("- MajorSubsystemVersion: %02X\n", ioh->MajorSubsystemVersion);
printf("- MinorSubsystemVersion: %02X\n", ioh->MinorSubsystemVersion);
printf("- Win32VersionValue: %04X\n", ioh->Win32VersionValue);
printf("- SizeOfImage: %04X\n", ioh->SizeOfImage);
printf("- SizeOfHeaders: %04X\n", ioh->SizeOfHeaders);
printf("- CheckSum: %04X\n", ioh->CheckSum);
printf("- Subsystem: %02X\n", ioh->Subsystem);
printf("- DllCharacteristics: %02X\n", ioh->DllCharacteristics);
printf("- SizeOfStackReserve: %04X\n", ioh->SizeOfStackReserve);
printf("- SizeOfStackCommit: %04X\n", ioh->SizeOfStackCommit);
printf("- SizeOfHeapReserve: %04X\n", ioh->SizeOfHeapReserve);
printf("- SizeOfHeapCommit: %04X\n", ioh->SizeOfHeapCommit);
printf("- LoaderFlags: %04X\n", ioh->LoaderFlags);
printf("- NumberOfRvaAndSizes: %04X\n\n", ioh->NumberOfRvaAndSizes);
// IMAGE_NT_HEADERS // Data Directory Table
printf("******************************** [Data Directory Table] *********************************\n\n");
for (i = 0; i < (ioh->NumberOfRvaAndSizes); i++)
{
printf("- DataDirectory[%d]: %04X\n", i, ioh->DataDirectory[i].VirtualAddress);
}
// 코드...
}
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER 의 출력 결과는 아래와 같다. Machine 값이 14C인 것을 보니 x32 이고 섹션 갯수는 총 7개임을 알 수 있다.
NumgerOfRvaAndSizes에 나와있듯이 DataDirectory 테이블 수는 16개(0x10)이다.
여기까지, C언어로 하는 PE Viewer(PE 뷰어) 개발을 위한 두번째 포스팅을 마쳤다. 개발은 다 됐는데 정리해서 올리는게 상당히 오래 걸린다. 다음 포스팅에서는 섹션 헤더를 들어가도록 하겠다.
'리버싱 > 리버싱' 카테고리의 다른 글
[원데이 취약점] GOM Player 2.0.12.3375 - '.asx' Local Stack Overflow (0) | 2020.03.11 |
---|---|
IDA 사용법 및 분석 시 유용한 팁 (0) | 2020.03.07 |
바이너리 디핑을 위한 bindiff 설치 및 사용법 (0) | 2020.02.21 |
PE Viewer 개발 (3) - IMAGE_SECTION_HEADER (0) | 2020.02.11 |
PE Viewer 개발 (1) - IMAGE_DOS_HEADER (0) | 2020.02.09 |