본문 바로가기

리버싱/기초 지식

Study 02 - 기초 지식(Register, Call Stack, Prologue/Epilogue)

Register

프로그램 내부의 명령어를 실행하는 것은 CPU의 제어장치와 레지스터로 이들이 프로그램을 실행하는 데 필요한 데이터는 레지스터에 있어야 한다. 그렇기 때문에 메모리의 데이터를 레지스터로 복사하는 과정이 필요하며 여러 프로그램이 하나의 CPU를 사용하려면 각 프로그램마다 사용하는 데이터가 다르기 때문에 레지스터의 내용을 바꿀 필요가 있다. 이 과정을 컨텍스트 스위칭(Context Switching)이라 하며 운영체제 수업에서 배웠듯이 빈도가 높을수록 성능 하락의 원인이 된다.

 

인텔 CPU의 레지스터 구조는 다음과 같다.

64-bit register

Lower 32 bits

Lower 16 bits

Lower 8 bits

rax

eax

ax

al

rbx

ebx

bx

bl

rcx

ecx

cx

cl

rdx

edx

dx

dl

rsi

esi

si

sil

rdi

edi

di

dil

rbp

ebp

bp

bpl

rsp

esp

sp

spl

레지스터는 용도에 따라 8비트 단위로 나뉘어 사용할 수 있는데 예를 들어 32비트짜리 EAX는 16비트짜리 AX로 분리할 수 있다. AX에서도 상위, 하위 각 8비트씩 AH, AL 레지스터로 사용할 수 있다. OllyDbg 등을 활용한 리버싱 예제 등에서 많이 볼 수 있는 32비트 환경의 경우 eax, ebx 레지스터 등을 볼 수 있었으나 요즘 운영체제 환경은 64비트이기 때문에 rax, rbx 레지스터 등을 더 자주 볼 수 있다.

64비트 파일
32비트 파일

32비트 레지스터 명칭으로 각 레지스터의 목적은 다음과 같다.

  • EAX: Extended Accumulator Register. 곱셈, 나눗셈 명령에서 사용. 함수 반환값이 저장됨.

  • EBX: Extended Base Register. ESI나 EDI와 결합해 인덱스에 사용된다.

  • ECX: Extended Counter Register. 반복 명령어를 사용할 때 반복 카운터를 저장한다.

  • EDX: Extended Data Register. EAX와 같이 사용되며 부호 확장 명령 등에 활용된다.

  • ESI: Extended Source Index. 데이터를 복사하거나 조작할 때 소스, 원본 데이터의 주소가 저장된다. 이 레지스터가 가리키는 주소의 데이터를 EDI 레지스터가 가리키는 주소로 복사하는 데 많이 쓰인다.

  • EDI: Extended Destination Index. 복사 작업을 할 때 목적지 주소가 저장된다. 주로 ESI가 가리키는 곳의 데이터가 복사된다.

  • EBP: Extended Base Pointer. 스택 프레임의 시작 주소가 저장된다. 해당 스택 프레임에서 동작하는 동안 이 주소는 변하지 않으며 함수 등을 호출한 곳으로 복귀하면 해당(원본) 스택 프레임을 가리키게 된다.

  • ESP: Extended Stack Pointer. 스택 프레임의 끝 주소가 저장된다. 명령어 PUSH, POP에 의해 4바이트씩 변한다.

  • EIP: Extended Instruction Pointer. 다음에 실행할 명령어의 주소가 저장된다.

PUSH, POP에 의해 4바이트씩 변하는 이유는?

PUSH RAX, PUSH AX 등은 가능하지만 PUSH EAX는 가능하지 않은 그 이유는?

Call Stack

스택(Stack)이란 메모리의 한 부분으로 LIFO, Last In First Out 방식으로 동작한다. 이 자료구조에 대해 PUSH, POP 명령을 수행하여 데이터를 집어넣거나 꺼낼 수 있는데 이를 컴퓨터 메모리에서는 콜 스택(Call Stack)으로 활용한다. 이는 크게 다음과 같이 사용된다.

  • 서브루틴(함수 호출 등)으로 인자를 전달한다.

  • 서브루틴 내에서 사용하는 지역 변수가 저장되는 공간을 제공한다.

  • 서브루틴 종료 후 원래 위치로 되돌아갈 때 되돌아갈 주소를 저장한다.

이런 스택에 PUSH로 데이터를 집어넣을 때 쌓이는 방향은 높은 주소(0xFFFFFFFF)에서 낮은 주소(0x00000000)로 다음 그림과 같다.

https://eli.thegreenplace.net/2011/02/04/where-the-top-of-the-stack-is-on-x86

스택에 데이터를 PUSH하면 스택의 주소(ESP)는 4바이트만큼 감소하며 POP하면 4바이트만큼 증가한다. 즉 스택 포인터는 데이터의 출입과 상관없이 항상 프레임의 맨 위에서 바로 아래에 위치하게 된다. 현실의 예시로 들면 동굴의 천장에서 내려오는 종유석을 생각하면 될 것이다.

 

이런 스택의 구조는 아키텍처마다 다르지만 대부분 하나의 스택 프레임은 종료되지 않은 하나의 호출(call)에 해당한다고 할 수 있다. 예를 들어 DrawSquare() 라는 함수에서 DrawLine() 이라는 함수를 호출했다고 할 때 이의 콜 스택 구조는 다음과 같이 나타낼 수 있다.

그림에서는 아래쪽에 높은 주소, 위쪽에 낮은 주소가 있다.

기본적으로 DrawSquare() 라는 함수 역시 어디선가 호출된 함수이므로 해당 함수를 실행하기 위한 공간, 즉 스택 프레임이 스택에 마련되어 있다. 스택 프레임에는 주로 다음과 같은 항목들이 차례대로 저장된다.

  • 함수(서브루틴)를 호출하기 위한 매개변수

  • 함수(Callee)를 호출한 함수(Caller)로 돌아가기 위한 복귀주소(DrawLine의 프레임 -> DrawSquare의 코드)

  • 함수에서 사용하는 지역 변수들

함수의 형태(필요한 매개변수, 사용하는 지역변수의 크기, 자료형 등)는 함수마다 다르기 때문에 함수 종료 시 스택에서 해당 스택 프레임을 제거하는 것은 스택 포인터를 프레임 포인터(베이스 포인터)로 복구시킴으로써 이루어진다. 즉 프레임 포인터는 스택에 저장된 함수 호출 전의 스택 포인터라고 할 수 있다. 이는 아래에서 설명할 함수의 프롤로그, 에필로그를 보면 이해가 잘 될 것이다. 참고할 것은 콜 스택에서 프레임 포인터(Frame Pointer)는 베이스 포인터(Base Pointer)와 동일하다는 것이다.

 

그렇다면 함수 내에서 사용되는 지역변수들에는 어떻게 참조할 수 있을까? 이는 ESP나 EBP를 스택 프레임에서 데이터 참조를 위한 절대 주소(기준)으로 활용하여 참조할 수 있다. 해당 포인터에서 일정 거리를 가감하여 상대적으로 참조하는 것인데 스택의 PUSH, POP에 따라 계속 변하는 ESP와 달리 EBP는 고정되어 있기 때문에 보통 이를 사용하여 참조하게 된다.

 

위의 그림에는 나와있지 않지만 스택 프레임에는 이전 프레임 포인터, 즉 이전 EBP를 저장하는 공간이 마련되어 있다(아래의 프롤로그 참고). 이는 함수 시작 때 저장되어 함수 종료 시 복구된다. 또한 항상 일정한 위치에 저장되어 있기 때문에 여러 함수 호출이 중첩되어도 연속적으로 복구할 수 있다.

Prologue, Epilogue

이와 관련해서 함수의 프롤로그(prologue), 에필로그(epilogue)라는 것이 있다. 이는 함수 호출 및 종료를 처리하기 위한 일종의 협약(convention)으로 많은 프로그래밍 언어와 컴파일러에서 적용되는 기법이다. 이는 해당 함수를 호출한 함수에서 매개변수와 복귀주소를 스택에 저장하는 작업이 끝난 후에 실행된다.

 

프롤로그는 함수가 호출되기 전 스택과 레지스터를 해당 함수가 사용할 수 있도록 준비시키는 과정으로 크게 다음과 같은 3단계 과정으로 이루어진다.

  1. 현재 스택의 베이스 포인터(EBP)를 스택에 저장(PUSH)하여 추후 복구할 수 있도록 한다.

  2. 현재 스택의 스택 포인터(ESP)를 베이스 포인터에 저장하여 새로운 스택 프레임이 이전 스택 프레임 위에 저장될 수 있도록 한다.

  3. 필요한 데이터가 스택에 저장될 수 있도록 스택 포인터를 이동시켜 메모리 공간을 마련한다. 인텔 x86 아키텍처 기준 스택 포인터는 낮은 주소 방향으로 감소된다.

이를 어셈블리어로 나타내면 다음과 같다.

push ebp
mov ebp, esp
sub esp, N

에필로그는 프롤로그와 반대로 함수가 끝난 후 다시 원래 스택 프레임으로 돌아가기 위한 작업을 수행하는 과정으로 크게 다음과 같은 3단계 과정으로 이루어진다.

  1. 현재 스택의 스택 포인터(ESP)를 베이스 포인터(EBP)까지 감소시켜 서브루틴에 할당된 메모리 공간을 해제시킨다.

  2. 스택에서 저장된 베이스 포인터를 복구(POP)하여 EBP에 저장, 프롤로그가 진행되기 이전의 값으로 되돌린다.

  3. 이전 스택 프레임의 프로그램 카운터(PC)를 스택에서 복구(POP)하여 점프(JMP)함으로써 되돌아간다.

이를 어셈블리어로 나타내면 다음과 같다.

mov esp, ebp
pop ebp
ret

인텔 x86 프로세서에서는 프롤로그, 에필로그를 다음과 같은 명령어를 사용하여 나타낼 수 있다.

# prologue
enter 0

# epilogue
leave
ret

 

[출처| https://en.wikipedia.org/wiki/Call_stack]

[출처| https://en.wikipedia.org/wiki/Function_prologue]

[출처| https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture]

'리버싱 > 기초 지식' 카테고리의 다른 글

Study04 - 기초 지식(패킹, 언패킹)  (0) 2020.11.23
Study03 - 기초 지식(IAT)  (0) 2020.11.15
Study 01 - 기초 지식(PE)  (0) 2020.08.26