| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 네트워크
- AWS 보안 아키텍처 분석
- reversing
- 버퍼 오버플로우
- 운영체제
- network
- 프로그래머스
- C
- programmers
- reversing.kr
- 리버싱
- Database
- 악성코드 분석
- operating system
- AWS 침해사고 사례 분석
- python
- Amazon S3
- 악성코드분석
- AWS
- 드림핵
- 파일 시스템
- dreamhack
- AWS 침해 사고 사례 분석
- crackme
- AWS 3 Tier Architecture
- terraform
- AWS 사고 사례 분석
- TryHackMe
- CrackMe 문제
- sql
- Today
- Total
lhywk 님의 블로그
[악성코드 분석] PE 파일 구조 본문
"로더가 실행 파일을 메모리에 올린다"고 했는데, 정확히는 로더가 파일을 처음부터 끝까지 무작정 읽는 게 아니라 정해진 구조를 따라가며 필요한 정보만 골라 읽는다. 그 구조가 바로 PE(Portable Executable) 포맷이다. 윈도우 실행 파일(.exe, .dll 등)은 전부 이 포맷을 따르고, 리버싱과 악성코드 분석의 거의 모든 작업이 이 구조를 들여다보는 데서 시작한다.
PE 구조, 큰 그림부터

핵심은 DOS 헤더에서 시작해 NT 헤더를 찾고, NT 헤더 안의 Optional 헤더 정보를 바탕으로 코드 실행 시작점과 필요한 함수들을 찾아 메모리에 올리는 과정이라는 것이다.
IMAGE_DOS_HEADER — 모든 PE 파일의 시작점
이 헤더는 파일의 처음 64바이트(오프셋 0x00 ~ 0x3F)를 차지한다. 실제 헥사 덤프로 보면 다음과 같다.

파일 시그니처 (Magic Number)
- 오프셋 0x00 (2바이트): 4D 5A -> ASCII로 "MZ"
- 이 파일이 MS-DOS 호환 실행 파일임을 나타내는 시그니처다. 'MZ'는 MS-DOS를 설계한 마크 즈비코프스키(Mark Zbikowski)의 이니셜이며, 모든 PE 파일은 이 시그니처로 시작해야 한다.
DOS 실행 파일 정보 (현대 윈도우에서는 대부분 무시됨)
| 오프셋 | 크기 | 값 | 의미 |
| 0x02 | 2바이트 | 90 00 | 파일의 마지막 페이지 바이트 수 |
| 0x04 | 2바이트 | 03 00 | 파일 내 페이지 수 |
| 0x08 | 2바이트 | 04 00 | 헤더 크기 (단락 단위) |
| 0x18 | 2바이트 | 40 00 | 재배치 테이블 위치 |
이 필드들은 진짜 MS-DOS 시절에 쓰던 정보로, 지금 윈도우 환경에서 PE 파일을 실행할 때는 사실상 무시된다. DOS 환경에서 이 파일을 실행하면 "This program cannot be run in DOS mode." 같은 메시지를 출력해주는 정도의 용도로만 남아 있다 (실제로 헥사 덤프 중간에 이 문자열이 그대로 박혀있는 걸 볼 수 있다).
PE 헤더 위치 정보 - e_lfanew (가장 중요)
- 오프셋 0x3C (4바이트): D8 00 00 00
- 필드명: e_lfanew
이 필드가 DOS 헤더에서 가장 중요한 부분이다. PE 헤더가 시작되는 위치(오프셋)를 알려주는 포인터다.
값은 리틀 엔디언(Little-endian) 방식으로 저장되므로 D8 00 00 00은 실제로 0x000000D8을 의미한다. 즉, PE 헤더는 파일의 0xD8 위치에서 시작된다는 뜻이다.
윈도우 로더는 "MZ" 시그니처를 확인한 후, 바로 이 e_lfanew가 가리키는 주소로 점프하여 진짜 PE 헤더 정보를 읽기 시작한다. 즉, e_lfanew는 DOS 헤더와 NT 헤더를 연결하는 다리 역할을 한다. 앞쪽 0x40바이트 가까이는 사실 거의 안 쓰이고, 로더가 진짜로 신경 쓰는 건 이 한 필드뿐이라고 봐도 무방하다.
IMAGE_NT_HEADERS - PE 헤더의 본체
e_lfanew가 가리키는 곳에 위치하며, PE 파일의 핵심 정보를 담고 있는 본체다. "PE\0\0"이라는 시그니처로 시작한다.
PEview 같은 도구로 실제 파일(mine.exe)을 열어보면 다음을 확인할 수 있다.

- Signature 오프셋: 000000D8
- 이는 mine.exe 파일의 시작으로부터 0xD8 바이트 떨어진 위치에 NT Headers가 시작된다는 의미다.
- 이 주소값은 바로 직전에 봤던 DOS 헤더의 e_lfanew 멤버에 저장되어 있던 그 값이다. 즉, 윈도우 로더는 DOS 헤더를 읽고 e_lfanew가 가리키는 0xD8 주소로 바로 점프하여 이 Signature를 확인한다.
NT Headers는 다시 두 개의 하위 구조체로 나뉜다.
(1) IMAGE_FILE_HEADER - 파일의 기본 정보

| 필드 | 의미 |
| NumberOfSections | 파일이 몇 개의 섹션(.text, .data 등)으로 구성되어 있는지 |
| SizeOfOptionalHeader | 바로 다음에 나올 Optional Header의 크기 |
| Characteristics | 파일의 속성(실행 가능한 EXE인지, 라이브러리 DLL인지 등)을 정의하는 플래그 |
(2) IMAGE_OPTIONAL_HEADER - 이름은 "옵션"이지만 사실 핵심 정보

이름에 "Optional"이 붙어 있지만, 실제로는 실행에 필수적인 가장 중요한 정보를 담고 있는 영역이다. PEview에서 보면 Magic, AddressOfEntryPoint, ImageBase, SizeOfImage, DataDirectory[16] 등의 필드가 줄지어 있는데
여기서는 일단 "DOS 헤더 -> e_lfanew -> NT 헤더(File Header + Optional Header) -> 섹션 헤더들"이라는 전체 뼈대만 잡아두자.
NT Headers가 File Header와 Optional Header로 나뉜다고 했다. 그중에서도 Optional Header - 이름은 "옵션"이지만 사실상 PE 파일의 "실행 명세서"라고 불러야 할 영역의 핵심 필드들을 하나씩 짚어본다.
Optional Header를 들여다보면
PEview로 mine.exe를 열어 Optional Header를 보면 다음과 같은 필드들이 줄지어 있다 (32비트 PE32 기준).
| Member | Offset | Value | Meaning |
| Magic | 0x000000F0 | 010B | PE32 |
| AddressOfEntryPoint | 0x00000100 | 00003E21 | .text |
| BaseOfCode | 0x00000104 | 00001000 | |
| ImageBase | 0x0000010C | 01000000 | |
| SizeOfImage | 0x00000128 | 00020000 | |
| SizeOfHeaders | 0x0000012C | 00000400 | |
| NumberOfRvaAndSizes | 0x0000014C | 00000010 |
이 중에서 분석할 때 실제로 가장 자주 보게 되는 필드들을 순서대로 정리한다.
Magic - 32비트인지 64비트인지
- 값: 010B
- 의미: 이 파일이 어떤 종류의 실행 파일인지 나타내는 표식이다.
- 010B는 PE32, 즉 32비트 실행 파일임을 의미한다.
- 만약 64비트 파일이라면 이 값은 020B(PE32+)가 된다.
AddressOfEntryPoint (AEP) - 가장 먼저 봐야 할 값
- 값: 00003E21
- 의미: 프로그램 실행이 시작되는 코드의 첫 주소(RVA, 상대 가상 주소)다. 운영체제(OS 로더)가 파일을 메모리에 올린 후, CPU가 가장 먼저 실행해야 할 명령어가 바로 이 주소에 있다.
AEP는 악성코드 분석에서 가장 먼저 살펴보는 핵심 필드다. 정상 프로그램이라면 AEP가 보통 .text 섹션 안쪽에 있다. 그런데 이 값이 .text가 아닌 이상한 섹션(예: UPX1)을 가리키고 있다면, 그 즉시 "이 파일은 패킹되어 있다"는 강력한 신호가 된다.
ImageBase - 메모리에 올라갈 선호 주소
- 값: 01000000 (Visual Studio 컴파일 기준. DevC++는 04000000을 쓰는 등 컴파일러마다 기본값이 다르다)
- 의미: 이 프로그램이 메모리에 로드될 때 선호하는 시작 주소다.
OS는 이 파일을 가상 메모리의 0x01000000 번지에 올리려고 시도한다. 만약 해당 주소를 다른 프로그램이 이미 사용 중이라면, OS는 비어 있는 다른 주소에 파일을 올리고 내부 주소 정보를 수정한다 - 이걸 재배치(Relocation, Re-basing)라고 부른다.
AEP와 ImageBase를 더하면 실제 메모리 주소가 나온다
실제 실행 시작 주소 = ImageBase + AddressOfEntryPoint
= 0x01000000 + 0x00003E21
= 0x01003E21
이 계산은 디버거에서 실제 EIP 값을 확인할 때 그대로 적용된다. PE 헤더에 적힌 AEP는 RVA(상대 주소)이고, 메모리에 올라간 뒤의 절대 주소를 구하려면 항상 ImageBase를 더해야 한다는 점을 기억해두자.
ImageBase와 ASLR
- 과거: EXE 파일은 프로세스가 생성될 때 가장 먼저 메모리에 올라가므로 다른 파일과 충돌할 일이 거의 없었다. 그래서 항상 ImageBase에 그대로 로드됐고, 재배치가 거의 필요 없었다.
- 현재 (ASLR 도입): Windows Vista 이후부터 보안을 위해 ASLR(Address Space Layout Randomization)이 도입되었다. 해킹 공격(버퍼 오버플로우 등)을 막기 위해 프로그램이 실행될 때마다 메모리 로딩 주소를 무작위로 변경하는 기술이다. 이제는 EXE 파일도 실행할 때마다 주소가 바뀌므로, PE 재배치 과정이 필수가 됐다.
SizeOfImage - 메모리에 올라간 전체 크기
- 값: 00020000
- 의미: 프로그램이 가상 메모리에 로드되었을 때 차지하는 전체 크기다.
NumberOfRvaAndSizes - Data Directory 항목 개수
- 값: 00000010 (16진수, 10진수로는 16)
- 의미: 바로 다음에 나오는 DataDirectory 배열에 총 16개의 항목이 있다는 뜻이다.
PE 파일 형식 표준에서는 이 Data Directory의 크기를 16개로 정의하고 있어서, 이 값은 거의 모든 윈도우 실행 파일에서 0x10(16)으로 고정되어 있다.
DataDirectory[16] - PE 파일의 "목차"

이게 Optional Header에서 가장 중요한 항목이다.
정의: 실행 파일 내의 중요한 데이터 영역들(Import 테이블, Export 테이블, 리소스 데이터 등)의 위치와 크기를 담고 있는 일종의 "목차" 또는 "바로가기 목록"이다.
운영체제는 NumberOfRvaAndSizes 필드를 읽고 "이 파일은 16개의 목차 항목을 가지고 있구나"라고 인지한 후, 그 개수만큼 Data Directory 배열을 읽어들여 필요한 정보를 찾아간다.
PEview에서 실제로 확인할 수 있는 16개 항목 중 분석에서 자주 쓰이는 것들만 추리면:
| 항목 | RVA (예시) | Size(예시) | 설명 |
| Export Directory | 00000000 | 00000000 | 이 파일이 다른 프로그램에게 제공하는 함수들의 목록 (주로 DLL에서 중요) |
| Import Directory | 0000415C | 00000084 | 이 파일이 다른 DLL로부터 가져와야 하는 함수들의 목록 |
| Resource Directory | 00006000 | 00018FAC | 아이콘, 이미지, 메뉴, 문자열 등 리소스 데이터의 위치 |
| Debug Directory | 000011D0 | 0000001C | 디버깅 정보(PDB 경로 등). 프로그램의 출처나 개발 환경 유추에 사용 가능 |
| Import Address Table (IAT) | 00001000 | 000001B8 | Import Directory에 명시된 함수들의 실제 메모리 주소가 기록되는 공간 |
Optional Header의 DataDirectory가 "어디에 무엇이 있는지" 알려주는 목차였다면, Section Header는 그 데이터들이 파일과 메모리에 실제로 어떻게 배치되는지 알려주는 "설계도"다. 그리고 그 설계도를 따라가다 보면 결국 만나게 되는 Import/Export 디렉터리까지 함께 정리
IMAGE_SECTION_HEADER - 코드와 데이터의 설계도
IMAGE_NT_HEADERS 바로 뒤에 나오며, NumberOfSections 개수만큼 존재한다. 각 섹션(코드, 데이터, 리소스 등)이 파일의 어디에 저장되어 있고, 메모리의 어디에 어떻게 로드되어야 하는지에 대한 정보를 담고 있다.

섹션 헤더의 주요 필드
| 필드 | 설명 |
| Name | 섹션의 이름. 관례적으로 용도에 따라 정해짐 (.text, .data 등) |
| VirtualAddress | 섹션이 메모리(RAM)에 로드될 때 시작될 상대 주소 (RVA) |
| VirtualSize | 메모리에서 이 섹션이 차지하는 실제 크기 |
| PointerToRawData | 파일(디스크) 상에서 이 섹션 데이터가 시작되는 위치(오프셋) |
| SizeOfRawData | 파일(디스크) 상에서 이 섹션이 차지하는 크기 |
| Characteristics | 이 섹션의 속성(권한) 플래그. 읽기/쓰기/실행 가능 여부를 정의 |
대표적인 섹션들
1) .text 섹션 (코드 영역)
- 프로그램의 실행 가능한 기계어 코드가 저장되는 곳. CPU가 직접 읽고 실행하는 명령어들이 여기 있다.
- Characteristics 예시(60500020): 실행(Execute)과 읽기(Read) 권한. 실행 중 코드가 변조되는 것을 막기 위해 보통 쓰기(Write) 권한은 없다.
2) .data 섹션 (데이터 영역)
- 초기값을 가지는 전역 변수나 정적(static) 변수들이 저장되는 공간 (예: int global_variable = 100;)
- Characteristics 예시(C0300040): 읽기(Read)와 쓰기(Write) 권한. 실행 중 값이 바뀌어야 하기 때문이다.
3) .rdata 섹션 (읽기 전용 데이터 영역)
- 읽기만 가능한 데이터들 - 문자열 상수(예: "Hello, World!")나 const로 선언된 상수들
- Characteristics 예시(40300040): 읽기(Read) 권한만. 실수로라도 상수가 변경되는 걸 막기 위함이다.
그 외 자주 보이는 섹션들
| 섹션 | 용도 |
| .bss | 초기화되지 않은 데이터(예: 초기화되지 않은 전역 변수)가 저장되는 공간 |
| .idata | IAT(Import Address Table) 정보가 저장됨 - 라이브러리 함수 이름과 주소 |
| .CRT | CRT(Cross Reference Table) 정보 |
| .tls | TLS(Thread Local Storage) 정보. 각 스레드마다 고유한 전역 변수를 저장하는 공간 |
| .rsrc | 리소스(아이콘, 이미지, 메뉴, 문자열 등) |
.data와 .bss의 차이를 헷갈리기 쉬운데, 핵심은 초기화 여부다.
int g_val_1; // 초기화 안 됨 → .bss 섹션 char g_val_2 = "string"; // 초기화 됨 → .data 섹션
이 섹션 구조는 패커(Packer) 탐지의 핵심 단서가 된다. 정상 파일이라면 .text는 읽기/실행 권한만, .data는 읽기/쓰기 권한만 갖는 게 일반적이다. 그런데 어떤 섹션이 읽기+쓰기+실행 권한을 동시에 가지고 있다면(예: UPX의 UPX0, UPX1 섹션), 이는 거의 확실하게 "실행 중에 코드를 풀어서 그 자리에 쓰고 곧바로 실행하겠다"는 패킹의 흔적이다.
Import Directory - "외부 부품 주문서"

DataDirectory[1]이 가리키는 영역으로, 프로그램이 외부 라이브러리(DLL)로부터 어떤 함수들을 빌려와야 하는지에 대한 목록이다.
프로그램이 스스로 모든 기능을 만들 수는 없기 때문에, 윈도우가 기본으로 제공하는 강력한 기능들(파일 열기, 화면에 글씨 쓰기 등)을 빌려 써야 한다. Import Directory는 바로 "어떤 라이브러리(DLL)에서 어떤 함수들을 빌려올 것인지"에 대한 목록이다.
PEview 같은 도구로 assembly_test_1.exe의 Import Directory를 열어보면 두 패널로 구성되어 있다.
상단 패널 - 필요한 DLL 목록
| Module Name | nFunctions | 설명 |
| KERNEL32.dll | 21 | 윈도우의 가장 핵심적인 기능 (메모리 관리, 프로세스/스레드 관리). 거의 모든 프로그램이 필수로 사용 |
| msvcrt.dll | 28 | Microsoft Visual C++ Runtime. C언어 표준 함수들(파일 입출력, 문자열 처리, 메모리 할당) |
하단 패널 - 실제 함수 목록 (msvcrt.dll 선택 시)
| 함수 | 역할 |
| exit | 프로그램 종료 |
| fprintf | 파일/콘솔에 서식 출력 |
| malloc, free | 동적 메모리 할당/해제 |
| memcpy | 메모리 특정 부분 복사 |
| strlen, strcmp | 문자열 길이/비교 |
왜 중요한가 - 프로그램 기능 분석의 핵심
Import Directory는 이 프로그램이 무엇을 할 수 있는지 예측할 수 있는 가장 확실한 단서다.
- 기능 유추: 위 함수 목록만 봐도 "이 프로그램은 콘솔에 글자를 출력하고, 메모리를 동적으로 사용하며, 문자열을 다루는 기본적인 C언어 프로그램"이라고 강하게 추측할 수 있다.
- 악성코드 분석: 만약 이 목록에 다음과 같은 함수가 있다면, 겉보기엔 평범한 프로그램이라도 악성 행위를 할 가능성이 높다고 판단할 수 있다.
- CreateRemoteThread - 다른 프로세스에 코드 주입
- SetWindowsHookEx - 키보드/마우스 입력 가로채기
- URLDownloadToFile - 인터넷에서 파일 다운로드
실전 분석 팁: 처음 의심스러운 PE 파일을 받았을 때, 코드를 한 줄도 안 읽고도 Import Directory만 훑어보는 것만으로 "이 파일이 대략 무슨 일을 할 수 있는 놈인지" 1차 스크리닝이 가능하다. 정적 분석의 가장 빠른 첫걸음이다.
Import Address Table (IAT) - 함수의 실제 주소가 적히는 곳
Import Directory가 "무엇을 빌려올지"의 목록이라면, IAT는 "그 함수들이 실제로 메모리의 어디에 있는지" 적히는 공간이다.
- RVA 예시: 00001000, Size: 000001B8
- 프로그램이 외부 함수를 호출할 때마다 이 테이블을 참조하여 해당 함수의 위치로 점프한다.
프로그램이 실행될 때 운영체제가 필요한 DLL들을 로드하고, "어떤 함수의 실제 주소는 여기다"라는 정보를 IAT에 채워준다.
'Malware Analysis' 카테고리의 다른 글
| [악성코드 분석] Procmon 동적분석 (0) | 2026.07.08 |
|---|---|
| [악성코드 분석] 프로세스/코드 인젝션 (0) | 2026.06.24 |
| [악성코드 분석] 메모리 공격 기법 (0) | 2026.06.24 |
| [악성코드 분석] 악성코드의 방어 기법 (0) | 2026.06.24 |
| [악성코드 분석] 리버싱을 위한 기초 지식 (0) | 2026.06.24 |