본문으로 바로가기

20151021 - pe 파일! 드디어 나왔다.

category 마이 스토리/내용 정리중인 글들.. 2015. 10. 21. 17:48



이미지 도스 헤더 라고 불린다.



참고 : http://haerakai.tistory.com/18



우리가 파일을 열면 


섹션 헤더를 가르키는 자료




왼쪽이 우리가 파일을 로드 시키면 나오는 형태의 구조체 그림이다. 잘 살펴보면 우리가 원하는 곳은 section(".text") 이곳이므로

그곳의 주소를 가르키고 있는 곳으로 가서 그곳을 가르키도록 저수준을 이용한다.

lseek를 이용하여 점프한다음 한방에 그 위치로 이동한다.




찾아보니 이런게 나왔다.

즉, windows.h 안에 저런 구조체들이 존재 하므로 우리가 원하는 code 영역으로 가기 위해서는 4번째 영역으로 lseek 로 점프 해야한다.

우선 헤더 포인터를 만들어 점프하도록 설계해보자.







PE(Portable Executable)파일은 말 그대로 옮겨다니면서 실행시킬수 있는 파일을 말합니다.

Microsoft가 다른 운영체제와 이식성을 좋게 하기 위해서 만든 파일 포멧입니다.
그러면 Microsoft가 이런 PE파일을 만들기 위해 제작한 PE 파일 구조에 대해서 공부를 해보겠습니다.

1. PE 파일
공식적으로 PE파일의 종류는
 실행 계열 : EXE, SCR 드라이버 계열 : SYS, VXD
 라이브러리 계열 : DLL, OCX, CPL, DRV 오브젝트 파일 계열 : OBJ
이렇게 OBJ를 제외한 나머지는 모두 실행 가능한 파일로 이루어져 있습니다.
2. 배우는 이유
PE파일 구조를 배우면 파일이 실행되기 위한 모든 정보를 얻을 수 있습니다.
예를 들어 어느 메모리의 주소에 로딩이 되는지, 프로그램이 사용하는 API의 정보 등 등을 얻을 수 있습니다.
3. PE 파일 구조





이런식으로 파일과 메모리에 영역이 구분되어서 PE파일들이 메모리에 맵핑이됩니다.
앞으로 이 PE구조 안에 있는 작은 DOS header, DOS stub 등 등을 공부해보겠습니다.

4. PE 헤더
PE헤더란 PE파일 구조중에서 DOS header부터 Section header까지를 말합니다.
그러면 PE헤더에 있는 각각의 부분에 대해서 가르쳐 드리겠습니다.
1) DOS header
typedef struct _IMAGE_DOS_HEADER {     
    WORD   e_magic;          // DOS signature : 4D5A ("MZ")
    WORD   e_cblp;                     
    WORD   e_cp;                       
    WORD   e_crlc;                     
    WORD   e_cparhdr;                  
    WORD   e_minalloc;                 
    WORD   e_maxalloc;                 
    WORD   e_ss;                       
    WORD   e_sp;                       
    WORD   e_csum;                     
    WORD   e_ip;                       
    WORD   e_cs;                       
    WORD   e_lfarlc;                   
    WORD   e_ovno;                     
    WORD   e_res[4];                   
    WORD   e_oemid;                    
    WORD   e_oeminfo;                  
    WORD   e_res2[10];                  
    LONG   e_lfanew;         // offset to NT header 
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

여기서 중요한 값은 e_magic,e_lfanew 이렇게 2개가 있습니다.

여기서 e_magic의 값은 DOS signature로 MZ라는 문자열입니다.

(MZ는 Mark Zbikowski라고 DOS실행파일을 설계한 사람의 이니셜입니다.)

또한 e_lfanew의 값은 NT header의 RVA형태의 주소값을 가지고있습니다.

(이 값은 꼭 40이상의 크기를 가질 필요는 없습니다.)


실제로 계산기를 HxD로 열어보니 이렇게 나오네요.


2) DOS stub

DOS 에서 돌아갈 명령어가 16bit명령어 형태로 있습니다.

이 부분은 없어도 상관없고 크기도 정해져있지 않습니다.

여러 프로그램들을 분석해보았을때 보통 프로그램들은 This program cannot be run in DOS mode를 출력하고 꺼지게 만듭니다.


실제로 계산기도 그렇게 표시되고 꺼지게 되있네요.


3) NT headers


typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                  // PE Signature : 50450000 ("PE"00)
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

NT header 구조체는 이렇게 크게 3개로 구분되 있습니다.

맨 첫번째 로는 Signature인데 이것은 PE라는 문자를 나타냅니다.

DOS signature과 마찬가지로 이 문자를 보면 아 이부분이 NT header의 시작이구나 하고 넘어갑시다.


실제로도 이렇게 나오는데 여기서 부터 NT header구조체의 시작이구나 하면서 판별을 합시다.


2번째 로는 IMAGE_FILE_HEADER 구조체 입니다.

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;

    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_FILE_HEADER구조체 에서는 바로 색칠된 4개의 값이 중요하다고 합니다.

1. Machine

Machine은 CPU별로 고유한 값을 가지면서 IA-32 호환 CPU는 14Ch의 값을, IA-64 호환 CPU는 200h의 값을 가집니다.

2. NumberOfSections

이 값은 이 다음에 나오는 Section들의 갯수입니다.

최소 1개 이상이여야 됩니다.

3. SizeOfOptionalHeader

이 값은 IMAGE_NT_HEADERS구조체의 마지막구조체 IMAGE_OPTIONAL_HEADER32의 구조체의 크기를 나타냅니다.

( 이 값과 e_lfanew값의 범위를 이상하게 지정할 수 있다는 것 때문에 변종 PE를 만들어 낼 수 있습니다.) 

4. 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 nunbers 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_LO         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   0x0400  // 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_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

이렇게 많은 속성들이 있는데 이 파일에 해당하는 부분들만 bit OR형식으로 조합되서 Characteristics값에 저장이 됩니다.
여기서 중요한것은 당연히 색칠된 2와 2000값이겠지요.

실제로는 이렇게 나오며 여기있는 부분을 해석하면

Machine : 014C (=IA-32호환)

NumberOfSections : 4 (text,data,rsrc,reloc)

SizeOfOptionalHeader : 00E0

Characteristics : 0102 (IMAGE_FILE_EXECUTABLE_IMAGE, IMAGE_FILE_32BIT_MACHINE)

이렇게 파일들에 대한 정보를 얻어 낼 수 있습니다.



이 다음에는 마지막 구조체인 IMAGE_OPTIONAL_HEADER32입니다.

typedef struct _IMAGE_DATA_DIRECTORY {

    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    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;

이 구조체에서는 이렇게 색칠된 값이 중요하다고 합니다.

1. Magic

이 구조체가 32bit용이라면 10Bh의 값을,64bit용이라면 20Bh의 값을 가지게 됩니다.

2. AddressOfEntryPoint

EP의 RVA 주소값이 들어있습니다.

물론 RVA값이니까 실제 EP는 AddressOfEntryPoint+ImageBase이겠죠

3. ImageBase

PE파일이 맵핑되는 시작 주소를 가리킵니다.

4. SectionAlignment

메모리에서의 섹션의 최소단위를 나타냅니다.

5. FileAlignment

파일에서의 섹션의 최소단위를 나타냅니다.

4,5번의 최소단위를 맞추기 위해서 PE구조에서 각 section들이 끝나면 NULL값을 집어넣는 NULL padding이란 기술을 사용합니다.

6. SizeOfImage

메모리에서의 PE구조의 크기를 나타냅니다.

7. SizeOfHeader

PE header의 크기를 나타냅니다.

8. Subsystem

1의 값을 가지면 드라이버 파일(SYS, VXD)파일이란 뜻입니다.

2의 값을 가지면 GUI파일이란 뜻입니다.

3의 값을 가지면 CUI파일이란 뜻입니다.

9. NumberOfRvaAndSizes

바로 밑에 있는 IMAGE_DATA_DIRECTORY구조체의 배열의 크기를 정합니다.

(맨 처음에 #define으로 16이라고 크기가 정해져있지만

windows에서는 이 값을 보고 구조체 배열의 크기를 정합니다.)


실제 IMAGE_OPTIONAL_HEADER32 구조체 입니다.

분석을 해보면

Magic : 010B (=32bit)

Address of Entry point : 00012D6C

Imagebase : 01000000

Section Alignment : 1000

File Alignment : 200

Size of Image : 0C0000

Size of Headers : 400

Subsystem : 2

Number of Directoris : 10

이렇게 값이 나오는 것을 알 수 있습니다.

(Number of Directoris 왜 10이냐고 16이 아니냐고 하면 10h 는 16입니다.)


일단 이정도로 PE 구조 정리-1 을 끝내겠습니다.

읽어주셔서 감사합니다.









좋은 그림을 찾았다.


즉 pe header 가 시작되는 부분은 저기라고 한다.

stpNH = (IMAGE_NT_HEADERS32*)(vpCode + (stpDos->e_lfanew));

nt 헤더가 시작하는 곳이 pe header 가 시작되는 곳이다.





즉 최종적으로,


iRet = read(iFd, vpMemStart, MEM_SIZE);

if (iRet != MEM_SIZE)

printf("파일이 작거나 문제가 있습니다.\n");

return;

}


stpDosH = (IMAGE_DOS_HEADER*)vpMemStart;

stpNtH = (IMAGE_NT_HEADERS*)((unsigned char*)vpMemStart + (stpDosH->e_lfanew)); // NT 헤더 주소

stpFileH = (IMAGE_FILE_HEADER*)((unsigned char*)stpNtH + sizeof(stpNtH->Signature)); // FILE 헤더 주소

stpOptionalH = (IMAGE_OPTIONAL_HEADER*)((unsigned char*)stpFileH + sizeof(IMAGE_FILE_HEADER));


peHeaderSize = stpOptionalH->SizeOfHeaders;


iRet = lseek(iFd, peHeaderSize, SEEK_SET); // 프로그램의 code 영역으로 점프

if (iRet < 0)

{

printf("code 영역 읽기 실패 ! \n");

return;

}


이런 코드 라인이 완성된다. (내가 한 거라서 주쌤이 다시 하면 아마 달라질 수 있음.)

lseek() 다시 살펴보자.


lseek() 함수 - 파일의 연산의 offset을 설정


파일에 읽기 혹은 쓰기 연산을 수행 할 때, 어디까지 읽었는지 혹은 어디까지 썼는지에 대한 정보가 필요하다. 모든 열린 파일은 현재 파일 offset을 가지고 있다. 이 값은 파일의 시작부분부터 몇 바이트 떨어진 곳에 작업을 수행중이었는지 나타내는 값이다. 따라서 파일에 대한 read(), write() 함수는 이 offset 값부터 연산을 시작하며, read(), write() 수행 이후 읽은 혹은 쓴 만큼 offset 값을 증가시키게 된다. 기본적으로 파일을 처음 열면 이 offset 값은 0으로 파일의 처음 부분을 가리키게 된다. 만약 open() 수행시 OI_APPEND 플래그를 세팅했다면, offset은 파일의 마지막 부분, 즉, 파일의 크기 만큼을 가지게 될 것이다. 


이런 offset을 read(), write()에 의해서가 아닌 특정한 값으로 설정할 수 있는 함수가 lseek() 함수이다. 


Prototype (함수 원형)


#include <unistd.h>


off_t lseek (int filedes, off_t offset, int whence);


반환 값


 성공 :  새로운 파일 offset

 실패 : -1



인자

 int filedes

 offset을 설정 할 파일의 FD (파일 디스크립터)

 off_t offset

 설정할 offset 값

 int whence

 적용할 offset 값을 어떻게 해석 할 것인가



lseek() 함수에서 offset은 whence 인자에 의해서 다르게 해석된다. whence 값으로 올 수 있는 값은



whence 값 

 설명 

 SEEK_SET

 offset이 파일의 시작점부터의 바이트 수를 의미한다.

 SEEK_CUR

 offset이 현재 값에서부터의 상대적인 값을 의미한다.

 SEEK_END

 offset이 파일의 마지막 부분에서부터의 상대적인 값을 의미한다.



이를 응용하여 현재 offset 값을 다음과 같이 구할 수 있다. 


off_t currpos;

currpos = lseek(fd, 0, SEEK_CUR);


이런 테크닉으로 현재 offset 값을 구할 수 있다. FIFO나 파이프, 소켓의 FD에 이런 연산을 수행하면, -1이 리턴되어 에러가 발생했음을 알리고, errno에 ESPIPE가 세팅된다. 


일반적으로 현재 offset은 양수이다. 어떤 디바이스에서는 음수도 가능하지만 일반적인 정규파일에서는 반드시 음수가 아닌 양수 (non-negative)가 된다. 이유는 lseek의 리턴 값이 -1 일때, 실제 음수 offset -1을 의미하는지, 에러를 의미하는 -1인지 판단하기가 애매하기 때문이다. 


lseek과 관련된 재미있는 사항은 'lseek은 I/O를 발생시키지 않는다'라는 것이다. 실제 파일의 내용에 접근하지 않고, 프로세스의 FD가 가리키는 구조체인 파일 테이블에 존재하는 offset 값만 바꾼다. (참조 : 파일 디스크립터)


이 offset 값은 파일 사이즈보다 클 수 있다. 이럴 경우 그 다음 쓰기 연산은 파일을 확장시킨다. offset을 파일 사이즈보다 크게 설정하고 쓰기를 수행하면 기존의 파일 끝부분에서 설정한 offset까지는 아무것도 쓰여지지 않은 상태가 된다. 다시말해서 파일에 구멍이 생길 수 있다. 이 구멍은 읽기 연산을 수행 할 때, \0으로 읽어진다. 하지만 실제로 디스크 블록을 할당하지는 않는다. 


lseek() 함수에서 사용하는 offset은 off_t 타입으로 표현되어 있다. off_t 타입이 가질 수 있는 값의 범위는 플랫폼마다 다르다. 유닉스 표준이나 sysconf() 등으로 알아볼 수 있으며, off_t 타입이 가질 수 있는 값의 한계가 곧 파일의 사이즈 한계를 결정한다. 32비트 시스템과 64비트 시스템에서 존재할 수 있는 파일의 크기가 다른 이유 중에 하나가 바로 이 off_t 타입 때문이다. 




어째껀



이런 정보가 있기에

peHeaderSize = stpOptionalH->SizeOfHeaders;


이런 코드 라인이 완성 되었고

그 exe 파일의 code 영역으로 점프 하려면 저기 저 헤더를 구하는 과정이 필요하다.

그곳에 접근하려면 저런식의


  stpDosH = (IMAGE_DOS_HEADER*)vpMemStart;

stpNtH = (IMAGE_NT_HEADERS*)((unsigned char*)vpMemStart + (stpDosH->e_lfanew)); // NT 헤더 주소

stpFileH = (IMAGE_FILE_HEADER*)((unsigned char*)stpNtH + sizeof(stpNtH->Signature)); // FILE 헤더 주소

stpOptionalH = (IMAGE_OPTIONAL_HEADER*)((unsigned char*)stpFileH + sizeof(IMAGE_FILE_HEADER));


주소를 알아야 하는 코드들이 필요한데 다시 이 그림을 살펴보자면,

3. PE 파일 구조


http://www.reversecore.com/19

http://zesrever.tistory.com/55


여기에 굉장히 자세한 정보들이 들어가 있는 것을 확인했다.

아주 훌륭하므로 나중에 꼭 확인하여 공부하도록 하자.


<내 방식대로 분석>

맨 처음 exe 파일이 우리 프로그램 위에 올려지면 왼쪽과 같은 주소 구조로 이루어져 있는 것을 이제는 알 수 있다.

1. 그렇다면 맨 처음 포인터가 저 위 DOS Header를 가르킬 것이고 그 포인터의 주소값을 가져온다.

2. 그런다음 DOS Stub의 헤더의 시작 위치를 알려 한다면

DOS Header의 사이즈 값이 고정되어 있기 때문에 그곳으로부터 저 사이즈만큼 더해버리면 DOS Stub의 시작 위치를 알 수 있다.

 하지만, 우리는 그저 code 영역의 주소만 어떻게든 알면 되기에 이 시작위치를 굳이 알 필요는 없었다.

DOS Header에 NT Header 의 시작 주소를 저장하는 구조체 변수가 존재했기 때문이다.

3.

1) DOS header
typedef struct _IMAGE_DOS_HEADER {     
    WORD   e_magic;          // DOS signature : 4D5A ("MZ")
    WORD   e_cblp;                     
    WORD   e_cp;                       
    WORD   e_crlc;                     
    WORD   e_cparhdr;                  
    WORD   e_minalloc;                 
    WORD   e_maxalloc;                 
    WORD   e_ss;                       
    WORD   e_sp;                       
    WORD   e_csum;                     
    WORD   e_ip;                       
    WORD   e_cs;                       
    WORD   e_lfarlc;                   
    WORD   e_ovno;                     
    WORD   e_res[4];                   
    WORD   e_oemid;                    
    WORD   e_oeminfo;                  
    WORD   e_res2[10];                  
    LONG   e_lfanew;         // offset to NT header 
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

여기서 중요한 값은 e_magic,e_lfanew 이렇게 2개가 있습니다.

여기서 e_magic의 값은 DOS signature로 MZ라는 문자열입니다.

(MZ는 Mark Zbikowski라고 DOS실행파일을 설계한 사람의 이니셜입니다.)

또한 e_lfanew의 값은 NT header의 RVA형태의 주소값을 가지고있습니다.

(이 값은 꼭 40이상의 크기를 가질 필요는 없습니다.)

그러므로 코드 라인은 이렇게 나타난다.


stpNtH = (IMAGE_NT_HEADERS*)((unsigned char*)vpMemStart + (stpDosH->e_lfanew)); // NT 헤더 주소


4. 다음 파일 헤더. 그림을 다시 살펴보면

 

이러한 구조로 되어있음을 다시금 확인 후

파일 헤더 size 만큼만 건너뛰면 optional header 시작위치로 점프가 가능할 것으로 보여진다.

그러므로 코드 라인은 이와같이 나타난다.


stpFileH = (IMAGE_FILE_HEADER*)((unsigned char*)stpNtH + sizeof(stpNtH->Signature)); // FILE 헤더 주소


5. 자 이제 저 optional header size만큼 건너뛰느냐, 아니면 좀 더 스마트 하게 정보를 찾아 다른 방법이 있느냐 의 문제만 남은 상태이다.

자세히 살펴보니 optional header 구조체 안에 

이런 구조체 변수가 존재했다.

그러므로 최종적인 peHeader size를 구하는 코드 라인은,


int peHeaderSize;

.

.

.    

     peHeaderSize = stpOptionalH->SizeOfHeaders;


즉, 이제 선생님께서 말씀하신 lseek를 사용하면 그 코드라인으로의 점프가 가능할 것만 같다.

코드라인은,


iRet = lseek(iFd, peHeaderSize, SEEK_SET); // 프로그램의 code 영역으로 점프

if (iRet < 0)

{

printf("code 영역 읽기 실패 ! \n");

return;

}


이와 같다.



'마이 스토리 > 내용 정리중인 글들..' 카테고리의 다른 글

20151105 - 커널 예제.  (0) 2015.11.05
20151103 - 어셈블리  (0) 2015.11.03
20151101 - stst()  (0) 2015.11.01
20151017 - 아.. 다시 갈아엎고 있다.  (0) 2015.10.17
20151005 - Stack의 구조  (0) 2015.10.06