리버싱 핵심원리 2부 1장 정리 1
Note
본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리하였습니다.
책의 내용과 일부 상이할 수 있고, 이해를 돕기 위해 강좌 형식의 말투와 적절한(?) 예시를 추가하였습니다.
본 게시글에서 사용되는 소스 코드와 파일, 프로그램은 리버싱 핵심원리에서 제공하는 파일과 언급되는 것들을 기반으로 하며, 일부 상이할 수 있습니다.
PE 파일
PE 파일(Portable Executable File)은 Windows 운영체제에서 사용되는 실행 파일 형식을 말합니다. Windows 프로그램, 라이브러리(DLL), 드라이버 등이 PE 형식을 따르고 있습니다.
PE 파일이라 하면 32비트 형태를 의미하고 PE32라 부르기도 합니다. PE+ 또는 PE32+이라 하면 64비트 형태를 의미합니다. PE64는 아닙니다.
PE 파일에는 실행 가능한 형식인 EXE와 동적 링크 라이브러리(DLL), Windows 드라이버 파일(SYS) 등이 있습니다.
PE 파일 구조
PE HEADER |
DOS Header |
DOS Stub |
NT Header |
Section Header (.text) |
Section Header (.data) |
Section Header (.rsrc) |
PE BODY |
NULL Padding |
Section (.text) |
NULL Padding |
Section (.data) |
NULL Padding |
Section (.rsrc) |
NULL Padding |
PE 파일의 구조는 대략 위와 같습니다.
PE 헤더의 끝 부분과 각 섹션의 끝 부분에는 널 패딩(NULL Padding)이라 불리는 공간이 있습니다. 이 공간은 아래에서 설명하겠지만 정렬(Alignment) 때문에 그렇습니다.
VA와 RVA
VA와 RVA는 실행 파일의 메모리 주소를 표현할 때 사용하는 개념입니다.
VA(Virtual Address)는 메모리 내에서 사용되는 전체 주소를 의미합니다. VA는 ImageBase + RVA로, 절대 주소라는 특징을 갖고 있습니다.
RVA(Relative Virtual Address)는 기준 주소(ImageBase)로부터의 상대 주소를 나타냅니다. RVA는 VA - ImageBase입니다.
RVA를 사용하는 이유
PE 파일은 기본적으로 ImageBase에 로드될 수 있도록 노력을 합니다. 하지만 상황에 따라 불가한 경우가 생기기도 합니다. 만약, RVA가 아닌 VA로 했다면 절대주소라는 특징을 갖기 때문에 모든 주소의 값을 일일이 변경해야 한다는 단점이 있습니다. 반면 RVA는 상대주소이기 때문에 어느 위치에 불러와지든 기반이 되는 주소에 RVA를 더하면 되기 때문에 RVA로 주소를 저장하고 관리합니다.
PE 헤더
DOS Header
DOS Header의 구조는 위와 같습니다. DOS Header에서 가장 중요하게 봐야할 것은 e_magic
과 e_lfanew
입니다.
e_magic
은 DOS의 시그니쳐로 무조건 MZ
(1)라는 값을 가집니다. e_lfanew
는 NT Header의 오프셋을 나타냅니다. 이는 파일에 따라 가변적인 값을 가지며, 이 오프셋이 가르키는 곳에 NT Header가 있어야 합니다. NT Header의 구조체 이름은 IMAGE_NT_HEADERS
입니다.
- Microsoft에서 DOS 실행 파일을 설계한 마크 주비코브스키(Mark Zbikowski)라는 사람의 이니셜
DOS Stub
Stub라는 단어를 보면 알 수 있듯이 DOS Stub는 MS-DOS 환경과의 호환성을 위해 남아있습니다.
기본적으로 PE 파일은 Windows 환경에서 작동하기 때문에 MS-DOS 환경에서 실행되지 않습니다. 만약을 위해(?) MS-DOS 환경에서 실행할 경우 DOS Stub에 작성된 코드가 실행됩니다. 대부분 PE 파일이 MS-DOS 환경에서 실행할 수 없다는 문구(This program cannot be run in DOS mode.
)를 출력합니다.
현재와선 딱히 필요없는 영역이지만 레거시 호환을 위해 남아있습니다.
NT Header
winnt.h | |
---|---|
Signature
는 0x50450000
("PE\0\0"
)이라는 값을 가지며, 헥스 에디터로 확인 시 PE
라는 문자열 값이 나타납니다.
FileHeader
는 PE 파일에 대한 기본적인 정보를 가지고 있습니다.
OptionalHeader
는 PE 파일 실행을 위한 부가적인 정보를 가지고 있습니다.
FileHeader
winnt.h | |
---|---|
Machine
winnt.h
파일에 정의된 Machine
변수가 가질 수 있는 고유 CPU 아키텍쳐 값입니다. 32비트 아키텍쳐는 IMAGE_FILE_MACHINE_I386
를, 64비트 아키텍쳐는 IMAGE_FILE_MACHINE_AMD64
의 값을 가집니다.
NumberOfSections
NumberOfSections
는 PE 파일에 포함된 섹션의 수입니다. 이 섹션의 수는 반드시 값이 1 이상이어야 합니다.
TimeDateStamp
TimeDateStamp
는 파일이 생성된 날짜와 시간을 나타내는 타임스탬프입니다.
PointerToSymbolTable
PointerToSymbolTable
는 심볼 테이블의 시작 위치를 나타냅니다.
NumberOfSymbols
NumberOfSymbols
는 심볼의 수입니다.
SizeOfOptionalHeader
SizeOfOptionalHeader
는 OptionalHeader 구조체의 크기를 나타냅니다. 32비트 형태의 파일의 경우 IMAGE_OPTIONAL_HEADER32
구조체를, 64비트 형태의 파일의 경우 IMAGE_OPTIONAL_HEADER64
구조체의 크기를 명시합니다.
Characteristics
Characteristics
는 PE 파일의 속성과 특성을 나타냅니다. 운영체제가 이 파일을 어떻게 처리할 지 결정하는 데 중요한 역할을 하죠.
Characteristics
는 위와 같은 값을 비트 마스크를 통해 특성을 여러 개 설정합니다.
IMAGE_FILE_DLL
플래그가 설정되면 PE 파일은 DLL로 취급되고, IMAGE_FILE_32BIT_MACHINE
플래그가 설정되면 32비트 시스템에서 실행할 수 있는 것으로 취급됩니다.
OptionalHeader
Magic
PE 파일의 형식을 나타내는 값으로, 32비트는 0x10B
의 값을 가지고 64비트는 0x20B
의 값을 가집니다. 0x107
이라는 ROM 이미지 값을 갖는 경우도 있습니다.
MajorLinkerVersion와 MinorLinkerVersion
메이저와 마이너 링커 버전을 나타냅니다.
SizeOfCode
.text 섹션에 작성된 실행 코드의 총 크기입니다.
SizeOfInitializedData
초기화된 데이터가 작성된 섹션(.data, .rdata 등)의 총 크기입니다.
SizeOfUninitializedData
미초기화된 데이터가 작성된 섹션(.bss)의 총 크기입니다.
AddressOfEntryPoint
실행 파일의 진입점(EntryPoint) 주소로, 주소는 RVA입니다.
BaseOfCode
.text 섹션의 시작 주소(RVA)입니다.
BaseOfData
.data 섹션의 시작 주소(RVA)입니다. PE32+에선 이 필드가 존재하지 않습니다.
ImageBase
파일이 메모리에 로드될 때의 기본 주소입니다. 실행 파일은 이 주소를 기준으로 불러오는데, 만약 ASLR 기법이 적용된 경우 이 주소가 아닐 수도 있습니다.
SectionAlignment
메모리 내 섹션의 정렬 단위입니다. 기본적으로 4096
크기를 가집니다.
FileAlignment
파일 내 섹션의 정렬 단위입니다. 기본적으로 512
나 1024
의 값을 가집니다.
MajorOperatingSystemVersion와 MinorOperatingSystemVersion
실행 파일을 실행하기 위해 요구되는 Windows 운영체제의 버전을 나타냅니다.
SizeOfImage
PE 파일이 메모리에 로드되었을 때 차지하는 크기를 의미합니다. 모든 섹션의 크기를 포함합니다.
SizeOfHeaders
모든 헤더(DOS Header, PE Header 등)의 총 크기입니다.
Subsystem
실행 파일이 어떤 환경에서 동작할 수 있는 지를 나타냅니다. 어떤 유형의 파일인지 구분하는 용도로 사용할 수 있습니다.
1은 드라이버, 2는 GUI, 3은 콘솔 프로그램을 의미합니다.
DllCharacteristics
DLL 동작 특성을 나타내는 플래그입니다.
SizeOfStackReserve와 SizeOfStackCommit
스택을 위해 예약된 메모리의 크기와 실제로 할당된 메모리의 크기를 나타냅니다.
SizeOfHeapReserve와 SizeOfHeapCommit
힙을 위해 예약된 메모리의 크기와 실제로 할당된 메모리의 크기를 나타냅니다.
LoaderFlags
사실상 사용되지 않는 필드라 0의 값을 가집니다.
NumberOfRvaAndSizes
DataDirectory의 수를 나타냅니다. 보통 16입니다.
DataDirectory
DataDirectory는 실행 파일이나 DLL이 메모리에 로드될 때 필요한 추가 정보를 제공합니다. DataDirectory 배열은 각각 아래의 특정 테이블을 의미합니다.
인덱스 | 이름 | 설명 |
---|---|---|
0 | Export Table | 함수 및 변수의 내보내기 테이블 |
1 | Import Table | DLL 및 함수의 가져오기 테이블 |
2 | Resource Table | 리소스 테이블 |
3 | Exception Table | 예외 처리 정보 테이블 |
4 | Certificate Table | 코드 서명 인증서 데이터 테이블 |
5 | Base Relocation Table | 재배치 테이블 |
6 | Debug Directory | 디버그 정보 테이블 |
7 | Architecture Specific Data | CPU 아키텍쳐 데이터 테이블 |
8 | Global Pointer Table(TLS) | TLS 테이블 |
9 | Load Configuration Table | 로드 설정 테이블 |
10 | Bound Import Table | 바인드된 가져오기 테이블 |
11 | Import Address Table(IAT) | 가져온 함수의 주소 테이블 |
12 | Delay Import Descriptor Table | 지연 로드된 가져오기 테이블 |
13 | CLR Runtime Table | 닷넷 실행 파일의 CLR 런타임 헤더 테이블 |
14 | Reserved | 예약 또는 사용되지 않음 |
15 | Reserved | 예약 또는 사용되지 않음 |
Section Header
PE 파일에서 프로그램의 실제 데이터를 저장하는 섹션(Section)에 대한 정보가 담겨있는 구조체입니다. 각 섹션은 코드(code), 데이터(data), 리소스(resource) 등 다양한 목적으로 구분되어 사용됩니다.
PE 파일에서 흔히 볼 수 있는 섹션은 아래 표와 같습니다:
이름 | 설명 |
---|---|
.text | 실행 코드 섹션 |
.data | 읽기 및 쓰기 가능한 전역 데이터 및 변수 섹션 |
.rdata | 읽기 전용 데이터 섹션 (상수, 문자열 등) |
.bss | 초기화되지 않은 데이터 섹션 (런타임 시 초기화 등) |
.rsrc | 리소스 섹션(아이콘, 이미지 등) |
.reloc | 재배치 정보 섹션 |
.debug | 디버깅 정보 섹션 |
필드 이름 | 설명 |
---|---|
Name |
섹션의 이름 |
VirtualSize |
섹션이 메모리에 로드되었을 때 차지하는 크기 |
VirtualAddress |
섹션의 상대 가상 주소(RVA), 메모리에서 섹션이 로드될 때의 위치 |
SizeOfRawData |
파일에서 섹션이 실제로 차지하는 크기 |
PointerToRawData |
파일에서 섹션이 시작되는 오프셋 |
Characteristics |
섹션의 속성 플래그 |
위 표에서 언급되지 않은 필드는 현재는 잘 사용되지 않는 것들입니다.
VirtualSize
와 SizeOfRawData
의 크기는 서로 다른 값을 가질 수 있기 때문에 주의하시기 바랍니다.
Characteristics
의 속성 플래그
IMAGE_SCN_CNT_CODE
, IMAGE_SCN_CNT_INITIALIZED_DATA
, IMAGE_SCN_CNT_UNINITIALIZED_DATA
, IMAGE_SCN_MEM_EXECUTE
, IMAGE_SCN_MEM_READ
, IMAGE_SCN_MEM_WRITE
속성이 주로 이용됩니다.
RVA to RAW
RVA를 Raw Offset(파일 오프셋)으로 변환하는 것을 RVA to RAW라 합니다. 헷갈릴 수 있으니 용어의 정의를 다시 짚고 넘어갑시다.
- RVA(Relative Virtual Address): PE 파일이 메모리에 로드되었을 때의 상대 가상 주소. 기준은 ImageBase.
- Raw Offset: PE 파일이 저장 장치에 저장된 상태에서의 파일 오프셋. 파일의 시작 부분을 기준으로 하는 절대 위치.
RVA를 RAW로 변환하는 방법
- RVA가 속해있는 섹션을 찾는다. 섹션 헤더의
VirtualAddress
와VirtualSize
를 이용해 어느 섹션 범위에 속하는 지 확인하면 된다. - RAW 계산식을 통해 산출한다.
Raw Offset = PointerToRawData + (RVA - VirtualAddress)
위의 식을 통해 RVA를 RAW로 변환할 수 있습니다.
예시 1
RVA는 0x1200
이고 각 섹션 헤더의 정보는 아래와 같을 때 RAW를 구하시오.
섹션 | VirtualAddress | VirtualSize | PointerToRawData |
---|---|---|---|
섹션 1 | 0x1000 | 0x200 | 0x400 |
섹션 2 | 0x2000 | 0x300 | 0x800 |
섹션 1과 섹션 2의 범위는 VirtualAddress
+ VirtualSize
- 1로 알아낼 수 있습니다. 섹션 1은 0x1000
~ 0x11FF
, 섹션 2는 0x2000
~ 0x22FF
범위를 가집니다. 주어진 RVA는 0x1200
으로 섹션 1의 범위에 속합니다.
Raw Offset = PointerToRawData + (RVA - VirtualAddress)
RAW는 위 식을 통해 구할 수 있고, 0x400
+ (0x1200
- 0x1000
)으로 0x600
이라는 값이 산출됩니다. 즉, RAW는 0x600
입니다.
RAW는 SizeOfRawData
를 넘길 수 없다
VirtualSize
와 SizeOfRawData
의 값이 서로 달라 발생할 수 있는 문제입니다.
예를 들어 아래 표와 같은 경우라 생각해봅시다.
섹션명 | VirtualAddress | VirtualSize | PointerToRawData | SizeOfRawData |
---|---|---|---|---|
섹션 1 | 0x1000 | 0x500 | 0x400 | 0x200 |
주어진 RVA가 0x1400
일 때 RAW를 구하면 0x600
이 산출됩니다. RVA는 메모리 내 섹션 범위에 해당하기 때문에 문제 없지만 이를 RAW로 변환한 값은 SizeOfRawData
의 값보다 큽니다. 즉, 파일 내 유효하지 않은(존재하지 않는) 주소란거죠. 이런 경우 RAW를 정의할 수 없습니다.