본문 바로가기

시스템

[시스템] Stack Frame (스택 프레임)

어셈블리를 통해 스택프레임의 생성과 소멸의 과정에 대해 살펴보자.

예제코드는 다음과 같다.

다음은 gcc명령어를 통해 컴파일한다.

gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -no-pie -fno-pic -o stackframe stackframe.c

옵션을 하나하나 살펴보자.

 

-m32 : 32bit로 컴파일

-fno-stack-protecotr : 버퍼오버플로우가 발생했을 때 gcc는 canary를 이용해 버퍼오버플로우가 발생한 것을 감지하고 프로그램을 종료하는데 우리는 이를 SSP(Stack Smashing Protection)라 부르는데 이러한 보호기법(SSP)을 off한다.

-mpreferred-stack-boundary=2 : 32bit로 컴파일할 경우 main함수에 불필요한 instruction을 제거해준다.

-no-pie : 주소 고정

-fno-pic : 위치 독립 코드 사용X

-o : 출력파일명 지정

 

gdb를 실행시킨다. -q옵션은 gdb실행시 출력되는 인사말(?)을 출력하지 않는다.

 

gdb를 실행하고 main함수를 disas한 결과이다.

 

어셈블리 코드에 대응하는 레지스터와 스택의 변화를 살펴보자.

살펴보기전 먼저 해야할 일이 존재하는데 사진을 보면서 설명하겠다.

먼저 b *main을 입력해 main함수에 breakpoint를 걸어준다.

함수의 이름외에도 메모리주소도 breakpoint지정이 가능하다. ex) b *0x000004ed

breakpoint 삭제는 d *[Num]으로 삭제가능하다. -> [Num]은 info b의 Num을 의미.

 

breakpoint를 걸어준 후 run명령어를 입력하면 breakpoint까지 프로그램이 실행된다.

run이 실행된 화면은 다음과 같다. (...)

나의 경우 run을 입력했을 때 warning: Breakpoint address adjusted from 0xf7fe5db0 to 0xfffffffff7fe5db0.와 같은 메세지가 출력되면서 실행오류가 발생했다.

해결방법은 다음과 같다.

sudo apt install gdb=8.1-0ubuntu3 # downgrade GDB to the working version

sudo apt-mark hold gdb # prevent upgrading (until the repository version is fixed)

대충 버전호환관련과 관련해서 발생한 문제인 것 같다.

 

다시 본론으로 돌아와 breakpoint부터 코드를 하나씩 실행시켜 가보자.

ni를 이용해 다음 명령어를 실행시킬 수 있으며 실행결과는 다음과 같다.

push ebp의 명령이 실행되면서 stack에 0x0이 들어간 것을 확인할 수 있다.

또한 stack에 값이 들어감으로써 esp의 값이 4바이트 감소한 것도 확인할 수 있다.

 

다시 ni를 눌러 다음 명령어를 실행시켜보자.

mov ebp, esp의 명령 실행으로 ebp와 esp가 가리키는 곳이 같아졌음을 확인할 수 있다.

여기까지의 명령어 push ebp와 mov ebp, esp가 실행되므로써 스택프레임이 형성되었다.

 

이어서 다음 명령어를 확인해보자.

sub esp, 0x4가 실행됨으로 인해 스택상에 4바이트만큼의 공간이 할당된 것을 확인할 수 있다.

그런데 할당된 공간에 0x1d4d6c가 들어가 있는 것을 확인할 수 있는데 이는 전에 스택이 사용되고 난 후 초기화되지 않아 값이 남아있는 것으로 쓰레기 값이다. (이 공간을 왜 할당한건가요??) <- 이에 대한 물음의 답은 다음과 같다. 이 명령어는 예제코드에서 int c = sum(1, 2)에 대응하는 명령어로써 sum함수호출에 대한 반환값을 저장하기 위한 int형 변수 c의 공간을 할당한 것이다. sum함수가 아직 호출되지 않았으므로 당연히 이 공간에는 쓰레기 값이 들어있을 수 밖에 없는 것이다.

 

ni를 입력해 다음 명령어를 실행해보자.

push 0x2로 스택에 0x2의 값이 들어간 것을 확인할 수 있다.

 

다음 명령어를 실행시켜보자.

push 0x1 명령어가 실행되면서 스택에 0x1이 들어간 것을 확인할 수 있다.

push 0x1명령어가 실행되고 eip가 가리키는 명령어는 call 0x8048412 <sum>으로 다음명령어를 실행하면 sum함수를 호출할 것이다. 이때 ni대신 si를 입력해 sum함수 내부로 들어가보자. (ni가 next into를 의미한다면, si는 step into를 의미한다.)

 

si를 입력해 sum함수 내부를 들여다보자.

그런데 여기서 이상한 점을 발견할 수 있다. 바로 sum함수를 호출함과 동시에 스택에 어떠한 값이 들어간 것이다.

과연 스택에는 어떤값이 들어간 것일까. 스택에 들어간 0x8048405라는 메모리주소는 우리가 실행한 call 0x8048412 <sum> 명령어가 들어가있는 0x8048400 메모리주소의 다음 명령어가 들어가 있는 주소이다. 즉, sum함수를 수행하고 main함수로 돌아와 이어서 실행할 명령어를 기억하기 위함이다. 이는 push eip가 일어난 것으로 함수의 프롤로그가 실행되기전 RET이 실행된 것이라고 할 수 있다.

 

다음 명령어를 실행시켜보자.

push ebp명령어가 실행되면서 스택에 ebp의 값이 들어간 것을 확인할 수 있다. 이는 sum함수의 스택프레임이 소멸되고 main함수의 스택프레임으로 돌아갔을 때 기존의 ebp(main함수의 ebp)로 돌아가기 위함이다. 이를 SFP라고 한다. 따라서 0xffffd1c8은 main함수의 ebp인 것을 알 수 있다.

 

ni를 입력해 다음 명령어를 실행시켜보자.

mov ebp, esp 명령어가 실행됨으로 인해 ebp의 값이 esp의 값과 같아진 것을 확인할 수 있다. ebp가 스택의 맨위로 올라온 것인데 이는 새로운 시작을 의미한다고 볼 수 있다.

 

다음 명령어를 실행시켜보자.

mov edx, dword ptr [ebp+0x8] 명령어가 실행되면서 현재 ebp에서 8바이트 증가한 주소에 들어있는 값을 edx레지스터에 넣는다. 현재 ebp는 0xffffd1b4로 8바이트 증가한 스택의 주소는 0xffffd1bc이며 이곳에는 0x1이라는 값이 들어있다.

 

다음 명령어를 실행시켜보자.

mov eax, dword ptr [ebp+0xc] 명령어가 실행되면서 eax레지스터에 ebp로부터 12바이트 증가한 곳에 들어있는 값을 넣는다. 현재 ebp는 0xffffd1b4로 12바이트 증가한 스택의 주소는 0xffffd1c0이며 이곳에는 0x2라는 값이 들어있다.

 

다음 명령어를 실행시켜보자.

add eax, edx명령어가 실행되면서 eax와 edx에 들어있는 값을 더한 후 eax에 넣는다.(eax = eax+edx)

 

다음 명령어를 실행시켜보자.

pop ebp명령어가 실행되면서 스택에서 push된 ebp가 사라진 것을 확인할 수 있다. 또한 ebp레지스터의 ebp가 push했었던 ebp로 바뀐것도 확인할 수 있다. sum함수의 스택프레임이 소멸된 것을 의미한다.

 

다음 명령어를 실행시켜보자. (끝이 안보이는 다.명.실.....)

ret명령어가 실행되면서 sum함수를 빠져나온다. ret명령어는 pop eip, jmp eip로 이루어져있으므로 sum함수가 호출되면서 스택에 push되었던 eip가 pop된 후 jmp하게 되어 eip는 main함수에서 call 0x8048412 <sum>명령어의 다음 명령어를 가리키게 된다.

 

다음 명령어를 실행시켜보자.

add esp, 0x8 명령어가 실행됨으로 인해 기존의 esp(0xffffd1bc)에서 8바이트가 증가한 0xffffd1c4가 esp가 된다. 여기에는 아까 살펴보았던 쓰레기값이 있는 곳이다.

 

다음 명령어를 실행시켜보자.

mov dword ptr [ebp-0x4], eax 명령어의 실행으로 인해 eax레지스터의 값이 ebp에서 4바이트만큼 감소한 곳에 들어가게 된다. 현재 ebp는 0xffffd1c8로 4바이트 감소한 곳의 주소는 0xffffd1c4이고 eax의 값은 0x3으로 0xffffd1c4에는 0x3이 들어가게 된다.

 

다음 명령어를 실행시켜보자.

mov eax, 0x0 명령어가 실행되면서 eax레지스터에 0x0의 값이 들어간다. 이는 예제코드에서 return 0;에 해당하는 부분으로 0 또한 main함수를 호출한 곳으로 반환될 것이다.

 

다음 명령어를 실행시켜보자.

leave 명령어가 실행되는데 leave명령어는 mov esp, ebp와 pop ebp로 이루어져 있다. 먼저 mov esp, ebp의 결과로 esp의 값이 ebp와 같아진다. 그 결과 프롤로그(엄밀히 말하면 mov ebp, esp) 후에 스택에 쌓인 모든 값들이 사라지게 된다. 다음은 pop ebp에 대한 결과로 ebp가 main함수를 호출한 스택프레임의 ebp로 돌아가게 된다.

 

다음으로 ret 명령어가 실행되면서 프로그램이 끝이나게 된다.

 

이상으로 어셈블리를 통해 스택프레임의 생성과 소멸에 대해 살펴보았다.

 

궁금한점

1. 왜 스택에 0x2부터 들어가는 것인가.

반응형