스택 프레임 (Stack Frame)은 프로그램에서 선언되는 로컬 변수와 함수 호출에 사용된다.
이를 이해하고 나면 스택에 저장된 함수 파라미터와 함수 로컬 변수 등이 쉽게 파악된다고 하니 열심히 공부해보자.
스택 프레임
스택 프레임은 ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 사용해 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법이다.
복습 겸, ESP는 스택 포이터 역할을 하고, ebp는 베이스 포인터 역할을 한다.
ESP는 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP를 기준으로 하면 프로그램을 만들기 어렵고, 위치를 찾기 힘들 것이다. 그러므로 나온 개념이 ESP를 임시로 EBP에 저장해주고 이를 함수 내에서 유지해주면 ESP가 변해도 EBP를 기준으로 변수, 파라미터, 복귀 주소 등에 안전하게 접근할 수 있을 것이다.
이게 EBP Register의 역할이다.
스택 프레임은 위와 같이 생겼다고 볼 수 있다.
먼저, PUSH EBP를 통해 현 EBP주소를 스택에 올린다. 이후, ESP의 값을 EBP에 옮겨준다.
그다음부터 함수가 실제로 진행될 때 ESP값은 막 바뀌겠지만, 함수가 끝날 때에 ESP에 다시 EBP의 값을 올려주고, EBP에는 처음 스택에 올려놓은 그 값을 빼와 넣어주며 함수가 마치게 된다.
이런 방법으로 정리해주면 호출이 아무리 복잡해져도 스택을 완벽하게 관리할 수 있다.
스택에 복귀 주소를 저장하면, 버퍼 오버플로우로 복귀 주소가 저장된 스택 메모리를 바꿀 수 있겠다...!
이 프로그램으로 확인해 보겠다. 책에서는 main주소를 알려주지만, 난 직접 찾아보겠다.
00401045에서 %c가 보인다. 더 들어가 보자.
어떤 함수가 있음을 확인할 수 있다.
위의 그림에서도 볼 수 있듯이, EBP-4에 1을, EBP-8에 2를 넣는다.
이후 EAX에 EBP-8을 넣고 스택에 PUSH 한다.
또한 ECX에 EBP-4를 넣고 스택에 PUSH 한다. 이후 밑에 있는 CALL에서 위 두 인자를 같이 넘겨준다. 이후 반환 값을 출력해주는 과정을 보아 main함수라고 예측할 수 있겠다.
리버싱 하기 편하게 위와 같이 comment를 달아주자.
이해가 안 가는 부분이 있는데, [EBP-4]에 1을 넣어주는데 왜 또 스택에 ECX를 PUSH 하는지 모르겠다.
무슨 소리냐면,
왜 스택을 이렇게 쌓는지 모르겠다. 아 스택을 PUSH POP 해줘야 해서 그런가 으ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ 알다가도 모르겠다.
우선, 본격적으로 main함수에 대해 분석해보자.
1. main() 함수 시작 & 스택 프레임 생성
main() 함수 시작에 BP를 걸고 실행해보자.
ESP = 19FF2C이고, EBP = 19FF70이다. ESP가 가리키는 19FF2C에 저장된 값 401250은 main() 함수가 끝나면 돌아갈
리턴 주소이다. 이 점을 기억해두자.
main() 함수는 시작하자마자 스택 프레임을 형성한다. 좀 더 구체적으로 들어가 보자. PUSH는 값을 스택에 집어넣는 명령이다.
PUSH EBP는 EBP값을 스택에 집어넣어라 정도로 해석할 수 있겠다. main() 함수에서는 EBP가 베이스 포인터 역할을 할 테니 이전에 가지고 있던 값을 백업하는 느낌이다.
그 이후로 있는 것이 MOV EBP, ESP이다. ESP의 값을 EBP로 옮기는 명령으로 해석할 수 있다.
결국 이 명령 이후부터는 EBP는 ESP와 같은 값을 가지게 된다. 그리고 main() 함수가 끝날 때까지 EBP값은 고정된다.
즉 스택에 저장된 함수 파라미터와 로컬 변수들은 EBP를 통해 접근한다. 왜냐면 생각해보면 ESP가 막 바뀔 텐데 어떻게 ESP를 가지고 값을 접근을 할까?? 그러니 EBP를 쓰는 것이다. ESP가 바뀌면서 스택에 값을 쌓아 나아가도 EBP를 기준으로 위치를 표현해줄 수 있는 게 그 이유라고 생각된다. (혼자 생각하는 아무 말이다.)
이후 스택 창을 볼 때는 Relative to EBP를 통하며 EBP 위치를 기준으로 스택을 확인하자.
우선 EBP가 19 FF28로 ESP와 동일함이 보인다. 또한 그 19FF28이 가리키는 값은 19FF70으로, 이 값은 main() 함수 시작할 때의 EBP이다.
2. 로컬 변수 세팅
우선 한 줄 한 줄 내려가 보자.
ESP에서 8을 빼고 있다. 저 위 상태를 보면 현재 ESP는 19FF28인데, 여기서 8을 빼게 되면 19FF20이 된다.
왜 8을 빼는지에 대해 먼저 생각해보자. 먼저 소스코드를 볼 때 a, b는 long으로 선언해주며 long의 크기는 4바이트이다.
어차피 변수를 선언해줄 때는 EBP레지스터를 기준으로 이리저리 해주면 되니 ESP를 8만 빼주면 우선 a, b의 크기는 정말 확보한 것이다.
4바이트씩 1과 2를 EBP-4와 EBP-8의 주소에 넣고 있다. DWORD는 4바이트다.
여기까지 실행하고 스택을 봐보자.
EBP-4 주소인 19FF24에는 1이 들어가 있고, EBP-8인 19FF20에는 2가 잘 들어가 있다. 이를 통해서 DEC ESP, 8명령도 이해할 수 있게 되었다.
3. add() 함수 파라미터 입력, 호출
add() 함수 호출 과정이다. 주의할 점은 [EBP-8]이 먼저 스택에 들어가고, 이후[EBP-4]가 스택에 들어간다는 것이다.
또한 add() 함수를 호출하기 전에 CPU는 add() 함수가 종료되고 복귀할 주소를 스택에 저장해야 한다.
40103C에서 호출을 하고 main() 함수에서 다음 명령은 401041이니 스택에는 401041이 들어가야 한다.
401041이 스택에 들어가졌다. 그래서 add함수 내에서는 함수가 끝나기 전까지 POP을 여러 번 할 것 같다. (아니면 모르겠당 ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ) 아 뒤에서 스택 프레임 형성할 때 EBP가 19FF14로 되겠구나.
이제 add() 함수 시작하는 곳으로 가보자.
4. add() 함수 시작 & 스택 프레임 생성
add() 함수도 위의 사진으로 보듯이 자신만의 스택 프레임을 형성한다.
이제 원래 사용하던 19FF28은 스택에 들어가게 되었으며 EBP는 19FF10이 됨을 확인할 수 있다.
5. add() 함수 지역 변수 (x, y) 세팅
add() 함수의 경우 파라미터가 존재한다. 소스코드를 보면 x, y를 위한 공간이 필요하다. 정확이는 long이 2개라 8바이트가 필요하다. 그렇게 ESP를 8 빼주면, 즉 스택을 8만큼 높이면 EBP와 ESP사이에 x, y를 위한 공간을 안전하게 만들어 줄 수 있다.
다음 코드인데 이런 주석을 달아보도록 하겠다.
해당 코드를 시행했을 때의 스택 상황이다. 코드대로 잘 들어간 것이 확인된다
여기서 나의 궁금증이 생겼다. 왜 EBP-8에 1이 들어가고 EBP-4에 2가 들어가야 하는지 모르겠다.
어셈블리어를 따라가다 보면 이해가 가지만, main() 함수 내에서의 변수 선언 경우 a(=1)는 EBP-4에, b(=2)는 EBP-8에 들어가게
되었다. 그런데, 왜 add() 함수 내에서는 x(=1)가 EBP-8에, y(=2)가 EBP-4에 들어가는지 모르겠다.
main() 함수의 경우 add() 함수를 호출하기 전에 Calling Convention에 의해 스택을 만들기 위해 main() 함수에서 저장된 변수 순서를 뒤집는 것이 이해되었지만, 왜 add() 함수에서는 이게 뒤집혀서 저장된다는 것인가.
으아ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ모르겠다.................
아는 형한테 여쭈어본 결과, 이런 순서는 컴파일러에서 지멋대로 정한다고 하셨다. 그래서 bof나 포너블 문제의 경우 그렇게 하지 못하게 컴파일 할 때 옵션도 넣어준다고 하셨다. 감사합니다 ㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ
어쨌든 이 밑으로 다시 분석을 해보겠다.
6. ADD 연산
코드상으로 보면 return(x+y)의 x+y값을 저장하는 부분에 해당한다.
무난하게 쉬운 코드 같다. EAX 레지스터에 a값을 넣어주고, 그 레지스터에 b를 더해준다.
참고로, EAX 레지스터는 산술 연산에도 사용되지만, 함수의 리턴 값 저장에도 사용된다. 그러므로 EAX에 저장하는 거라고 생각해도 되려나? EAX 레지스터를 확인해 보겠다.
짜자자자자자자자자자ㅏㅈ자자자자잔 확인이 되었다.
7. 스택 프레임 해제 & 함수 종료(리턴)
위의 코드는 스택 프레임을 해제하는 코드이다. ESP에 EBP값이 들어가게 되고, POP명령을 통해 EBP에 넣어준다.
먼저 MOV ESP, EBP를 실행해보자.
실행하기 전의 ESP = 19FF08, 실행하고 난 후의 ESP = 19FF10이다.
실행을 하니 ESP는 EBP의 값이 되었고, 그렇게 스택도 19FF10을 가리키고 있다. 다시 생각해보면 add() 함수 내에서 생성된 x, y라는 변수는 더 이상 유효하지 않다는 의미이다.
이제 POP EBP를 실행해보겠다.
ESP는 0019FF14를 가리키고, EBP는 0019FF28이 될 것이다.
딩동 댕댕댕댕댕댕댕ㄷ애댕댕댕댕댕댕댕댕 지금처럼 내가 예측한 내용이 맞았을 때 난 기분이 좋다.ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ
EBP는 main() 함수 내에서의 EBP값이 되었다. ESP가 가리키는 스택의 값도 main() 함수에서 add() 함수를 CALL 한 이후의 주소이다. RETN 명령을 해주면 스택에서 가리키는 값으로 리턴이 될까?
이제 RETN을 해보자.
짜자자자자자자ㅏ자자자잔 리턴이 성공적으로 됐다. CALL 명령 이후 코드로 넘어가졌다.
프로그램은 이런 식으로 스택에 의존하여 주소에 접근하게 된다. 스택에 변수, 파라미터, 리턴 주소 등을 한 번에 보관하기에
Stack Buffer Overflow가 발생할 가능성이 클 것 같다.
8. add() 함수 파라미터 제거 (스택 정리)
main() 함수에서 변수 선언도 하였지만 add() 함수를 CALL 해주기 위해 파라미터를 스택에 저장하였다.
a, b라는 변수는 main() 함수 내에서도 사용할 일이 있겠지만 이미 파라미터로 지나간 부분은 제거해주면 메모리를 좀 더 효율적으로 사용할 수 있겠다.
만약 파라미터 제거를 진행하지 않는다면 함수 호출이 많으면 많아질수록 스택에 가능한 공간은 사라지게 될 것이다.
그러므로 ESP를 증가시켜 a, b를 파라미터로 넘기는 주소보다 ESP를 이상으로 만들어주면 될 것이다.
그러기 위해 이 명령을 하는 것이다. ESP에 8을 더해줌으로써 아래에서 볼 수 있는 파라미터를 무효화한다.
현재 스택이 19FF18을 가리키므로, ADD명령을 해주면 19FF20을 가리킬 것이다.
딩동댕................................~%~%~%~%~%
9. printf() 함수 호출
EAX에 a+b의 값인 3이 들어가 있다. 또한 "%d" (0040B384)를 스택에 집어넣고 CALL을 한다.
그러니 printf(StackFra.0040B384, EAX)가 되는 것이고, printf("%d", 3)이 되는 것이다.
printf()를 호출했다면, 00000003과 "%d"라는 파라미터를 무효화해줘야 한다.
32비트 레지스터 + 32비트 상수 = 64비트 = 8바이트 이므로 ESP에 8을 더해줘야 한다.
그래서 ADD ESP, 8 명령을 진행해야 한다.
정상적으로 실행이 됐다. 또한 파라미터도 스택에서 정리가 되었다.
10. 리턴 값 세팅
main() 함수의 return값을 0으로 맞춰주자. 정확히는 지금은 0이라는 숫자를 만드는 것이다. (누구나 main() 함수의 평범한 경우의 리턴 값이 0이라는 것은 알고 있을 것이다.)
리턴값은 XOR 연산으로 만들어준다. 같은 값을 XOR 해주면 0이 된다. (XOR은 Exclusive OR Bit 연산이다.)
MOV EAX, 0 을 해주어도 되지만, XOR 명령이 MOV 보다 더 빠르다.
그래서 레지스터를 0으로 만들어줘야 할 때는 위의 방법을 자주 쓴다.
어쨌든 이렇게 EAX에는 0이 들어갔다.
11. 스택 프레임 해제 & main() 함수 종료
끝ㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌ
OllyDbg 옵션 다이얼로그의 Show ARGs and LOCALs in procedures 명령을 활성화 시키자. 변수와 파라미터를 표시해준다.
'공부 > 리버싱핵심원리' 카테고리의 다른 글
8장 abex' crackme #2 (abexcm2) (1) | 2021.01.23 |
---|---|
8장 abex' crackme #2 (abexcm2) - 나의 풀이 (0) | 2020.12.17 |
6장 abex' crackme #1 - 책의 풀이 (0) | 2020.12.01 |
6장 abex' crackme #1 - 나의 풀이 (0) | 2020.11.30 |
5장 스택 (0) | 2020.11.30 |