악성코드 분석할 때 pe 구조를 알아야한다고 해서 pe parser를 만들어보았다!

 

모두가 아는 나뭇잎 책을 보며 윈도우 구조체와 의미있는 멤버들을 뽑아서 출력하도록 하였다.

( 저는 개념을 익히기 위해 먼저 32bit pe 파일을 기준으로 코딩하였습니다. )

 

pe파일을 헥스에디터로 열었을 때 제일 먼저 보이는 구조체는 "_IMAGE_DOS_HEADER"이다.

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // 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
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

010 editor로 본 구조체 모습

< 주요 멤버 >

e_magic: 항상 "MZ" (0x5a4d)

e_lfanew: file에서의 nt header 시작 offset을 가리킨다. 

DOS HEADER 출력 모습

그 다음 알아볼 구조체는 "_IMAGE_NT_HEADER" 이다. 이 구조체 안에 2개의 구조체가 더 있다. 함께 알아보도록 하자.

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

010 editor로 본 구조체 모습

< 주요 멤버 >

Signature:  항상 "PE" (0x4550)

FileHeader:

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;

nt header안의 file header 모습

     < 주요 멤버 >

     Machine: 0x14C(32bit라서) , 0x8664(64bit)

     NumberOfSections: section의 갯수를 말한다.( .text, .data, .rsrc 가 있다면 0x3 )

     SizeOfOptionalHeader: 뒤에 올 _IMAGE_OPTIONAL_HEADER의 크기를 말한다.

     Characteristics: File의 성격을 알려준다.

//Characteristic
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved external 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  // Aggressively 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.

     OR 연산으로 이루어지는데 위의 사진을 예로 들자면 0x10f라면 0x1 | 0x2 | 0x4 | 0x8 | 0x100 이겠죠? 그 뜻은 위에 주석처리된 부분을 참고하세요~

OptionalHeader: (저는 _IMAGE_OPTIONAL_HEADER32를 가져왔습니다.)

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;

0xE0 크기의 Optional Header

     < 주요 멤버 >

     Magic: 0x10b(32bit), 0x20b(64bit)

     AddressOfEntryPoint: 프로그램에서 최초로 실행되는 코드의 시작 주소의 RVA값(파일에서 접근하려면 RAW 값으로 접근해야함)

     ImageBase: 프로그램이 실행되고 메모리상의 파일의 시작 주소 ( pe 파일 실행할 때 eip의 값이 ImageBase + AddressOfEntryPoint로 세팅 )

     SectionAlignment: 메모리에서의 섹션 최소 단위

     FileAlignment: 파일에서 섹션 최소 단위

     SizeOfImage: 가상메모리에서 pe image가 차지하는 크기

     SizeOfHeaders: pe 파일에서의 header 크기

     Subsystem: 0x1(Driver file), 0x2(GUI), 0x3(CLI)

     NumberOfRvaAndSizes: 맨 마지막 멤버인 DataDirectory의 배열 크기

 

NT Header 출력 모습

이제는 "_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;

image header 3개의 모습( 하나에 0x28 크기 )
하나만 띄어놓은 모습

< 주요 멤버 >

VirtualSize: 메모리에서 섹션이 차지하는 크기

VirtualAddress: 메모리에서의 섹션 시작 주소 (RVA)

SizeOfRawData: 파일에서 섹션이 차지하는 크기

PointerToRawData: 파일에서의 섹션 시작 주소 (RAW)

Characteristics: 섹션의 성격 ( 마찬가지로 or 연산 , 되게 많은 관계로 생략,,, )

Section Header 출력 모습

마지막으로 알아볼 구조체는 "_IMAGE_IMPORT_DESCRIPTOR" 입니다. ( 어떤 라이브러리를 import하고있는 지 )

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

_IMAGE_IMPORT_DESCRIPTOR 한 개 모습

< 주요 멤버 >

OriginalFirstThunk: Import Name Table의 주소(RVA) -> 주소 raw로 계산해서 따라가보면 table이 쭉 나오고 다 파싱해서 가져옴

Name: 라이브러리 문자열을 담고 있는 주소(RVA) -> 주소 raw로 계산해서 따라가보면 라이브러리 나옴(null까지 문자 받기)

FirstThunk: Import Address Table의 주소(RVA) -> 잘 모르겠음.. ㅎㅎ

 

이 부분은 말로만 해서 잘 모르겠어서 같이 따라가면서 확인해보도록 하자.

OriginalFirstThunk값이 0x7990이다. RAW로 바꾸면 0x6d90.

0x6d90으로 가보면

OriginalFirstThunk 따라가기

이렇게 나타나는데 0x00 00 00 00 이 나올때까지 받으면 그게 테이블의 끝이 된다.

Name 부분은 0x7aac 인데 RAW로 바꾸면 0x6eac이다. 

Name 따라가기

따라가보면 이름이 나온다.

FirstThunk는 따라가봤는데 이상한게 나와서 잘 머르겠다밍~~~ 위와 같은 방법으로 따라가보면 뭔가 있긴 한데 뭔지 모르겠다.. 아시는 분은 댓글로 알려주세요!!

FirstThunk 따라가기
Import Descriptor의 일부 출력 모습

위에서 계속 언급했는데 RVA는 뭐고 RAW는 뭘까?

RVA는 메모리상에서의 주소, RAW는 파일에서의 주소라고 생각하면 편하다. 내가 계속 RAW를 구했던 이유는 메모리에 올라가있는 걸 본 게 아니라 파일형태로 되어있는 상태로 따라가고 있기 때문이다.

나뭇잎책에서 가져온 비례식

RVA( 메모리 상의 주소 ) - VirtualAddress( 메모리 상의 시작주소 ) : 시작으로부터 얼마나 떨어져있는 지

PointerToRawData( 파일에서의 섹션 시작 주소 )를 더해주면 같은 만큼 떨어져있던 친구를 구할 수 있따~ 라고 이해하면 편하다! --> 이렇게 해도 모르겠는 분들을 위한 예제!!! ( 이것도 나뭇잎책 ㅎㅎ )

이 상황에서 RVA가 0x2000이라면?

1. RVA는 Memory에서의 주소기 때문에 0x2000이 속해있는 섹션을 찾습니다.

2. 그럼 0x1000 부터 시작되는 text 섹션이되겠네요. (imagebase 값 무시하고 생각, 여기서 imagebase는 0x1000000)

3. 그럼 0x2000(RVA)-0x1000(메모리에서의 text 섹션 시작주소==VirtualAddress)+0x400(파일에서의 text 섹션 시작주소==PointerToRawData) 하면???

4. RAW 값 구하기 쉽다.

 

위의 내용을 c언어로 구현해보았다. 아직 미숙하지만 github에 올려놨다 ㅎㅎ

https://github.com/kimheejoo/peParser/blob/master/peParser.c

 

kimheejoo/peParser

Contribute to kimheejoo/peParser development by creating an account on GitHub.

github.com

보고 많은 도움이 되었기를 바랍니다. 다음에는 64bit pe 파일도 도전해보겠습니다!!

'Forensics' 카테고리의 다른 글

Little Endian & Big Endian  (0) 2020.04.26
[디지털포렌식과제] Mission4  (0) 2020.04.26
[디지털포렌식과제] Mission2  (0) 2020.04.19
[디지털포렌식과제] Mission1  (0) 2020.04.19
[steganography] jpg 스테가노그래피  (5) 2019.11.28