일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- JIT
- Dynamic안티디버깅
- Visual Studio
- Python
- 안티디버깅
- x32dbg
- 서비스프로세스
- OllyDbg
- 리버싱핵심원리
- DebugBlocker
- 리버싱
- UTF-16
- 서비스디버깅
- Static안티디버깅
- IL코드
- PEImage
- Advanced안티디버깅
- ImageSwitching
- Self-Creation
- .net
- hxd
- dotPeek
- UTF-32
- CodeEngn
- PEImageSwitching
- 디버깅
- 분석 보고서
- CP949
- Exe2Aut
- RedLine Stealer
- Today
- Total
-榮-
디버깅 실습3 - PE Image Switching 개념 및 동작 원리 본문
1. PE Image
‘PE Image’(혹은 Process Image나 Image라고 함)란 간단히 말해 ‘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 Image 사이에는 형태적으로 아래의 그림과 같이 파이가 발생하는데, 이것은 일반적으로 PE 파일은 File Alignment와 Section Alignment가 다르고, 각 섹션에서의 Raw Data Size와 Virtual Size가 다르기 때문이다.
2. PE Image Switching
PE Image Switching은 다른 프로세스를 실행한 후 가상 메모리의 PE Image를 자신의 것과 바꿔버리는 기법이다. 정확히는 어떤 프로세스(A.exe)를 SUSPEND 모드로 실행한 후 전혀 다른 PE 파일(B.exe)의 PE Image를 매핑시켜 A.exe 프로세스 메모리 공간에서 실행하는 기법이다.
PE Image를 변경하면 프로세스 이름은 원본 그래도 A.exe이지만 실제 프로세스 메모리에 매핑된 PE Image는 B.exe이기 때문에 원본(A.exe)과는 전혀 다른 동작을 한다.
이 경우, A.exe를 겉모습만 남아있는 ‘껍데기’ 프로세스라고 말할 수 있고, B.exe를 실제로 실행되는 ‘알맹이’ 프로세스라고 말할 수 있다.
이러한 기법은 PE 프로텍터에서 안티 디버깅으로 쓰이기도 하고, 악성코드에서 정상 프로세스로 위장하고자 할 때 사용된다.
3. 디버깅 실습
- 실행 화면
간단한 예제 파일들(fake.exe, real.exe, DebugMe3.exe)로 확인해 보자.
fake.exe 파일의 실행 화면을 보면 CUI(Console User Interface) 기반의 프로그램인 것을 알 수 있다.
real.exe 파일은 실행 화면은 다이얼로그에 간단한 문자열을 출력하는 GUI(Graphic User Interface) 기반의 프로그램이다.
fake.exe와 real.exe 파일은 서로 다른 사용자 환경을 가지고 있다. PE Image Switching 기법을 이용하면 fake.exe를 껍데기 프로세스로 real.exe를 알맹이 프로세스로 실행시킬 수 있다.
Process hacker로 fake.exe 프로세스를 확인하면 실행되는 프로세스의 이름은 fake.exe이지만 실제로는 real.exe 프로세스가 실행되는 것을 확인할 수 있다.
4. 구체적인 동작 원리
OllyDbg로 실행 파라미터(fake.exe real.exe)를 입력하여 DebugMe3.exe를 연다.
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() |
위의 내용을 기반으로 코드 흐름도를 그려보면 아래의 그림과 같다.
- 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 구하기
40128C와 40128F 주소의 명령들은 real.exe 파일의 PE 헤더 정보를 읽어서 ImageBase를 구하는 코드이다.
EDI 레지스터 값은 MEM_FILE_REAL 주소로, SubFunc_1()에서 할당받은 메모리 시작 주소이며, real.exe 파일의 내용이 저장되어 있다. 즉, EDI는 real.exe의 PE 헤더를 가리킨다.. 따라서 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.exe의 PE Image가 매핑되려는 주소 400000에 이미 fake.exe의 PE Image가 매칭되어 있다. 그대로 real.exe를 매칭시키면 충돌이 발생하기 때문에 먼저 fake.exe의 PE Image를 언매핑 해야 한다.
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.exe의 PE Image를 위해 필요한 공간을 할당하고 real.exe를 매핑시키면 된다. 그리고 fake.exe 프로세스의 PE Image의 주소가 real.exe의 매핑 주소라는 것을 PEB의 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 Header와 PE Section 위치에 real.exe의 PE 정보를 쓴다. 이후 EP를 변경하면 fake.exe 프로세스의 PE Image를 기존의 ‘fake.exe’에서 ‘real.exe’로 변경하는 작업이 완료된다.
● real.exe의 PE Image를 위한 메모리 할당
트레이싱 하다 보면 VirtualAllocEx() API 함수 호출 코드를 만난다. 이 함수의 파라미터에서 할당받고자 하는 메모리의 시작주소(Arg2 = 00400000 ; real.exe의 ImageBase)와 할당받을 메모리 크기(Arg3 = 0000A000 ; real.exe의 SizeOfImage)를 확인할 수 있다. 즉, 이 함수는 fake.exe 프로세스에 real.exe의 PE Image를 위한 메모리 공간을 할당받기 위한 것이다.
● PE Header Mapping & PE Section Mapping
WriteProcessMemory() API를 이용하여 real.exe의 PE 헤더를 VirtualAllocEx() API로 할당받은 메모리 영역에 쓰고 있다.
이후 섹션의 개수만큼 루프를 돌면서 fake.exe 섹션 위치에 real.exe 섹션을 쓴다 이 작업을 마치면 real.exe 파일은 fake.exe 프로세스의 400000(real.exe의 ImageBase) 주소에 완전히 매핑된다.
● EP 변경
현재 fake.exe 프로세스는 SUSPEND 모드로 생성된 상태이다. SUSPEND 모드로 생성된 프로세스가 Resume되면, 가장 먼저 ntdll!RtlUserThreadStart() API(CONTEXT.Eip)를 실행하고, 결국 리턴 값인 EP(Entry Point) 주소(CONTEXT.Eax)로 이동된다. 따라서 fake.exe 프로세스의 PE Image를 ‘real.exe’로 변경했으므로 CONTEXT.Eax를 real.exe의 EP 주소(401060)으로 변경해야 정상 실행이 된다.
- ResumeThread()
마지막으로 ResumeThread()를 호출하여 fake.exe 프로세스를 Resume 시킨다.
; Resume Thread | ||
004010F0 004010F1 |
PUSH ECX CALL DWORD PTR DS:[409038] |
; -hThread = 00000034 (widnow) ; ResumeThread() |
real.exe의 PE Image를 가진 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 |