2014. 1. 29. 16:13 :: 리버싱

안녕하세요. Seek입니다.

오늘은 디버깅할 때 큰 도움이 되는 내용을 공부하고자 합니다.

바로 스택 프레임입니다. 스택 프레임을 이해하고 나면 스택에 저장된 함수 변수와 함수 로컬 변수 등이 쉽게 파악되기 때문에 디버깅에 큰 도움이 됩니다.

 

1. 스택 프레임

 

   스택 프레임이란 ESP(스택 포인터)가 아닌 EBP(베이스 포인터)레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말합니다.

 

   ESP 레지스터는 스택 포인터 역할을 하고, EBP 레지스터는 베이스 포인터 역할을 합니다. ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 어려워집니다. 따라서 어떤 기준 시점(함수 시작)의 ESP 값을 EBP에 저장하고 이를 함수 내에서 유지해주면, ESP 값이 아무리 바뀌어도 EBP를 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있게 되는 것입니다. 이러한 역할을 하는것이 EBP레지스터입니다.

 

<스택 프레임의 구조>

스택 프레임을 어셈블리 언어로 보았을때 이런 구조로 되어 있습니다. 

 PUSH EBP

; 함수시작(EBP를 사용하기 전에 초기 값을 스택에 저장) 

 MOV EBP, ESP  

; 현재의 ESP를 EBP에 저장 

 

 

 ...

; 함수의 본체 

; 여기서 ESP가 변경되더라도 EBP가 변경되지 않으므로 

; 안전하게 로컬변수와 파라미터를 엑세스할 수 있음 

 

 

 MOV ESP, EBP 

; ESP를 정리(함수가 시작했을 때의 초기값으로 복원) 

 POP EBP

; 리턴되기 전에 저장해 놓았떤 원래 EBP 값으로 복원

 RETN

; 함수 종료 

이런 식으로 스택 프레임을 이용해서 관리를 한다면 아무리 함수 호출이 복잡해져도 스택을 완벽하게 관리할 수 있게 됩니다.

 

 

 

2. Stackframe.exe(실습 예제)

 

   실습예제를 통해 실제 어떻게 동작하는지 보도록 하겠습니다.

 

<C언어 실습 코드>

 

<OllyDbg로 실습 프로그램 실행> / 잘안보이시면 클릭해서 확대해서 보세요~

 

실행시키시고 Ctrl + G로 40100 주소로 간 화면입니다. 그곳에 add()함수와 main()함수가 있기 때문입니다.

 

2-1. main()함수 시작 & 스택 프레임 생성

  

C언어코드에서 아래 코드 부분에 해당되는 내용입니다.

 

 

<main()>

 

main()함수 401020에 BP(Break Point)를 설치[F2]한 후 실행[F9]하시면 BP에서 멈출 것입니다.

 

<main() 함수 시작 시 스택의 상태> 

 

위 사진에서 ESP=18FF44, EBP=18FF88 입니다. 특히 ESP(18FF44)에 저장된 값 401250은 main()함수의 실행 이 끝난 후 돌아갈 리턴 주소입니다.

 

이제 한줄씩 알아보도록 하겠습니다.

 

[ 00401020    PUSH EBP ]

   main()함수는 시작하자마자 스택 프레임을 생성합니다. PUSH EBP는 'EBP 값을 스택에 집어널어라'입니다. main()함수에서 EBP가 베이스 포인터 역할을 하게 되니 EBP가 이전에 가지고 있던 값을 스택에 백업을 한다고 생각하시면 됩니다.

(main()함수 마지막에 함수가 종료되기 전에 이 값을 회복시키는것을 보실수 있을겁니다.)

 

[ 00401021     MOV EBP, ESP ]

   'ESP의 값을 EBP로 옮겨라'라는 의미입니다. 이 명령 이후에는 EBP는 ESP와 같은 값을 가지게 됩니다. 그리고 main()함수 끝날 때까지 EBP는 값이 고정됩니다. 이 의미는 스택에 저장된 함수 파라미터와 로컬 변수들을 EBP를 통하여 접근하겠다는 것입니다.

   401020과 401021의 주소에 두 명령어에 의해서 main()함수의 대한 스택 프레임이 생성되었습니다.

 

   OllyDbg에서 오른쪽 아래 스택창에서 우클릭후 'Address'에서 'Relative to EBP'를 선택해 주시면 이제부터 EBP의 위치를 확일하여 어떻게 이동하고 변동되는지 확인하실 수 있습니다.(EBP 기준으로 표시되기 때문에 좀 더 이해에 도움이 될것입니다.)

 

<main()에서 스택프레임 생성 후 초기 값>

 

 

위 사진을 보시면 ESP, EBP는 18FF40으로 동일해졌습니다. 이유는 처음 EBP인 18FF88 주소를 PUSH 명령어로 스택 18FF40에 저장하면서 ESP가 18FF40을 가르키게되었고 사진에서 보시면 18FF40에 18FF88이 들어가 있는것을 확인하실 수 있습니다. 그리고 MOV명령어로 ESP(18FF40)를 EBP에 옮겼기 때문에 두개의 값이 같아 졌습니다. (18FF88은 EBP가 함수 시작할 때 가지고 있던 초기값입니다.)

 

2-2. 로컬 변수 세팅

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

스택에 두 로컨 변수인 a, b를 위한 공간을 만들고 값을 입력하는 부분입니다.

 

[ 401023     SUB ESP, 8 ]

    SUB는 빼기 명령어 입니다. 'ESP 값에서 8을 빼라' 라는 의미입니다. 현재 ESP는 18FF40입니다.( 사진<main()에서 스택프레임 생성 후 초기 값> 참고) 왜 ESP에서 8을 빼는 빼냐면 함수의 로컬 변수는 스택에 저장된다고 했습니다. 그리고 a, b는 long 타입으로 각각 4바이트 크기이고 2개의 변수를 스택에 저장해야되기 때문에 8바이트가 필요함으로 8을 빼서 두 변수에게 필요한 메모리 공간을 미리 확보해두는 의미입니다. 이렇게 미리 확보 해두었기 때문에 이제부턴 ESP 값이 변해도 a, b의 메모리 공간은 훼손되지 않게 됩니다.

 

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

[ 40102D     MOV DWORD PTR SS:[EBP-8], 2] ; b

   위 명령어를 해석하면 'EBP-4 주소에서 4바이트 크기의 메모리에 1을 넣고, EBP-8 주소에서 4바이트 크기의 메모리에 2를 넣어라' 가 됩니다. 즉, EBP-4는 a를 의미하게되고 EBP-8은 b를 의미하게 됩니다. 여기까지 실행 결과 사진이 아래의 사진입니다. 

 

위 사진을 보시면 EBP는 계속 18FF40을 유지하고 있는것을 확인하시면서 넘어가시면 되겠습니다. 그리고 EBP-4에 1이 EBP-8에 2가 저장되어 있는 것을 확인하실수 있습니다.

  

2-3. add()함수 파라미터 입력 및 add()함수 호출

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

 

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

[ 401037     PUSH EAX ]

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

[ 40103B     PUSH ECX ]

[ 40103C     CALL 00401000]

 

위에 5줄의 코드는 a, b를 스택에 넣어주고 add()함수를 호출하는 코드입니다. 한줄식 살펴보도록 하겠습니다. 401034는 EBP-8에 있는 b를 EAX 레지스터에 넣어주라는 의미입니다. 그다음줄 401037에서는 b가 들어가있는 EAX를 스택에 넣어주게 됩니다. 401038에서는 a(EBP-4)를 ECX에 넣어주게 되며 4번쨰 줄은 역시 a가 들어가있는 ECX를 스택에 넣게 됩니다. 마지막 40103C에서는 401000을 불러오라고 되있는데 401000은 add()함수를 의미합니다.

 

여기서 보시면 a를 먼저 꺼내서 스택에 넣는것이 아니라 b를 먼저 꺼내서 스택에 넣으라고 되어있습니다. C언어와는 반대로 저장이 되는데 이것을

'함수 파라미터의 역순 저장'이라고 합니다. 이렇게 거꾸로 넣어야지 나중에 다시 꺼낼때 C언어와 동일한 순서로 불러올수 있게 되는것입니다.

 

아래 사진은 여기까지 실행한 결과입니다. 

 

이제는 add()함수 401000 으로 가보겠습니다.

 

 

가기전에 CALL 명령어가 실행되어서 다른 함수로 가기전에 다시 돌아와야할 주소(복귀주소:return address)를 CPU는 무조건 스택에 저장합니다. 위 사진을 보시면 빨강색 표시된 부분을 보시면 40103C에서 CALL 401000 이 되어서 add()함수로 가라고 되어있습니다. 그리고 다음줄 401041주소가 add()함수가 끝나고 돌아와야할 복귀주소입니다.

 

그래서 아래의 사진은 CALL 명령이 실행되고 난 후 스택의 모습입니다. 

 

EBP-14에 복귀주소인 401041 주소가 들어가어 있는것을 보실 수 있습니다.

 

2-4. add()함수 시작 & 스택 프레임 생성

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다.  

 

 

add()함수에서도 역시 함수가 시작될때 자신만의 스택프레임을 생성하게 됩니다.

[ 401000     PUSH EBP ]

[ 401001     MOV EBP, ESP ]

 

 

위 2둘을 실행하고 add()함수에서 스택프레임이 생성된 결과입니다. 보시면 main()에서 사용하던 18FF40 의 값이 백업되어서 스택에 새롭게 저장이되고 이것을 가르키고 있는 새로운 주소 18FF28으로 EBP와 ESP가 다시 세팅된것을 보실수 있습니다.

 

2-5. add() 함수의 로컬 변수(x, y) 세팅

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

이제 main함수에서 넘어온 a, b를 x, y에 대입하게 됩니다.

 

[ 401003     SUB ESP, 8 ]

 

변수 x, y에 대한 스택에서 메모리를 확보하기 위한 코드입니다.

 

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

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

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

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

 

add() 함수에서 새롭게 스택프레임이 생성되면서 EBP 값이 변했습니다. EBP+8 은 a를 가리키고, EBP+C는 b를 가리킵니다.

그리고 EBP-8은 x, EBP-4는 ,y를 의미합니다. 아래 사진은 여기까지 실행시킨 스택의 모습입니다.

 

 

위 사진에서 보시면 설명이 이해가 가실껍니다. EBP+8 는 1이 저장되어있는 a, EBP+C는 2가 저장되어있는 b를 의미합니다. 그리고 add()함수 시작 할 때 새롭게 지정된 EBP 기준으로 위로 EBP-8이 x, EBP-4가 ,y 가 됩니다.

 

2-6. ADD연산

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

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

EAX에 변수 x의 값(EBP-8 = 1)을 넣습니다.

 

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

의미는 'EAX에 변수 y(EBP-4 = 2)를 더하라'입니다. 그러면 이 윗줄에서 이미 EAX에 1을 넣었고 거기에 방금 2를 더했으니 EAX에는 3이란 값이 들어있게 됩니다.

 

참고로 EAX는 레지스터를 예전에 설명할때 산술 연산에 사용되며 다른 특수 용도로는 리턴 값으로 사용된다고 설명하였습니다. 지금 처럼 리턴 직전에 EAX에 값이 입력되어 있다면 그대로 리턴 값으로 출력되게 됩니다.

 

지금까지의 실행시키고 스택의 변화는 없습니다. 이유는 방금 2줄의 코드에서는 스택을 변경한것은 없기때문에 EAX의 변화만 있었고 스택의 변화는 없습니다.

 

2-7. add() 함수의 스택 프레임 해제 & 함수 종료(리턴)

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

위 2-6이랑 같은 코드 부분입니다. 이제 더하기 연산은 종료가 되어 최종적인 값인 3이 EAX에 저장되어있습니다. 다시 설명드리면 EAX는 리턴 값으로 사용됩니다.

 

이제 리턴되기 전에 스택프레임을 해제해야 합니다.

 

[ 401018     MOV ESP, EBP ]

 

함수 시작당시의 값으로 ESP를 원래 상태로 돌려 놓는것입니다. 함수 초반에 EBP를 스택에 저장하여 백업하고 ESP를 EBP 넣었었습니다. 이제 다시 복구하는 것이죠. 그리고 EBP가 ESP로 들어가면서 x, y 변수를 만들때 사용된 [SUB ESP, 8] 이란 명령어의 효과는 사라집니다. ESP는 현재의 스택의 끝을 나타내니까 다시 떙겨졌다고 생각하면 되겠습니다. 아래 사진을 보시면 이해되실거 같습니다.

 

 

검정색 표시된 부분이 ESP입니다. 그리고 그위에 두줄이 아까 지정하고 add 연산에 사용된 변수 x, y의 스택 메모리 공간입니다. 방금 명령으로 인해 아래로 댕겨졌으니 저 두 변수의 공간은 무효해진것이라 생각하시면 됩니다.

 

[ 40101A     POP EBP ]

 

마지막으로 add() 시작 시에 백업했던 EBP의 값을 스택에서 꺼내서 복원합니다. 복원 값은 18FF40이 되겠네요.

< POP EBP 명령 실행 전 > 

< POP EBP 명령 실행 후 > 

 

 

 

 

 

아래 사진은 스택 프레임을 해제난 후에 결과 사진입니다. 

 

ESP = 18FF2C 에 저장되어 있는 값인 401041 은 앞에서 main()함수에서 CALL 401000 명령으로 CPU가 스택에 입력한 복귀 주소입니다. EBP는 백업했던 값으로 돌아온것을 확인 할 수 있습니다. 즉, add()함수가 호출되기 직전의 상태로 돌아온것입니다.

 

[ 40101B     RETN ]

 

위 명령이 실행되면 저장되어있던 복귀 주소 401041 로 돌아오게 됩니다. 아래 사진은 그 결과입니다. 

 

이 사진과 add()함수에 들어가기 직전인 CALL 명령이 실행되기 전의 사진(2-3 부분을 보시면 있는 사진)과 비교해보세요. 스택의 값이 동일합니다.

 

프로그램들은 이런 식으로 스택을 관리하기 때문에 함수 호출등이 계속 충첩되더라도 스택이 겹치거나 깨지지 않고 잘 유지 됩니다.

 

2-8. add() 함수 파라미터 제거(스택 정리)

 

이제 RETN 명령으로 인하여 main()함수로 돌아왔습니다.

 

[ 401041     ADD ESP, 8 ]

 

위쪽에 401041으로 돌아온 결과 사진을 보시면 EBP-10, EBP-C 가 있습니다. 이는 add()함수로 넘겨준 a, b입니다. 이제는 add()함수가 완전히 종료되었기 때문에 필요가 없습니다. 그래서 현재 ESP인 18FF30에 8을 더하여 두변수를 제거하면서 스택을 정리해주는 것입니다.(a, b 파라미터는 각각 long 타입으로 4바이트이기 때문에 8바이트입니다.)

 

여기까지 실행 후 스택의 결과사진입니다. 

 

EBP-10, EBP-C가 당겨진 모습입니다. 두 변수가 사라진것이죠.

 

2-9. printf() 함수 호출

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

 

[ 401044     PUSH EAX ]

[ 401045     PUSH 0040B384 ]

[ 40104A    CALL 00401067 ]

[ 40104F    ADD ESP, 8 ]

 

첫번째 명령 PUSH EAX 는 add()함수에서 넘어온 리턴값인 "3"입니다. 즉, 의미는 'EAX인 3을 스택에 넣어라'가 되겠죠. 그 다음줄은 위에 C언어 코드를 보시면 printf 함수에 파라미터가 2개인것을 보실 수 있습니다. "%d\n" 와 add(a,b) 이렇게 2개입니다.

첫번째 명령인 EAX를 스택에 넣는 것은 printf 에서 add(a, b) 인자가 되겠지요. 그리고 다음중 PUSH 0040B384 은 %d\n 를 의미한다고 알아두시면 될거 같습니다.

 

즉, 파라미터가 2개이니까 두번 PUSH를 한다. 그중에 하나는 저희가 알고 있고 지금까지 계산해온 EAX를 넣는다. 이정도가 되겠습니다. 

PUSH EAX, PUSH 40B384 이 2개 명령을 실행 한 결과 사진입니다. 

printf 함수가 호출 되기전의 스택의 모습에서 추가가 된것을 보실수 있습니다.

EBP-C 에 EAX가 들어갔으니 3이 들어있는 모습

EBP-10 에 %d\n이 들어간 모습 입니다.

 

3번째 줄은 CALL 401067 으로 바로 printf 함수를 불러오는 것입니다. 2개의 파라미터를 앞에서 스택에 넣어줬으니 마지막에 함수호출을 하여 넣어둔 2개의 파라미터를 이용하면 되겠지요?

 

여기서 401067 은 Visual C++ 에서 생성한 C표준 라이브러리 입니다. 이부분은 이정도만 코멘트하고 넘어 가겠습니다.

 

그리고 마지막 4번째 줄에서 스택을 다시 정리하는 명령인것을 알수 있습니다.

3번째 줄에서 printf 함수를 호출하고 종료되고 돌아왔기 때문에 사용 완료된 스택을 정리하는 것입니다.

 

2-10. 리턴 값 세팅

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다. 

 

main() 함수의 리턴 값을 0으로 초기화 세팅해줍니다.

 

[ 401052     XOR EAX, EAX ]

 

XOR 명령은 Exclusive OR bit 연산입니다. 연산 방법은 같은 값을 XOR 하면 0이 되는 방식입니다.

여기까지 실행 한 스택 결과 사진입니다.

 

 

보시면 스택을 변동하는 명령어는 없기때문에 그대로이고 EAX가 0으로 바뀐것을 보실수 있습니다. 위에서 리턴값은 EAX의 값이라고 했습니다. 목적 역시 main()함수까지도 끝났으니 리턴값을 초기화 0으로 세팅한다고 했으니 원하는대로 0으로 세팅된것을 확인할 수 있습니다.

 

2-11. main() 함수 스택 프레임 해제 & main() 함수 종료

 

C언어코드에서 아래 코드 부분에 해당되는 내용입니다.  

 

main()함수가 종료되는 부분입니다. 위에서 했던 add()함수 종료시에 했던 스택 프레임 해제와 동일합니다.

 

[ 401054     MOV ESP, EBP ]

[ 401056     POP EBP ]

 

위 두함수로 인하여 스택프레임은 해제 되었습니다. 동시에 main()함수의 변수인 a, b 역시 더이상 무효하게 되었습니다. 여기까지 실행한 후 스택의 결과 사진입니다.

어디서 많이 본 스택의 모습이지 않습니까? 기억 못하시겠지만 main()함수 시작할 때 스택의 모습입니다.

2-1<main() 함수 시작 시 스택의 상태> 사진을 보시면 동일하다는 것을 확인할 수 있습니다.

기억은 못하시더라도 이런식으로 main() 함수가 종료되면서 스택프레임을 해제하면 초기의 스택모습으로 복원 된다는것을 계속해서 인식하면 될것입니다.

 

[ 401057     RETN ]

 

이제 main()함수가 종료되면서 리턴주소인 401250 으로 돌아가게(점프) 됩니다. 이러함으로써 프로그램이 종료되게 됩니다.

 

3. 마무리

 

스택 프레임을 간단히 설명하자면 수시로 변경되는 ESP 레지스터 대신 EBP 레지스터를 사용하여 로컨변수, 파라미터, 복귀 주소 등을 관리하는 방법입니다. 아마 여기까지 잘 이해하셨다면 디버깅에 대해 조금 자신감(?)이 생겼을거라 생각됩니다. 저도 이부분을 공부하면서 함수의 로컬변수, 파라미터, 복귀주소, 리턴 값 등이 어떻게 처리되고 어떻게 동작하는 원리를 이해했기 때문입니다. 많은 도움 되셨기를 바랍니다.

 

 

참고서적 : 리버싱 핵심 원리 / 저자 : 이승원

 

':: 리버싱' 카테고리의 다른 글

UPack PE 헤더 분석 + 디버깅  (1) 2016.12.31
[정보 보안 개론] Chapter 06 악성코드  (0) 2016.11.07
[Red_Seek] abex' crackme #1 (크랙미 #1)  (0) 2014.01.23
[Red_Seek] 스택  (0) 2014.01.22
[Red_Seek] IA-32 Register  (0) 2014.01.14
posted by Red_Seek