컴퓨터 해킹의 역사에서 엄청나게 굵고 큰 한 획을 그었던 Morris Worm 의 컴퓨터 파괴 원리는 바로 버퍼 오버플로우이다. 요즘의 언어인 JAVA나 Python 같은 언어는 메모리를 자체적으로 관리하기 때문에 버퍼 오버플로우에 대해서는 안전할 수 있겠지만, 프로그래머에게 엄청난 자유(?)를 선사하는 어셈블리어, C, C++ 등은 시스템에 매우 밀접하게 동작하므로 버퍼 오버플로우에 대한 고려를 프로그래머가 직접 해야 한다. Morris Worm은 바로 C 언어의 표준 함수 중 버퍼 오버플로우에 노출된 strcpy, strcat, gets, sscanf, sprintf 함수 등의 취약점을 노린 것이며 이 worm으로 인하여 전 세계 UNIX 머신의 10%가 공격을 당하여 천문학적인 피해를 입었다.
(여담.. 이 Worm을 만든 해커 모리스는 미국의 컴퓨터 범죄법에 따라 유죄를 받고 집행유예 3년, 사회 봉사 400시간에 10000달러가 넘는 벌금을 받았다.)
버퍼 오버플로우는 웹과 애플리케이션 환경에 맞는 새로운 언어들이 많이 나와 쓰이게 되고, 그리하여 C, C++ 프로그램의 구현 비율이 상대적으로 낮아짐에 따라 그 심각성은 점차 줄어들고 있으나, 아직도 시스템을 다루는 프로그램은 C, C++이 가장 많이 쓰이고 있으므로 위험성까지 줄어든 것은 아니다.
그렇다면, 버퍼 오버플로우가 무엇인지, 백문이 불여일견, 다음 코드를 보자.
#include <stdio.h> #include <string.h> int main( int argc, char *argv[] ) { char buff[200]; strcpy( buff, argv[1] ); printf("User Input : [%s]\n", buff); return 0; }
사용자의 문자열 입력을 받아 buff 에 저장하고 출력하는 프로그램으로서 얼핏 보면 별 문제가 없어 보이지만, 만일 사용자가 200자가 넘는 문자를 입력했을 경우에는 이 프로그램은 정상 동작을 보장할 수 없을 뿐 아니라 치명적 문제가 발생할 수 있다. 200자 이후의 문자가 메모리 어느 영역에 저장될 지 알 수 없기 때문이다.
문제는 strcpy 이다. strcpy는 source 문자열에서 null 문자가 나올 때까지 멈추지 않고 계속 복사를 진행한다. 문자열을 받았다면 언젠가는 종료가 되겠지만 만일 source 문자열 포인터에 임의로 문자열이 아닌 다른 것을 넣어 null 문자가 없는 data를 가리킬 경우 따로 구현해보지 않아도 심각한 문제를 발생할 수 있을 것이다.
같은 이유로 gets 함수 또한 마찬가지이다.
#include <stdio.h> int main() { char buff[1000]; printf("Input string : "); gets(buff); printf("[%s]\n", buff); return 0; }
역시 앞선 코드와 마찬가지로 buffer overflow를 유발할 수 있다. (당연한 이야기지만 buff 를 아무리 충분히 크게 잡는다고 해서 이 문제가 해결되지는 않을 뿐더러 그 방법은 바람직하지도 않다.)
그렇다면 이러한 버퍼 오버플로우를 유발하는 함수는 모두 사용하지 말아야 하는가?
C 표준 함수를 살펴보면 이러한 함수를 대체할 수 있는 함수들이 몇몇 존재한다. 즉 strcpy 대신 strncpy를, strcat 대신 strncat 함수를 사용하여, 혹은 sscanf, sprintf 에서 포맷을 더욱 신중히 정형화하여 복사 혹은 붙여넣기의 횟수를 제한하는 방법이 있다. 그리고 strcpy, strcat, sprintf, sscanf 함수를 user input 에 그대로 사용하지 않고 제한된 경우에 조심스럽게 사용을 한다면 괜찮을 수 있겠다..
하지만 gets의 경우는 다르다. gets는 온전히 user input에 관련한 함수이므로 gets를 그대로 사용하면서 버퍼 오버플로우를 방지할 수 있는 방법은 없다. (일부 라이브러리에서 gets를 비표준으로 재정의하여 버퍼입력을 제한하도록 다시 설계해 놓았다면 모를까. 하지만 그러한 컴파일러는 극히 일부에 불과하다고 봐야 한다.)
따라서 gets 대신 입력 크기를 제한할 수 있는 fgets를 사용하라고 강력하게 권장하는 것이다.
다만 fgets는 gets와 달리 user input 끝에 오는 '\n'을 제거하지 않는다. 따라서 fgets로 문자열을 입력받은 다음 '\n'을 제거하는 구문을 추가하면 되며 그 방법은 아래와 같이 처리할 수 있다.
#include <stdio.h> #include <string.h> int main() { char buff[1000] = {0}; char *p; printf("Input string : "); fgets(buff, sizeof(buff)-1, stdin); if( (p=strchr(buff, '\n')) != NULL ) *p = '\0'; printf("[%s]\n", buff); return 0; }
C 언어는 사용자에게 상당히 많은 자유를 주는 언어지만 크만큼 사용자가 책임을 져야 하는 부분 또한 많다. 하지만 이 부분에서 신중을 기하여 여러 위험요소를 직접 예방해 가며 차근 차근 코드를 작성해 나간다면 다른 언어보다 훨씬 더 robust한 프로그램이 될 수 있다는 것은 C언어의 큰 매력이다.
### Wafting ....... Done !!!
### ...
### ;;