2016. 12. 31. 14:31 :: 리버싱

 

안녕하세요. Message 입니다.

오늘은 오랜만에 리버싱과 관련된 2가지 주제로 포스팅을 하려 합니다.

① UPack PE 헤더 상세 분석

② 디버깅 - OEP 찾기

 

<리버싱 핵심 원리 - 이승원님> 책에 있는 내용을 베이스로 하지만,

공부하는 입장에서 진행과정을 조금 더 보강하거나

당연한 내용이기에 자연스럽게 책에서 생략된(저는 모르는..) 내용들도 추가하려 합니다.

얼마전 실행압축으로 인해 분석을 보류했던 악성코드가 있었습니다. 각종 도구들을 이용해도 탐지가 안되더군요.

이번 공부가 패킹된 악성코드와 PE구조를 이해하는데 좋은 밑거름이 되었으면 좋겠습니다.

 


 

-----------------------------------------------------------------------------------------------------------------

0x00 준비

-----------------------------------------------------------------------------------------------------------------

 

홈페이지가 변경되어, 책에 있는 링크가 아닌 아래의 링크에서 UPack 0.39 final 버전을 다운받습니다.

Windows 버전도 있는듯 하지만, CMD 버전으로 다운로드 받았습니다.

URL : http://www.geocities.jp/dwingj/mycomp.htm

 

명령어도 매우 간단합니다. UPack으로 notepad.exe 파일을 패킹합니다.

사실 저는 처음에 OK! 문구 하나만 확인하고 그냥 창을 꺼버렸습니다만,

UPack이 어떻게 패킹을 수행하는지 대략적인 흐름을 알 수 있는 문구들이 있습니다.

지금은 그냥 읽어보고 넘어가세요. 분석 중간중간에 해당 사항들을 언급하겠습니다.

 - Rebuilding import table [00C8]

 - Recompiling resuorce [008304]

 - Remocing debug data [001C]

 - Transforming code

 - Compressing data <Use 128KB dict>

 - Building new PE data

 

파일의 원본과 UPack 으로 패킹된 프로그램의 헤더를 살펴봅니다.

섹션헤더는 모두 사라졌고, NT_Header 에서 중요한 OPTIONAL_Header 역시 찾아볼 수 없습니다.

또한 IMAGE_DOS_Header에서 KERNEL32.DLL을 비롯한 API 이름까지 보이고 있습니다.

확실히 정상적인 PE구조와는 확연히 다름을 알 수 있습니다.

 

 

 

-----------------------------------------------------------------------------------------------------------------

0x01 헤더분석

-----------------------------------------------------------------------------------------------------------------

 

1. 헤더 겹쳐쓰기(=e_lfanew 조작)

 

DOS_Header에서는 2가지 멤버가 중요합니다.

그 외는 프로그램 실행에 아무 의미가 없습니다. 위치상으로는 첫번째와 마지막 멤버입니다.

① e_magic : DOS Signature (4D5A, "MZ")

② e_lfanew : NT_header의 시작주소(파일에 따라 가변적)

 

정상적인 프로그램의 e_lfanew 값이 계산되는 방식은 통상적으로 아래와 같습니다.

e_lfanew = DOS_Header(40) + DOS_Stub(A0, 가변) = E0(224)

하지만 Upack에서는 e_lfanew 값이 10(리틀엔디안) 입니다.

 

PE 스펙에 어긋나진 안지만 스펙 자체의 허술함을 이용한 것이며,

NT_Header를 의미하는 COFF_File_Header를 클릭하면 두번째 라인부터 시작되며,

헤더의 크기 측면으로 보면 E0 → 10 으로 변경되면서 D0(208) 의 공간이 절약되었습니다.

 

 

 

2. IMAGE_FILE_HEADER.SizeOfOptionalHeader 조작

 

UPack은 NT_Hedaer 안에 있는 FILE_Header에서 SizeOfOptionalHeader 값을 조작합니다.

이 값을 변경하여 헤더 안에 디코딩 코드를 삽입하기 위한 목적입니다.

원래 SizeOfOptionalHeader 값의 의미는 말그대로 뒤따르는 OPTIONAL_Header의 구조체 크기입니다.

원래 정상적이라면 E0(224) 값을 가져야 하지만, Upack은 148로 변경합니다.

 

그렇다면 왜 UPack은 SizeOfOptionalHedaer 의 값을 변경할까요?

SECTION_HeaderOPTIONAL_Header 바로 뒤에 위치하는게 당연할거라고 생각되지만

OPTIONAL_Header의 시작주소(Offset)SizeOfOptionalHedaer 값을 더한 위치에서 시작합니다.

즉, 간단하게 헤더의 시작위치에서 헤더의 크기를 더하는 공식에서 헤더의 크기인 SizeOfOptionalHedaer 값을 조작한거죠.

아래 그림은 원본파일(notepad.exe)의 1st 섹션(.text)이 계산된 과정을 보여줍니다.

 

UPack은 이러한 특성을 이용하여 PE 헤더를 꽈배기처럼 꼬아놓고

남는 공간에 디코딩에 필요한 코드를 적절히 끼워 넣는 특성을 가집니다.

SECTION_Header1D8이 아닌 170에서 시작하고 있습니다.

 

결과적으로, 아래 그림처럼 OPTIONAL_Header의 뒷부분에 존재하는 DATA_DIRECTORY가 끝나는 108(264) 부터

SECTION_Header가 시작되는 28(OPTIONAL_Header Offset) + 148(SizeOfOptionalHedaer) = 170(368) 사이에

68(104)의 공간이 생겼습니다. byte 길이 계산하실때 실수를 방지하기 위해 HxD 툴을 사용하면 편리합니다.

 

 

 

3. IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes 조작

 

Upack은 OPTIONAL_Hedaer 안에 있는 NumberOfRvaAndSizes 값 역시 변경합니다.

이 값의 의미는 바로 뒤에 이어지는 DATA_DIRECTORY 구조체 배열의 원소 개수입니다.

해당 값을 변경하는 이유는 방금전 위에서 확보했던 68(104)만큼의 공간에 추가적인 공간을 확보하여

자신의 코드를 삽입하기 위한 목적입니다.

 

정상적인 파일에서는 DATA_DIRECTORY 구조체 배열의 원소 개수는 이미 10(16) 이지만,

UPack에서는 A(10) 으로 변경됩니다. 따라서 TLS_Table 원소 뒤에 존재하는 6개의 원소는 무시됩니다.

사실, DATA_DIRECTORY에서 중요한 원소는 EXPORT, IMPORT, RESOURCE, TLS Directory 이므로,

UPack이 TLS_Directory 원소 이후의 원소를 무시하는것은 자연스러운 결과일지도 모르겠습니다.

 

결국, 위에서 SizeOfOptionalHedaer의 값을 변경하여 얻은 68 크기의 공간에서

NumberOfRvaAndSizes 값 변경으로 DATA_DIRECTORY 6개 원소 크기 30을 더해 98 만큼의 공간이 생겼습니다.

 

UPack에서는 해당 공간에 아래와 같은 디코딩 코드를 삽입합니다.

처음엔 NT_HedaderSECTION_Header 사이에 코드를 삽입해서 어떻게 사용할건지 의문이 들었습니다.

해당 영역은 메모리에 올라가면 010000D8 주소에 있을테니까요.

 

아래 부분을 이해하려면 바로 아래 파트의 "섹션 겹쳐 쓰기"를 먼저 공부해야합니다.

하지만 SECTION_Header에 섹션의 시작 오프셋을 가리키는 PointerToRawData10임을 주목해야 합니다.

책에 있는 그림 18.17 [UPack의 겹쳐쓰기 특징] 처럼 PE 헤더 영역에 속한 D8 주소에 있는 데이터가

1st 섹션 영역에서 동일하게 나타남을 의미합니다. 아래 그림처럼 해당 섹션의 VirtualOffset(1000)이 추가된 영역에서 말이죠.

눈치 빠르신 분은 010010D8 주소에서 PointerToRawData10을 뺀 주소가 아니라 의아함을 느끼실 수 있습니다만,

RVA to RAW 파트에서 해당 부분에 대해 자세하게 살펴보겠습니다. 어쨋든 동일한 코드가 존재한다는게 중요합니다.

  헤더영역

  섹션영역

 

사실 저같은 경우, 분석을 하면서 NumberOfRvaAndSizes 값이 변경되었다는 사실을 단번에 파악하기엔

초보운전자가 도로상황을 전부 파악하기 힘든것과 일맥상통하다고 생각합니다.

하지만 시각적인 단서가 있다면 좀더 빨리 알아차릴 수 있겠죠.

아래 그림처럼 CFF에서 살펴보면, UPack과 원본파일의 DATA_DIRECTORY 구조체 개수와

Invalid 항목등을 통하여 NumberOfRvaAndSizes 값 변경을 통한 패킹을 의심해볼만 합니다.

 

 

 

 

4. 섹션 겹쳐쓰기

 

UPack은 SECTION_Header에서도 프로그램 실행시 사용되지 않는 항목에 자신의 데이터를 기록합니다.

Stud_PE를 이용해서 UPack의 SECTION_Header를 살펴보면 수상한 점이 2개입니다.

 

첫번째 수상한 점은 1st 섹션 & 3st 섹션이 겹쳐있다는 점입니다.

Stud_PE를 보면 RawOffset, RawSize 두가지 요소가 각각 10(16), 1F0(496)으로 동일합니다.

(참고로 오프셋은 10(16)은 아까 보았던 NT_Header가 시작되는 영역입니다.)

UPack은 동일한 파일 이미지로 각각 다른 위치/크기의 메모리 이미지를 만들 수 있는 헛점을 이용했습니다.

 

두번째 수상한 점은 1st 섹션 & 3st 섹션과는 달리, 매우 큰 2st 섹션의 크기입니다.

2st 섹션의 크기는 무려 AE28(44,584) 이며, 파일의 대부분을 차지하고 있습니다.

또한 1st 섹션의 VirtualSize 역시 파일의 RawSize에 비해 매우 큰 14,000(81,920) 입니다.

일반적으로 SECTION_HeaderVirtual SizeSize of Raw Data보다 월등히 크다면 패킹을 의심합니다.

 

Upack이 이러한 특성을 보이는 이유는 2st 섹션에 원본파일(notepad.exe)이 압축되어 있기 때문이며,

두번째 섹션이 메모리에 로딩될때 압축이 풀리며 1st 섹션에 기록하기 때문입니다.

VirtualSize 멤버 값은 메모리에서 섹션이 차지하는 크기를 의미하는데,

원본파일(notepad.exe)이 메모리에 로딩될때의 사이즈인 OPTIONAL_Header - SizeOfImage 값과 동일합니다.

따라서 AE28(44,584) 크기로 압축된 2st 섹션이 메모리에 로딩될 때 온전하게 1st 섹션에 기록될 수 있습니다.

또한, 원본파일(notepad.exe)의 이미지가 통째로 풀리기 때문에 프로그램이 정상적으로 실행됩니다.

 

  

 

5. RVA to RAW

 

각종 PE 유틸리티들이 UPack으로 패킹된 PE를 만나서 강제 종료되었던 이유는

RVA → RAW 변환에 어려움을 겪었기 때문이라고 합니다.

UPack의 제작자는 많은 테스트를 통해서 Windows PE 로더의 버그를 알아낸 후 이를 UPack에 적용합니다.

아래는 책에 기재되어 있는 RVA → RAW 변환 공식입니다.

RAW - PointerToRawData = RVA - VirtualAddress

RAW = RVA - VirtualAddress + PointerToRawData

-----------------------------------------------------------

*PointerToRawData : 파일에서 섹션의 시작 위치

*VirtualAddress : 메모리에서 섹션의 시작주소 = RVA

 

위 공식대로 UPack 샘플에서 EP의 RAW(파일오프셋)를 계산해봅니다.

위의 공식대로 RAW를 계산하려면 3가지를 알아야 합니다.

RVA

VirtualAddress

PointerToRawData

 

RVA 값은 OPTIONAL_HedaerAddressOfEntryPoint 1018(4,120) 입니다.

 

VirtualAddress 값은 RVA값이 속해 있는 섹션의 메모리 오프셋입니다.

1018(4,120) 값은 Stud_PE에서 VirtualSize의 값과 VirtualOffset을 고려하여 판단합니다.

첫번째 섹션 영역이 1000 ~ 14FFFF 이므로, 해당 주소는 첫번째 섹션에 속합니다. 

따라서 VirtualAddress = 1000(4096) 입니다.

자연스럽게 PointerToRawData 값은 첫번째 섹션의 RawOffset = 10(16) 입니다.

 

구한 값들을 바탕으로 공식을 적용하면 아래와 같습니다.

RAW = 1018 - 1000 + 10 = 28

Hex editor로 살펴보면, 뭔가 이상합니다. 코드가 아니라 문자열이 존재하기 때문이지요.

이것은 UPack의 특성중 하나인, PointerToRawData 값을 이용한 트릭입니다.

 

일반적으로 섹션 시작의 파일 오프셋을 가리키는 PointerToTawData 값은 FileAlignment의 배수입니다.

UPack의 FileAlignment의 값은 NT_Header - OPTIONAL_Header에서 200으로 명시되어 있으며, 

일반적으로도 PointerToTawData 값은 0, 200, 400 등의 값을 가진다고 합니다.

 

하지만 UPack으 PointerToRawData 값이 10으로 지정되어 있습니다.

FileAlignment(200)의 배수가 아니기 때문에 PE로더는 강제로 FileAlignment 배수에 맞춰서 인식합니다. (이경우는 0)

이것이 바로 UPack 파일이 정상적으로 실행은 되지만, PE 유틸리티에서 에러가 발생한 이유입니다.

따라서 이것을 적용한 공식은 아래와 같습니다.

RAW = 1018 - 1000 + 0 = 18

 

그렇다면 이제 디버거를 통해 메모리에 올라간 이미지와(1018) 파일의 오프셋(18)에 있는 데이터를 비교하여

코드가 동일하다면, RVA → RAW 변환이 잘 이루어진 것입니다. (아래그림 참조)

 

 

 

6. Import Table(IMAGE_IMPORT_DESCRIPTOR array)

 

① 원본파일 Import Table

UPack의 IMPORT_Table 역시 매우 특이하게 구성되어 있습니다.

차이점을 알기 위해서 원본파일(notepad.exe)의 정상적인 구조부터 살펴보겠습니다.

PE파일은 자신이 어떤 라이브러리를 임포트하는지 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체에 명시합니다.

IID는 이번 소단원의 제목과 같이 IMPORT_Directory_Table(IDT)이라는 용어로 부르기도 합니다.

아래 그림은 VIsual Studio 에서 확인한 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체입니다.

 

IID or IDT는 NT_Header 내부의 OPTIONAL_Header에 위치하며,

DATA_DIRECTORY 구조체의 두번째 멤버 IMPORT_Table 항목에 RVA, Size 값이 명시되어 있습니다.

 

해당 주소값을 찾아가보면, IMPORT_Table이 존재하는 곳은 PE 헤더가 아닌 PE 바디입니다.

PE View 도구에서는 IMPORT_Directory_Table 항목으로 명시해주고 있으며,

20byte 단위의 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체 목록이 존재함을 확인할 수 있습니다.

마지막은 NULL 구조체로 구성됨을 체크하고 넘어갑니다.

 

IID에서는 API 목록 RVA 값을 가지는 멤버 2개가 존재합니다.

OriginalFirstThunk = INT(Import Name Table)

FirstThunk = IAT(Import Address Table)

해당 멤버들이 실제 PEView에서 아래와 같은 위치에 표현되고 있음을 체크합니다.

 

실제로 INT(Import Name Table), IAT(Import Address Table) 내부를 들여다보면

HintName이 존재하는것은 동일하지만, IAT는 VA, INT는 RVA값이 존재하고 있습니다.

 

마지막으로 해당 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체 목록을 파일 RAW에서도 살펴봅니다.

20byte의 구조체 목록임을 확인하고, IMPORT_Table이 NULL 구조체로 끝나는지 확인합니다.

START RAW = 7604 - 1000 + 400 = 6A04

END RAW = 76C8 - 1000 + 400 = 6AC8

 

 

② UPack Import Table

정상적인 원본파일(notepad.exe)의 IMPORT_Table을 살펴보았으니, 이제 UPack의 차례입니다.

UPack에서는 PEView가 정상적으로 동작하지 않으므로, 직접 파일의 RAW에서 값을 살펴봅니다.

방법은 동일하게 DATA_DIRECTORY에서 RVA를 얻습니다. 값은 271EE 입니다. (리틀엔디안 주의)

 

271EE가 속하는 섹션은 3st 섹션의 VirtualOffsetVirtualSize를 통해 27000 임을 알 수 있습니다.

PointerToRawData의 경우 10 이지만, 위에서의 트릭에 유의하여 0으로 수정합니다.

이를 이용한 계산 결과는 아래와 같습니다.

RAW 271EE 27000 - 0 = 1EE

 

Hex Editor를 통해 해당 주소값을 살펴보면 아래와 같습니다.

구조체의 대부분이 0 이기 때문에, 각 멤버의 값을 분별하기 어렵지만 구조체를 보면서 값을 채워봅니다.

 

4byte씩 끊어서 보면 아래와 같으며, Name 멤버는 KERNEL32.DLL을 나타냅니다.

계속 섹션에서 RVA → RAW 계산 하느라 정신없지만, 헤더영역은 RVA와 RAW값이 동일합니다.

- INT :: OriginalFirstThunk = 0000 0000  (0일 경우 IAT값 참조)

- TimeDataStamp = 0000 0000

- FowarderChain = 0000 0000

- Name = 0000 0002 (KERNEL32.DLL)

- IAT :: FirstThunk = 0000 11E8 (Little Endian)

 

하지만, 문제는 그 다음 구조체입니다.

Name 멤버가 가르키는 문자열도 없으며, IMPORT_Table의 끝을 나나태는 NULL 구조체도 아닙니다.

- INT :: OriginalFirstThunk = 0000 0000 (0일 경우 IAT값 참조)

- TimeDataStamp = 0000 0000

- FowarderChain = 0000 0000

- Name = 0003 0008

- IAT :: FirstThunk = 0005 0000 (Little Endian)

 

이것은 PE스펙에 어긋난 듯이 보이지만, 섹션이 로딩될때의 헛점을 노린 UPack의 트릭입니다.

다시한번 Stud_PE에서 3st 섹션의 멤버값들을 확인해보겠습니다.

3st 섹션의 크기는 1F0이며, 시작지점은 10이므로, 로딩되는 영역은 10 ~ 1FF 입니다.

정확한 크기값은 Hex Editor로 체크하면 정확합니다.

 

하지만 RawOffset은 PE로더에 의해 10 0 으로 강제 변환 되므로

결론적으로 VA 27000 위치에 파일의 0 ~ 1FF 영역이 로딩되며, 길이는 200입니다.

실제로 3st 섹션의 시작부분인 VA 27000을 살펴보면, DOS_Header의 시그니처인 "MZ"로 시작되고 있습니다.

 

또 염두해두어야 할 부분은 3st의 VirtualSize1000 이므로, 271FF 영역 이후부터는

NULL 값으로 채워집니다. 즉, IMPORT_Table의 마지막 멤버가 NULL 구조체가 아니어도 정상 실행됩니다.

 

그렇다면 마지막으로 UPack이 KERNEL32.DLL에서 어떤 API를 임포트하는지

실제로 IAT를 따라가서 확인해 보겠습니다. IAT의 RVA 값은 11E8 이었으므로, RAW값은 아래와 같습니다.

RAW = 11E8 - 1000 + 0 = 1E8

 

1E8 주소로 가보면, INT가 있습니다.

INT, IATIMAGE_IMPORT_BY_NAME 구조체를 가리키는 Name_Pointer(RVA) 배열이며, 끝은 NULL입니다.

따라서 UPack의 경우 2개의 API를 임포트하고 있음을 알 수 있습니다.

 

IMAGE_IMPORT_BY_NAME 구조체는 2byte Hint 멤버와 Name 배열 멤버로 구성됩니다.

 

첫번째 주소는 28이며, Hint0B01과 함께 LoadLibraryA API 이름이 존재합니다.

두번째 주소는 BE이며, Hint0000과 함께 GetProcAddress API 이름이 존재합니다.

 

 

 

-----------------------------------------------------------------------------------------------------------------

0x02 디버깅 - OEP 찾기

-----------------------------------------------------------------------------------------------------------------

 

1. OllyDbg 실행 에러 

Upack은 PE헤더를 독특하게 변경하는 것이 문제가 되고, 안티 디버깅 기법은 없습니다.

일단 OllyDbg를 이용해 패킹된 파일을 실행시키면 에러 메시지가 출력된다고 합니다.

하지만 OllyDbg 201 버전에서 실행하면 오류는 커녕 EP까지 잘 찾아주는 현상(?)이 발생했습니다.

 

크리티컬한 에러가 아니라고 저자분께서 적어 주셨기 때문에, 그냥 넘어가도 될것 같지만.. 

디버깅을 연습도 할겸 버전을 낮추어 실행해보기로 했습니다.

OllyDbg 110 버전에서 실행시키면 아래와 같이 오류창이 뜹니다.

에러의 원인은 UPack이 OPTIONAL_Header에서 NumberOfRvaAndSizes 값을 10(16)  A(10)

변경했기 때문에 OllyDbg의 초기검증 과정에서 에러가 발생합니다.

 

또한 위와 같은 에러로 인하여 OllyDbg는 EP로 가지 못하고 아래와 같이 ntdll.dll 영역에서 멈춥니다.

OllyDbg의 버그 또는 엄격한 PE체크 때문에 발생하는 현상입니다.

 

그렇다면 직접 EP를 설정해주기 위해 Stud_PE를 통해 기본 정보를 수집합니다.

EntryPoint의 RVA값은 1018이며, ImageBase01000000입니다.

따라서 EntryPoint의 VA값은 01001018입니다.

 

OllyDbg의 Code창에서 01001018로 이동한후

"New Origin here" 명령을 이용하여 강제로 EIP를 변경합니다.

이미 EP로 설정된 곳에서 우클릭하면 해당 메뉴가 나타나지 않으니 주의하세요.

 

 

 

2. BP걸고 달리기

 

모든 패커에는 디코딩(Decoding Loop) 루프가 존재한다고 합니다.

압축/해제 알고리즘 자체가 많은 조건 분기와 루프로 구성되어 있다보니 필연적입니다.

이러한 디코딩 루프를 디버깅할 때에는 조건 분기를 적절히 건너뛰어서 루프를 탈출해야 합니다.

레지스터를 잘 보면서 어떤 주소에 값을 쓰고 있는지 잘 살펴야 하지만.. 많은 경험이 필요합니다.

 

Upack은 두번째 섹션에 압축된 원본 데이터가 존재하고,

이 데이터를 디코딩 루프를 돌면서 첫번째 섹션에 압축해제 합니다.

그럼 EP코드부터 디버깅을 시작합니다!

 

처음 두명령은 010011B0 주소에서 4byte를 읽어서 EAX에 저장하는 명령어입니다.

이는 원본 notepad의 OEP(Original Entry Point) 입니다만, 아직은 모른다는 가정하에 진행합니다.

LODS 명령어는 ESI가 가르키는 주소에서 4byte를 읽어서 EAX 레지스터에 저장하는 기능이며, ESI 값을 증가시킵니다.

 

EAX에 저장된 명령어가 OEP인줄 알고 있다면, BP를 걸고 달리면 된다고 합니다.

해당 주소로 가보면 NULL로 채워져 있는 공간 뿐이어서 당황했습니다.

 

하지만 BP를 걸고 달리면 없던 코드가 생겨나면서 OEP로 추정되는 코드가 나타납니다.

패킹되지 않은 원본파일의 OEP와 비교해보니, 오른쪽의 설명을 제외한 코드가 모두 일치했습니다.

이로서 "BP를 걸고 달린다"의 의미를 어느정도 알 것 같네요.

  UPack      정상

 

 

 

 

3.압축해제, 디코딩을 위한 초기 세팅(?)

이어서 트레이싱을 계속 진행합니다.

OEP와 LoadLibrary 함수를 스택에 PUSH한 이후에 JMP 명령어로 010010A0 주소로 이동합니다.

GetProcAddress 함수와 01013FFF 주소에 있는 값을 동일하게 PUSH 합니다.

해당값들은 추후에 IAT를 새롭게 구성할때 POP 하여 사용합니다.

 

이후에는 아래 명령어를 실행하게 되는데, 결론부터 말하면 3st 섹션 → 2st 섹션으로 복사하는 코드입니다.

REP 명령어로 27(ECX) 횟수 만큼 반복문을 수행하기 위해 EDI, ESI레지스터에 주소값을 저장합니다.

EDI 레지스터에는 2st 섹션의 0101FE28 주소값이 저장되며, 어떤 영역인지 계산해보면

15000(VA) + AE28(RawSize) = 1FE28 이므로, 2st 섹션 끝에 채워진 NULL(0) 영역임을 알 수 있습니다.

REP MOVS DWORD PTR ES : [EDI], DWORD PTR DS : [ESI]

 

ESI 레지스터에는 3st 섹션의 010270F0 주소값이 저장되며, 구조상 1st 섹션과 동일하므로

정상적이라면 F0 주소에는 Delay_Import_Descriptor 값이 존재해야 하지만,

위에서도 살펴보았듯이 UPack이 OPTIONAL_Header에서 NumberOfRvaAndSizes 값을 10(16)  A(10)

변경하여 본인의 디코딩 코드를 삽입하기 위해 만들었던 공간 영역입니다.

 

다만, 해당 영역의 모두를 복사하고 있지 않으며, JMP 구문 이후에 영역을 복사합니다.

아마 디코딩 코드들을 한군데에 몰아넣고, 필요한 부분만 꺼내 쓰는 모양입니다.

 

아래 사진을 보시면, 위에서 공백이었던 0101FE28 주소에 4byte씩 데이터가 복사되고 있음을 알 수 있습니다.

PE헤더 분석에서 2st 섹션에는 원본파일이 압축되어 있다고 분석한바 있습니다.

왜 3st 섹션에 있는 명령어 코드를 2st 섹션 빈공간으로 복사하고 있는지는 모르겠지만, 일단은 넘어가기로 했습니다.

 

반복문이 종료된 이후의 코드를 계속 살펴봅니다.

PUSH 명령어로 DS : [ESI+4] = 190 주소가 가리키는 값 132를 Stack에 저장합니다.

3st 섹션은 1st 섹션과 동일하므로, 1st 섹션에서 190이 어떤 값인지 살펴보면 Relocations Number 입니다.

이후에 무슨 이유인지는 모르겠지만 EAX 값을 FFFFFFFF로 설정합니다.

이것이 문자열의 길이를 알아낼때 반복문의 최대치를 설정한 것인지, -1로 설정한 것인지 아직 파악이 되지 않습니다.

 

JMP 명령을 하기전에 ES : [EDI] = 0101FEC4 주소값에 EAX레지스터 값을 저장합니다.

해당 주소값은 3st 섹션에 있는 데이터를 2st 섹션에 복사한곳의 바로 뒷부분, NULL(0) 공간입니다.

 

JMP 010010D8 명령은, 방금 위에서는 뒷부분만 복사했던 디코딩 코드의 처음으로 이동합니다.

INC EAX 명령은 EAX값을 0으로 되돌리는 목적으로 보이며,

STOS 명령으로 EAX값을 저장하는데, EAX값이 0이므로 헤딩 주소의 NULL(0) 값이 유지됩니다.

 

이후에 REP명령어로 Write할 값 EAX = 1 을 저장하고, 반복횟수 ECX = 4 를 저장한뒤

아래 그림과 같이 16byte에 EAX값을 Write 합니다. (리틀엔디안 주의)

 

다음 명령어 역시 동일한 방법으로 EAX = 400, ECX = 1C00(7,168) 를 레지스터에 저장한뒤 

아래 그림과 같이 0101FEDC 주소부터 01026ECB 까지 1C00(7,168) 횟수만큼 EAX값을 Write 합니다.

이후에 "MZKERNEL32.DLL" 문자열과 ECX(=0) 레지스터를 스택에 PUSH합니다.

 

 

 

4. decode()

 

JMP 0101FD13 명령어를 통해 해당 주소로 넘어옵니다.

세번째 라인을 보면, CALL DWORD PTR DS : [ESI] 명령이 존재합니다.

이때 ESI 레지스터의 값은 0101FCCB 이며, 이게 바로 decode() 함수의 주소입니다.

 

책에서 decode() 함수라고 적혀 있어서 미리 알게되었지만,

어떤 행동을 하는지 한눈에 파악이 안됩니다만, 계속 트레이싱을 하다보면 굉장히 자주 호출됨을 알 수 있습니다.

 

잘 파악이 안될때에는 일단 실행 결과를 보면서 파악하곤 합니다.

일단 해당 함수를 CALL 하기 전에 특정 인자를 PUSH 한것은 아니지만,

레지스터로 인자를 전달하는 함수도 있으므로, 바로 전에 EDX에 할당한 주소를 체크합니다. (아래 그림 파란색)

그리고 F8 트레이싱을 하게되면, 0101FEC4 주소의 4byteFFFFFFFF 7FFFFC00 으로 변경됩니다.

정확히 어떤 메커니즘인지는 모르겠지만, 일단 2st 섹션의 특정 값을 변동시킵니다.

 

해당 작업이 다 이루어지고 나면 아래와 같이 대부분의 데이터가 채워집니다.

아마 어떤 방식으로든 채운 데이터를 활용하여 1st 섹션에 원본파일을 기록할 것으로 생각됩니다.

 

 

 

5. 디코딩 루프의 끝 + IAT 세팅

 

UPack의 디코딩 어셈블리 코드를 세세하게 분석하기엔, IDA Hex-ray에서 제공해주는 코드조차 매우 난잡합니다.

하지만 패킹의 경우, 특히 UPack은 메모리상에서 2st 섹션의 압축 원본이 1st 섹션이 풀리므로

디버깅의 초점을 섹션간의(3st 2st 또는 2st 1st) Write 행위로 바꾸었습니다.

그렇게 진행하다보면 책의 저자분이 책에 기재해놓으신 코드들 위주로 눈에 들어오게 됩니다.

 

먼저 1st 섹션에 데이터가 Write 되는 순간을 포착하기 위해 Hardware Breakpoint를 설정합니다.

이때 단일 Data size 에만 Breakpoint를 설정하면 그냥 넘어가는 케이스가 발생하여

동일주소에 3가지 Data size를 모두 설정하였습니다.

 

Breakpoint를 설정하고 F9를 눌러 첫번째 트레이싱을 진행하면 STOS BYTE OTR ES: [EDI] 코드에서 처음으로 멈춥니다.

EDI 주소에 EAX 레지스터의 값을 BYTE 단위만큼 저장하는 명령어입니다.

실제로 해당 주소에 가보면, Breakpoint를 걸었던곳의 1byte가 NULL(0)로 채워진것을 볼 수 있습니다.

반복적으로 트레이싱을 하면, UPack이 1st 섹션의 데이터를 지울때 항상 해당 명령어로 1byte를 먼저 지우는 패턴을 보입니다.

 

두번째로 멈추는곳은 몇줄 위에 있는 0101FE57 주소입니다.

REP 명령어로 ECX 레지스터에 할당된 횟수만큼 반복하며 EDI 레지스터가 가리키는 주소에 값을 씁니다.

 

F8 트레이싱으로 REP 명령어를 실행시키면

아래와 같이 1st 섹션이 NULL(0) 값으로 채워짐을 볼 수 있습니다.

UPack은 2st 섹션의 데이터를 1st 섹션에 압축해제하기 이전에 이와 같이 NULL(0) 값으로 덮어씁니다.

 

트레이싱을 계속 진행하면 NULL(0) 값으로 써진 1st 섹션에 새로운 데이터가 써집니다.

원본파일(notepad.exe)의 1st 섹션인 SECTION.text 값과 바이너리를 비교해보면 초반부는 동일합니다.

 

OllyDump와 같은 플러그인을 이용해서 원본파일(notepad.exe)의 메모리값과 비교해보면 완벽하게 일치하지 않습니다.

압축해제 되는 과정에서 약간의 변화 또는 손실이 일어나는것으로 보이지만, 실행되는데 문제는 없습니다.

첫번째로 원본과 틀린 부분은 1350 주소부터 시작하는 IMAGE_DEBUG_DIRECTORY 입니다. 

  원본 IDD

  원본 IDD

   UPack Dump

 

IMAGE_DEBUG_DIRECTORY의 경우에는 UPack으로 실행압축을 하는 과정에서

"Removing debug data [001C]" 라는 출력 문구를 통해, 실행압축 과정에서 삭제되었음을 유추할 수 있습니다.

001C의 경우 원본파일 IMAGE_DEBUG_DIRECTORY 사이즈 값과 동일한데, 사이즈를 출력해준것으로 판단됩니다.

 

두번째로 원본과 틀린 부분은 7604 주소의 IDT, INT, Hints/Names&DLL_names 입니다.

해당 멤버들은 하나같이 Import_Table과 관련된 항목들인데, NULL(0)로 채워져 있습니다.

하지만 해당 부분들이 아예 사라진것은 아닙니다. IAT세팅에서 다시 들여다 보겠습니다.

  원본 IDT

  원본 IDT

  UPack Dump

 

이부분도 역시 실행압축 과정에서 출력된 문구에서 리빌딩, 리컴파일로 언급되어 있는 사항들입니다.

대괄호로 출력된 16진수는 원본파일 IDT, RDT 항목들 사이즈와 일치합니다.

 

REP 명령어는 0101FE5E 주소에 있는 CMP 명령어가 만족될때까지 루프를 돌며 여러번 수행되며

[ ESI + 34 ] 주소가 가리키는 값은 0102718C + 34 = 010271C0 으로서, 4byte 01014B5A 값입니다.

해당값은 1st 섹션이 끝나는 주소인 15000(VA)과 매우 밀접합니다.

 

 

 

6. IAT 세팅 

 

Upack을 포함한 일반적인 패커는 디코딩 루프가 끝나면 원본 파일에 맞게 IAT를 새롭게 구성합니다.

위에서 분석했다 싶이, UPack은 kernel32.dll 하나만을 임포트하고 있으며,

 

IAT를 따라가 보면 LoadLibraryA, GetProcAddress 두개의 API 만이 존재하고 있음을 확인했습니다.

두개의 함수를 호출하여 0101FEAC 주소에 있는 STOS 명령으로 1st 섹션에 IAT를 채웁니다.

 

 

이때 참고하는 영역이 원본 파일에는 없는 01014000 주소 영역입니다.

원래 원본파일은 OllyDbg, Dump 등을 살펴보면 010133F0 주소 근처가

PADDING으로 채워진 이미지의 마지막 영역입니다.

 

하지만 UPack은 IAT를 재구성 하기 위해 01014000 주소 영역에

원본파일(notepad.exe)의 IDT 리스트 순서대로 DLL이름과 API 이름들을 저장해두었습니다.

이것을 읽어들이면서 1st 섹션의 IAT를 복원합니다.

 

 

 

7. BONUS - IDA에서 살펴보기

 

OllyDbg를 이용하여 디버깅을 진행하다보니

복잡한 압축해제 알고리즘을 Hex-ray로 보면 좀 더 쉬워질까? 라는 생각이 들었습니다.

결과적으로 도움이 되지는 않았지만, 다른 케이스에서는 도움이 되길 바라는 마음으로 남깁니다.

 

IDA로 패킹된 notepad.exe 를 Open하면 정상적인 PE구조가 아니기 때문에

아래와 같은 오류창이 3~4개 정도 연달아 뜹니다. 당황하지 말고 모두 OK 눌러줍니다.

 

"Can't find translation..." 관련 오류창만 짚고 넘어가겠습니다. (다른 케이스에 종종 나타나므로..)

해당 오류가 발생하는 곳은, DATA_DIRECTORY 영역으로서, Relocation Directory RVA, Size 멤버가 위치합니다.

CFF에서 Invalid 값으로 표현해주고 있군요.

 

지금까지 UPack을 분석하면서의 짧은 경험으로도, 해당 값은 RVA로 사용되기엔 너무 큽니다.

차근차근 분석해보면...일단 UPack은 1st 섹션의 시작 위치를 10으로 잡기 때문에, B0 주소의 데이터는

메모리에 로딩되면 1st 섹션 RVA(1000) + ImageBase(01000000) = 010010B0에 로딩됩니다.

주소에 찾아가보면 REP MOVS DWORD PTR ES : .... 명령어 코드가 존재하며

 

위에서 분석했던 "디코딩, 압축해제를 위한 초기 세팅" 부분의 코드임을 알 수 있습니다.

UPack이 NT_Header 영역 또한 일부 무시하고 디코딩 코드를 삽입했기 때문에 이러한 현상이 발생하는 것이고

이와 비슷한 오류창이 뜬다면, 해당 영역이 소스코드로 사용되는 패킹일 수 있음을 염두해둡니다.

 

말이 나온김에, 이와 같은 사실을 이용해서 PE헤더 영역에서 코드로 사용된 부분을 다시 살펴보면

아래 그림의 영역까지 포함하여 총 2개(1018 ~ 1023, 10A0 ~ 10BB) 영역입니다.

 

해당 영역을 노란색으로 그려보면 헤더 분석시에 그렸던 그림이 아래와 같은 그림으로 변경됩니다.

 

결과적으로 OPTIONAL_HeaderDATA_DIRECTORY에서 아래에 표시된 상당수의 변수가 무시되고, 

UPack의 디코딩 코드가 삽입 되었음을 알 수 있습니다. 아마 무시되도 실행에는 문제가 없는 멤버겠지요.

 

다시 본론으로 돌아와서...

로딩이 완료되면 Function window에 start 함수 달랑 1개만 존재함을 볼 수 있습니다.

이때 F5를 눌러 Hex-ray를 호출하면, 어느정도 복잡한 코드가 생성되지만 온전한 코드는 아닙니다.

사실 지금 생성된 코드만 해도 수십개의 변수가 존재하며, 코드도 엄청 복잡합니다. (스샷엔 2/5 정도만 담김)

 

온전한 코드가 생성되지 않는 정확한 이유를 단정지어 설명드리기 어렵지만,

개인적인 생각으로는 IDA가 UPack이 생성한 바이너리를 소스코드로 인식하지 않아서 발생하는 문제로 보입니다.

아래 스샷은 0101FCCB 주소로 Jump 했을때 이동된 장소이며, 위에서 보았던 decode() 함수입니다.

어셈블리 코드로 해석되지 않고, 16진수 데이터로 표현되어 있습니다.

 

디버깅을 진행하다보면 실행 흐름에 따라 16진수 데이터가 새로운 어셈블리 코드로 변경됩니다.

이러한 점을 이용하여 IDA에서 어느정도 트레이싱을 진행한 다음 Hex-ray를 호출하면 됩니다.

보통 OllyDbg에서 OEP를 걸고 디버깅 하듯이, IDA에서도 동일하게 진행합니다.

일단 Debugger의 종류를 Local Win32 debugger로 설정하고 Start합니다.

 

보통 OllyDbg에서 Ollydump 플러그인을 이용한 언패킹을 진행할때에는

책에서 학습한대로 OEP에 브레이크 포인트(F2)를 걸고 달리게 되지만,

Upack의 경우 원본파일이 우리가 관찰해야될 1st 섹션의 코드 부분에 압축해제 됩니다.

디코딩 코드를 관찰하기 위해 1st 섹션에 데이터를 쓰는 0101FE57 주소의 명령어가

실행되기 전의 주소에(근처) BP를 걸고 달립니다. 

 

어느정도 트레이싱이 진행되었으므로, Debugger - Take memory snapshot을 실행합니다.

저장할 세그먼트는 무엇을 해도 상관없지만, all segments를 선택할 경우 너무 많은 함수목록이

Function window에 나타나서 번잡스럽게 느껴졌습니다.

이후 정지버튼 또는 Debugger-Terminate process 기능을 이용해 디버깅을 종료시킵니다.

 

이후에 Options - General - Analysis 옵션에서 Reanalyze program을 실행하고서

Hex-ray를 실행하면 더 길고 복잡한 온전한(?) 코드를 얻을 수 있습니다.

처음으로, 차라리 OllyDbg를 이용한 트레이싱이 더 낫겠다...라는 생각을 하게 만들더군요. 

 

 

-----------------------------------------------------------------------------------------------------------------

0x03 마무리

-----------------------------------------------------------------------------------------------------------------

 

언젠간 해야지 해야지...하고 미뤄두기만 했었던 실행압축에 대해 공부했습니다.

모르는 부분을 겸사겸사 계속 추가했더니 내용이 또 길어졌네요.

그래도 이번 기회에 PE구조에 대해 좀더 익숙해졌고, 리버싱에 대한 막연함이 많이 사라진것 같습니다.


개인적으로..

그냥 공부했다면 너무 어려웠을 내용을 책으로 쉽게 내주신 이승원 저자분에게 감사드립니다.

잘못된 부분이나 오타는 댓글 남겨주세요.

 

 

posted By Message.

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

 

':: 리버싱' 카테고리의 다른 글

[정보 보안 개론] Chapter 06 악성코드  (0) 2016.11.07
Red_Seek :: 스택 프레임  (0) 2014.01.29
[Red_Seek] abex' crackme #1 (크랙미 #1)  (0) 2014.01.23
[Red_Seek] 스택  (0) 2014.01.22
[Red_Seek] IA-32 Register  (0) 2014.01.14
posted by Red_Message