-榮-

디버깅 실습3 - PE Image Switching 개념 및 동작 원리 본문

리버싱 핵심원리

디버깅 실습3 - PE Image Switching 개념 및 동작 원리

xii.xxv 2024. 1. 31. 20:15

1. PE Image

‘PE Image’(혹은 Process ImageImage라고 함)란 간단히 말해 ‘PE 파일이 프로세스 메모리에 매핑(mapping)된 모습이라고 할 수 있다.

PE 파일(notepad.exe)을 프로세스로 실행시키면 그 프로세스의 가상 메모리는 아래 그림과 같은 형태가 된다. , OS는 프로세스를 위한 가상 메모리를 생성하고, USER 메모리 영역에 notepad.exe 파일을 매핑시킨다. 그리고 notepad.exe에 사용되는 Import DLL 파일들(kernel32.dll, user32.dll, gui32.dll )을 차례대로 매핑시킨다. 이때, 프로세스의 USER 메모리에 매핑된 Notepad.exe 영역을 (notepad.exe 파일에 대한) PE Image라고 표현한다.

PE 파일과 프로세스 관계

실제 PE 파일과 PE Image 사이에는 형태적으로 아래의 그림과 같이 파이가 발생하는데, 이것은 일반적으로 PE 파일은 File Alignment와 Section Alignment가 다르고, 각 섹션에서의 Raw Data Size와 Virtual Size가 다르기 때문이다.

PE 파일 프로세스 메모리에 매핑된 PE Image

 


 

2. PE Image Switching

PE Image Switching다른 프로세스를 실행한 후 가상 메모리의 PE Image를 자신의 것과 바꿔버리는 기법이다. 정확히는 어떤 프로세스(A.exe)SUSPEND 모드로 실행한 후 전혀 다른 PE 파일(B.exe)PE Image를 매핑시켜 A.exe 프로세스 메모리 공간에서 실행하는 기법이다.

PE Image Switching 개념도

PE Image를 변경하면 프로세스 이름은 원본 그래도 A.exe이지만 실제 프로세스 메모리에 매핑된 PE ImageB.exe이기 때문에 원본(A.exe)과는 전혀 다른 동작을 한다.

이 경우, A.exe를 겉모습만 남아있는 껍데기프로세스라고 말할 수 있고, B.exe를 실제로 실행되는 알맹이프로세스라고 말할 수 있다.

이러한 기법은 PE 프로텍터에서 안티 디버깅으로 쓰이기도 하고, 악성코드에서 정상 프로세스로 위장하고자 할 때 사용된다. 

 


 

3. 디버깅 실습

- 실행 화면

간단한 예제 파일들(fake.exe, real.exe, DebugMe3.exe)로 확인해 보자.

fake.exe 파일의 실행 화면을 보면 CUI(Console User Interface) 기반의 프로그램인 것을 알 수 있다.

fake.exe 실행 화면

real.exe 파일은 실행 화면은 다이얼로그에 간단한 문자열을 출력하는 GUI(Graphic User Interface) 기반의 프로그램이다.

real.exe 실행 화면

fake.exereal.exe 파일은 서로 다른 사용자 환경을 가지고 있다. PE Image Switching 기법을 이용하면 fake.exe를 껍데기 프로세스로 real.exe를 알맹이 프로세스로 실행시킬 수 있다.

DebugMe3.exe 실행 화면

Process hackerfake.exe 프로세스를 확인하면 실행되는 프로세스의 이름은 fake.exe이지만 실제로는 real.exe 프로세스가 실행되는 것을 확인할 수 있다.

PE Image Switching 기법으로 실행된 fake.exe

 


 

4. 구체적인 동작 원리

OllyDbg로 실행 파라미터(fake.exe real.exe)를 입력하여 DebugMe3.exe를 연다.

OllyDbg의 File \ Open 메뉴

main() 함수 위치로 이동한다.

main() 함수

main() 함수에서 중요한 내용만 추리면 아래의 내용과 같다.

main()
00401000
00401001
00401003
00401006

...

00401063

...

00401079
0040107A
0040107E
0040107F
00401081
00401083
00401085
00401087
00401089
0040108B
0040108C
0040108E

...

004010B2

...

004010D1

...

004010F0
004010F1

...

00401116
00401118
00401119

...

00401149
0040114B
0040114C
PUSH EBP
MOV EBP, ESP
AND ESP, FFFFFFF8
SUB ESP, 5C



CALL 00401150



PUSH EAX
LEA ECX, DWORD PTR SS:[ESP+24]
PUSH ECX
PUSH 0
PUSH 0
PUSH 4
PUSH 0
PUSH 0
PUSH 0
PUSH EDX
PUSH 0
CALL DWORD PTR DS:[40900C]



CALL 004011D0



CALL 00401320



PUSH ECX
CALL DWORD PTR DS:[409038]



PUSH 1
PUSH EDX
CALL DWORD PTR DS:[409010]



MOV ESP, EBP
POP EBP
RETN







; SubFunc_1()



; -pProcessInfo = 0018FEE8

; -pStartupInfo = 0018FEF8
; -CurrentDir = NULL
; -pEnvironment = NULL
; -CreationFlags = CREATE_SUSPENED
; -InheritHandles = FALSE
; -pThreadSecurity = NULL
; -pProcessSecurity = NULL
; -CommandLine = “fake.exe”
; -ModuleFileName = NULL
;CreateProcessW()



; SubFunc_2()



; SubFunc_3()



; -hThread = 00000034(widnow)
; ResumeThread()



; -Timeout = INFINITE
; -hObject = 00000038(window)
; WaitForSinfleObject()






위의 내용을 기반으로 코드 흐름도를 그려보면 아래의 그림과 같다.

main() 함수의 코드 흐름

-  SubFunc_1()

SubFunc_1() 함수 내부의 주요 코드를 추리면 아래와 같다.

코드의 내용은 real.exe 파일을 통째로 메모리에 읽어 들이고 있으며, 메모리에 파일 내용을 저장한 뒤 main() 함수로 돌아간다. real.exe 파일이 저장된 메모리 주소를 MEM_FILE_REAL이라고 부르겠다.

SubFunc_1()
00401150
00401151
00401153
00401154
00401155
00401157
0040115C
0040115E
00401160
00401162
00401167
00401168
0040116F
PUSH EBP
MOV EBP, ESP
PUSH ECX
PUSH ESI
PUSH 0
PUSH 80
PUSH 3
PUSH 0
PUSH 1
PUSH 80000000
PUSH EAX
MOV DWORD PTR SS:[EBP-4], 0
CALL DWORD PTR DS:[409020]




; -hTemplateFile = NULL
; -Attributes = NORMAL
; -Mode = OPEN_EXISTING
; -pSecurity = NULL
; -ShareMode = FILE_SHARE_READ
; -Access = GENERIC_READ
; -FileName = “real.exe”

; CreateFileW()

...
00401185
00401187
00401188
0040118E
00401190
00401191
PUSH 0
PUSH ESI
CALL DWORD PTR DS:[409004]
MOV EBX, EAX
PUSH EBX
CALL 00401624
; -pFileSizeHigh = NULL
; -hFile
; GetFileSize()

; -bufsize = A000 (40960)
; new()

...


004011A6
004011A8
004011AB
004011AC
004011AD
004011AE
004011AF
004011B5
004011B6
PUSH 0
LEA ECX, DWORD PRT SS:[EBP-4]
PUSH ECX
PUSH EBX
PUSH EDI
PUSH ESI
CALL DWORD PTR DS:[40901C]
PUSH ESI
CALL DWORD PTR DS:[409030]
; -pOverlapped = NULL

; -pBytesRead = 0018FECC
; -ByteToRead = A000 (40960)
; -Buffer = 00392EF8
; -hFile = 00000030 (window)
; ReadFile()
; -hObject = 00000030 (window)
; CloseHandle()

...
004011C4 RETN  

 

-  CreateProcess(“fake.exe”, CREATE_SUSPENDED)

fake.exe 프로세스를 SUSPEND 모드로 생성하는 함수이다. SUSPEND 모드로 생성하는 이유는 프로세스 실행을 멈춘 상태에서 메모리를 조작하기 위함이다.

00401079
0040107A
0040107E
0040107F
00401081
00401083
00401085
00401087
00401089
0040108B
0040108C
0040108E
PUSH EAX
LEA ECX, DWORD PTR SS:[ESP+24]
PUSH ECX
PUSH 0
PUSH 0
PUSH 4
PUSH 0
PUSH 0
PUSH 0
PUSH EDX
PUSH 0
CALL DWORD PTR DS:[40900C]
; -pProcessInfo = 0013FEE8

; -pStartupInfo = 0013FEF8
; -CurrentDir = NULL
; -pEnvironment = NULL
; -CreationFlags = CREATE_SUSPENED
; -InheritHandles = FALSE
; -pThreadSecurity = NULL
; -pProcessSecurity = NULL
; -CommandLine = “fake.exe”
; -ModuleFileName = NULL
; CreateProcessW()

 

- SubFunc_2()

SubFunc_2() 함수 내부의 주요 코드를 추리면 아래와 같으며, 이 코드가 PE Image Switching 기법의 핵심이다.

SubFunc_2()
004011D0
004011D1
PUSH EBP
MOV EBP, ESP
 

...

0040120C
0040120D
0040120E
00401218
PUSH ECX
PUSH EDX
MOV DWORD PTR SS:[EBP-2D0], 10007
CALL DWORD PTR DS:[409000]
; -pContext
; -hThread

; GetThreadContext()

...
00401246
0040124C
0040124E
00401250
00401252
00401258
00401259
0040125C
0040125D
0040125E
MOV ECX, DWORD PTR SS:[EBP-22C]
MOV EDX, DWORD PTR DS:[ESI]
PUSH 0
PUSH 4
LEA EAX, DWORD PTR SS:[EBP=2D4]
PUSH EAX
ADD ECX, 8
PUSH ECX
PUSH EDX
CALL DWORD PTR DS:[409018]
; CONTEXT.Ebx = address of PEB

; -pBytesRead = NULL
; -pBytesToRead = 4

; -Buffer

; -pBaseAddress = PEB.ImageBase
; -hProcess
; ReadProcessMemory()

...
0040128C


0040128F

00401293
00401297

0040129D
MOV EAX, DWORD PTR DS:[EDI+3C]


MOV ECX, DWORD PTR DS:[EAX+EDI+34]

LEA EAX, DWORD PTR DS:[EAX+EDI+34]
CMP ECX, DWORD PTR SS:[EBP-2D4]

JNZ SHORT 004012EA
; EDI = MEM_FILE_REAL
= Address of IDH
; [EDI+3C] = IDH.e_lfanew
; EAX+EDI = Address of INH
; EAX+EDI+34 = INH.IOH.ImageBase

; ECX = ImageBase of real.exe
; [EBP-2D4] = ImageBase of fake.exe


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; case 1 : ImageBase of real.exe == ImageBase of fake.exe
0040129F
004012A4
004012A9
004012AF
004012B0
004012B6
004012BC
004012BE
004012BF
004012C0
PUSH 40B258
PUSH 40B270
CALL DWORD PTR DS:[409014]
PUSH EAX
CALL DWORD PTR DS:[409028]
MOV EDX, DWORD PTR SS:[EBP-2D4]
MOV DCX, DWORD PTR DS:[ESI]
PUSH EDX
PUSH ECX
CALL EAX
; -Name = “ZwUnmapViewOfSection”
; --pModule = “ntdll.dll”
; --GetModuleHandleW()
; -hModule
; GetProcAddress()


; -ProcHandle = Process Handle of fake.exe
; -BaseAddress = ImageBase of fake.exe
; ZwUnmapViewOfSection()

...

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; case 2 : ImageBase of real.exe != ImageBase of fake.exe
004012EA
004012F0
004012F2
004012F4
004012F5
004012F7
004012FA
004012FB
004012FC
MOV EDX, DWORD PTR SS:[EBP-22C]
PUSH 0
PUSH 4
PUSH EAX
MOV EAX, DWORD PTR DS:[ESI]
ADD EDX, 8
PUSH EDX
PUSH EAX
CALL DWORD PTR DS:[409034]
; Address of PEB (“fake.exe”)
; -pBytesWritten = NULL
; -BytesToWrite = 4
; -Buffer = ImageBase of “real.exe”


; -Address = PEB.ImageBase
; -hProcess
; WriteProcessMemory()

...
00401311
00401313
00401314
MOV ESP, EBP
POP EBP
RETN
 

 

● fake.exe 프로세스의 실제 매핑 주소 구하기

401218 주소에서 GetTjreadContext() API를 호출하여 fake.exe 프로세스의 메인 스레드 컨텍스트(CONTEXT)를 구한다.

0040120C
0040120D
0040120E
00401218
PUSH ECX
PUSH EDX
MOV DWORD PTR SS:[EBP-2D0], 10007
CALL DWORD PTR DS:[409000]
; -pContext
; -hThread

; GetThreadContext()

fake.exe 프로세스의 메인 스레드 컨텍스트를 구하는 이유는 PEB를 구하기 위해서이다. 프로세스의 실제 매핑 주소는 PEB.ImageBase 멤버에 저장되어 있어서 ReadProcessMemory() API를 호출하여 fake.exe 프로세스의 매핑 주소를 구한다.

00401246
0040124C
0040124E
00401250
00401252
00401258
00401259
0040125C
0040125D
0040125E
MOV ECX, DWORD PTR SS:[EBP-22C]
MOV EDX, DWORD PTR DS:[ESI]
PUSH 0
PUSH 4
LEA EAX, DWORD PTR SS:[EBP=2D4]
PUSH EAX
ADD ECX, 8
PUSH ECX
PUSH EDX
CALL DWORD PTR DS:[409018]
; CONTEXT.Ebx = address of PEB

; -pBytesRead = NULL
; -pBytesToRead = 4

; -Buffer

; -pBaseAddress = PEB.ImageBase
; -hProcess
; ReadProcessMemory()

 

● real.exe 파일의 ImageBase 구하기

40128C40128F 주소의 명령들은 real.exe 파일의 PE 헤더 정보를 읽어서 ImageBase를 구하는 코드이다.

EDI 레지스터 값은 MEM_FILE_REAL 주소로, SubFunc_1()에서 할당받은 메모리 시작 주소이며, real.exe 파일의 내용이 저장되어 있다. , EDIreal.exePE 헤더를 가리킨다.. 따라서 EDI+3C가 의미하는 것은 IMAGE_DOS_HEADER 구조체의 e_lfanew 멤버이다.

0040128C

MOV EAX, DWORD PTR DS:[EDI+3C]

; EDI = MEM_FILE_REAL = Address of IDH
; [EDI+3C] = IDH.e_lfanew

EAX+EDI = IDH.e_lfanew + Start of PE = IMAGE_NT_HEADERS 구조체가 시작 주소, EAX+EDI+34 = IMAGE_OPTIONAL_HEADER.ImageBase가 멤버를 의미한다.

0040128F

MOV ECX, DWORD PTR DS:[EAX+EDI+34]

; EAX+EDI = Address of INH
; EAX+EDI+34 = INH.IOH.ImageBase

 

● 비교 : ImageBase of fake.exe & ImageBase of real.exe

fake.exe 프로세스의 실제 매핑 주소와 real.exe 파일의 ImageBase 값을 비교한다.

ImageBase 값의 일치 여부에 따라 실행 분기가 달라진다.

00401297

0040129D
CMP ECX, DWORD PTR SS:[EBP-2D4]

JNZ SHORT 004012EA
; ECX = ImageBase of real.exe
; [EBP-2D4] = ImageBase of fake.exe

 

◎  ImageBase가 같은 경우

이 경우 real.exePE Image가 매핑되려는 주소 400000에 이미 fake.exePE Image가 매칭되어 있다. 그대로 real.exe를 매칭시키면 충돌이 발생하기 때문에 먼저 fake.exePE Image를 언매핑 해야 한다.

ImageBase가 같은 경우

004012BE
004012BF
004012C0
PUSH EDX
PUSH ECX
CALL EAX
; -ProcHandle = Process Handle of fake.exe
; -BaseAddress = ImageBase of fake.exe
; ZwUnmapViewOfSection()
ZwUnmapViewOfSection() API
NTSYSAPI NTSTATUS ZwUnmapViewOfSection(
[in] HANDLE ProcessHandle,
[in, optional] PVOID BaseAddress
);

출처 : MSDN

 

◎ ImageBase가 다른 경우

fake.exe 프로세스의 가상 메모리 공간에 real.exePE Image를 위해 필요한 공간을 할당하고 real.exe를 매핑시키면 된다. 그리고 fake.exe 프로세스의 PE Image의 주소가 real.exe의 매핑 주소라는 것을 PEBImageBase 멤버를 변경해서 알려줘야 된다.

ImageBase가 다를 경우

004012EA
004012F0
004012F2
004012F4
004012F5
004012F7
004012FA
004012FB
004012FC
MOV EDX, DWORD PTR SS:[EBP-22C]
PUSH 0
PUSH 4
PUSH EAX
MOV EAX, DWORD PTR DS:[ESI]
ADD EDX, 8
PUSH EDX
PUSH EAX
CALL DWORD PTR DS:[409034]
; Address of PEB (“fake.exe”)
; -pBytesWritten = NULL
; -BytesToWrite = 4
; -Buffer = ImageBase of “real.exe”


; -Address = PEB.ImageBase
; -hProcess
; WriteProcessMemory()

 

- SubFunc_3()

이 함수에서는 real.exe 파일을 fake.exe프로세스에 매핑시키는 코드를 가지고 있다.

매핑은 매핑할 위치에 가상 메모리를 할당하여, fake.exe 프로세스의 PE HeaderPE Section 위치에 real.exePE 정보를 쓴다. 이후 EP를 변경하면 fake.exe 프로세스의 PE Image를 기존의 ‘fake.exe’에서 ‘real.exe’로 변경하는 작업이 완료된다.

● real.exePE Image를 위한 메모리 할당

트레이싱 하다 보면 VirtualAllocEx() API 함수 호출 코드를 만난다. 이 함수의 파라미터에서 할당받고자 하는 메모리의 시작주소(Arg2 = 00400000 ; real.exeImageBase)와 할당받을 메모리 크기(Arg3 = 0000A000 ; real.exeSizeOfImage)를 확인할 수 있다. , 이 함수는 fake.exe 프로세스에 real.exePE Image를 위한 메모리 공간을 할당받기 위한 것이다.

VirtualAllocEx() 호출 : real.exe의 PE Image 메모리 할당

 

● PE Header Mapping & PE Section Mapping

WriteProcessMemory() API를 이용하여 real.exePE 헤더를 VirtualAllocEx() API로 할당받은 메모리 영역에 쓰고 있다.

PE Header 매핑

이후 섹션의 개수만큼 루프를 돌면서 fake.exe 섹션 위치에 real.exe 섹션을 쓴다 이 작업을 마치면 real.exe 파일은 fake.exe 프로세스의 400000(real.exeImageBase) 주소에 완전히 매핑된다.

PE Section 매핑

 

● EP 변경

현재 fake.exe 프로세스는 SUSPEND 모드로 생성된 상태이다. SUSPEND 모드로 생성된 프로세스가 Resume되면, 가장 먼저 ntdll!RtlUserThreadStart() API(CONTEXT.Eip)를 실행하고, 결국 리턴 값인 EP(Entry Point) 주소(CONTEXT.Eax)로 이동된다. 따라서 fake.exe 프로세스의 PE Image‘real.exe’로 변경했으므로 CONTEXT.Eaxreal.exeEP 주소(401060)으로 변경해야 정상 실행이 된다.

SetThreadContext()

 

- ResumeThread()

마지막으로 ResumeThread()를 호출하여 fake.exe 프로세스를 Resume 시킨다.

; Resume Thread
004010F0
004010F1
PUSH ECX
CALL DWORD PTR DS:[409038]
; -hThread = 00000034 (widnow)
; ResumeThread()

real.exePE Image를 가진 fake.exe 프로세스의 실행이 재개된다.

PE Image가 real.exe로 변경된 fake.exe 프로세스

 


 

5. 마무리

실습 예제 파일의 디버깅을 통해 PE Image Switching 기법의 동작 원리를 살펴보았다. 이 동작 원리에서는 DebugMe3.exe를 디버깅한 것이다. 디버깅 기법으로 따지면 일반적인 응용 프로그램 디버깅과 다를 게 없다. 리버서 입장에서는 PE Image Switching 기법의 동작 원리도 중요하지만, 그 기법이 적용된 프로세스를 디버깅하는 것에 더 관심이 있을 것이다.

 

'리버싱 핵심원리' 카테고리의 다른 글

디버깅 실습4 - Debug Blocker 동작원리  (2) 2024.02.08
디버깅 실습2 - Self-Creation (JIT 디버깅)  (0) 2024.01.16
Advanced 안티 디버깅  (0) 2024.01.10
Dynamic 안티 디버깅  (0) 2024.01.10
Static 안티 디버깅  (1) 2024.01.10