스택 프레임 훑어보기 -2 (gdb 사용)
이전 포스팅에서 메인 함수의 스택 프레임 크기가 12라는 걸 알았다. (sub $esp, 12)
이제 스택의 구조를 파악하여, 어떻게 배치되어 있는지 확인해보자.
스택 프레임의 맨 아래(바닥) 부분을 가리키는 레지스터가 ebp라고 했다. (extended base pointer)
그리고 스택 프레임의 맨 위를(꼭대기) 가리키는 레지스터는 esp라고 했다. (extended stack pointer)
메인 함수의 스택 프레임을 파악하기 위해, 메인 함수에 브레이크 포인트(Break Point)를 걸자.
브레이크 포인트는 우리 말로 '중단점' 이라고 한다.
중단점을 걸고 프로그램을 실행시키면, 쭉 진행되다가 브레이크 포인트에 실행이 멈추게 된다.
따라서 메인 함수에 중단점을 걸게 될 경우엔, 메인 함수로 진입하고나자마자실행이 멈추게 될 것이다.
중단점을 거는 명령어는 다음과 같다.
(gdb) b [중단점을 걸 지점] // 우리는 b main 이 되겠다.
중단점을 걸었으면, 잘 걸렸는지 확인한다.
(gdb) info break 또는 i b
이제 프로그램을 실행할 것이다. 프로그램을 실행하는 명령어는 다음과 같다.
(gdb) run 또는 r
우리가 메인 함수에 중단점을 걸었으므로, 실행 시엔 메인 함수에 진입하자마자 멈출 것이다.
그렇기 때문에, run 명령을 입력할 경우 메인 함수의 가장 첫 번째 부분인 'int i = 7;' 라인에서 멈추는 걸 알 수있다.
이때의 스택 프레임을 확인해보자. 명령어는 다음과 같다.
(gdb) info reg $esp $ebp 또는 i r $esp $ebp
info reg 는 'information register'라는 걸 유추할 수 있을 것이다. 또는 줄여서 i r로도 표시할 수도 있다.
esp가 0xbffffb5c 이고, ebp가 0xbffffb68이다.
스택은 나무와 같아서, 아래에서부터 위로 큰다고 했다.
따라서 esp의 메모리 주소가 작아질 수록 스택의 사이즈는 커진다.
스택이 거꾸로 자라는 이유는 스택 아랫 부분이 OS의 커널 영역이기 때문이다. 스택이 위에서 아랫 방향으로 향한다고 해보자.
사이즈가 무한히 증가할 경우엔 커널 영역을 침범하게 되고, 이러면 시스템 자체가 날아갈 수 있는 위험성이 있다.
그래서 설계자들이 OS의 안정성을 위해서 스택은 거꾸로 자라게끔 한 거다.
스택 프레임의 크기를 구하기 위해 ebp의 주소에서, esp의 주소를 빼보자.
정확히 12가 나온 걸 알 수 있다.
메모리 구조 열람을 위해 x 명령어를 입력한다. 그런데 스택 프레임의 사이즈가 12바이트이므로,
넉넉하게 20바이트 정도를 구경해보자
gdb의 명령어는 개인적으로 공부하는 것이 좋다. wx 옵션은 16진수 형태의 word 단위로 읽는 것을 뜻한다.
1 word가 4바이트이니, 5wx면 20바이트를 읽을 것이다. 즉 위의 명령어는 $esp로부터 하위 20바이트를 열람한다는 뜻이다.
메인 함수에 중단점을 걸고 실행한 상태에서, $esp를 본다는 것은 당연히 메인 함수의 스택 프레임 맨 꼭대기를 본다는 뜻이다.
아까 esp의 주소가 0xbffffb5c 였고, ebp(맨 아래) 주소가 0xbffffb68 이었으므로, 그 차이는 12바이트가 된다.
그런데 이 명령은 20바이트를 보는거니, 아랫 부분은 main 함수의 스택 프레임 부분이 아니다. (애초에 6c로 시작한다.)
따라서 위 메모리 주소에서 메인 함수의 스택 프레임 부분은
빨간 줄로 감싸진 저 3개의 영역이 바로 스택 프레임이 되겠다. 노란 건 ebp를 나타낸다.
여기서 헷갈리는 부분이 ebp인데, 아까는 0xbffffb68 라고 했으면서 왜 갑자기 뒷 부분이 88로 바뀌었냐면
이건 ebp의 값이 바뀐 게 아니라... 0xbffffb68이 가리키는 주소가 0xbffffb88 이라는 주소라는 뜻이다.
포인터 개념이라고 보면 된다. 이해가 안된다면 아랫 부분을 모두 읽고 다시 정독하는 것이 좋다.
그러나 우린 메인 함수가 진입하자마자 중단점이 걸린 상태에서 메모리 영역을 본 거니,
i, j, k의 변수들이 아직은 선언되기 전의 상태인 것이다.
어셈블리 코드의 중간쯤 (main+6 부터 +20)의 코드를 보면,
mov DWORD PTR [$ebp-4], 0x7
mov DWORD PTR [$ebp-8], 0x8
mov DWORD PTR [$ebp-12], 0x9
와 같은 것을 볼 수가 있다. 이거는 어셈블리를 처음 보더라도, 직감이 올 것이다.
뒤의 7, 8, 9를 각각 ebp에서 4를 뺀 값, 8을 뺀 값, 12를 뺀 값에 넣으라는 건(mov)
일전에 우리가 C 언어 코드에 선언해놓은 int형 변수의 갯수 및 초기화 값들과 일치한다.
int형의 사이즈가 4바이트 이므로, 스택의 맨 아랫 부분인 ebp로 부터 4바이트, 8바이트, 12바이트에 떨어진 곳에
값을 집어넣으라는 게 딱 맞아 떨어진다. 이걸 그림으로 보자
요렇게 구성된다.
그럼 메인 함수에 중단점이 걸린 상태에서, 변수 3개가 선언된 이후까지 진행해보고 다시 메모리를 보자.
gdb에서 다음 라인을 실행하는 명령어는 n이다. 참고로 엔터를 누르면 이전의 실행했던 명령을 재실행한다.
그림처럼, ebp (0xbffffb88)의 위 3개 영역이 다 변수 값에 알맞게 변한 걸 볼 수 있다.
****
정리해보자. main 함수의 어셈블리 코드를 통해 스택 프레임의 사이즈를 확인해보니 (sub $esp, 12)
스택 프레임의 사이즈는 12라는 걸 알 수 있었다. 이에 따라 스택 프레임의 꼭대기와 밑바닥을 가리키는 레지스터인
esp와 ebp의 주소 값을 확인해보니 (info reg $esp $ebp), 각각 주소 값들이 나왔고 이걸 빼보니까 12가 나왔다.
이후 변수가 3개 선언된 부분까지 진행 후(n), 메모리 주소를 열람해보니 7, 8, 9 이렇게 사이좋게 스택 프레임에 쌓인 걸
알 수 있다.
여기서 또 이런 궁금증이 생길 수 있다.
분명히 7, 8, 9 순차적으로 초기화했는데 왜 저 이미지는 9, 8, 7로 들어갔나요?
다시 언급하지만 스택은 밑바닥에서 위로 크는 나무와 같다.
사진의 왼쪽 주소가 더 높은 주소이고 (esp 쪽), 오른쪽 주소가 더 낮은 주소이다. (ebp 쪽)
위에서 내가 정성스럽게 그림판에서 그린 스택 그림을 보면 이해할 수 있을 것이다.
이제 메인 함수의 스택 프레임을 파악했으니, 다음 포스팅은
나머지 func1 함수와 func2 함수의 스택 프레임까지 살펴보고, 3개 함수 전부의 메모리 구조를 열람할 것이다.
거기다 ebp 영역 그 너머에 있는 RET 영역까지 설명할 것인데
이것은 시스템 해킹을 할 때 항상 공격 대상이 되는 부분이므로 반드시 알아야한다.