일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- c 환경설정
- python
- 기초
- AES
- 유니티기초
- c++
- 게임 오브젝트
- C언어
- 상속
- 빈 프로젝트
- 유니티3d
- 유니티3D 2018.2
- VisualStudio2017
- 유니티 설치방법
- Visual Studio
- 유니티 월드
- 엔진기초
- 컴포넌트
- 비쥬얼 스튜디오
- CryptoJS
- cryptography
- aes128
- crypto-js
- Today
- Total
Basic of Computer Skills
스택 프레임 훑어보기 - 1 (gdb 사용) 본문
저번 포스트에서 스택 프레임에 관한 개요를 살펴봤다.
정리하자면, 스택 프레임이란 스택 내부에서 함수가 갖는 자기만의 공간 이라고 말할 수 있다.
예를 들어 집에서(스택) 자기 방(스택 프레임)과 같은 개념인 것이다.
따라서 스택 프레임은 스택의 부분 집합에 속한다.
C 언어는 메인 함수부터 시작하여, 그외 유저들이 만든 여러 함수들 까지
일단 호출되고나면 그 함수의 스택 프레임이 생성되고, 함수가 기능을 다해 종료될 때
그 함수의 스택 프레임이 정리되고 반납된다.
결국 이런 저런 함수들이 작동하면서 스택 프레임이 생성되었다가 반납되었다가를 반복하다가,
최종적으로 메인 함수의 스택 프레임까지 반납되면 비로소 프로그램이 종료되는 것이다.
여기서 함수란, 메인 함수를 포함하여 유저가 직접 만든 함수만을 한정한다.
printf() 라든지, scanf() 라든지, strcpy()와 같이 기본적으로 헤더파일 내부에서 제공해주는 함수들은
'라이브러리' 라는 영역에 있다. 이를 알기 위해선 plt 및 got 라는 개념을 따로 학습해야한다. 여기선 논외로 친다.
이제 이에 관한 예시를 살펴보자.
참고로 해당 실습은 기존에 소개한 LOB(Lord of BufferOverflow) 환경에서 진행됐고, 32비트 리눅스이다.
32비트 환경이기 때문에 레지스터가 ebp, esp, eip 등 각각 접두사에 'e' 가 붙게된다. ('extended = 확장된'의 약자)
만약 자신의 환경이 64비트라면 ebp, esp, eip와 같은 레지스터가 rbp, rsp, rip와 같은 접두사 'r'로 변할 것이다.
그러나 OS의 환경이 다르다고해서 문제되지는 않는다.
함수가 3개 있고, 각 함수는 int형 변수 3개를 갖는다. (지역변수)
이것의 출력 결과는
==========
main has called
func1 has called
func2 has called
==========
가 될 것이다. (변수는 써먹질 않았으니, 당연한 결과이다.)
이제 단순한 출력 결과를 초월해, 스택에 어떤식으로 나타나있는지 확인한다.
gcc로 컴파일을 수행할 때 -g 옵션을 붙여준다. 이는 디버깅할 때 프로그램의 정보를 제공해준다는 옵션이다.
(그냥 gdb를 쓸 때 무조건 주는 옵션이라고 생각하면 편하다.)
위의 파일 이름을 stackframe.c 라고 가정하면, 다음 명령어를 순차적으로 입력해준다. (출력 파일명은 알아서 해도 상관 없음)
$ gcc -g -o stackframe stackframe.c
컴파일이 성공했으면, gdb를 통해 진입한다. ( -q 옵션은 줘도 안줘도 상관없다. 이는 그냥 제작자의 인삿말을 무시하는 옵션이다.)
$ gdb -q stackframe
gdb를 통해 프로그램을 디스어셈블(disassemble)하면 어셈블리 코드가 나오게되는데, 이는 또 두 가지의 문법으로 나뉜다.
at&t와 intel 문법으로 나뉘는데, 이건 본 포스팅의 주제와는 조금 상관없는 얘기라 패스하도록 한다.
나는 intel 문법을 사용하므로 intel 문법을 적용하기 위해 다음과 같은 명령어를 입력한다. (디폴트 문법은 at&t이기 때문)
(gdb) set disassembly-flavor intel
그리고 프로그램에 어떤 함수들이 있는지 확인한다.
(gdb) info functions 또는 i fu
아랫 부분의 Non-debugging symbols 부분은 지금은 몰라도 된다.
상단 부분을 보면 함수가 func1(), func2(), main() 이 3개가 사용됨을 알 수 있다.
이제 메인 함수를 분석해보자.
(gdb) disas main
intel 문법의 어셈블리 코드가 나왔다. 이를 기존의 C 언어 메인 함수와 비교해보면 비슷한 부분과 함께 헷갈리는 부분도 있을 것이다.
여기서 더 파고들면 이는 스택 프레임이 아니라 리버스 엔지니어링(리버싱) 포스팅이 될테니, 우리는 적당히 알아야 할 부분만 본다.
push $ebp
mov %ebp, $esp
이 두 줄의 코드는 모든 함수의 시작점에서 공통점으로 갖는 부분이다. 이는 스택 프레임을 생성하는 코드이다.
함수가 호출되면 스택 프레임부터 생성되므로, 처음부터 이 과정이 실행되는 것이다. 따라서 이를 함수 프롤로그(function prologue)라 한다.
이건 중요한 개념이라 반드시 알아야한다. 최대한 간단하게 설명하려 했지만 ebp와 esp는 스택 프레임을 이해하기 위한 기초이다.
ebp는 'extended base pointer'의 약자로써, 스택 프레임의 맨 아래를 가리키는 부분이다.
esp는 'extended stack pointer'의 약자로써, 스택 프레임의 맨 꼭대기를 가리키는 부분이다.
따라서 esp의 주소값에서 ebp의 주소값을 빼면 스택 프레임의 사이즈를 알 수 있다.
만약에 실습 환경이 64비트라면, ebp와 esp는 각각 rbp, rsp로 바뀔 것이다. (그냥 알아두면 좋다.)
이렇듯, 함수 프롤로그가 실행된 이후에는 스택 프레임의 사이즈를 정하는 부분이 나온다.
sub $esp, 12
스택 꼭대기를 12만큼 뺀다는 뜻인데, 이는 함수 프롤로그 부분에서 mov %ebp, %esp를 통해 함수의 꼭대기(esp)를
함수의 밑바닥(ebp)에 대입한다는 의미이므로, 결국엔 스택 프레임의 윗 부분과 아랫 부분과 같게된다.
예를 들어, 전교생이 1명인데 얘는 전교 1등이 될 수도 있고, 전교 꼴등이 될 수도 있다는 것처럼....
따라서 스택 프레임의 공간을 키워주기 위해, 해당 상태에서 esp의 크기를 빼주는 것이다. (스택은 주소가 낮아질 수록 커짐)
즉, 위 코드는 $esp에서 12를 빼므로, 메인 함수의 스택 프레임 크기는 12라는 것을 알 수가 있다.
12인 이유는 메인함수에서 선언된 지역변수가, 4bytes의 크기를 가진 int형 3개밖에 없기 때문이다. (i, j, k)
만약에 char buffer[256]; 이 선언되었다면?
char형은 사이즈 크기가 1byte 이므로, 총 256bytes의 크기가 요구될 것이기 때문에 해당 스택 프레임은
sub $esp, 256 또는 sub $esp, $0x100
이 될것이다.
그러나 가끔씩 스택 프레임의 사이즈가 지정한 지역변수의 총 사이즈와 일치하지 않는 경우가 있다.
예를 들어 char buffer[100]; 을 선언했는데 sub $esp, 120이 나온다든지..
이런 경우엔 나머지 20바이트가 남게된다. 그리고 이를 Dummy(더미)라고 한다.
그러므로 스택 프레임의 사이즈를 파악할 때, 항상 더미가 있는지 없는지 확인하는 습관을 가져야한다.
메모리 주소를 보는 건 다음 포스팅에
'보안 > Pwnable' 카테고리의 다른 글
스택 프레임 훑어보기 (gdb 사용) (完) (1) | 2018.08.03 |
---|---|
스택 프레임 훑어보기 -2 (gdb 사용) (1) | 2018.08.02 |
스택 프레임(Stack Frame) 개요 (0) | 2018.07.31 |
LOB 맛보기 (1번 예시) (0) | 2018.07.30 |
LOB 준비 (0) | 2018.07.30 |