본문 바로가기

리버스 엔지니어링

UPack PE헤더 상세 분석 -2

5-6 RVA to RAW

각종 PE 유틸리티들이 UPack 앞에서 맥을 못 추던 원인이 바로 RVA-> RAW 변환에 어려움을 겪었기 때문이다. UPack 제작자는 많은 테스트를 통해서 Windows PE로더의 버그를 알아낸 후 이를 UPack에 적용하였다.
이 기법을 처음 접한 유틸리티들은 대부분 '잘못된 메모리 참조에 의한 비정상종료'를 당했다.

먼저 일반적인 RVA -> RAW 변환 방법인 이렇다

 

RAW - PointerToRawData =RVA - VirtualAddress

                           RAW = RVA -VirtualAddress + PointerToRawData

 

 

그렇다면 위 공식대로 EP의 파일 옵셋(RAW)를 계산해보자. UPack의 EP는 RVA는 1018이다.

 

 

 RAW = 1018 - 1000 + 10 =28

1st Section의 VirtualAddress = 1000이고 PointerToRawData = 10이다.

 

 

RAW 28 영역을 Hex Editor로 확인해보자.

 

 

 

RAW 28 영역은 코드가 아니라 "LoadLivraryA"문자열 영역이다. 비밀은 바로 첫 번째 섹션의 PointerToRawData 값 10에 있다.
일반적으로 섹션 시작의 파일 옵셋을 가리키는 PointerToRawData 값은 FileAlignment의 배수가 되어야 한다. UPack의 FileAlignment는 200이므로 PointerToRawData 값은 0, 200, 400, 600 등의 값을 가져야 한다. 따라서 PE 로더는 첫 번째 섹션의 PointerToRawData(10)가 FileAlignment(200)의 배수가 아니므로 강제로 배수에 맞춰서 인식한다.(이 경우에는 0). 이것이 바로 UPack 파일이 정상적으로 실해애되지만 많은 PE 관련 유틸리티에서 에러가 난 이유다.

정상적인 RVA->RAW 변환은 아래와 같다.

 

 RAW = 1018 - 1000 + 0 =18
*PointerToRawData = 0으로 인식

 

 

디버거로 해당 영역의 코드를 보면 다음과 같다

 

 

 

 

5-7 Import Table(IMAGE_IMPORT_DESCRIPTOR array)

 

UPack의 Import Table은 매우 특이하게 구성되어있다.

Hex Editor를 이용해서 IMAGE_IMPORT_DESCRIPTOR 구조체를 따라가자. 먼저 Directory Table에서 Import Directory Table(IMAGE_IMPORT_DESCRIPTOR 구조체 배열) 주소를 얻어야 한다.

 

 

 

위의 오른쪽 박스 부분의 8바이트 크기의 data가 바로 Import Table을 가리키는 IMAGE_DATA_DIRECTORY 구조체이다. 앞의 4바이트가 Import Table의 주소(RVA), 뒤의 4바이트가 Import Table의 크기(Size)를 나타낸다. 따라서 위의 그림을 보면 Import Table의 RVA는 271EE이다.
 이를 Hex Editor로 보기 위해서 RVA -> RAW 변환을 해야 한다. 먼저 이 RVA 값이 어느 섹션에 속해있는지 알아야 하는데, 메모리 주소 271EE는 메모리 세번째 섹션 영역이다.

 

 

RVA -> RAW 변환을 하면 다음과 같다.

 RAW = RVA(271EE) - VirtualOffset(27000) + RawOffset(0) = 1EE
* 3rd Section의 RawOffset 값이 10이 아니라 0으로 강제변환되는 사실을 기억하자.

 

 

Hex Editor로 파일 옵셋 1EE를 살펴보자.

 

 

 

 

 

PE 스펙에 따르면 Import Table은 IMAGE_IMPORT_DESCRIPTOR 구조체 배열로 이루어지고 있고 마지막은 NULL 구조체로끝나야 한다. 선택된 영역이 Import Table을 나타내는 IMAGE_IMPORT_DESCRIPTOR 구조체 배열이다. 1EE~201 옵셋까지 첫 번째 구조체이며,  그 뒤는 두 번째 구조제도 아니고,그렇다고(Import Table의 끝을 나타내는) NULL 구조체도 아니다.

얼핏 보면 이는 분명 PE 스펙에 어긋난 듯이 보인다.. 하지만 그림의 200 옵셋 위에 있는 굵은선을 주목해보자. 이 선은 파일에서 세 번째 섹션의 끝을 의미한다. 따라서 실행할 때 200 옵셋 이하는 세 번째 섹션 메모리에 매핑되지 않는다. 그림을 봅시다.

 


세 번째 섹션이 메모리에 로딩될 때 파일 옵셋 0~1FF 영역이 메모리 주소27000~271FF에 매핑되고 (세 번째 섹션의 남은 메모리 영역인) 27200~28000 영역은NULL로 채워진다. 똑같은 영역을 디버거로 확인해보겠습니다.

 

 

정확하게 01027FF까지만 매핑됙, 01027200 이후부터는 NULL로 채워진 것을 볼 수 있다.

다시 PE 스펙의 Import Table로 돌아가서 01027202 주소 이후부터 NULL 구조체가 나타난다고 본다면 PE 스펙에 어긋나지 않는 셈이다. 이것이 바로 섹션을 이용한 UPack의 트릭이다. 파일로 볼 때는 Import Table이 깨진 것 처럼 보이지만, 실제 메모리에서는 Import Table이 정확히 나타나는 것이다.
대부분의 PE유틸리티들이 파일에서 Import Table을 읽을 때 이 트릭에 걸려서 잘못된 주소를 쫒아가다가 메모리 참조 에러로 죽어버린다.

 

 


5-8 IAT(Import Address Table)

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

 

 Offset

 member

 RVA

 1EE

 OriginalFirstThunk(INT)

 0

 1FA

 Name

 2

 1FE

 FirstThunk(IAT)

 11E8

 

먼저 Name의 RVA 값은 2이고, 이는 Header 영역에 속해있다. (첫 번째 섹션이 RVA 1000에서 시작하기 때문)
헤더 영역에서는 그냥RVA와 RAW 값이 같으므로 Hex Editor로 파일 옵셋(RAW) 2를 살펴보자

 

 

 

KERNEL32.DLL 문자열이 보인다. 이 위치는 DOS 헤더(IMAGE_DOS_HEADER)의 사용되지 않는 영역이라서 UPack이 이곳에 Import DLL 이름을 써두었다. DLL 이름을 알았으니 어떤 API를 임포트 하는지 알아보자

 

보통은 OriginalFirstThunk(INT)를 쫒아가면 API 이름이 문자열이 나타나지만 UPack과 같이 OriginalFirstThunk(INT)가 0일 때는 FirstTunk(IAT) 값을 따라가도 상관이 없다.

 

 

 

먼저 Name의 RVA 값은 2이고, 이는 Header 영역에 속해있다. (첫 번째 섹션이 RVA 1000에서 시작하기 때문)
헤더 영역에서는 그냥 RVA와 RAW 값이 같으므로 Hex Editor로 파일 옵셋(RAW) 2를 살펴보자

 

 

KERNEL32.DLL 문자열이 보인다. 이 위치는 DOS 헤더(IMAGE_DOS_HEADER)의 사용되지 않는 영역이라서 UPack이 이곳에 Import DLL 이름을 써두었다. DLL 이름을 알았으니 어떤 API를 임포트 하는지 알아보자

보통은 OriginalFirstThunk(INT)를 쫒아가면 API 이름이 문자열이 나타나지만 UPack과 같이 OriginalFirstThunk(INT)가 0일 때는 FirstTunk(IAT) 값을 따라가도 상관이 없다.

위 그림에 의하면 IAT 값 11E8은 첫 번째 섹션이므로 RVA-> RAW 변환을 하면 아래와 같다.

 

 RAW = RVA(11E8) - VirtualOffset(1000) + RawOffset(0) = 1E8

 

IAT 파일 옵셋 1E8은 아래 그림에 나타나있다.

 

 

그림에 박스로 표시된 영역은 IAT이면서 동시에 INT 역할도 하고 있다. 즉 이곳은 Name Pointer(RVA) 배열이고 배열의 끝은 NULL이다. 또 2개의 API를 임포트 하는 것을 알 수있다. 각각 RVA 28과 BE이다.

이 RVA 위치에 Import 함수 [ordinal + 이름 문자열]이 있다. 모두 헤더 영역이므로 RVA 값은 RAW 값과 동일하다.

 

 

위 그림을 보면 'LoadLibraryA'와 'GetProcAddress'API를 임포트 한다는 것을 알 수 있따. 이 두 함수는 원본 파일의 IAT를 구성할 때 편리하기 때문에 일반적인 패커에서돋 많이 임포트해서 사용되고 있다.