리버싱
리버싱 기초: PE 파일, 레지스터, 스택 프레임과 자주 쓰는 어셈블리 명령
디버거를 켜기 전에 알아야 했던 것들입니다. 실행 파일이 메모리에 어떻게 올라가는지, 레지스터와 스택 프레임이 무슨 일을 하는지 정리했습니다.
리버싱을 처음 시작하면서 디버거 화면을 읽으려면 무엇을 알아야 하는지 정리한 노트입니다. 리버싱 입문서 1장과, 공부하면서 따로 모은 어셈블리 명령 정리를 합쳤습니다.
실행 파일이 메모리에 올라가는 과정
Windows 실행 파일은 PE(Portable Executable) 형식입니다.
- 헤더: 프로그램을 실행하는 데 필요한 정보 (어디서 시작하는지, 어떤 DLL이 필요한지 등)
- 바디: 코드, 데이터, 리소스
실행 파일을 열면 OS의 로더가 헤더를 읽고, 바디의 코드와 데이터를 메모리에 배치합니다.
| 영역 | 들어가는 것 |
|---|---|
| 코드 | 프로그램 코드 |
| 데이터 | 전역 변수, 정적 변수 |
| 스택 | 지역 변수, 함수 인자, 복귀 주소 |
| 힙 | 실행 중에 동적으로 할당한 메모리 |
로딩되면 코드·데이터 영역이 먼저 채워지고, 실행되면서 스택과 힙이 쓰이며 크기가 바뀝니다. 실행은 헤더에 적힌 엔트리 포인트부터 시작합니다.
CPU가 계산하려면 데이터가 레지스터에 있어야 해서, 메모리 값을 레지스터로 옮기는 일이 계속 일어납니다. 여러 프로그램이 CPU를 번갈아 쓸 때 쓰던 레지스터 값을 메모리에 저장해 두는 것을 컨텍스트 스위칭이라고 합니다.
레지스터 (IA-32)
| 레지스터 | 주로 하는 일 |
|---|---|
| EAX | 곱셈·나눗셈, 함수의 반환값 |
| EBX | 추가 저장 공간, 인덱스 계산 보조 |
| ECX | 반복 횟수 카운터 |
| EDX | EAX와 함께 쓰임, 부호 확장 |
| ESI | 복사할 원본 주소 |
| EDI | 복사할 목적지 주소 |
| EBP | 현재 스택 프레임의 기준 주소 |
| ESP | 스택의 꼭대기 주소. PUSH/POP마다 4바이트씩 바뀜 |
| EIP | 다음에 실행할 명령의 주소 |
32비트 레지스터의 아래 16비트는 AX, 그 위·아래 8비트는 AH·AL처럼 따로 부를 수 있습니다.
스택과 스택 프레임
스택은 함수에 인자를 넘기고, 지역 변수를 담고, 함수가 끝난 뒤 돌아갈 주소를 저장하는 메모리입니다. 마지막에 넣은 값이 먼저 나오고(LIFO), 높은 주소에서 낮은 주소 방향으로 쌓입니다.
스택 프레임은 함수 하나가 쓰는 스택 구간입니다. 함수가 호출되면 생기고, 끝나면 사라집니다. 호출 과정은 보통 이렇습니다.
PUSH arg2 ; 인자를 오른쪽부터 넣고
PUSH arg1
CALL func ; 복귀 주소를 스택에 넣고 func로 이동
; --- func 시작 ---
PUSH EBP ; 이전 함수의 기준 주소 백업
MOV EBP, ESP ; 지금 위치를 이 함수의 기준 주소로
...
MOV ESP, EBP
POP EBP ; 이전 기준 주소 복구
RETN ; 복귀 주소로 돌아감
이렇게 정한 EBP를 기준으로 EBP+8(첫 인자), EBP-4(첫 지역 변수)처럼 값을 찾습니다.
PE 헤더의 주요 구조
| 구조 | 역할 |
|---|---|
IMAGE_DOS_HEADER |
DOS에서 실행했을 때의 처리, 실제 PE 헤더 위치(e_lfanew)를 가리킴 |
| DOS Stub | DOS에서 실행하면 보여줄 오류 메시지 |
IMAGE_NT_HEADERS |
시그니처 + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER |
IMAGE_OPTIONAL_HEADER |
ImageBase, AddressOfEntryPoint, BaseOfCode, 16개의 데이터 디렉터리 |
주소는 세 가지로 부릅니다.
- 파일 오프셋(RAW): 파일 안에서의 위치
- RVA: 메모리에 올라간 뒤 ImageBase로부터의 상대 주소
- VA: 실제 가상 주소.
VA = ImageBase + RVA
IAT(Import Address Table) 는 외부 DLL 함수의 주소를 모아 둔 표입니다. 로더가 로딩할 때 IAT에 적힌 API 이름으로 실제 주소를 찾아 채워 넣습니다.
자주 쓰는 명령
| 명령 | 하는 일 |
|---|---|
MOV a, b |
b의 값을 a에 복사 |
LEA a, [b] |
b의 주소를 a에 넣음 |
PUSH / POP |
스택에 넣기 / 꺼내기 |
CALL / RET |
복귀 주소를 저장하고 함수로 이동 / 저장한 주소로 돌아감 |
INC / DEC |
1 증가 / 1 감소 (결과가 0이면 ZF=1) |
CMP a, b |
a-b를 계산해 플래그만 바꿈. 같으면 ZF=1 |
TEST a, b |
a AND b를 계산해 플래그만 바꿈. 결과가 0이면 ZF=1 (TEST EAX, EAX는 EAX가 0인지 확인) |
JE / JNE |
ZF=1이면 / ZF=0이면 점프 |
JL |
앞의 값이 더 작으면 점프 |
JMP |
무조건 점프 |
PUSHAD / POPAD |
범용 레지스터 8개를 한꺼번에 저장 / 복구 |
PUSHAD는 EAX → ECX → EDX → EBX → ESP → EBP → ESI → EDI 순서로 넣고, POPAD는 그 반대로 꺼냅니다. 이때 ESP 값은 버리고 복구하지 않습니다.
따로 메모해 둔 것: FS:[30]
MOV EAX, DWORD PTR FS:[30]
Windows에서 FS 레지스터는 현재 스레드의 TEB(스레드 정보 블록)를 가리킵니다. TEB의 0x30 위치에는 PEB(프로세스 정보 블록)의 주소가, 0x18에는 TEB 자신의 주소가 있습니다. 디버거 탐지처럼 프로세스 정보를 직접 읽는 코드에서 자주 보입니다.