System Hacking

스택 프레임 (Stack Frame)

jir4vvit 2020. 7. 2. 17:05
출처 : 동빈나님 유튜브 youtu.be/TxWOaKE5w_s

유튜브를 보며 공부하던 중 두고두고 복습이 필요할 것 같아 정리를 하려고 한다..


스택 프레임(Stack Frame)을 이해하기 위해서 간단한 예제를 만들어보겠다.

 

main() 함수를 살펴보면 1과 2를 더한 값(sum(1,2))을 변수 c에 삽입하고 프로그램을 종료한다.

 

main() 함수 실행 -> main() 함수에서 sum(1, 2) 실행 ->  sum() 함수에서 1과 2를 더한 값 반환 -> main() 함수에서 변수 c에 그 값을 저장 -> 프로그램 종료

 


 

이제 c언어 코드파일인 sum.c를 어셈블리어인 sum.a로 변환해주도록 하겠다. 

 

 

저번 시간에 스택영역에서는 다양한 취약점이 존재한다고 했다. gcc에서는 이 취약점들을 기본적으로 다 방어해주고 있다. 우리는 실습을 위해 -fno-stack-protector 옵션을 이용하여 그 기능을 꺼주도록 하겠다.

그리고 64비트 환경에서 컴파일한다는 것을 stack boundary를 4로 지정하여 명시해주도록 하겠다.

 


 

이제 vi 에디터로 sum.a를 열어보기 전에 이론상의 작동과정을 살펴보자.

 

sum.c가 컴파일 되고 실행이 되면 가장 먼저 main() 함수가 실행이 된다.

메모리구조에서 stack영역 구조

 

main() 함수가 실행되고 stack의 특성상 아래서부터 스택이 쌓인다고 했을 때 맨 아래에는 RET인 Return Address가 존재한다. 

사실 맨 처음 실행되는 건 main() 함수가 아니라 start 함수인데 start의 구조상 main을 부르게 된다. main() 함수가 끝나고 다시 어디로 돌아갈 지 명시해줘야하는데 이것이 Return Address이다.

모든 함수는 이런식으로 Return Address를 가지게 된다.

 

RBP는 스택이 여기서부터 시작할 것이라는 베이스 포인터를 뜻한다. 한마디로 여기서부터 스택이 차례대로 위쪽으로 쌓일 것이라는 것을 의미한다.

 

RBP 위에 변수 c버퍼가 자리잡게 된다.

 

 

이제 main() 함수에서 sum() 함수를 호출했을 때 스택모양이 어떻게 변하는지 살펴보자.

위에 빨간 부분이 sum() 호출 이후에 새로 쌓인 sum() 함수의 스택프레임이다. 아래의 파란부분은 기존의 main() 함수의 스택프레임이다.

 

main() 함수가 끝나기 전에 sum() 함수를 불러왔기 때문에 main() 함수 위로 sum() g함수가 차곡차곡 쌓이게 된다. 이후에 sum() 함수가 할 일을 다 마치고 리턴을 하게 되면 위에서부터 하나하나 다 POP이되어 스택에는 main() 함수의 스택프레임만 남게 된다.

 

빨간 부분의 sum() 함수의 스택프레임을 살펴보자.

일단 RET가 보인다. RET는 sum()을 다 실행하고 나서 돌아가야할 위치다.

특이하게 RET 전에 변수 x와 변수 y가 먼저 쌓여있는데 이것들은 sum() 함수의 매개변수를 뜻한다. 여기서는 각각 1과 2로 생각할 수 있겠다.

RET 위로 여기서부터 스택이 쌓일 것이라는 RBP버퍼 공간이 보인다.

 

파란 부분의 main() 함수의 스택프레임에서 변수 c에 sum() 함수가 리턴되어 sum() 함수에서 실행한 결과값인 3이 담기게 될 것이다.

 

 

다시한번 말하지만 Return Address란 함수가 실행되고 난 뒤에 돌아가는 메모리 주소라고 했다. 이 Return Address를 해커가 임의로 조작함으로써 서버에 나쁜 짓을 끼칠 수 있다. 이게 바로 버퍼오버플로우다.


이제 어떻게 돌아가는지 어셈블리어 코드를 통해 실제로 확인해보자.

 

 

mian 함수부터 살펴보자.

sum.a의 일부

sum을 call하는 부분까지 살펴보겠다. 

참고로 main함수가 불러옴과 동시에 Return Address가 제일 아래에 자리잡게 된다.

pushq      %rbp            // RBP를 push해서 리턴어드레스 위로 들어가게 됨 
movq       %rsp, %rbp    // rsp는 rbp가 된다. 둘이 동일한 위치를 가리키게 됨 
subq       $16, %rsp       // rsp를 16만큼 빼준다. 그럼 16만큼 위로 점프하게 되는데 이는 16만큼 공간을 확보해서 그 안에서 어떠한 코드를 작성하겠다라는 뜻임 
movl       $2, %esi         // esi에 2를 넣고 
movl       $1, %edi         // edi에 1을 넣는다 (매개변수) 
call          sum             // sum함수를 호출

* //은 내가 임의로 정한 주석표시이다.

 

이걸 이미지로 한 눈에 보게되면 아래처럼 된다.


이제 sum 함수를 살펴보자.

 

sum.a의 일부

방금 main에서 rsp가 16만큼 공간을 확보했었다.

pushq      %rbp               // RBP를 push해서 리턴어드레스 위로 들어가게 됨
movq      %rsp, %rbp        // rsp는 rbp가 된다. 둘이 동일한 위치를 가리키게 됨 
movl       %edi, -4(%rbp)   // 1을 넣음
movl       %esi, -8(%rbp)   // 2를 넣음
movl       -4(%rbp), %edx  // 1의 값을 edx 레지스터에 넣음
movl       -8(%rbp), %eax  // 2의 값을 eax 레지스터에 넣음
addl       %edx, %eax       // edx값을 eax에 더해줌
popq       %rbp              // 마지막으로 rbp를 삭제해줌으로써 ret로 돌아가게 함
ret                               // 그럼 이제 sum이란 함수를 불렀던 위치로 돌아가게 됨

* //은 내가 임의로 정한 주석표시이다.

참고로 rax(eax) 같은 경우는 특정한 함수가 끝날 때 그 반환값을 가지고 있는 레지스터이다.

 

 

 

sum() 함수가 종료되면 아래처럼 될 것이다.

 


이제 아까 main() 함수에서 남은 네 줄을 보자.

sum.a 중 일부

movl       %eax, -4(%rbp)   // eax값(3)을 -4(%rbp)위치에 둠
movl       -4(%rbp), %eax   // 3이란 값을 다시 eax에 넣음
leave
ret                               // 최종적으로 3이라는 값이 반환이 됨

* //은 내가 임의로 정한 주석표시이다.

실제로 어셈블리에서는 _start 함수가 main()을 불러내기때문에 최종적으로 3이라는 값을 그쪽으로 전달된다고 한다.

 

 

따로 코드 실행은 하지 않겠다..

 

 

 

공부를 해본 결과, 스택프레임이란 한마디로 "함수 자신만이 갖는 공간"이라고 말할 수 있겠다..