1. 프로그램 실행 구조
PE (Portable Executable) 파일
- 윈도우 실행 파일
- PE 포맷 = header + body
- header: 프로그램 실행하는데 필요한 기본 정보, 배치 정보(파일의 메모리 상 위치 정보)
- body: 코드, 데이터
- exe, dll, ocx 등 다양한 종류가 있다.
- PE 포맷인 실행파일을 클릭하면, 운영체제의 **Loader(로더)**는 PE 헤더에 있는 정보를 분석해서 PE 바디의 코드, 데이터를 메모리에 배치한다.
- 메모리 구성
- Code 영역: 프로그램 코드 들어가는 영역
- Data 영역: 정적 변수, 전역 변수 저장
- Stack 영역: 함수 호출 시 사용되는 매개 변수, 지역변수 저장
- Heap 영역: 동적 메모리 할당에 사용
- PE가 로딩되면 코드 영역, 데이터 영역에 데이터가 들어가고, 프로그램 실행되면 스택 영역, 힙 영역에 데이터가 쌓인다.
엔트리 포인트(Entry Point)
- PE 파일 실행이 시작되는 주소
- 운영체제는 메모리의 PE 파일을 실행하기 위해 PE 헤더 정보에서 Entry Point를 찾아 그곳에서부터 프로그램을 실행한다.
2. 레지스터
- CPU에서 사용하는 고속 기억장치
- CPU는 연산 수행 위해 메모리의 데이터를 CPU 내부의 레지스터로 가지고 온다. 연산 중간마다 레지스터에 데이터를 저장한다.
Register |
Description |
EAX (Extended Accumulator Register) |
곱셈, 나눗셈 명령에 사용. 함수 반환값 저장 |
EBX(Extended Base Register) |
인덱스로 사용한다. |
ECX(Extended Counter Register) |
반복 명령어 반복 횟수를 저장하고 반복 작업 수행한다. |
EDX(Extended Data Register) |
EAX와 같이 사용한다. 부호 확장 명령 등에 활용 |
ESI(Extended Source Index) |
ESI 레지스터가 가리키는 주소의 데이터를 EDI 레지스터가 가리키는 주소로 복사하는 용도로 사용 |
EDI(Extended Destination Index) |
ESI 레지스터가 가리키는 주소의 데이터가 EDI가 가리키는 주소로 복사된다. |
EBP(Extended Base Pointer) |
스택 프레임 시작 주소가 저장된다. 사용하는 스택 프레임이 살아있는 동안 EBP 값은 변경되지 않고, 현재 사용하던 스택 프레임이 사라지면 이전 스택 프레임을 가리킨다. |
ESP(Extended Stack Pointer) |
스택 프레임 끝 지점 주소를 저장한다. PUSH, POP 명령어 따라 ESP의 값이 4바이트씩 (32bit이므로) 변한다. |
EIP(Extended Instruction Pointer) |
다음에 실행할 명령어가 저장된 메모리 주소가 저장되어, 현재 실행하던 명령어가 끝나면 EIP 레지스터에 저장된 주소에 있는 명령어가 실행된다. |
3. 스택과 스택 프레임
스택(Stack)
- LIFO(Last In First Out) 방식으로 동작하는 자료 구조
- 가장 마지막에 들어간 데이터가 가장 처음에 나오는 방식
- 스택은 high address에서 low address로 데이터가 쌓인다. 따라서 스택의 Top은 low address이다
- ESP 레지스터는 현재 프로그램이 사용하는 스택 위치를 저장하는 stack pointer
- PUSH
- 데이터를 스택에 넣는 명령어.
- ESP + 4h == 주소가 4byte 감소하여 데이터가 스택에 들어간다.
- POP
- 데이터를 스택에서 꺼내는 명령어POP: 데이터를 스택에서 꺼내는 명령어
- ESP - 4h == 주소가 4byte 증가하여 스택에서 데이터를 꺼낸다.
스택 프레임(Stack Frame)
- 서브루틴(함수)이 가지는 자신만의 스택 영역
- 서브루틴 내부에서 사용하는 데이터가 스택 프레임에 저장된다.
- 함수가 호출될 때 해당 함수의 스택 프레임이 생성되며, 함수 종료 후 해당 함수를 호출한 함수로 리턴하여, 즉 복귀 주소로 돌아가면 해당 함수의 스택 프레임은 사라진다.
- 복귀 주소는 서브루틴 실행 전 스택에 넣고, 이전 루틴이 사용한 EBP 레지스터 내용을 백업해놓는다.
- EBP(Extended Base Pointer)는 스택 프레임에서 데이터 참조 위한 기준 주소인 Frame Pointer이다. 스택 내의 데이터 접근 시 사용한다.
- ESP는 프로그램이 실행하면서 계속 변하므로 변하지 않는 EBP를 프레임 포인터로 사용한다.
4. PE 파일
Compiler
- 고급 언어(Java, C 같은)를 운영체제가 이해할 수 있는 기계어로 번역하는 프로그램
PE (Portable Executable) 파일
- 운영체제가 이해할 수 있는 기계어
- 윈도우 운영체제에서 사용하는 실행 파일 형식 중 하나
PE 포맷
- 윈도우에서 프로그램을 실행하기 위해 준수해야 하는 규칙
- 이 규칙을 지켜야 윈도우 환경에서 프로그램이 실행될 수 있다.
PE 파일 종류
- 윈도우에서 사용하는 실행 파일에는 EXE, DLL, OCX, SYS 등이 있다.
PE 파일 구조
Header
- PE 파일 동작에 대한 규칙, 파일 구성 관련 정보 저장
- PE Header, Section Header로 구성된다.
- PE Header
- IMAGE_DOS_HEADER: 윈도우용 PE 헤더 위치 가리키는 역할, DOS 운영체제가 윈도우 PE 파일 실행 시 오류 메시지를 보여준다.
- MS-DOS Stub Program: DOS 운영체제에서 윈도우 PE 파일 실행 시 보여줄 오류 메시지 저장
- IMAGE_NT_HEADERS
- Signature (4byte) 2개
- IMAGE_FILE_HEADER
- IMAGE_OPTIONAL_HEADER
- PE 구조에서 핵심적인 역할
- 이 구조체 뒤에는 16개의 IMAGE_DATA_DIRECTORY 구조체가 있다.
- Image Base: 메모리에 PE 파일이 저장되는 시작 주소
- Section Header: PE 파일은 최소 1개 이상의 Section Header를 가진다.
Body
- 실행되는 기계어, 프로그램에 사용되는 코드, 데이터 저장
- Header에 적힌 규칙에 따라 Body에 저장된 기계어들이 실행된다.
RVA(Relative Virtual Address)
- 메모리에 로딩되면서 Image Base 값과 합산된 주소인 VA(Virtual Address)에 저장된다.
- 디버거로 프로그램 열었을 때 코드 영역의 맨 윗부분 주소
Entry Point의 주소
- 프로그램 실행에 대한 제어권이 커널 영역에서 코드 영역으로 처음 넘어오는 주소인 Entry Point를 가리킨다.
Base of Code
- 코드 영역이 시작되는 주소(RVA)를 가리킨다.
주소 지정 방법
- PE 파일 분석 시 찾는 데이터 위치를 헤더 내의 주소 값을 통해 찾을 수 있다.
- pFile, RVA(Relative Virtual Address), VA(Virtual Address) 세 가지의 주소 형식이 있다.
pFile
- PE 파일 내부 오프셋 값
- 파일
- PE 파일이 하드디스크에 저장되었을 때 의미있다.
RVA(Relative Virtual Address)
- PE 파일이 메모리로 로드됐을 때 저장되는 상대 주소(기준값에서 떨어진 정도)
- 메모리
- 메모리에 로드됐을 때 의미있다.
VA(Virtual Address)
- 가상 메모리 상에서 저장되는 실제 주소
- 메모리
- 메모리에 로드됐을 때 의미있다.
DLL(Dynamic Linking Library)
- 프로그램에서 사용하는 라이브러리를 사용 시점에서 동적으로 연결한다.
IAT(Import Address Table)
- PE 파일(실행 파일) 안에서 어떤 라이브러리의 어떤 함수를 가져다 쓰는지 기록해놓은 정보
- Loader는 PE 파일을 메모리로 로딩 시 IAT에 기록된 API 이름을 참조해서 실제 주소를 찾아 IAT에 API를 가리키는 주소를 적는다.
- 코드에서 라이브러리 참조 시 IAT 내부 함수 주소를 이용한다.
6. 어셈블러
어셈블러
- 어셈블러: 기계어를 사람이 알아볼 수 있도록 만든 매크로 모음
- 어셈블러와 기계어는 1:1로 매칭된다.
어셈블러 기본 구조
윈도우 어셈블러 기본 구조
- 어셈블러는 최대 3개 인자까지 받아들일 수 있다.
- 어셈블러 명령어 흐름: 오른쪽에서 왼쪽으로
인자 2개 명령어
- ADD EAX, EBX: EAX 레지스터 값에 EBX 레지스터 값을 더해 EAX 레지스터에 저장한다.
- 16진수로 표현하면 0x01D8에 해당한다.
인자 1개 명령어
- INS ESI: ESI 레지스터 값을 1만큼 증가시키는 명령어.
- 고급언어로 ESI = ESI + 1 또는 ESI++로 구현된다.
- 16진수 46으로 표현될 수 있다.
- PUSH 인자: 인자를 스택의 맨 위에 입력한다.
- PUSH 0: 스택 맨 위에 '00000000'을 입력한다.
- PUSH는 16진수 0x6A에 해당하며, 인자 값을 그대로 헥사 코드로 사용한다.
인자 없는 명령어
RETN
- 뒤에 하나의 인자를 입력받을 수 있다.
- 스택 맨 위의 값을 EIP 레지스터(다음 실행할 명령어 저장)에 저장한다.
- RETN 명령어로 스택 맨 위 주소의 명령어를 실행한다.
- 현재 루틴에서 서브루틴으로 이동 시 돌아올 주소를 스택에 넣어주고, 서브루틴 동작이 완료되면 스택에서 돌아올 주소를 꺼내 프로그램 실행 흐름이 변경되도록 한다.
- 서브루틴의 맨 마지막엔 대부분 RETN 명령어가 있다.