2 minute read

보안개론 강의 OT시간에 교수님이 scanf_s를 언급하셨습니다. 개인적으론 scanf에 문제점이 있고 scanf_s가 존재 한다는 것도 몰라서 찾아보게되었습니다.

1. scanf의 문제점

먼저 scanf의 문제점부터 살펴보겠습니다.

A beginners’ guide away from scanf()

여기에 버퍼 오버플로우 이외에 여러가지 scanf를 사용할때의 발생하는 오류를 보여주고 있습니다.

먼저 c에서 string(char 배열)을 저장할때 배열의 마지막에 ‘\0’가 필요합니다. 만약 scanf가 저장할 배열 이상의 입력을 받으면 오류가 발생하거나 “의도하지 않은 행동”(undefined behavior)을 할수도 있습니다.

Source code:

#include <stdio.h>

int main(void)
{
    char name[12];
    printf("What's your name? ");
    scanf("%s", name);
    printf("Hello %s!\n", name);
}

output: (사이트에 표시된 출력)

$ ./example3 What’s your name? Paul Hello Paul!

$ ./example3 What’s your name? Christopher-Joseph-Montgomery Segmentation fault

$

mac에서 구동시

mac에서 실행한 결과 char형 변수 name의 크기인 12를 넘어서 출력하는 모습입니다.

위 링크에서:

The problem here is: %s matches any string, of any length, and scanf() has no idea when to stop reading.

라고 말한것 처럼, %s를 사용하면 어디까지 읽어야 하는지 알 수 없음으로 들어온 문자열 만큼 읽게 됩니다.

2. scanf_s()

그래서 scanf_s()를 사용해보기 위해 MS에서 제공하는 예제를 사용해봤습니다.

char s[10];
scanf_s("%9s", s, (unsigned)_countof(s)); // buffer size is 10, width specification is 9

처음에는 에러가 발생했는데, _countof()은 MSVC 매크로라고 나와있어서 _countof()를 변경했습니다.

char s[10];
scanf_s("%9s", s, (unsigned)sizeof(s)/sizeof(s[0]));

그런데 이렇게 변경해도 오류가 발생합니다:

macOS_gcc-12 arm64 macOS에서 컴파일한 결과

혹시 arm64 환경이라서 발생한건가 싶어서 x86 우분투 서버에도 컴파일해봤습니다: x86_ubuntu_gcc-11 x86 Ubuntu 22 LTS에서 컴파일한 결과

그래서 다시 찾아본 바로는 scanf_s()가 MSVC에서만 사용하고 gcc나 clang같은 컴파일러에서는 사용하지 않는다고 하는것 같습니다.

stack overflow의 “poorly supported”

Annex K에 관련된 보고서에서는

As a result of the numerous deviations from the specification the Microsoft implementation cannot be considered conforming or portable.

대부분 이식성이 좋지 못하다고 말하는것 같습니다.

그래도 scanf의 문제점이 존재하는것은 사실이므로, 리눅스같은 MSVC를 사용하지 않는 환경에서는 어떤 방법을 사용해야 하는지 찾아봤습니다.

3. scanf()를 안전(?)하게 사용하는법

#include <stdio.h>

int main(void)
{
    char name[40];
    printf("What's your name? ");
    scanf("%39s", name);
    printf("Hello %s!\n", name);
}

와 같이 scanf() 함수의 %s 부분에 숫자를 넣음으로서 읽는 문자의 개수를 제한할 수 있습니다. (40개 중 마지막 ‘\0’을 제외한 39개)

4. scanf 대신 다른 함수를 사용하는 방법

맨처음에 링크에서 scanf 대신 사용하라는 함수로 fgets()가 나옵니다.

#include <stdio.h>
#include <string.h>

int main(void)
{
    char name[40];
    printf("What's your name? ");
    if (fgets(name, 40, stdin))
    {
        name[strcspn(name, "\n")] = 0;
        printf("Hello %s!\n", name);
    }
}

fgets()는 newline character (‘\n’)도 읽는다고 나와있으므로, strcspn()같은 함수로 NULL로 변경해서 사용합니다.

숫자를 받고 싶으면 stdlib.h에 정의된 atoi()를 사용하라고 나와있습니다.

결론:

  1. scanf()는 매우 강력한 함수로, 가능하면 사용하지 말자
  2. scanf_s()는 버퍼 오버플로우같은 scanf()의 문제점을 막아주지만, 사용하는 경우에는 이식성이 없어진다 (MSVC에서만 사용가능)
  3. scanf()대신 fgets()를, 숫자를 받고 싶을땐 atoi()를 사용하자

P.S.

첫번째 링크에서

fflush(stdin); // <- never do that!

fflush()를 사용하지 말라고 적혀있습니다. c에는 input stream을 flushing하는 행위는 undefined behavior로 취급한다고 합니다. 어떤 시스템에서는 작동 할 수도 있지만, 실제로 input stream을 flusing하지 않는 시스템도 많다고 합니다.

Comments