[조립하면서 배우는 PE] 여덟번째 이야기 : Section Table 그리고 드디어 완성! Reverse Engineering

리버싱 2017. 1. 10. 16:41

덟번째 이야기입니다. 연재도 거의 끝이 보입니다. ^^; 이번 이야기는 섹션 테이블에 관한 이야기입니다. (익스포트에 관해서는 PE 파일을 다 만든 다음에...)만들던 PE 파일 마저 조립해야죠. 이번 이야기 마지막에는 PE 파일을 완성할 수 있을 것입니다. 고고싱~~

섹션 테이블은 IMAGE_SECTION_HEADER 타입의 엘리먼트로 구성된 배열입니다. 섹션 헤더는 섹션의 이름, 섹션의 파일 상에서의 위치 및 사이즈 정보, 메모리 상에서의 위치 및 사이즈 정보 그리고 메모리 상에서의 속성 값에 대한 정보를 가지고 있습니다.  요컨대 섹션 헤더에는 로더가 각 섹션을 메모리에 로드하고 속성을 설정하는데 필요한 정보들이 담겨져 있는 것입니다. 아 그러고 보니 섹션에 대한 이야기를 빠뜨렸군요. 섹션은 동일한 성질의 데이터가 저장되어 있는 영역이라고 생각하시면 되겠습니다.  섹션은 왜 필요할까요? 이는 윈도우에서 사용하는 메모리 프로텍션 매커니즘과 연관이 있습니다. 윈도우의 경우 메모리 프로텍션의 최소 단위가 페이지입니다.  페이지 단위로 PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_READONLY, PAGE_READWRITE 같은 속성을 설정해두고 속성에 위배되는 오퍼레이션을 시도할 때 Access violation을 발생시켜 메모리를 보호한다는 이야기입니다. 다시 말해 페이지의 일부는 읽기만 가능하고, 페이지의 일부는 읽고쓰기가 가능하도록 설정하는 방식의 프로텍션은 허용하지 않는다는 이야기죠. 이는 성격이 다른 데이터들을 하나의 페이지에 담을 수 없다는 것을 의미합니다. 그렇다보니  프로그램에 포함된 데이터들 중 읽기와 실행이 가능해야 하는 데이터 즉 실행코드와 읽고 쓰는 것이 가능한 데이터, 읽기만 가능한 데이터등을 별도의 페이지에 두어야 하는데 로더에 입장에서는 이를 구분할 방법이 없으므로 섹션이라는 개념을 두어 실행 파일 생성 단계에서 구분해 놓도록 하는 것입니다. 


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 : 섹션의 이름입니다. 최대 사이즈는 8bytes이구요. IMAGE_SIZEOF_SHORT_NAME이 8이라는 거죠. 이 필드와 관련해서 기억해야 할 것은 Name은 로딩 과정과는 아무 상관이 없다는 것입니다. 로더는 이 값을 거들떠 보지도 않습니다. -.-;; 섹션의 이름은 마음대로 정할 수 있으며 심지어 널이어도 관계가 없습니다. 섹션 이름은 C언어에서의 문자열과는 달리 NULL로 끝나지 않아도 됩니다. 

VirtualAddress : 섹션이 로드될 가상 주소를 나타내는 필드입니다. PE 포맷 내에서 모든 가상 주소값이 그렇듯이 이 필드 역시 RVA 값입니다. 

SizeOfRawData : 이 필드는 파일 상태에서의 섹션의 사이즈 값을 가지고 있습니다. 물론 file alignment의 배수이어야 하겠죠. 

PointerToRawData : 파일 상태에서의 섹션의 시작 위치를 나타냅니다. 

Characteristics : 섹션의 속성 값입니다. 속성 값 중 중요한 것들은 글의 서두에서 언급했던 것처럼 메모리 프로텍션과 관련된 것들로 excute, read, write 등이 있습니다. 

요컨대 로더는 PointerToRawData가 지정한 곳에서 부터 SizeOfRawData 만큼의 데이터를 읽어들여 VirtualAddress에 맵핑한 후에 Characteristics에 설정된 속성 정보를 이용하여 페이지 프로텍션을 적용하는 것입니다. 나머지 필드의 값은 지금은 그다지 중요하지 않으므로 생략하도록 하겠습니다.
 
뭐 별거 없지 않습니까? ^^; 바로 PE 파일을 만들어 보도록 하겠습니다. 만들면서 아래의 그림을 참조하도록 하세요. 

사용자 삽입 이미지


 [그림 1]  제작 중인 파일의 섹션 레이아웃

Step 1. 섹션 테이블을 위한 공간을 할당합니다. 
IMAGE_SECTION_HEADER의 사이즈는 40bytes이며, 섹션 테이블에는 섹션의 개수와 동일한 수의 IMAGE_SECTION_HEADER가 필요하므로 120bytes 공간을 할당하면 됩니다. (앞서의 글에서 우리는 실행코드를 위한 섹션과 데이터를 위한 섹션 그리고 임포트 테이블을 구성하기 위한 섹션을 만들것이라고 이야기했습니다. 또한 섹션의 개수는 PE헤더에 기록되어 있음을 떠올려보세요. 만약 섹션의 개수가 어딘가에 기록되어 있지 않다면 로더의 입장에서 섹션의 개수를 알 수 없으므로 섹션 테이블의 끝을 나타내는 IMAGE_SECTION_HEADER가 하나 더 추가되어야 할 것입니다.) 

Step 2. 실행 코드를 위한 섹션 헤더를 완성해 보도록 하겠습니다. 
- 먼저 섹션의 이름은 일반적으로 컴파일러가 하는 것처럼 .text로 하도록 하겠습니다. 

- 그 다음으로 채워야 할 값은 VirtualSize입니다. 이 필드에는 섹션 영역의 실제 사이즈를 채워넣으면 됩니다. 우리가 사용할 코드는 32bytes사이즈를 가지고 있습니다. 따라서 VirtualSize는 리틀엔디언임을 고려하여 20 00 00 00 으로 채우면 되겠습니다. 

- 그 다음으로 채워야 할 값은 VirtualAddress 입니다. .text 섹션의 VirtualAddress는 [그림 1]에서 볼 수 있는 것처럼 0x1000이 됩니다. 따라서 00 10 00 00 으로 채우면 되겠습니다. 

- 그 다음으로 채워야 할 값은 SizeOfRawData입니다. 역시 [그림 1]을 참고하여 0x200으로 하도록 합니다. 이는 코드의 사이즈가 32bytes이고 앞서의 글에서 PE 헤더의 FileAlignment 값을 0x200으로 설정하였기 때문입니다. 00 02 00 00 으로 채우면 되겠습니다. 

- [그림 1]을 살펴보면 PointerToRawData 값은 0x200 임을 알 수 있습니다. 실행코드의 실제 사이즈가 32bytes이고 이는 앞에서 설정한 FileAlignment 단위인 0x200(512bytes)보다 작기 때문에 패딩을 추가해야 하기 때문입니다.(이해가 되지 않으시면 앞의 글들을 다시 확인해 보세요) 00 02 00 00 으로 채우면 되겠습니다. 

- PointerToRelocations 부터 NumberOfLinenumbers 까지의 12bytes는 모두 0으로 채웁니다. 
 
- Characteristics를 채울 차례군요. .text 섹션에 실행 코드를 두어야 하므로 CODE, MEM_READ, MEM_EXECUTE 속성을 지정할 것입니다. 이 값은 0x60000020 입니다. 이 값에 대한 내용은 구글신에게 기도를 드려보세요. 20 00 00 60 을 채워넣으면 되겠습니다. 

지금까지의 작업 내용은 아래와 같습니다. 

사용자 삽입 이미지

 [그림 2] 완성된 .text 섹션 헤더의 모습


Step 3. .data 섹션을 위한 헤더를 완성합니다. 
[그림 3]은 [그림 1]을 참고하면서 완성한 .data 섹션 헤더의 모습입니다. 속성값은 C0000040으로 설정하였는데 이는 INITIALIZED, MEM_READ, MEM_WRITE를 의미하는 값입니다. 

사용자 삽입 이미지

 [그림 3] 완성된 .data 섹션 헤더의 모습

Step 4. .rdata 섹션을 위한 헤더를 완성합니다. 
앞에서와 마찬가지로 .rdata 섹션을 위한 섹션 헤더를 완성한 모습이 아래 [그림 4]입니다. 속성값은 40000040으로 설정하였는데 이는 INITIALIZED, MEM_READ를 의미하는 값입니다. 우리는 .rdata 섹션에 임포트 테이블을 둘 것입니다.(요즘의 컴파일러들은 우리와 달리 임포트 테이블을 별도의 섹션에 두지 않고 .코드 섹션이나 데이터 섹션에 임포트 테이블을 두는 경향이 강한 것 같습니다.)

사용자 삽입 이미지

 [그림 4] 완성된 .rdata 섹션 헤더의 모습 

Step 5. .text 섹션을 생성하고 내용을 채워 넣습니다. 
섹션 테이블을 드디어 완성했습니다. 이제는 섹션을 만들차례입니다.
 
먼저 .text 섹션을 생성하기 전에 FlieAlignment를 고려하여 0x1FF까지 0x00을 패딩합니다.  우리는 FileAlignment를 0x200으로 설정하였으므로 첫번째 섹션인 .text 섹션은 아래 [그림 5]에서 볼 수 있는 것처럼 0x200에서 시작합니다. 패딩을 끝냈으면 섹션을 위한 영역을 할당해야 겠습니다. 우리가 사용할 코드의 사이즈는 32bytes 이지만 역시 FileAlignment를 고려해야 하므로 512bytes만큼의 빈 공간을 추가하면 되겠습니다. 

끝으로 첨부된 code.bin을 복사하여 0x200 위치에 붙여넣으면 됩니다. 완성된 모습은 아래와 같습니다. 

사용자 삽입 이미지


 [그림 5] 완성된 .text 섹션의 모습 

Step 6. .data 섹션을 생성하고 내용을 채워 넣습니다.
앞의 단계에서와 마찬가지로 512bytes 공간을 추가한 다음 첨부된 data.bin을 복사하여 0x400에 붙여 넣습니다. 완성된 모습은 아래와 같습니다. 

사용자 삽입 이미지

 [그림 6] 완성된 .data 섹션

Step 7. .rdata 섹션을 생성하고 임포트 테이블을 완성합니다. 
이 부분은 앞에서의 다른 섹션과 달리 직접 수작업으로 완성해 가도록 하겠습니다. .rdata 섹션의 시작점에 임포트 테이블을 작성할 것입니다. 임포트 테이블 및 관련 정보의 모양은 [그림 7]과 같습니다. 우리가 사용하는 코드는 MessageBoxA를 이용하여 메시지 박스를 띄우는 프로그램입니다. MessageBoxA는 user32.dll에서 익스포트하는 API입니다. 이를 염두해 두시고 아래 그림을 살펴보시기 바랍니다

사용자 삽입 이미지

 [그림 7] 임포트 테이블 및 관련 정보 레이아웃

[그림 7]과 여섯번째 이야기의 [그림 2]를 참고하여 데이터를 채워나가면 아래와 같습니다. (사이즈를 줄이기 위해서 위의 배치를 조금 변경할 수도 있습니다. 하지만 KISS 원칙에 입각하여 ㅋㅋ 무식하게 채웠습니다.) 참고로 오디널값은 00으로 채워도 무방합니다. 이러한 경우 로더는 뒤에 나온 이름을 이용하여 함수의 주소값을 찾게 됩니다. 

사용자 삽입 이미지

 [그림 8] 완성된 임포트 테이블 및 관련 데이터의 모습

Step 8. 자 이제 데이터 디렉토리에 임포트 테이블의 주소(RVA)와 사이즈를 기록해야 겠습니다. 

다섯번째 이야기에서 데이터디렉토리를 위한 공간 128bytes를 생성한 바 있습니다. 그 때 생성한 데이터 디렉토리의 파일 상의 offset은 확인해 보시면 0xB8입니다. 각 엔트리의 사이즈는 8bytes이며 IMPORT_DIRECTORY_ENTRY_IMPORT는 두번째 엔트리이므로 우리는 0xC0에 임포트 테이블에 대한 VirtualAddress와 Size를 차례대로 채워넣어야 겠습니다. VirtualAddress에는 00 30 00 00 을 입력하고 Size는 60 00 00 00 으로 입력하면 됩니다. 완성된 데이터 디렉토리의 모습은 아래와 같습니다. 

사용자 삽입 이미지

 [그림 9] 완성된 데이터 디렉토리의 모습(일부)

Step 9. 짜잔!!!!  드디어 완성입니다. 실행시켜보겠습니다. 

사용자 삽입 이미지

 [그림 10] 우리의 첫번째 작품입니다.

잘 실행되셨습니까? 축하드립니다. 


점프 테이블 
이번 글을 끝내기 전에 한가지 언급해야 할 일이 있습니다. 다름 아닌 점프 테이블에 관한 이야기입니다. 지금까지 알아본 내용을 바탕으로 생각해보면 컴파일러는 컴파일을 수행할 때 임포트한 API의 주소를 알 수가 없습니다. (pre binding 기능 역시 링커가 해주는 것이죠) 그렇다면 CALL MessageBoxA와 같은 코드는 어떤 형식으로 컴파일 될까요? 완성된 코드를 Ollydbg로 열어 그 매커니즘을 확인해보세요. 아래 간단한 설명을 달아 놓았습니다. 조금만 생각해보시면 쉽게 이해하실 수 있을 것이라 믿습니다. 

사용자 삽입 이미지

 [그림 11] ollydbg에서 확인한 점프 테이블 


지금까지 PE 파일의 대략의 구조를 살펴보면서 손수 실행 파일을 만들어보았습니다. 재미있으셨는지 모르겠네요. 이제 연재도 끝나가네요. 다음번에는 마지막으로 익스포트에 대해서 알아보도록 하겠습니다. 나머지 재배치나 리소스 섹션에 대한 이야기는 인터넷 상에 자료가 많으니 별도로 공부하시기 바랍니다. 이호동씨의 책에도 매우 자세히 잘 나와있습니다. 오늘도 즐핵하세요_~

* 제 잘못된 습관 중 하나가 글을 쓴 후 잘 읽지를 않는 것이어서 오탈자도 많을 테고, 내용상 오류가 있을 지도 모르겠습니다. 댓글달아주시면 수정하도록 하겠습니다. 


첨부파일 1. 코드섹션


첨부파일 2. 데이터섹션



출처 - http://zesrever.tistory.com/74

: