2014년 3월 1일 토요일

gets 함수는 사용하지 말아야 한다.

C 프로그래밍을 하는 고수 프로그래머의 대부분(아니 거의 전부) 는 다들 gets 함수를 사용하지 말라고 조언한다. 그 이유에 대해서 1988년 엄청난 피해를 입혔던 모리스 웜(Morris Worm) 에 대해 언급하고자 한다.

컴퓨터 해킹의 역사에서 엄청나게 굵고 큰 한 획을 그었던 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 !!!
### ...
### ;;

댓글 12개:

  1. gets 함수의 위험성을 잘설명하셧네요
    잘보고 갑니다!

    답글삭제
  2. 감사합니다 공부 많이 하고 갑니다~

    답글삭제
  3. gets 오류를 알아보러 왔다가 좋은글 보고 갑니다~.

    답글삭제
  4. 2017 비주얼 스튜디오에서 gets를 사용하지 못하게 한 이유가 있었네요.

    답글삭제
    답글
    1. 네. 2017 비주얼 스튜디오에서 gets가 완전히 삭제되었더군요. C11 표준에서 삭제 예고되었고 C14 표준에서 완전히 삭제된 것으로 예상됩니다. 2017 비주얼 스투디오는 최소 C14 이상의 표준을 따르므로 gets가 삭제된 것으로 보여집니다.

      이를 대체하기 위하여 위에서 언급했듯이 fgets 를 사용하거나, 혹은 비주얼 스튜디오 사용자이시라면 윈도우계열 컴파일러에서 제안했던 gets 의 대체함수인 gets_s 라는 함수가 C11 이후 표준의 optional 표준함수로 등재되었으니 이를 사용하시면 될 듯합니다. gets_s 함수는 gets 와 동일하지만 max 버퍼사이즈를 추가 인자로 받는것만 다릅니다.

      다만 gcc 등의 비 윈도우 컴파일러는 gets_s 를 사용하지 못할 수도 있습니다. gets_s 가 표준함수이긴 하지만 optional 함수라서 채택하지 않아도 되거든요. 이런 경우는 fgets 등으로 우회하시면 될듯합니다.

      삭제
  5. 처음 하시는 분에게 : 처음 배우시는 분이시라면 쫄지 마세요.
    gets() 함수 사용해서 "위험하다", "심각하다."라고, 컴퓨터 폭발 할 것처럼 이야기 하는데요.
    그래봐야, 겨우 콘솔 창에서 실행이 정지되는 정도예요.
    그리고 디버깅(ctrl +f5) 할 때 눈에 보이니까, 다른 함수로 수정하면 됩니다.

    공부할 때는, 컴퓨터가 불 날 정도라면 아예 비주얼 스튜디오 2019에서 실행을 못하게 하니,
    맘 편하게 걱정하지 마시고 이것 저것 다 해보세요. 그리고 처음부터 부정적인 함수란 편견을 갖지 마세요.
    부실한 문제 함수를 보아야, 대안 함수를 만들 수 있으니, 이는 달인의 길로 인도하는 좋은 가이드라고 생각하시면 될 것 같습니다.

    너무 잘 만들어진 printf() 함수 때문에 printf() 함수를 만들 볼 생각조차 안 하게 되면, 4년 졸업을 해도 stdio.h에
    printf() 함수가 있는지 없는지도 모르고, printf() scanf() 함수 기능을 가진 함수를 만들 줄도 모르는 상황이 발생하게 됩니다.
    무려 1학년 hello world 를 배우면서 처음 접하는 것이 printf()함수인데도 말입니다.

    문제있는 함수는 나를 발전 시킨다. (주인님 잘 보고 갑니다.)

    답글삭제
    답글
    1. 맹목적으로 gets가 위험하다고 여기거나 아님 별거 아니라고 여기거나 하는 일은 없었으면 하는 뜻이라면 일리 있는 말씀입니다.

      gets가 가진 잠재적 문제는 어떤 환경에서 어떻게 사용되냐에 따라 심각도가 천차만별이거든요. 실제 gets 등의 터미널 입력함수는 터미널을 띄워서 키보드로 입력할때만 사용하는 것이 아니고, 요즘 SW에선 표준입출력 함수 사용시, 다른 물리 I/O 혹은 가상 I/O 등에 표준입출력을 리디렉팅하여 활용하는 경우도 많이 있기 때문입니다.

      함수의 원리를 잘 알기 위해 공부하면서 gets를 사용해보고 오류도 경험해보고 하는 것은 적극 권합니다. 하지만 이런 노력을 들이고 싶지 않은 분이나 여유가 없으신 분들이 잠재적 위험을 내포하고 있는 프로그래밍을 하는 것이 정말 위험한 것이죠. 그리고 그 분들을 위해서라도 gets의 위험성을 알리는 것은 필요하다고 보았기에 글을 쓴 것입니다. ^^

      삭제
  6. http://wafter.com/ 홈페이지 주인장이신가요? 실례가 안된다면 회원가입 해보고 싶습니다.

    답글삭제