스택 스매싱 감지


246

내 a.out 파일을 실행 중입니다. 실행 후 프로그램이 얼마 동안 실행 된 후 메시지와 함께 종료됩니다.

**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*

이에 대한 가능한 이유는 무엇이며 어떻게 수정해야합니까?


2
스택 스매싱의 원인이되는 코드의 어느 부분을 식별하여 게시 할 수 있습니까? 그렇다면 왜 그런 일이 발생했는지 그리고 어떻게 수정해야하는지 정확하게 지적 할 수있을 것입니다.
Bjarke Freund-Hansen

오버플로 오류와 동의어라고 생각합니다. 예를 들어, 5 개의 요소를 초기화하고 배열하면 6 번째 요소 또는 배열 경계 외부의 요소를 쓰려고 할 때이 오류가 나타납니다.
DorinPopescu

답변:


349

여기서 스택 스매싱은 실제로 버퍼 오버플로 오류를 감지하기 위해 gcc에서 사용하는 보호 메커니즘으로 인해 발생합니다. 예를 들어 다음 스 니펫에서 :

#include <stdio.h>

void func()
{
    char array[10];
    gets(array);
}

int main(int argc, char **argv)
{
    func();
}

컴파일러 (이 경우 gcc)는 알려진 값을 가진 보호 변수 (카나리아라고 함)를 추가합니다. 크기가 10보다 큰 입력 문자열은이 변수가 손상되어 SIGABRT가 프로그램을 종료시킵니다.

통찰력을 얻으려면 -fno-stack-protector 컴파일하는 동안 옵션 을 사용하여 gcc 보호를 비활성화하십시오 . 이 경우 잘못된 메모리 위치에 액세스하려고 할 때 다른 오류가 발생할 수 있습니다. 참고 -fstack-protector이 보안 기능이기 때문에 항상 출시에 켜져 있어야 빌드.

디버거를 사용하여 프로그램을 실행하여 오버플로 지점에 대한 정보를 얻을 수 있습니다. Valgrind는 스택 관련 오류와 함께 잘 작동하지 않지만 디버거와 마찬가지로 충돌의 위치와 원인을 정확히 찾아내는 데 도움이 될 수 있습니다.


3
이 답변에 감사드립니다! 내 경우에는 내가 쓰려고했던 변수를 초기화하지 않았다는 것을 알았습니다.
Ted Pennings

5
거기 빨간색 영역을 추가 할 수 있기 때문에 Valgrind의가, 스택 관련 오류에 대해 잘 작동하지 않습니다
toasted_flakes

7
이 답변은 정확하지 않으며 위험한 조언을 제공합니다. 우선 스택 보호기를 제거하는 것이 올바른 솔루션이 아닙니다. 스택 스매싱 오류가 발생하면 코드에 심각한 보안 취약점이있을 수 있습니다. 정답 은 버기 코드수정하는 것 입니다. 둘째, grasGendarme이 지적했듯이 Valgrind를 추천하는 것은 효과적이지 않습니다. Valgrind는 일반적으로 스택 할당 데이터에 대한 불법 메모리 액세스를 감지하는 데에는 효과가 없습니다.
DW

22
OP는이 동작에 대한 가능한 이유를 묻습니다. 제 대답은 예와 그것이 알려진 오류와 어떻게 관련되는지 보여줍니다. 또한 스택 보호기를 제거하는 것은 해결책이 아니며 문제에 대한 더 많은 통찰력을 얻기 위해 할 수있는 일종의 실험입니다. 조언은 실제로 어떻게 든 오류를 수정하는 것입니다 .valgrind를 지적 해 주셔서 감사합니다.이를 반영하기 위해 내 대답을 편집 할 것입니다.
sud03r

4
@DW 스택 보호는 릴리스 버전에서 해제해야합니다. 처음에는 스택 스매싱 탐지 메시지가 개발자에게만 도움이 되기 때문 입니다. 둘째, 응용 프로그램은 아직 생존 할 수있는 기회가 없었습니다. 셋째, 이것은 작은 최적화입니다.
Hi-Angel

33

분해 분석을 통한 최소 재생 예

main.c

void myfunc(char *const src, int len) {
    int i;
    for (i = 0; i < len; ++i) {
        src[i] = 42;
    }
}

int main(void) {
    char arr[] = {'a', 'b', 'c', 'd'};
    int len = sizeof(arr);
    myfunc(arr, len + 1);
    return 0;
}

GitHub의 상류 .

컴파일하고 실행하십시오.

gcc -fstack-protector -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out

원하는대로 실패합니다.

*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)

Ubuntu 16.04, GCC 6.4.0에서 테스트되었습니다.

분해

이제 분해를 살펴 보겠습니다.

objdump -D a.out

포함하는:

int main (void){
  400579:       55                      push   %rbp
  40057a:       48 89 e5                mov    %rsp,%rbp

  # Allocate 0x10 of stack space.
  40057d:       48 83 ec 10             sub    $0x10,%rsp

  # Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
  # which is right at the bottom of the stack.
  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

  40058e:       31 c0                   xor    %eax,%eax
    char arr[] = {'a', 'b', 'c', 'd'};
  400590:       c6 45 f4 61             movb   $0x61,-0xc(%rbp)
  400594:       c6 45 f5 62             movb   $0x62,-0xb(%rbp)
  400598:       c6 45 f6 63             movb   $0x63,-0xa(%rbp)
  40059c:       c6 45 f7 64             movb   $0x64,-0x9(%rbp)
    int len = sizeof(arr);
  4005a0:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)
    myfunc(arr, len + 1);
  4005a7:       8b 45 f0                mov    -0x10(%rbp),%eax
  4005aa:       8d 50 01                lea    0x1(%rax),%edx
  4005ad:       48 8d 45 f4             lea    -0xc(%rbp),%rax
  4005b1:       89 d6                   mov    %edx,%esi
  4005b3:       48 89 c7                mov    %rax,%rdi
  4005b6:       e8 8b ff ff ff          callq  400546 <myfunc>
    return 0;
  4005bb:       b8 00 00 00 00          mov    $0x0,%eax
}
  # Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
  # If it has, jump to the failure point __stack_chk_fail.
  4005c0:       48 8b 4d f8             mov    -0x8(%rbp),%rcx
  4005c4:       64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
  4005cb:       00 00 
  4005cd:       74 05                   je     4005d4 <main+0x5b>
  4005cf:       e8 4c fe ff ff          callq  400420 <__stack_chk_fail@plt>

  # Otherwise, exit normally.
  4005d4:       c9                      leaveq 
  4005d5:       c3                      retq   
  4005d6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4005dd:       00 00 00 

에 의해 자동으로 추가 된 편리한 코멘트를 주목하라 objdump인공 지능 모듈을 .

GDB를 통해이 프로그램을 여러 번 실행하면 다음을 볼 수 있습니다.

  • 카나리아는 매번 다른 임의의 값을 얻습니다.
  • 의 마지막 루프 myfunc는 정확히 카나리아의 주소를 수정하는 것입니다.

로 설정하여 카나리아가 무작위 화되었습니다 %fs:0x28.

디버그 시도

이제부터 코드를 수정합니다 :

    myfunc(arr, len + 1);

대신 :

    myfunc(arr, len);
    myfunc(arr, len + 1); /* line 12 */
    myfunc(arr, len);

더 흥미롭게.

그런 다음 + 1전체 소스 코드를 읽고 이해하는 것보다 자동화 된 방법으로 범인을 정확히 찾아 낼 수 있는지 알아 봅니다 .

gcc -fsanitize=address Google의 주소 살균제 (ASan)를 활성화하는 방법

이 플래그로 다시 컴파일하고 프로그램을 실행하면 다음과 같이 출력됩니다.

#0 0x4008bf in myfunc /home/ciro/test/main.c:4
#1 0x40099b in main /home/ciro/test/main.c:12
#2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400798 in _start (/home/ciro/test/a.out+0x40079

더 많은 컬러 출력이 뒤 따릅니다.

이것은 문제가있는 12 행을 분명하게 지적합니다.

이에 대한 소스 코드는 https://github.com/google/sanitizers에 있지만 예제에서 보았 듯이 이미 GCC로 업스트림되었습니다.

ASan은 메모리 누수와 같은 다른 메모리 문제도 감지 할 수 있습니다 . C ++ 코드 / 프로젝트에서 메모리 누수를 찾는 방법은 무엇입니까?

발 그린 드 SGCheck

마찬가지로 다른 사람에 의해 언급 , Valgrind의는 이러한 문제를 해결하기에 좋지 않다.

SGCheck라는 실험 도구가 있습니다 .

SGCheck는 스택 및 글로벌 어레이의 오버런을 찾는 도구입니다. 스택 및 전역 배열 액세스 가능성에 대한 관찰에서 파생 된 휴리스틱 접근 방식을 사용하여 작동합니다.

그래서 오류를 찾지 못했을 때 크게 놀라지 않았습니다.

valgrind --tool=exp-sgcheck ./a.out

오류 메시지는 다음과 같이 나타납니다. Valgrind missing error

GDB

중요한 관찰은 GDB를 통해 프로그램을 실행하거나 core사실 후에 파일을 검사 한다는 것입니다.

gdb -nh -q a.out core

그런 다음 어셈블리에서 보았 듯이 GDB는 카나리아 검사를 수행 한 함수의 끝을 가리켜 야합니다.

(gdb) bt
#0  0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2  0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37
#4  0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28
#5  0x00000000004005f6 in main () at main.c:15
(gdb) f 5
#5  0x00000000004005f6 in main () at main.c:15
15      }
(gdb)

따라서이 함수가 수행 한 호출 중 하나에서 문제가 발생했을 수 있습니다.

다음으로 카나리아를 설정 한 직후에 첫 번째 스텝 업으로 정확한 실패 호출을 찾아 내려고합니다.

  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

그리고 주소를보고 :

(gdb) p $rbp - 0x8
$1 = (void *) 0x7fffffffcf18
(gdb) watch 0x7fffffffcf18
Hardware watchpoint 2: *0x7fffffffcf18
(gdb) c
Continuing.

Hardware watchpoint 2: *0x7fffffffcf18

Old value = 1800814336
New value = 1800814378
myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
3           for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0  myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1  0x00000000004005cc in main () at main.c:12

자, 이것이 바로 잘못된 명령에 잎에게 우리를 수행합니다 len = 5그리고 i = 4,이 특별한 경우에, 범인 라인 (12) 우리를 지적했다.

그러나 역 추적이 손상되었으며 일부 휴지통이 포함되어 있습니다. 올바른 역 추적은 다음과 같습니다.

#0  myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1  0x00000000004005b8 in main () at main.c:11

이로 인해 스택이 손상되어 추적을 볼 수 없습니다.

또한이 방법은 카나리아 검사 기능의 마지막 호출이 무엇인지 알아야합니다. 그렇지 않으면 역방향 디버깅사용 하지 않는 한 오 탐지가 항상 가능하지는 않습니다 .


16

다음 상황을보십시오.

ab@cd-x:$ cat test_overflow.c 
#include <stdio.h>
#include <string.h>

int check_password(char *password){
    int flag = 0;
    char buffer[20];
    strcpy(buffer, password);

    if(strcmp(buffer, "mypass") == 0){
        flag = 1;
    }
    if(strcmp(buffer, "yourpass") == 0){
        flag = 1;
    }
    return flag;
}

int main(int argc, char *argv[]){
    if(argc >= 2){
        if(check_password(argv[1])){
            printf("%s", "Access granted\n");
        }else{
            printf("%s", "Access denied\n");
        }
    }else{
        printf("%s", "Please enter password!\n");
    }
}
ab@cd-x:$ gcc -g -fno-stack-protector test_overflow.c 
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out wepassssssssssssssssss
Access granted

ab@cd-x:$ gcc -g -fstack-protector test_overflow.c 
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepassssssssssssssssss
*** stack smashing detected ***: ./a.out terminated
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0xce0ed8]
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0xce0e90]
./a.out[0x8048524]
./a.out[0x8048545]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xc16b56]
./a.out[0x8048411]
======= Memory map: ========
007d9000-007f5000 r-xp 00000000 08:06 5776       /lib/libgcc_s.so.1
007f5000-007f6000 r--p 0001b000 08:06 5776       /lib/libgcc_s.so.1
007f6000-007f7000 rw-p 0001c000 08:06 5776       /lib/libgcc_s.so.1
0090a000-0090b000 r-xp 00000000 00:00 0          [vdso]
00c00000-00d3e000 r-xp 00000000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d3e000-00d3f000 ---p 0013e000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d3f000-00d41000 r--p 0013e000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d41000-00d42000 rw-p 00140000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d42000-00d45000 rw-p 00000000 00:00 0 
00e0c000-00e27000 r-xp 00000000 08:06 4213       /lib/ld-2.10.1.so
00e27000-00e28000 r--p 0001a000 08:06 4213       /lib/ld-2.10.1.so
00e28000-00e29000 rw-p 0001b000 08:06 4213       /lib/ld-2.10.1.so
08048000-08049000 r-xp 00000000 08:05 1056811    /dos/hacking/test/a.out
08049000-0804a000 r--p 00000000 08:05 1056811    /dos/hacking/test/a.out
0804a000-0804b000 rw-p 00001000 08:05 1056811    /dos/hacking/test/a.out
08675000-08696000 rw-p 00000000 00:00 0          [heap]
b76fe000-b76ff000 rw-p 00000000 00:00 0 
b7717000-b7719000 rw-p 00000000 00:00 0 
bfc1c000-bfc31000 rw-p 00000000 00:00 0          [stack]
Aborted
ab@cd-x:$ 

스택 스매싱 프로텍터를 비활성화했을 때 오류가 감지되지 않았습니다. "./a.out wepassssssssssssssssss"

따라서 위의 질문에 대답하기 위해 스택 스매싱 프로텍터가 활성화되어 있고 프로그램에 스택 오버플로가 있음을 발견했기 때문에 "** 스택 스매싱 감지 : xxx"메시지가 표시되었습니다.

그 위치를 찾아서 고치십시오.


7

valgrind를 사용하여 문제를 디버깅하려고 시도 할 수 있습니다 .

Valgrind 배포판에는 현재 메모리 오류 감지기, 스레드 오류 감지기 2 개, 캐시 및 분기 예측 프로파일 러, 호출 그래프 생성 캐시 프로파일 러 및 힙 프로파일 러 등 6 가지 프로덕션 품질 도구가 포함되어 있습니다. 또한 힙 / 스택 / 글로벌 어레이 오버런 검출기 와 SimPoint 기본 블록 벡터 생성기의 두 가지 실험 도구가 포함되어 있습니다. X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux 및 X86 / Darwin (Mac OS X) 플랫폼에서 실행됩니다.


2
예, 그러나 Valgrind는 스택 할당 버퍼의 오버플로에 대해 잘 작동하지 않습니다.이 오류 메시지가 나타내는 상황입니다.
DW

4
스택 어레이 오버런 검출기를 어떻게 사용할 수 있습니까? 정교하게 할 수 있습니까?
Craig McQueen

나는 최소한의 예에 Valgrind의의 실험 발견 SGCheck 스택 스매싱 검출기를 사용하려고했습니다 @CraigMcQueen : stackoverflow.com/a/51897264/895245를 하지만 실패했습니다.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

4

이는 Buffer overflow 의 결과로 인해 잘못된 변수로 스택의 일부 변수에 썼음을 의미합니다 .


9
스택 오버플로는 스택이 다른 것으로 스매싱하는 것입니다. 여기 다른 방법이 있습니다. 무언가가 스택에 박살났습니다.
Peter Mortensen

5
실제로는 아닙니다. 스택의 한 부분이 다른 부분으로 스매싱됩니다. 따라서 실제로 스택 오버플로가 아니라 스택의 다른 부분으로 만 "버퍼 오버 플로우"됩니다.
Bas Wijnen

2

이에 대한 가능한 이유는 무엇이며 어떻게 수정해야합니까?

하나의 시나리오는 다음 예제에 있습니다.

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

void swap ( char *a , char *b );
void revSTR ( char *const src );

int main ( void ){
    char arr[] = "A-B-C-D-E";

    revSTR( arr );
    printf("ARR = %s\n", arr );
}

void swap ( char *a , char *b ){
    char tmp = *a;
    *a = *b;
    *b = tmp;
}

void revSTR ( char *const src ){
    char *start = src;
    char *end   = start + ( strlen( src ) - 1 );

    while ( start < end ){
        swap( &( *start ) , &( *end ) );
        start++;
        end--;
    }
}

이 프로그램에서 예를 들어 reverse()다음과 같이 호출하면 문자열 또는 문자열의 일부를 되돌릴 수 있습니다 .

reverse( arr + 2 );

다음과 같이 배열의 길이를 전달하기로 결정한 경우 :

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

void swap ( char *a , char *b );
void revSTR ( char *const src, size_t len );

int main ( void ){
    char arr[] = "A-B-C-D-E";
    size_t len = strlen( arr );

    revSTR( arr, len );
    printf("ARR = %s\n", arr );
}

void swap ( char *a , char *b ){
    char tmp = *a;
    *a = *b;
    *b = tmp;
}

void revSTR ( char *const src, size_t len ){
    char *start = src;
    char *end   = start + ( len - 1 );

    while ( start < end ){
        swap( &( *start ) , &( *end ) );
        start++;
        end--;
    }
}

잘 작동합니다.

그러나 이것을 할 때 :

revSTR( arr + 2, len );

당신은 얻을 :

==7125== Command: ./program
==7125== 
ARR = A-
*** stack smashing detected ***: ./program terminated
==7125== 
==7125== Process terminating with default action of signal 6 (SIGABRT)
==7125==    at 0x4E6F428: raise (raise.c:54)
==7125==    by 0x4E71029: abort (abort.c:89)
==7125==    by 0x4EB17E9: __libc_message (libc_fatal.c:175)
==7125==    by 0x4F5311B: __fortify_fail (fortify_fail.c:37)
==7125==    by 0x4F530BF: __stack_chk_fail (stack_chk_fail.c:28)
==7125==    by 0x400637: main (program.c:14)

그리고 이것은 첫 번째 코드에서 길이 arr가 내부에서 확인 revSTR()되었지만 두 번째 코드에서 길이를 전달하기 때문에 발생합니다.

revSTR( arr + 2, len );

길이는 이제 말할 때 실제로 전달한 길이보다 깁니다 arr + 2.

strlen ( arr + 2 )! =의 길이입니다 strlen ( arr ).


1
이 예제는 getsand와 같은 표준 라이브러리 함수에 의존하지 않기 때문에 좋아합니다 scrcpy. 추가로 최소화 할 수 있을지 궁금합니다. 나는 적어도 제거하는 것 string.hsize_t len = sizeof( arr );. gcc 6.4, 우분투 16.04에서 테스트되었습니다. 또한 arr + 2복사 붙여 넣기를 최소화하기 위해 실패한 예를 제공합니다 .
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1

버퍼 오버플로로 인해 일반적으로 스택 손상이 발생합니다. 방어 적으로 프로그래밍하여 방어 할 수 있습니다.

배열에 액세스 할 때마다 액세스가 범위를 벗어나지 않도록 어설 션을 먼저 배치하십시오. 예를 들면 다음과 같습니다.

assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];

이를 통해 배열 범위에 대해 생각할 수 있으며 가능하면 트리거 할 테스트를 추가 할 수도 있습니다. 이 어설 션 중 일부가 정상적인 사용 중에 실패 할 수 있으면 규칙적으로 바꾸십시오 if.


0

malloc ()을 사용하여 구조체에 메모리를 할당하는 동안이 오류가 발생했습니다 *이 디버깅 코드를 사용한 후 free () 함수를 사용하여 할당 된 메모리를 해제하고 오류 메시지가 사라졌습니다.


0

스택 스매싱의 또 다른 원인은 vfork()대신 (의 ) 잘못된 사용 입니다 fork().

방금 자식 프로세스가 execve()대상 실행 파일 에 액세스 할 수 없었고 호출하는 대신 오류 코드를 반환하는 경우를 디버깅했습니다 _exit().

vfork()그 자식을 낳았 기 때문에 , 부모의 프로세스 공간 내에서 실제로 실행되는 동안 반환되어 부모의 스택을 손상시킬뿐만 아니라 "다운 스트림"코드에 의해 두 개의 서로 다른 진단 세트가 인쇄됩니다.

변경 vfork()fork()고정 된 문제를 모두 같은 아이의 변화 한 return에 문을 _exit()대신.

그러나 자식 코드 앞에 execve()다른 루틴에 대한 호출 (이 경우에는 uid / gid를 설정하기 위한 호출)이 있기 때문에 기술적으로의 요구 사항을 충족하지 않으므로 vfork()사용하도록 변경하는 것이 fork()여기에 맞습니다.

(문제가 있음을 참고 return문이 실제로 같은 코딩되지 않았다 - 대신 매크로를 호출하고, 그 매크로를 결정할지 여부 _exit()또는 return전역 변수에 따라이 아이 코드가 부적합 것을 즉시 명확하지 그래서. vfork()사용. )

자세한 내용은 다음을 참조하십시오.

fork (), vfork (), exec () 및 clone ()의 차이점

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.