strncpy, strncat, snprintf

cinsk의 이미지

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

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

이 함수들을 검사하기 위해, 먼저 주어진 버퍼의 내용을 그대로 출력해 주는 함수를 만들어 봅시다. 보통 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(*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'으로 끝나는 올바른 문자열을 만들어 줍니다.

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

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

자, 위 코드는 잘못된 코드입니다. 왜냐하면 some_string의 길이가 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에 항상 신경써야 하는 코드를 작성한다면, 이와 같은 사항은 꼭 기억해 두어야 합니다. 그럼 이만.

Forums: 
cinsk의 이미지

strncpy(3)나 strncat(3)의 경우, 다음과 같이 해결할 수도 있습니다:

char buf[LEN] = { 0, };
strncpy(buf, some_string, LEN - 1);

--
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html
Korean Ver: http://www.cinsk.org/cfaqs/

ktd2004의 이미지

좋은 강좌 감사합니다.

char buf[LEN] = { 0, };
strncpy(buf, some_string, LEN - 1);

마지막 예로 드신 위 코드에서 맨 밑에 "buf[LEN-1] = '\0';"이 빠진 건 아닌지요?

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

위의 strncat의 정확한 사용예로 드신 코드에서는 맨 마지막에 "buf[LEN-1] = '\0';"이 빠져도 되는건 아닌지 모르겠습니다.
cinsk의 이미지

To: KTD

아니오. buf를 모두 '\0'으로 초기화했고, 마지막 문자를 건드리지 않았으므로 괜찮습니다.

--
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html
Korean Ver: http://www.cinsk.org/cfaqs/

ktd2004의 이미지

To:cinsk

^^; 제가 변수의 초기화를 생각을 못했네요..
좋은 강좌 감사합니다. ^^;

cliffet의 이미지

char buf[LEN] = { 0, };
 
strncpy(buf, some_string, LEN - 1);

위에서 \0으로 초기화를 했기 때문에 괜찮은게 아니라 strncpy자체가 마지막에 \0을 찍어주기 때문에 괜찮은겁니다.

char buf[8] = {'1', '2', '3', '4', '5', '6', '7', '8'};
strncpy(buf, "abcdefgh", 7);

이 경우 buf가 \0으로 초기화가 되어 있지 않지만 strncpy호출 후에
buf는 abcdefg\0 이 되는거죠. 이는 strcpy도 마찬가지이며 src string 카피는 마지막 \0까지 자동적으로 포함되기 때문에 buffer overflow문제가 생기는 겁니다.

hyperhidrosis의 이미지

cliffet //
strncpy 는 마지막에 \0 을 복사하지 안습니다.
초기화하면서 버퍼에 전부 \0 을넣었기 때문에 괜찮은 겁니다.

그리고 buf[len]={0,};
와 같은 코드는 len 만큼 실행 코드에 0 이 추가 되기 때문에
메모리 사용 측면에서 그다지 좋은 해결법은 아닙니다..

cliffet의 이미지

//hyperhidrosis

앞에서 두번째 예의 결과는 'abcdefg8'인거 같군요. 이 부분은 제가 틀렸네요
제가 말씀드리고자 하는 부분은 strncpy함수의 세번째 인자는 주로 src string의 길이를 인자로 주는게 아니라 버퍼의 크기를 인자로 사용하는 습관 때문에 실수한듯 합니다.

이 부분은 서두에서 cinsk님이 이미 언급해주신 부분인데 strncpy 세번째 인자가 src string 길이보다 작다면 \0은 붙지 않으나 클 경우에는 붙는다는 차이가 있습니다. '초기화하면서 버퍼에 전부 \0 을넣었기 때문에 괜찮다'라는 말씀은 잘못된 내용입니다. 아래 코드를 보시면

(gdb) l
1       #include <string.h>
2
3       int main()
4       {
5               char buf[32];
6
7               strncpy(buf, "abcd", 8);
8       }
(gdb) b 7
Breakpoint 1 at 0x8048364: file strncpy.c, line 7.
(gdb) run
Starting program: /home/r2/a.out 
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0xebf000
 
Breakpoint 1, main () at strncpy.c:7
7               strncpy(buf, "abcd", 8);
(gdb) x/32b buf
0xbf864068:     0x88    0x40    0x86    0xbf    0x92    0x83    0x04    0x08
0xbf864070:     0xf4    0x4f    0xa9    0x00    0x01    0x00    0x00    0x00
0xbf864078:     0x00    0x00    0x00    0x00    0xf4    0x4f    0xa9    0x00
0xbf864080:     0xa0    0xbc    0x96    0x00    0x78    0x83    0x04    0x08
(gdb) n
8       }
(gdb) x/32b buf
0xbf864068:     0x61    0x62    0x63    0x64    0x00    0x00    0x00    0x00
0xbf864070:     0xf4    0x4f    0xa9    0x00    0x01    0x00    0x00    0x00
0xbf864078:     0x00    0x00    0x00    0x00    0xf4    0x4f    0xa9    0x00
0xbf864080:     0xa0    0xbc    0x96    0x00    0x78    0x83    0x04    0x08
(gdb) 

여기서 buf는 초기화되지 않았지만 strncpy이후에 buf의 처음 8byte가 변경되었으며 src string 4byte를 제외한 나머지는 0으로 세팅된걸 볼 수 있습니다.

소타의 이미지

오호라;;
전 snprintf만 주로 쓰고 str[n]cpy종류 쓸 일이 있으면 memcpy를 쓰고 널로 막아주는데 자주 안쓰던 strcpy에 이런 다른 동작이 있었군요 -.-;
좋은 실험(?) 감사합니다 :)

falaris의 이미지

왜 저는 강좌보다, 폰트에 눈독을 들이는 거일까요 ㅡㅡ;;
후암 강좌는 자주 보고 있습니다 (정말 감사합니다)

그리고.....폰트가 무엇인지 좀..알려 주실수 있는지...
그럼 즐거운 하루 보내세요

--------------
젠투교+emacs교로 가려고 발버둥 치는중!!
현재 젠투교+vim교 ~_~!!

cinsk의 이미지

색상과 bold를 제외하고, 폰트 종류는 제가 지정한 것이 아닙니다. KLDP(drupal)에 글 쓸 때, 프로그램 소스 붙이기을 참고하세요.

--
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html
Korean Ver: http://www.cinsk.org/cfaqs/

Fe.head의 이미지

질문있습니다.

위의 string 관련 함수들은 모든 시스템에서 동일하게 동작된다는 보장이 있나요?

-----------------------
과거를 알고 싶거든 오늘의 네 모습을 보아라. 그것이 과거의 너니라.
그리고 내일을 알고 싶으냐?
그러면 오늘의 너를 보아라. 그것이 바로 미래의 너니라.

고작 블로킹 하나, 고작 25점 중에 1점, 고작 부활동
"만약 그 순간이 온다면 그때가 네가 배구에 빠지는 순간이야"

cinsk의 이미지

예. 위 함수 모두 동작 방식이 ISO C 표준에 정의되어 있습니다.

--
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html
Korean Ver: http://www.cinsk.org/cfaqs/

...의 이미지

스크롤의 압박...

ohhara의 이미지

전혀 관계없는 딴지(^^;)일 수 있겠지만 위의 memdump 함수 정상적으로 작동되나요? char가 signed이면 isprint하고 putchar에 negative value를 넘기게 되는데 왠지 crash가 날 거 같은 느낌이 무럭무럭... ^^;;

Taeho Oh ( ohhara@postech.edu, ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
PLUS ( Postech Laboratory for Unix Security ) http://www.plus.or.kr

Taeho Oh ( ohhara@postech.edu ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Alticast Corp. http://www.alticast.com

ins878의 이미지

strncpy()를 쓴다고 해서 무족건 버퍼오버플로우를 방지 할 수 없다는 이야기는 들은 적도 있고, 경험한 적도 있었는데~~. (처음에~ strncpy땜시 무척 고생했습니다.~~^^;)
그런데. 이렇게 memdump 까지 하면서, 여러 strn* 류의 함수를 비교 해주시니~~ 이해가 잘 됩니다. 많은 도움이 된 것 같습니다.
감사합니다...

cinsk의 이미지

실수했네요. :oops:

예. isprint(*p)가 아니라 isprint((unsigned char)*p)가 되야 합니다.

그리고 putchar()는 음수가 들어가도 상관없습니다.

--
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html
Korean Ver: http://www.cinsk.org/cfaqs/

ohhara의 이미지

참고사항

그리고 제 기억으로는 예전에 (Visual Studio 6시절) Visual Studio의 _snprintf하고 gcc의 snprintf가 다르게 작동했었습니다. Visual Studio는 buffer가 넘쳤을 때 끝에 마지막 글자를 null로 끝내주지 않는데 gcc는 buffer가 넘쳤을 때 끝에 null로 끝내줬습니다.

요즘 버전 Visual Studio는 test를 안해봐서 모르겠군요. 솔직히 compiler에서 제공해 주는 string.h, ctype.h, stdio.h, stdlib.h 같은 것들은 compiler마다 전부 작동양상이 달라서 저는 믿지 않아서 특별히 문제가 없지만 믿고 쓰는 사람들이 많은 듯 하더군요.

Taeho Oh ( ohhara@postech.edu, ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
PLUS ( Postech Laboratory for Unix Security ) http://www.plus.or.kr

Taeho Oh ( ohhara@postech.edu ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Alticast Corp. http://www.alticast.com

rain의 이미지

GCC로 종종 snprintf쓰던 습관이 있었는데, 어제 VS에서 _snprintf해보는데 null termination을 해주지 않아 이제껏 잘못 알고 사용했나 싶었습니다.
그래서 검색해보던 중 이 글을 보게 되었네요.ㅋㅋ

세상에서 가장 이해하기 힘든 것은 내 자신이 그것을 이해할 수 있다는 것이다.
- 알베르트 아인슈타인 -

세상에서 가장 이해하기 힘든 것은 내 자신이 그것을 이해할 수 있다는 것이다.
- 알베르트 아인슈타인 -

dasomoli의 이미지

다시 찾아 읽어보려고 보니 소스들의 색상때문에 글이 제대로 보이지 않습니다.
저만 그런겁니까?;



dasomoli의 블로그(http://dasomoli.org)
dasomoli = DasomOLI = Dasom + DOLI = 다솜돌이
다솜 = 사랑하옴의 옛 고어.
Developer! ubuntu-ko! 다솜돌이 정석


dasomoli의 블로그(http://dasomoli.org)
dasomoli = DasomOLI = Dasom + DOLI = 다솜돌이
다솜 = 사랑하옴의 옛 고어.
Developer! ubuntu-ko! 다솜돌이 정석
익명 사용자의 이미지

저도 좀 그런편인데...
lcd가 아니라서 그런가 19인치 crt 사용자 입니다.

익명 사용자의 이미지

흰색 글씨라 읽기에 눈이 아프네요.
전 Ctrl+a 해서 읽었습니다..

정건호의 이미지

첫번째 보기중에
snprintf(buf, "%s", "123", 5);
써도 잘 동작하는거 같네요...
원래는 두번째 파라메터가 사이즈 아닌가 해서요...

익명 사용자의 이미지

정말 오래 전 글이지만 정말 도움이 되었습니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.