Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
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
관리 메뉴

Study

리버싱 핵심원리_07_Stack Frame 본문

Reversing/리버싱 핵심원리

리버싱 핵심원리_07_Stack Frame

마늘부추 2019. 1. 15. 02:19

07. Stack Frame

 

7.1 Stack Frame이란?

 

ESP(스택 포인터)가 아닌 EBP(베이스 포인터)를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법

 

- ESP는 프로그램 실행 과정 중 스택 내에서 수시로 변경되기 때문에 이를 기준으로 프로그램을 작성하면 프로그래머도 힘들고, CPU도 정확한 위치를 참고할 때 어려움이 있다.

- 따라서 EBP를 이용하여 함수가 실행되는 동안 일정한 위치를 유지해주면 그것을 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있다.

 

 

 

7.2 StackFrame.exe 분석

 

 

예제 코드를 준비하고 OllyDbg에서 열어 main()을 호출하는 CALL 명령어부터 보기로 한다.

 

 

 

 

CALL StackFra.00401020

main()을 호출하는 명령어이다. main()의 시작 주소는 401020.

CALL 명령어는 의미상 다음과 같은 처리 과정으로 볼 수 있다.

① PUSH EIP

② JMP 401020

 

※ EIP

다음에 실행할 명령어 위치를 담는 레지스터

 

JMP와의 차이점은 JMP는 EIP 값을 수정하지 않는다는 점이다.

 

 

 

 

Stack Window를 보면 ESP 위치에 CALL의 다음 명령어 주소였던 401250이 저장되어있음을 알 수 있다.

 

 

 

 

PUSH EBP

드디어 main()에서의 첫 번째 명령어가 실행되었다.

EBP의 값 18FF80의 값이 ESP가 가리키는 위치에 저장되었다.

 

 

 

 

MOV EBP,ESP

ESP의 값을 EBP에 복사한다.

이는 현재 ESP의 위치를 베이스 포인터로 사용한다는 뜻이다.

이전 함수의 EBP를 기준으로 새 함수의 변수 등에 접근하기 불편하므로 이전 EBP 값을 스택에 저장 후 그 위치를 새 EBP로 사용한다.

 

 

 

 

이전 스택의 모습들인데 이처럼 함수를 호출하면 반드시 위 두 명령어를 통해 이전 EBP를 저장 후 그 위치를 해당 함수에서 사용할 EBP로 지정한다.

나는 처음에 EBP가 돌아갈 스택 주소(베이스 포인트)가 아닌 돌아갈 명령어 주소(40xxxx)를 담는다고 생각하고 있었다...

책을 잘 읽어야겠다.

 

 

 

 

SUB ESP,8

ESP에서 8byte 만큼의 주소를 뺀다(=스택에 8byte 크기만큼의 공간을 확보한다).

이는 선언할 변수 a, b를 위한 공간 할당 과정이다.

a와 b 모두 long 타입 변수이므로 각각 4byte 총 8byte를 차지한다.

 

 

 

 

MOV DWORD PTR SS:[EBP-4],1

변수 a를 의미하는 EBP-4 위치 4byte 공간에 상수 1을 저장한다.

스택 18FF34 위치의 값이 00000001로 세팅되었다.

 

 

 

 

MOV DWORD PTR SS:[EBP-8],2

마찬가지로 EBP-8 위치에 상수 2를 저장한다.

해당 위치 스택의 값이 변경되었다.

 

 

 

 

MOV EAX,DWORD PTR SS:[EBP-8]

PUSH EAX

곧 호출할 add()에 넘겨줄 파라미터를 스택에 저장하기 위해 우선 EAX 레지스터에 변수 b의 값을 저장한다.

그 후 EAX의 내용을 스택에 저장한다.

 

 

 

 

MOV ECX,DWORD PTR SS:[EBP-4]

PUSH ECX

마찬가지로 a의 값까지 파라미터로 저장한 후의 스택, 레지스터의 모습이다.

 

※ 여기서 잠깐!

스택에 저장하는 순서가 기존 a, b와 반대인 이유는

변수가 인자로써 넘겨질 경우엔 스택의 특성인 LIFO에 의해 나중에 들어간 값부터 인자로 전달되기 때문이다.

add(a, b)를 수행하기 위해 a가 먼저 인자로 전달되어야 한다.

 

 

 

CALL StackFra.00401000

드디어 add() 영역에 진입했다.

 

PUSH EBP

MOV EBP,ESP

main()의 시작에서 보았듯 add()에서도 처음 두 명령어는 EBP를 갱신하는 공통적인 과정이 수행된다.

 

 

 

 

SUB ESP,8

MOV EAX,DWORD PTR SS:[EBP+8]

MOV DWORD PTR SS:[EBP-8],EAX

MOV ECX,DWORD PTR SS:[EBP+C]

MOV DWORD PTR SS:[EBP-4],ECX

저장하려는 값이 상수가 아닌 레지스터의 내용일 뿐 main()에서 a, b 변수에 공간을 할당하고 값을 저장하는 과정과 똑같다.

EBP-8 = x

EBP-4 = y

 

MOV EAX,DWORD PTR SS:[EBP-8]

ADD EAX,DWORD PTR SS:[EBP-4]

EBP-8 위치에 저장된 00000001을 EAX로 복사하고

EBP-4 위치에 저장된 00000002를 EAX에 더한다.

 

EAX 레지스터는 함수의 반환값을 저장하는데 쓰인다. 이로써 코드의 return (x + y); 에서 리턴 값 세팅이 완료되었다.

 

 

 

 

MOV ESP, EBP

함수의 종료 과정이다.

ESP에 EBP의 값을 복사함으로써 18FF20 스택 주소가 ESP가 되었고 그 위에 변수를 위해 할당했던 8byte의 주소 공간은 의미가 없게 되었다.

실제로 값이 지워진 것은 아니나 ESP를 끌어내림으로써 의미상으로 존재하지 않는 값이 되어버렸고

새 값이 스택에 쌓이면 자연스레 덮어 씌워지게 된다.

 

 

 

 

POP EBP

RETN

스택에 POP을 함으로써 18FF20 주소가 담고 있던 18FF38 스택 주소가 EBP에 입력되었다.

이로써 현재 베이스 포인터는 main()의 베이스 포인터로 되돌아오게 되었다.

 

그다음, RETN 명령이 수행되는데 이것도 CALL과 같이 의미상 두 가지 처리 과정으로 볼 수 있다.

① POP EIP

② JMP EIP

 

RETN이 아직 수행되지 않은 현재 Stack Window를 보면 ESP 위치에 401041이 저장되어 있음을 볼 수 있다.

이 401041은 main()의 CALL StackFra.401000의 다음 명령어 주소이고,

이를 EIP 레지스터에 담아 다음 수행할 명령어로 지정 후 JMP 한다.

 

 

 

 

401041 위치로 돌아온 모습

 

 

 

 

ADD ESP,8
PUSH EAX
PUSH StackFra.0040B384
CALL StackFra.00401067

코드의 printf("%d\n", add(a, b)); 부분을 실행하기 위한 명령어들이다.

add() 호출 때와 마찬가지로 넘겨줄 파라미터를 저장할 공간을 확보(ADD ESP,8) 하고

EAX(그림에선 이미 printf까지 실행이 끝나 00000002이지만 PUSH EAX가 실행될 때는 00000003이었음) 값을 스택에 저장한다.

401067에는 ASCII 문자열 "%d"가 존재한다. 참조하여 스택에 넣어주는 것이다.

 

위 그림은 "%d"와 2를 인자로 printf()를 수행하고 난 모습이다.

 

※ 여기서 잠깐!

이때 EAX 값을 보면 00000002로 바뀌어 있는데

EAX는 반환값을 저장하는 레지스터이다.

그런데 왜 하필 2가 저장되어 있는가?

이유는 printf()의 반환값이 출력한 문자의 개수이기 때문이다.

실행한 코드는 printf("%d\n", add(a, b)); 였기 때문에 숫자 3과 줄바꿈 문자 \n 총 2개의 문자가 출력되었으므로

EAX의 값이 00000002로 세팅된 것이다.

 

 

 

 

ADD ESP,8
XOR EAX,EAX
MOV ESP,EBP
POP EBP

여기까지 main()을 정리하는 과정으로 앞서 나온 add()의 종료 과정과 비슷하다.

다른 점은 XOR EAX,EAX 부분인데 이는 코드의 return 0; 부분에서 리턴 값 0을 세팅하는 과정이다.

Registers Window를 보면 EAX값이 0으로 세팅되었음을 볼 수 있다.

 

Stack Window를 보면 ESP가 가지고 있는 값은 401250으로 메인 함수를 호출하는 명령어의 바로 다음 명령어 주소이다(첫 번째 그림 참조).

마지막 RETN 명령어에 의해 EIP에 이 주소가 담기고 바로 다음 실행 위치가 된다.

 

 

 

7.3 Comment

티스토리에 처음 올리는 정리 글인데 생각보다 정리 과정에서 많은 시간이 소요되었다.

공부시간의 몇 배나 되는 시간이 걸렸지만, 정리하며 내가 알고 있다고 착각했던 부분을 발견하게 되었고 다시 한번 꼼꼼히 공부하는 계기가 되었다.

 

StackFrame 파트는 리버싱을 시작하며 반드시 꼼꼼하게 짚고 넘어가야 할 부분이다.

ESP와 EBP가 어떻게 변하는지, 스택엔 어떤 값이 어떻게 담기는지, 함수 호출이 어떤 방식으로 이루어지는지 등

리버싱의 기초적이면서도 핵심적인 이론을 설명하기 때문이다.

 

다른 독자분들도 처음 리버싱을 접해보았다면 이 파트만큼은 반드시 정리해보는 시간을 가졌으면 한다.

 

 

글을 읽으시는 분 중에 틀린 곳을 발견하신 분은 댓글로 달아주시면 감사드립니다^^