Preventing buffer overflows with strncpy, strncat, and snprintf

Preventing Buffer overflows

흔히 버퍼 오퍼플로우를 막기 위해 쓰는 함수가, strncpy(3), strncat(3), snprintf(3)⁠입니다. 이들 함수는 버퍼의 크기를 미리 지정받아, 복사할 문자열의 길이가 버퍼의 크기보다 클 경우, 복사를 중지해서 버퍼를 벗어나는 복사를 막아줍니다. 하지만, 버퍼의 크기를 해석하는 방식이 약간씩 다르다는 것이 문제입니다.

버퍼의 크기가 M이고, 복사해 넣을 문자열의 길이가 N이라고 합시다. 이 때 두 가지 경우를 생각할 수 있습니다. 첫째, 버퍼의 길이가 충분히 클 때 (즉, M > N), 둘째 버퍼의 길이가 짧을 때 (즉 M < N).

이 함수들을 검사하기 위해, 먼저 주어진 버퍼의 내용을 그대로 출력해 주는 함수를 만들어 봅시다. 보통 C 언어가 제공하는 문자열 함수들은 '\0'을 만나면 출력을 멈추기 때문에, 버퍼의 내용 전체를 알아 보기에는 좋지 않습니다. 따라서 다음과 같이 버퍼의 내용을 전체 다 출력해 주는 함수를 만듭니다 (필요한 표준 헤더 파일은 생략합니다):

void
memdump(const void *s, size_t size)
{
  const char *p = (const char *)s;
  const char *end = p + size;

  while (p < end) {
    if (isprint((unsigned char)*p))
      putchar(*p);
    else
      putchar('.');
    p++;
  }
}

예를 들어 char buf[10]⁠에 "ABCDEFGHI"가 들어있다고 가정하면, memdump(buf, 10)⁠은 다음과 같이 출력합니다:

ABCDEFGHI.

이제, 각각의 함수가 앞에서 다룬 두 가지 경우에 어떤 식으로 동작하는지 살펴봅시다. 먼저 첫번째 경우 (버퍼가 충분히 클 경우)를 알아보는 코드는 다음과 같습니다 (BUF_MAX⁠는 매크로이며 10입니다):

memset(buf, '#', BUF_MAX);
strcpy(buf, "123");
memdump(buf, BUF_MAX);  putchar('\n');

memset(buf, '#', BUF_MAX);
strncpy(buf, "123", 5);
memdump(buf, BUF_MAX);  putchar('\n');

memset(buf, '#', BUF_MAX);
buf[0] = '\0';
strncat(buf, "123", 5);
memdump(buf, BUF_MAX);  putchar('\n');

memset(buf, '#', BUF_MAX);
snprintf(buf, "%s", "123", 5);
memdump(buf, BUF_MAX);  putchar('\n');

위 코드에서 문자열 "123"을 복사해 넣으면서, 버퍼의 길이는 5라고 치고 각 함수를 테스트합니다. 이 때, 출력은 다음과 같습니다:

123.######
123..#####
123.######
123.######

strncpy(3)⁠를 제외하고, 나머지 세 함수는 예상대로 동작합니다. 즉 문자 '1', '2', '3'을 복사해 넣고, 문자열 끝을 알리는 '\0'까지 복사합니다. 이 네 문자 모두 버퍼의 길이라고 지정한 5보다 작기 때문에 문제는 전혀 없습니다. 하지만, 두번째 줄인 strncpy(3)⁠는, 123을 복사해 넣고, 나머지 공간을 모두 '\0'으로 채운다는 것이 다릅니다! 즉, 버퍼가 충분히 클 경우에도, strcpy(3)⁠와 strncpy(3) 동작 방식은 서로 다릅니다!

두번째 경우, 즉 버퍼가 충분히 크지 못할 경우를 살펴 봅시다. 이제 strcpy(3)⁠의 경우, 테스트할 필요가 없으므로 뺐습니다:

memset(buf, '#', BUF_MAX);
strncpy(buf, "12345", 3);
memdump(buf, BUF_MAX);  putchar('\n');

memset(buf, '#', BUF_MAX);
buf[0] = '\0';
strncat(buf, "12345", 3);
memdump(buf, BUF_MAX);  putchar('\n');

memset(buf, '#', BUF_MAX);
snprintf(buf, 3, "%s", "12345");
memdump(buf, BUF_MAX);  putchar('\n');

이 경우, 다음과 같은 출력을 얻을 수 있습니다:

123#######
123.######
12.#######

세가지 함수 모두 다르게 동작한다는 것을 알 수 있습니다. 먼저 strncpy(3)⁠의 경우, 버퍼의 길이가 부족할 경우, 버퍼의 크기만큼 써 줍니다. 이 때 공간이 부족하더라도 '\0'을 써 주지 않습니다. 따라서 strncpy(3)⁠의 경우, 완전하지 못한 문자열을 얻을 수 있습니다.

strncat(3)⁠의 경우, 무조건 n개 문자를 복사합니다. 따라서 이 경우, 123을 복사한 다음 '\0'까지 써 줍니다. 사실 strncat(3)⁠의 경우, 버퍼의 길이를 지정하는 것이 아니라, '\0'을 제외한 실제 복사할 문자의 갯수를 지정하는 것입니다.

snprintf(3)⁠의 경우, strncat(3)⁠과 다르게, 버퍼의 크기를 지정합니다. 따라서 버퍼의 길이가 짧을 경우, 그 버퍼의 길이 - 1개의 문자를 복사하고, '\0'을 써 줍니다. 즉, strncpy(3)⁠와 다르게, 어떤 경우에도 '\0'으로 끝나는 올바른 문자열을 만들어 줍니다.

이제 이 차이를 알았으면, 실제 코드에서 어떤 식으로 써야 하는지 알아봅시다. 먼저 사용자가 입력한 문자열 somestring이 있다고 가정하고, 다음 코드를 보기 바랍니다:

char buf[LEN];
strncpy(buf, some_string, LEN);

자, 위 코드는 잘못된 코드입니다. 왜냐하면 somestring의 길이가 LEN보다 클 경우, buf에 들어가는 문자열이 '\0'으로 끝나지 않을 수 있기 때문입니다. 따라서 다음과 같이 써 주어야 합니다:

char buf[LEN];
strncpy(buf, some_string, LEN - 1);
buf[LEN - 1] = '\0';

다음 코드는 안전할까요?

char buf[LEN];
buf[0] = '\0';
strncat(buf, some_string, LEN);

아닙니다. strncat(3)⁠은, 버퍼의 크기가 아니라, 복사할 문자열의 길이를 지정하는 것이므로, 마찬가지로 '\0'으로 끝나지 않은 문자열을 만들 가능성이 있습니다. 이것도 다음과 같이 써야 합니다:

char buf[LEN];
buf[0] = '\0';
strncat(buf, some_string, LEN - 1);
buf[LEN - 1] = '\0';

그럼 snprintf(3)⁠를 쓴 코드를 봅시다:

char buf[LEN];
snprintf(buf, LEN, "%s", some_string);

위 코드는 안전할까요? 예. 그렇습니다. 안전합니다. snprintf(3)⁠는 버퍼의 길이를 받아서 어떤 상황에서도 '\0'으로 끝나는 완전한 문자열을 만들어 줍니다.

안전한 프로그램, buffer overflow에 항상 신경써야 하는 코드를 작성한다면, 이와 같은 사항은 꼭 기억해 두어야 합니다. 그럼 이만.

댓글

Comments powered by Disqus