[Stack] Stack Frame 공부하기 - 2

두비니

·

2021. 3. 2. 18:53

 

 

 

 


Stack Frame

-2-


 

오늘은 Stack Frame에 대해서 더 자세히 들여다보도록 합시다.

저번에는 전체적인 흐름에 대해서 파악해보았다면, 이번에는 하나의 함수에만 집중해서 정확히 어떤 일들이 일어나는지 확인해보도록 하겠습니다.

(참고)앞으로의 내용은 기본적으로 메모리 주소(ex. C언어의 pointer 등등..)에 대한 기본적인 이해는 있다는 전제 하에 작성된 글입니다.

 

1. 더 자세히 들여다보기

 

이번에는 조금은 다른 코드를 통해서 분석해보도록 합시다. 

 

//gcc -m32 -g -o ex2 ex2.c
#include <stdio.h>

int main()
{
	int a = 5;
	char arr[64];
	printf("main함수가 호출되었습니다\n");
	return 0;
}

위 코드는 저번 글에서 사용한 코드에서 일부 부분을 변형한 코드입니다.

func1(), func2()는 삭제하고, 단순히 main함수에서 변수를 몇 가지 더 만든 것이 끝입니다.

 

실행결과는 너무 단순한지라 따로 스크린샷을 첨부하지 않겠습니다. 당연히 "main함수가 호출되었습니다"가 뜨겠져?

 

다음은 프로그램을 실행시켰을 때 stack구조입니다.

 

 

이전 글보다는 뭔가 비교적 많이 생긴 걸 볼 수가 있죠? 참고로 뭔가 영어로 쓰여있는 것은 이름이고, 대괄호 안에 쓰여져 있는 것은 크기입니다.

하나하나 천천히 알아가보도록 합시다.

 

(1) ret

ret는 return 레지스터의 줄임말로, 여기에는 함수가 끝나고 난 뒤 돌아갈 주소가 적혀있습니다. 이전 글에서 main, func1, func2함수가 각각 불리워지고 끝나는걸 다시 생각해보면, ret가 실행됨에 따라 다시 func2 > func1 > main함수로 돌아올 수 있었던 것입니다. 

ret 레지스터의 크기는 32bit 운영체제에서는 4byte, 64bit 운영체제에서는 8byte입니다.

 

 

(2) sfp

다음은 sfp입니다. sfp는 Stack Frame Pointer의 줄임말로, 이전 함수에서 ebp가 가리키던 곳을 저장하고 있습니다. ebp에 대해서는 저번 글에 잘 설명해놨으니 모르는 사람들은 참조하도록 합시다.

사실 sfp같은 경우에는 아직은 자세히 알 필요는 없습니다. 나중에 어차피 자세히 배우게 될테니, ret와 변수들 사이에 뭔가 4byte가 더 있구나 정도로 이해하면 좋을 것 같습니다.

sfp같은 경우에도 32bit 운영체제에서는 4byte, 64bit 운영체제에서는 8byte입니다.

 

(3) 각종 변수들(a, arr)

코드를 보면 총 2개의 변수가 선언된 것을 확인할 수 있습니다.

	int a = 5;
	char arr[64];

각각 a와 arr인데, stack을 확인해보면 선언이 된 순서대로 stack에 쌓인 것(pop)을 볼 수 있습니다. 그리고 각각의 크기는 4byte와 64byte인데, 이건 c언어를 배웠으면 다들 기본적으로 알고 계신 부분이라고 생각하고 넘어가도록 하겠습니다.

 

몇가지만 더 덧붙이자면, a가 쓰여져있는 부분에는 5라는 값이 있을 것이고(초기화되었기 때문에), arr배열이 있는 자리에는 쓰레기값이 들어가 있는 것 또한 유추해낼 수 있습니다.

 

2.1 GDB를 활용한 실습 - 준비

 

이번에는 어셈블리어를 직접 관찰해보면서 이론적으로 배웠던 스택 구조가 실제로 이행이 되고 있는지 확인해볼 예정입니다.

실습환경은 Ubuntu 16.04이며, wsl, mac등 터미널 사용만 용이하다면 어떤 환경도 상관없으나 처음 해보신다면 그냥 우분투로 하시는걸 추천드립니다ㅎ

 

우선 다음 코드를 직접 작성해 볼 예정입니다. 지금 실습은 다음 글에 있는 플러그인들이 모두 설치되었다는 가정 하에 진행됩니다.

 

dokhakdubini.tistory.com/39?category=801022

 

우분투를 처음 시작할 때 다운받을 플러그인

우분투를 처음 시작하는 당신에게 -우분투 각종 플러그인 모음- 1. ubuntu 다운로드: https://jimnong.tistory.com/673 우분투 리눅스 다운로드 방법(Desktop 버전) 컴퓨터로 우분투 리눅스(Ubuntu Linux) 공식..

dokhakdubini.tistory.com

 

터미널 창에 다음과 같이 입력하면 vim 창이 뜹니다. 이걸 통해서 코드를 입력해 줍시다.

 

$ vi ex2.c

 

 

//gcc -m32 -fno-stack-protector -g -o ex2 ex2.c
#include <stdio.h>

int main()
{
	int a = 5;
	char arr[64];
	printf("main함수가 호출되었습니다\n");
	return 0;
}

 

 

맨 처음 입력할 때는 i를 눌러서 입력 모드로 바꾸고, 모두 입력한 뒤에는 `를 눌러서 명령 모드로 바꾸고, :wq로 저장하고 나가기를 합시다.

 

 

정상적으로 잘 진행이 되었다면 다음과 같이 되어있을 거예요.

이제는 직접 컴파일링을 해봅시다. 컴파일링은 저 c코드 상태에서 진짜 실행이 가능한 프로그램으로 변환하는 과정입니다.

 

$ gcc -m32 -fno-stack-protector -g -o ex2 ex2.c

 

저걸 터미널에다가 입력해주세요. 각각 옵션들은 궁금하면 구글링을 통해서 더 찾아보는걸로 합시당.

 

 

실행파일이 잘 생긴걸 확인할 수 있죠? 실제로 실행시켰을때 화면에도 잘 출력되는것도 확인할 수 있습니다.

이젠 이걸 gdb로 확인해봅시다.

 

 

2.2 GDB를 활용한 실습

 

이제 기본적인 준비는 완료되었으니, 본격적인 실습을 해봅시다. gdb를 통해서 파일을 열어봅시다.

 

$ gdb -q ex2

 

 

우선 가장 먼저 info func를 통해서 함수들을 확인해 봅시다. 가장 먼저 보이는 부분과 Non-debugging symbols 부분으로 나눌 수 있을 것 같아요. 첫 번째 부분은 사용자가 직접 선언한 부분이 들어가있고, Non-debugging symbols같은 경우에는 기본 함수들이라던가, 프로그램을 실행시키기 위한 기본적인 함수들이 쓰여져 있습니다. 일단은 밑부분에는 그렇게 큰 관심을 주지 않아도 괜찮습니다.

 

그럼 이제 본격적인 어셈 분석을 시작해 봅시다.

 

 

 

peda가 없는 경우에는 그냥 disas main이라고 하시면 됩니다. 다만 저는 peda가 깔려있는지라 pd main이라고 했어요. 차이점은 pd가 함수들같은걸 다른 색깔로 보여줍니다!

 

가장 먼저 볼 부분은 <+10>부분을 봅시다.

 

   0x08048415 <+10>: push   ebp
   0x08048416 <+11>: mov    ebp,esp

 

가장 먼저 볼 부분은 함수의 프롤로그 부분입니다. 앞부분은 생략하고 이 부분부터 보는 이유는 저 두 줄은 모든 프로그램의 시작부분에 공통적으로 들어가 있기 때문에 프롤로그 부분부터 봐도 무방합니다.

추가로 그럼 저 앞부분들은 뭐냐면, 정말 간략하게 설명하자면 프로그램을 구동하기 위해 자체적으로 알아서 시행되는 부분들입니다. 앞으로 더 공부하면 더 자세히 알 수 있게 되실거에요ㅎㅅㅎ

 

그 다음 부분은 다음과 같습니다.

 

   0x08048419 <+14>: sub    esp,0x54

 

sub명령어는 subtraction의 줄임말로, 뺀다는 뜻입니다. 첫번째 글에서 알아봤지만, stack에서는 무언가 쌓이면 주소가 낮아진다고 했죠? 그래서 그렇습니다. 그럼 저 어셈블리어의 뜻은 esp에서 0x54(10진수 84)만큼 무언가가 쌓였다는 것입니다. 그럼 84의 구성을 조금 알아봅시다.

우리가 선언한 부분은 4(int a) + 64(char arr[64]) = 68인데, 나머지 16바이트는 어디서 온걸까요?

이렇게 16바이트가 남는 경우를 dummy라고 합니다. 이건 컴파일할 때 설정할 수 있습니다.

 

   0x0804841c <+17>: mov    DWORD PTR [ebp-0xc],0x5

 

이게 거의 마지막 부분입니다. 

다음 부분은 ebp에서 0xc(10진수 10)만큼 위의 부분에 5를 대입하라는 부분입니다. 이건 int a= 5부분이 어셈블리어로 바뀌었다고 생각할 수 있겠죠? 

 

이후로 볼 어셈블리어는 아래 두가지 정도인 것 같습니다.

 

   0x0804842b <+32>: call   0x80482e0 <puts@plt>

 

여기는 단순히 함수를 부르는 부분입니다. 분명 작성할 때 printf로 작성했는데 puts로 바뀐 이유는 잘...모르겠습니다ㅎ 가끔씩 printf("~~\n")으로 작성하면 알아서 puts로 바뀌는 경우가 있더라구요. 뭔가 컴파일링하면서 최적화 관련 문제인것같습니다. 이런 함수 부분 파악을 위해서 보통 pd명령어를 사용합니다.

 

   0x0804843b <+48>: leave  
   0x0804843c <+49>: lea    esp,[ecx-0x4]
   0x0804843f <+52>: ret    

 

진짜 마지막입니다!

이건 마지막 함수 에필로그 부분인데요, 보통은 leave와 ret만 있습니다. 앞서 있었던 함수 프롤로그와 비슷하게 모든 함수의 마무리 부분에 있으며, 그동안 사용했던 스택을 정리하기 위함입니다.

 

이게 기본적인 어셈을 읽을때 알고있어야 하는 흐름이며, 프로그램이 복잡해질수록 변수 선언이 많아진다던가, 함수 호출이 많아지는 정도입니다. 어렵지 않으니 포기하지 맙시당

 

 

오늘은 이정도로 마무리하려고 합니다.

제가 오늘 gdb 분석을 한 걸 보면, 한줄한줄 모든걸 해석하지 않은 것을 볼 수 있습니다. 이런 어셈분석을 할 때는 리버싱을 하지 않는 한, 그냥 전체적인 흐름만 이해하고 넘어가는 것이 보통입니다. pwnable은 그런 큰 흐름을 이해하는 것이 더 중요하고, 거기에다가 ida라는 강력한 툴도 있으니까요ㅎㅅㅎ

 

아무튼 이런 분석을 바탕으로 앞으로는 주소구하기, 각 레지스터에 들어있는 값 확인, 동적 디버깅 등등 많은 일들을 하게 될 것입니다. 포기하지말고 열심히 해봅시당!

'SYSTEM HACKING > PWNABLE&REVERSING' 카테고리의 다른 글

[STACK] mprotect ROP  (0) 2021.03.17
그래서 우리가 포너블을 하는 이유  (2) 2021.03.07
[Stack] Stack Frame 공부하기 - 1  (0) 2021.03.01
함수의 got 구하기  (0) 2020.10.09
[Stack] Fake EBP에 대하여  (2) 2020.08.31