Notice
Recent Posts
Recent Comments
«   2024/05   »
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
Tags
more
Archives
Today
Total
관리 메뉴

topcue

스택 프레임과 함수 호출(Stack Frames and Function Calls) 본문

바이너리 분석

스택 프레임과 함수 호출(Stack Frames and Function Calls)

topcue 2021. 9. 14. 17:12

Stack

스택은 반환 주소(return address), 함수 매개 변수, 지역 변수 등 함수 호출과 관련된 데이터를 위한 메모리 영역이다.

스택(Stack)은 후입선출(LIFO, Last-In-First-Out) 자료구조다. 바이너리의 스택이라는 이름이 여기서 유래되었다.

  • Stack Layout (Reversed top-bottom for easy explanation).

LIFO 방식이 함수를 호출하고 반환하는 방식과 일치하기 때문에 함수 호출을 위해 스택 자료 구조를 사용한다. 즉 마지막으로 호출된 함수가 가장 먼저 반환하게 된다.

  • Pushing the value f onto the stack and then popping it into rax

위 그림을 보면 스택은 0x7ffffffff8000에서 시작하며 a부터 e까지의 값을 가지고 있다. 더 낮은 주소에서는 초기화되지 않은 메모리가 '?'로 표시되어 있다. x86에서는 스택이 낮은 메모리 주소 방향으로 증가한다.

스택 포인터 레지스터인 rsp는 항상 스택의 최상단을 가리키며, 가장 최근에 push한 값이 여기에 위치한다. 처음(왼쪽 그림)은 e를 최근에 삽입하여, rsp가 e를 가리키고 있는 모습이다.

새로운 값 f를 push하면 rsp가 f의 위치를 가리키도록 감소한다. x86에는 스택에 값이 삽입되거나 제거될 때 rsp를 자동으로 업데이트하는 push와 pop 명령어가 존재한다.

pop 명령어로 스택 최상단의 값을 꺼내면 pop의 피연산자에 그 값을 넣고, rsp를 증가시켜 스택의 새로운 최상단을 가리키게 된다.

위 그림에서 pop rax 명령어로 스택의 값 f를 rax에 복사하고 rsp가 다음 스택 최상단인 e를 가리키도록 업데이트(증가)했다.

이때 pop은 스택의 값을 삭제하는 것이 아니라 단순히 rsp만을 변경하는 것임을 알 수 있다.

Stack Frame

x86 리눅스 프로그램의 각 함수들은 스택에 자체 함수 프레임(스택 프레임)이 있고, 이는 이 스택 프레임의 하단을 가리키는 base pointer 레지스터 rbp와 상단을 가리키는 rsp로 구분된다.

함수 프레임은 함수의 스택 기반 데이터를 저장하는 데 사용된다.

함수 호출이 수행될 때마다 새 스택 프레임이 생성된다. caller 함수의 스택 프레임이 복원되고 실행이 calling 함수로 실행이 넘어갈 때부터, return할 때까지 그 함수의 스택 프레임이 유지된다.

Function Calls

Function Prologue

함수 프롤로그(function prologue)는 함수가 호출될 때 실행된다. 이는 함수 시작 부분에서 실행되는데, 스택과 레지스터를 준비하는 몇 줄의 코드로 구성된다.

Function Epilogue

함수 에필로그(function epilogue)는 함수가 종료되며 리턴하려고 할 때 실행된다. 함수가 호출되기 전의 스택 및 레지스터를 복원하기 위해 함수의 마지막 부분에서 실행되는 몇 줄의 코드로 구성된다.

Calling Conventions

(함수) 호출 규약(calling conventions)은 caller 함수와 calle 함수 간에 함수를 호출할 때 필요한 정보들에 대한 규칙이다.

x86에서는 스택을 이용해 매개 변수를 전달하고, x64에서는 대부분 레지스터를 통해 전달한다.

예를 들어, Windows x64에서는 호출 규칙이 하나만 존재하며 처음 4 개의 매개 변수가 전달된다. Linux에서는 처음 6 개의 매개 변수가 전달된다.

Calling Conventions for x86

x86 아키텍처에서는 함수를 호출하기 전에 스택에 모든 함수 인자를 push한다.

  • Function Prologue
push  ebp         ; Save the stack-frame base pointer (of the calling function).
mov   ebp, esp    ; Set the stack-frame base pointer to the current location on the stack.
sub   esp, N      ; Grow the stack by N bytes to reserve space for local variables.
  • Function Epilogue
mov   esp, ebp    ; Put the stack pointer back where it was when this function was called.
pop   ebp         ; Restore the calling function's stack frame.
ret               ; Return to the calling function.

함수 에필로그의 처음 두 instruction인 mov esp, ebppop ebp는 leave instruction으로 치환될 수 있다.

Calling Conventions for x64

x64 아키텍처에서는 처음 6개의 함수 인자를 다음 순서에 따라 레지스터에 설정한다: RDIRSIRDXRCXR8, R9

만약 인자의 개수가 7개 이상이라면 나머지 인자들은 스택에 push해 전달한다.

Windows에서는 RCX, RDX, R8, R9 순서로 네 가지 레지스터를 사용한다.

  • Function Prologue
mov    [rsp + 8], rcx    ; Saves argument register in home-location.
push   r15               ; Saves the volatile register r15.
push   r14               ; Saves the volatile register r14.
push   r13               ; Saves the volatile register r13.
sub    rsp, N            ; Grow the stack by N bytes to reserve space for local variables.
lea    r13, 128[rsp]     ; Establish a frame pointer to point 128 bytes into the allocated space.
  • Function Epilogue
lea   rsp, -128[r13]    ; Frame pointer's value is restored if it was used in the function.
                        ; epilogue proper starts here
add   rsp, N            ; Destroy the stack frame by pointing the stack pointer before the frame.
pop   r13               ; Restore the volatile register r13.
pop   r14               ; Restore the volatile register r14.
pop   r15               ; Restore the volatile register r15.
ret                     ; Return to the calling function.

Flow of Control

다음은 x86에서의 함수 호출 flow control 예시다.

  1. 함수 인자들은 push instruction을 통해 스택에 배치된다.
  1. call memory_location를 통해 함수가 호출된다. 그러면 EIP 레지스터가 가리키는 current instruction address가 스택에 push 된다. 이 주소는 함수가 종료되면 메인 코드로 돌아가는 데 사용된다. 함수가 시작되면 EIP는 함수의 시작 코드인 memory_location으로 설정된다.
  1. 함수 프롤로그에 의해 스택에 지역 변수를 위한 공간이 할당되고, EBP가 스택에 push된다. 이는 calling 함수에 대한 EBP를 저장하기 위해 수행된다.
  1. 함수가 작업을 수행한다.
  1. 함수 에필로그에 의해 스택이 복구된다. ESP는 지역 변수를 free하기 위해 조정된다(실제로 사라지지는 않음). calling 함수가 해당 변수를 적절히 처리할 수 있도록 EBP가 복원된다.
  1. 함수는 ret instruction을 호출함으로써 return한다. 이 명령어는 반환 주소(return address)를 스택에서 EIP 레지스터로 pop한다. 그러면 프로그램은 원래 호출이 이루어진 곳으로 돌아가 실행될 것이다.
  1. 스택은 전달한 인자들을 나중에 다시 사용할 수 없도록하기 위해 조정된다.

Stack and Frame Analysis

  • Individual stack frame
  • ESP는 스택의 꼭대기를 가리킨다. (memory address 0x12F02C)
    • 데이터가 스택에 push 될 때마다 ESP는 감소한다. 이는 스택이 낮은 주소 방향으로 자라기 때문이다.
    • 만약 push eax instruction이 실행된다면, ESP는 ESP는 EAX의 바이트 크기인 4 만큼 감소하여 0x12F028가 된다. 그리고 EAX의 데이터가 0x12F028에 복사된다.
    • 만약 pop eax instruction이 실행된다면, 0x12F028에 있는 데이터가 EAX로 복사되고, ESP는 4 만큼 증가한다.
    • 다만 값은 여전히 그 자리에 남아있다. 이는 (정상적인 경우에) 시스템에 의해 접근할 수 없으며, overwrite되거나 clear되기를 기다리고 있다.
  • 함수가 수행되는 동안 EBP0x12F03C로 설정 되어있다. 이는 지역 변수와 인자들이 사용하기 위해 EBP를 참조할 수 있도록 하기 위함이다.
  • 함수 호출 전에 스택에 push된 인자들은 스택 프레임의 아래쪽에서 위치한다.
  • 그 위(더 낮은 주소)에는 return address가 위치한다. 이는 call instruction에 의해 스택에 자동으로 push된다.
  • 그다음에는 old EBP가 위치한다. 이는 caller 함수의 스택 프레임 EBP다.

참고 및 인용

[1] https://practicalbinaryanalysis.com/

[2] http://www.acornpub.co.kr/book/binary-analysis

[3] https://hexterisk.github.io/blog/posts/


Comments