Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Study

리버싱 핵심원리_18_UPack PE 헤더 상세 분석 본문

Reversing/리버싱 핵심원리

리버싱 핵심원리_18_UPack PE 헤더 상세 분석

마늘부추 2019. 2. 18. 04:25

18. UPack PE 헤더 상세 분석

 

18.1 UPack 설명


- 중국의 dwing이라는 사람이 만든 PE 패커
- PE 헤더를 독특하게 변형하는 패커

 

 

 

18.2 UPack으로 실행압축된 notepad.exe

 

 

PEview가 notepad의 헤더를 제대로 읽어내지 못한다.

 

그래서 다른 유틸리티인 Stud_PE를 이용해서 본다.

 

 

 

 

18.3 PE 헤더 비교

 

 

원본의 PE 헤더이다. IMAGE_DOS_HEADER와 DOS Stub, IMAGE_NT_HEADER 순의 전형적인 PE header가 보인다.

 

 

그러나 실행 압축된 PE 파일을 살펴보면 맨 앞에 MZ 문자열이 나타나는 것 말고는 원본 PE와 크게 다름을 알 수 있다.

 

 

 

18.4 상세 분석

 

18.4.1 헤더 겹쳐쓰기

 

이는 Header를 겹쳐 쓰는 기법으로, IMAGE_DOS_HEADER와 IMAGE_NT_HEADER가 겹쳐진 모습니다.
헤더를 겹쳐 씀으로 헤더 공간을 절약하며, 동시에 분석을 어렵게 만드는 효과가 있다.

 

압축 PE에 나타난 IMAGE_NT_HEADER의 시작은 offset 00000010이다.

 

(offset  0) e_magic : Magic Number = 4D5A('MZ')
(offset 3C) e_lfanew : File address of new exe header

 

이 두 가지 필드 외에는 큰 의미는 없다. 다른 필드는 없어도 실행에 문제가 되지 않으며 PE 스펙에 어긋나지 않는다. 따라서 나머지 영역에 다른 헤더를 덮어쓸 수 있는 것이다.

 

압축 PE에 나타난 IMAGE_NT_HEADER의 시작은 offset 00000010이다.

 

 

 

18.4.2 IMAGE_FILE_HEADER.SizeOfOptionalHeader

 

 

IMAGE_FILE_HEADER.SizeOfOptionalHeader 필드를 본다.


이는 다음 헤더 구조체인 IMAGE_OPTIONAL_HEADER 구조체의 크기를 정의하는 값으로 원래는 00E0 값을 갖지만 여기선 0148로 정의한다.

구조체의 멤버와 크기가 이미 정해져 있는데 크기를 따로 설정할 수 있도록 필드를 제공하는 이유는 PE 파일의 형태에 따라서 각각 다른 IMAGE_OPTIONAL_HEADER 형태의 구조체를 바꿔 낄 수 있도록 설계되었기 때문이다.
(64비트 용 PE+포맷의 IMAGE_OPTIONAL_HEADER의 크기는 F0)

 

또한 IMAGE_SECTION_HEADER의 영역이 IMAGE_OPTIONAL_HEADER가 끝나고 바로 시작하는 것 같지만 정확히는 IMAGE_OPTIONAL_HEADER 시작 offset에 SizeOfOptionalHeader 값을 더한 위치부터 IMAGE_SECTION_HEADER가 나타난다.

따라서 이 실행 압축 파일에서 IMAGE_SECTION_HEADER는 IMAGE_OPTIONAL_HEADER의 시작 offset(28) + SizeOfOptionalHeader(148) = offset 170부터 시작한다.

 

이렇게 섹션 헤더와 옵션 헤더 사이의 공간을 늘리는 이유는 헤더를 꼬아놓고 이 헤더 영역에 디코딩 코드를 삽입하기 위함이다.

이 영역을 HxD로 본다. IMAGE_OPTIONAL_HEADER의 끝(D7)과 IMAGE_SECTION_HEADER의 시작(170) 영역을 보면 된다.

 

※ 헷갈렸던 부분

IMAGE_OPTIONAL_HEADER의 끝이 D7인 이유?
이 멤버인 NumberOfRvaAndSize 멤버는 offset 00000084에 있고 총 4byte이다.
그런데 이 값이 0000000A이므로 DataDirectiory 배열의 개수는 A 개가 된다(이유는 뒤에 나온다).
NumberOfRvaAndSize 멤버 바로 뒤부터 DataDirectory 구조체 배열이 이어지는데 이 구조체는 각각 4byte의 VirtualAddress 멤버와 Size 멤버로 이루어져 있다.
따라서 두 멤버의 합 8byte씩 A 개의 구조체가 이어지므로 offset 00000087에 offset 50(80byte) 만큼 더한다.

그래서 D7이 되는 것이다.

 

 

이 부분이 디코딩 코드가 되는 것이다.

 

 

 

18.4.3 IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSize

 

IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSize 값을 00000010 -> 0000000A로 변경한다.
이 값의 의미는 뒤에 이어지는 IMAGE_DATA_DIRECTORY 개수를 정의하는 멤버이고, 헤더에 자신의 코드를 삽입하기 위한 목적이다.
이렇게 마지막 6개의 원소들은 무시된다.

굵은 이탤릭체로 쓰인 항목은 잘못 변경했을 때 실행 에러가 발생하는 항목들이다.

 

※ 의문

잘못 변경하면 실행 에러가 발생한다면서 어떻게 A까지만 사용하고 뒤는 제외하지?
다른 값으로 덮어씌워질 텐데 문제가 없는 건가

 

이중 A 번째 LOAD_CONFIG Directory까지만 사용된다. 이렇게 무시된 영역(B~F = 48byte)까지 이용해 디코딩 코드를 삽입한다.

 

 


위 그림에서 노란색으로 표시된 영역이 정상 파일의 IMAGE_DATA_DIRECTORY이고, 빨간색으로 표시된 부분이 무시된 부분이다.

 

※ 참고

파일을 Ollydbg로 열어보면

 

 

이런 경고 창이 뜨는데, 이것은 Ollydbg가 시작될 때 파일의 NumberOfRvaAndSize 값이 10인지 검사하고 다른 값일 경우 뜨는 에러창이다.
(무시해도 됨)

 

 

 

18.4.4 IMAGE_SECTION_HEADER

 

IMAGE_SECTION_HEADER 구조체에서 프로그램 실행에 사용되지 않는 항목들에 UPack 자신의 데이터를 기록한다.

 

IMAGE_FILE_HEADER.NumberOfSections 필드를 보면 섹션의 개수는 3개임을 알 수 있다.
한 섹션 헤더 구조체는 40byte(offset 28)이므로 총 3개의 섹션 헤더의 영역은 78(170~1E7) 크기이다.

 

이만큼이 섹션 헤더이다.

 

 

 

IMAGE_SECTION_HEADER의 구조체는 위와 같은데, 여기서 PointerToRelocations, PointerToLinenumbers, NumberOfRelocations, NumberOfLinenumbers 필드는 프로그램 실행에 아무런 의미가 없는 멤버들이다.

그렇기 때문에 이곳에도 다른 값을 덮어씌워놓았는데, 일례로 파일 offset 1B0 위치에 있는 offset to relocations의 값 0100739D는 원본 notepad.exe의 EP 값이다.

 

 

 

18.4.5 섹션 겹쳐쓰기

 

이제 본격적으로 섹션에 대해서 알아본다.
UPack의 주요 특징 중 하나가 섹션과 헤더를 겹쳐 쓴다는 것이다.

 

 

Stud_PE를 이용하여 섹션의 파일, 메모리에서의 위치를 확인해본 결과 파일에서의 위치가 조금 이상한 걸 볼 수 있다.
우선 Section 2를 보면 파일 크기가 offset 200~AE28로 굉장히 큰 값을 갖는다.
그 이유는 바로 이곳에 원본 파일이 압축되어 있기 때문이다.
또한 Section 1의 메모리 크기가 14000으로 매우 넓은 영역을 차지하는데 바로 이곳에 Section 2의 내용이 압축 해제되기에 충분한 크기의 영역이 필요한 것이다.

 

그다음 Section 1,3의 파일 크기를 보자. 두 섹션 모두 offset 10~1FF까지를 영역으로 가진다.
이곳은 분명 헤더 영역인데 섹션의 영역도 겹쳐있는 것이다.

반면 메모리상에서의 위치는 세 가지 섹션이 모두 떨어져 있는데, 이유는 바로 offset 0~1FF 영역이 메모리에 올라갈 때 3군데 다른 위치(헤더, Section 1, Section 3)에 각각 매핑되기 때문이다. PE 스펙에 따르면 그렇게 하면 안 된다는 내용이 없다.

위 개념을 그림으로 그린 것이다.
0~1FF 부분이 메모리에 올라갈 때 세 영역으로 나누어지는 모습이다.

 

 

 

 

실제로 Ollydbg에 파일을 올려보면 파일 offset 시작 부분인 4D5A~부분이 세 영역에 걸쳐 반복적으로 나타남을 알 수 있다.

 

일단 같은 내용을 이용해 메모리상에 펼쳐놓은 후 디코딩 코드를 실행시켜 Section 2의 내용을 Section 1에 압축 해제하는 것이다.

 

 

 

 

18.4.6 RVA to RAW

 

먼저 일반적인 RVA -> RAW 변환 방법을 복습해보자.

 

RAW - PointerToRawData = RVA - VirtualAddress
                           RAW = RVA - VirtualAddress + PointerToRawData

 

 

AddressOfEntryPoint는 1018, VirtualAddress는 Section 1이므로 1000값을 갖는다.

위 공식에 대입하면 RAW = 1018 - 1000 + 10 = 28이다.

이 영역을 HxD로 확인해본다.

 

 

위 공식대로라면 이 영역이 코드 영역이어야 하는데 코드가 아니라 라이브러리 문자열 영역이 나타난다.

이렇게 실제 영역의 위치와 값이 들어맞지 않는 이유는 다음과 같다.

 

일반적으로 섹션 시작의 파일 offset을 가리키는 PointerToRawData 값은 FileAlignment의 배수가 되어야 한다.
이 PE의 FileAlignment는 200이므로 PointerToRawData 값은 0, 200, 400, 600 등의 값을 가져야 한다.

따라서 첫 번째 섹션의 시작 위치(10)가 FileAlignment(200)의 배수가 아니므로 강제로 배수에 맞춰야 한다(이 경우는 0).

 

RAW = 1018 - 1000 + 0 = 18

 

※ 생각해봄

만약 PointerToRawData가 190이라고 가정했을 때는 어떻게 맞출 것인가에 대해 생각해 보았다.
FileAlignment에 맞게 200으로 올려버릴지 아니면 10과 마찬가지로 0으로 맞출지..

수 자체가 200에 가까워서 올림이 되지 않을까 생각해 보았는데, 그러면 EntryPoint의 RAW는 208이 된다.
이때 FileAlignment가 200이므로 섹션의 시작은 반드시 그 값의 배수여야 하고(이 예시에선 200이 된다), 그렇게 되면 208 - 200 = 8이 되므로 섹션의 앞부분에서 10만큼의 offset이 잘려나가는 셈이다.

따라서 PointerToRawData를 FileAlignment에 맞출 땐 반드시 내려야 한다.

 

 

 

18.4.7 Import Table(IMAGE_IMPORT_DESCRIPTOR array)

 

Stud_PE에서 Data Directory의 Import Table을 찾아 그 값을 따라 이동해본다.

 

 


Import Table의 위치는 271EE이며, 이는 Section 3의 영역이다.
(RVA = 271EE / RAW = 1EE)

 

 

PE 스펙에 따르면 Import Table은 IMAGE_IMPORT_DESCRIPTOR 구조체의 배열로 이루어지고 마지막은 NULL 구조체로 끝나야 한다.

 

HxD로 해당 위치를 찾아본다.

 

 

1EE 위치부터 14만큼의 범위(~201)까지가 Import Table이자 하나의 IMAGE_IMPORT_DESCRIPTOR 구조체이다.
그런데 그 뒤를 확인하니 NULL 구조체도 아니고(구조체 크기만큼의 영역이 모두 NULL이 아니기 때문), 두 번째 구조체도 아니다.

 

이는 분명 PE 스펙에 어긋난 듯 보이지만 메모리에 올라갔을 때는 얘기가 달라진다.


파일과 메모리상에서의 모습이다.
offset 0~1FF범위가 메모리에 그대로 올라가며 27200부터 SectionAlignment값 1000을 더한 28000까지는 모두 NULL값으로 채워진다.

 

여기서 다시한번 HxD를 보면

 

 

빨간 선으로 그은 부분에 E00(= 28000 - 27200) 범위만큼 NULL 값이 채워지는 것이다.
이렇게 될 경우 IMAGE_IMPORT_DESCRIPTOR 구조체의 다음은 NULL 구조체로 취급되어 Import Table의 끝이라고 인식하게 된다.

 

 

HxD로 확인했던 영역이다. 구조체 뒤가 전부 NULL로 채워졌음을 볼 수 있다.
여기서 빨간색으로 감싼 범위가 NULL 구조체로 취급되는 것이고, 이렇게 PE 스펙에 어긋나지 않는 모습을 갖추게 된다.

 

 

 

18.4.8 IAT(Import Address Table)

 

UPack이 어떤 DLL에서 어떤 API를 임포트 하는지 실제로 IAT를 따라가서 확인해보자.

 

먼저 Name의 값은 2이고, 이는 Header 영역에 속해 있다.

 

 

offset 00000002로 가보니 KERNEL32.DLL 문자열이 나타난다.
이 위치는 DOS 헤더의 사용되지 않는 영역이라서 이곳에 DLL 이름을 써두었다.

 

그다음 INT를 보자. 보통 INT를 따라가면 API의 이름 문자열이 나타나지만 이렇게 INT가 0일 경우 IAT 값을 따라가도 상관 없다(INT, IAT 둘 중 하나에만 문자열이 나타나면 되므로).
IAT 값 11E8은 첫 번째 섹션 영역이므로 RAW는 1E8이 된다.

 


각각 RVA(=RAW. 헤더영역이므로) 28과 BE다.

 

 

각각의 위치에 [Ordinal + 이름문자열]이 있다.

 

 

18.5 Comment

 

18장 역시 PE 포맷에 좀 더 익숙해지도록 연습하는 과정인 것 같다.

더 알아낸 내용이 없어 책에 있는 내용을 그대로 베낀 글을 올리는 게 싫었는데, 이번에는 스터디원의 생각지 못했던 질문으로 추가하게 된 내용이 있어서 정리하는 의미가 좀 더 있었다.