반응형

https://gun-ny.tistory.com/15

전에 scanf와 gets 함수의 버퍼 오버플로우 취약점에 대해 얘기한 적이 있었다.

 

나는 주로 Visual Studio Code(VS Code)를 쓰지만 Visual Studio IDE(VS IDE)를 쓰면 VS IDE에서 기본적으로 제공하는 scanf_s 함수로 scanf의 버퍼 오버플로우를 해결할 수 있다.

 

gets도 똑같은 취약점을 가지고 있고 gets_s 함수를 제공하길래 써봤는데 내가 못 쓰는 건지 해결이 안 된다.

그래서 scanf와 scanf_s 기준으로 글을 작성해 보려고 한다. (전에도 gets는 뒷전이었지만..)

 

전에 썼던 코드를 그대로 컴파일 해보았다.

응..? 그런데 컴파일 에러가 뜬다.

 

'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.

 

번역하면

'scanf': 이 함수 또는 변수는 안전하지 않을 수 있습니다. 대신 scanf_s를 사용하는 것을 고려해 보십시오. 더 이상 사용하지 않도록 설정하려면 _CRT_SECURE_NO_WARNINGS를 사용하십시오. 자세한 내용은 온라인 도움말을 참조하십시오.

 

요약하면

scanf는 안전하지 않다.

scanf_s를 써라

그래도 쓸거면 _CRT_SECURE_NO_WARNINGS를 사용하라고 한다.

 

마이크로소프트에서는 scanf의 취약점을 인지하고 2010년대 초반부터 VS IDE에서 scanf를 사용하지 못하도록 하였다.

 

scanf_s? 쓰면 되지

C11 표준에 scanf_s가 수록되었다곤 하는데 10년이 지난 지금도 VS IDE의 컴파일러 cl 이외 컴파일러에서 scanf_s를 지원하지 않는 컴파일러들이 있다.

모두 다 VS IDE를 쓰면 상관이 없지만 개개인마다 다른 컴파일러를 쓰는 경우가 있어서 VS IDE에서 잘 컴파일 됐던 파일을 다른 팀원이 받아서 컴파일 했는데 안되는 경우가 생길 수 있다.

나 같은 경우도 GCC, G++ 컴파일러를 사용하는데 GCC 컴파일러를 사용하는 C 파일에서 작성된 scanf_s는 잘 컴파일 됐는데 G++ 컴파일러를 사용하는 CPP 파일에서 컴파일 하니 안된 기억이 있다.

이런 경우들 때문에 그들을 위해 scanf를 사용하는 일이 생긴다.

 

scanf 한번 써보자

경고 메시지에서는 _CRT_SECURE_NO_WARNINGS를 사용하라고 한다.

#define _CRT_SECURE_NO_WARNINGS

코드 상단에 복붙해주면 VS IDE에서도 scanf를 사용할 수 있다.

 

컴파일 되었다.

이왕 여기까지 온 거 VS IDE에서 디버깅도 한번 해보자

 

그때처럼 다른 str 메모리 영역을 침범해 덮어씌우지 않고 다른쪽 메모리를 손상시켜 VS IDE는 런타임 에러를 띄운다.

 

Run-Time Check Failure #2 - Stack around the variable 'str_1' was corrupted.

런타임 검사 실패 #2 - 변수 'str_1' 주변 스택이 손상되었습니다.

 

이번엔 scanf_s를 써보자

scanf_s("%s", str, sizeof(str) / sizeof(str[0]));

기존 scanf 인수에서 버퍼 사이즈를 추가로 지정해주면 된다.

 

디버깅을 해보면 입력 자체를 안 받은 걸 확인할 수 있다.

이렇게 입력 자체를 안 받아버리면 테스트 중 문제점을 발견하기도 쉽고 메모리 침범도 없어 디버깅이 쉬워지는데......

왜 printf("Hello World"); 마냥 킹표준이 되지 못하는 걸까 하는 생각이다.

반응형
반응형

문제 설명

문자열 s를 숫자로 변환한 결과를 반환하는 함수, solution을 완성하세요.

 

제한 조건

  • s의 길이는 1 이상 5이하입니다.
  • s의 맨앞에는 부호(+, -)가 올 수 있습니다.
  • s는 부호와 숫자로만 이루어져있습니다.
  • s는 "0"으로 시작하지 않습니다.

입출력 예

예를들어 str이 "1234"이면 1234를 반환하고, "-1234"이면 -1234를 반환하면 됩니다.

str은 부호(+,-)와 숫자로만 구성되어 있고, 잘못된 값이 입력되는 경우는 없습니다.


solution.c

int StringToNumber(const char *string)
{
    int number = 0, string_len = 0, flag = 1;

    for (int i = 0; string[i] != '\0'; i++)
        string_len++;

    if (string[0] >= '1' && string[0] <= '9')
        flag = 0;

    for (int i = flag; i < string_len; i++)
        number = (number * 10) + (string[i] - 48);

    if (flag && string[0] != '+')
        number *= -1;

    return number;
}

// 파라미터로 주어지는 문자열은 const로 주어집니다. 변경하려면 문자열을 복사해서 사용하세요.
int solution(const char *s)
{
    return StringToNumber(s);
}

 

반응형
반응형
#include <stdio.h>

int main()
{
	char str_3[5];
	char str_2[5];
	char str_1[5];

	printf("Input str_3 : ");
	scanf("%s", str_3);

	printf("Input str_2 : ");
	scanf("%s", str_2);

	printf("Input str_1 : ");
	scanf("%s", str_1);

	printf("%s\n", str_3);
	printf("%s\n", str_2);
	printf("%s\n", str_1);

	return 0;
}

위와 같은 코드에

 


Input str_3 : aaa
Input str_2 : bbb
Input str_1 : ccc

이렇게 입력을 했다고 했을때

 

보통 정상적인 상황이라면

각 문자열 변수의 상태는 이럴 것이고

위와 같이 출력 되었을 것이다.

 


Input str_3 : aaa
Input str_2 : bbb
Input str_1 : abcdefgijklmnop

하지만 이렇게 입력하면 어떻게 될까?

 

난리난다.

 

표로 정리하면(PC 또는 태블릿 모드 추천)

 

str_1 메모리 영역 str_2 메모리 영역 str_3 메모리 영역 다른곳에서 쓰는 메모리 영역
c c c \0 \0 b b b \0 \0 a a a \0 \0          
↑ 시작 주소    끝 주소  ↑ 시작 주소    끝 주소  ↑ 시작 주소    끝 주소   

위와 같은 정상적인 상황의 메모리가

 

str_1 메모리 영역 str_2 메모리 영역 str_3 메모리 영역 다른곳에서 쓰는 메모리 영역
a b c d e f g h i j k l m n o p \0      
↑ 시작 주소    끝 주소  ↑ 시작 주소    끝 주소  ↑ 시작 주소    끝 주소  (메모리 영역 침범)

str_1의 버퍼 오버플로우로 이렇게 바뀐 것이다.

 

str_1에 입력한 문자열이 str_2와 str_3을 덮어썼고 각 문자열 변수에 담겨있던 문자열의 끝을 알려주는 문자(\0)도 덮어씌어져 먼저 출력을 하는 str_3는 덮어씌어진 시작 주소 k부터 다른곳에서 쓰는 메모리 영역에 있는 \0까지 출력하여 klmnop를 출력한다.

두번째 출력하는 str_2도 덮어씌어진 시작 주소 f부터 똑같이 \0까지 출력하여 f부터 p까지 출력한다.

마지막으로 출력하는 str_1 역시 마찬가지로 a부터 p까지 출력한다.

 

이것이 버퍼 오버플로우다.

 

나 같이 현재 배우고 있는 입장에서는 별거 아니네 할 수 있지만 현업에서는 큰 문제로 이어질 수 있다고 한다.

 

협업 프로젝트로 프로그램을 만들고 있는데 내 실수로 다른 팀원이 쓰는 메모리 영역에 침범하여 값을 바꿔서 나에게는 문제가 일어나지 않지만 그 팀원 코드에서만 문제점이 생긴다던지

게임을 예를 들면 맵에 있는 몬스터들의 피가 실행된 게임의 메모리 영역에서 관리되고 있는데 난 1번 몬스터를 때려서 피를 깎았는데 2번 몬스터의 피에도 영향을 미친다는 것이다.

 

모두 버퍼 오버플로우로 인한 버그다.

 

gets 함수도 마찬가지다.

 

해결법에 대한 글은 기회가 된다면 쓰는걸로

 

스택 오버플로우도 비슷한 원리로 일어나는 문제인데 이것도 기회가 된다면 쓰는걸로..

반응형
반응형

scanf


헤더 파일(Header file)

stdio.h

 

함수 원형(Function prototype)

int scanf( const char *format [, argument]... );

 

매개변수(Parameters)

format

포맷

argument

인수

 

반환 값(Return)

할당된 필드 수

 

예제_1(Example_1)

#include <stdio.h>

int main()
{
	int num;
	printf("Input num : ");
	int result_num = scanf("%d", &num);
	printf("Result num : %d\n", result_num);

	printf("\n");

	int num_1, num_2;
	printf("Input num 1 2 : ");
	int result_num_1_2 = scanf("%d %d", &num_1, &num_2);
	printf("Result num 1 2 : %d\n", result_num_1_2);

	printf("\n");

	int err_num;
	printf("Input err_num : ");
	int result_err_num = scanf("%d", &err_num);
	printf("Result err_num : %d\n", result_err_num);

	return 0;
}

 

출력_1(Output_1)


Input num : 123
Result num : 1  

Input num 1 2 : 11 22
Result num 1 2 : 2

Input err_num : abc
Result err_num : 0

 

풀이_1

첫번째 호출에서 num의 인수를 받고 나온 정수형 반환 값을 result_num에 대입

출력하면 할당된 필드가 1이란 것을 알 수 있다.

 

두번째 호출에서는 정수형 변수 2개에 대입할 인수 2개를 입력 받는다.

출력하면 2개를 입력받아 할당된 필드가 2라고 출력된다.

 

세번째 호출은 정수형 입력에 문자열을 입력

scanf 함수 내부에서 변환에 실패해 할당된 필드가 없어 0을 반환한다.

 

예제_2(Example_2)

#include <stdio.h>

int main()
{
	int num;
	printf("Input int : ");
	scanf("%d", &num);
	printf("Output int : %d\n", num);

	printf("\n");

	float f_num;
	printf("Input float : ");
	scanf("%f", &f_num);
	printf("Output float : %.10f\n", f_num);

	printf("\n");

	double d_num;
	printf("Input double : ");
	scanf("%lf", &d_num);
	fflush(stdin);
	printf("Output double : %.20f\n", d_num);

	printf("\n");

	char let;
	printf("Input let : ");
	scanf("%c", &let);
	printf("Output let : %c\n", let);

	printf("\n");

	char str[5];
	printf("Input str : ");
	scanf("%s", str);
	printf("Output str : %s\n", str);

	return 0;
}

 

출력_2(Output_2)


Input int : 123
Output int : 123

Input float : 1.1234567890
Output float : 1.1234568357

Input double : 3.12345678901234567890
Output double : 3.12345678901234570000

Input let : a
Output let : a

Input str : abc
Output str : abc

 

풀이_2

첫번째 호출은 정수를 입력받고 출력한다.

int형 정수(4바이트) 기준으로

부호없는 정수(unsigned)는 0 ~ 약 42억

부호있는 정수(signed)는 약 -21억 ~ 0 ~ 약 21억까지 표현 가능하다.

 

두번째 호출은 float형 실수(4바이트)를 %f로 입력받고 %.10f로 소수점 10자리까지 출력한다.

결과를 보면 float형 실수는 소수점 6자리까지만 표현이 가능하다.

 

세번째 호출은 double형 실수(8바이트)를 %lf로 입력받고 %.20f로 소수점 20자리까지 출력한다.

결과를 보면 double형 실수는 소수점 15자리까지만 표현이 가능하다.

 

네번째 호출은 문자를 입력받고 출력한다.

 

다섯번째 호출은 문자열을 입력받고 출력한다.

 

 

gets


헤더 파일(Header file)

stdio.h

 

함수 원형(Function prototype)

char *gets( char *buffer );

 

매개변수(Parameters)

buffer

입력된 문자열이 저장될 문자열 변수

 

반환 값(Return)

입력된 문자열이 저장된 문자열 변수 주소

 

예제(Example)

#include <stdio.h>

int main()
{
	char str[5];
	char *str_addr;

	printf("Input : ");
	str_addr = gets(str);
	printf("str_addr Value 	: %p\n", str_addr);
	printf("str Address 	: %p\n", &str);
	printf("str Value 		: %s\n", str);
	printf("str_addr Reference Value : %s\n", str_addr);

	return 0;
}

 

출력(Output)


Input : abc
str_addr Value  : 000000000061FE0E
str Address     : 000000000061FE0E
str Value               : abc
str_addr Reference Value : abc

 

풀이

호출시 문자열을 입력받고 입력받은 문자열이 저장된 문자열 변수 주소의 값을 반환 받는다.

출력을 해보면 실제 문자열이 저장된 변수의 주소와 반환된 값의 주소가 같음을 알 수 있다.

당연히 가리키는 곳의 주소가 같으니 값을 출력하면 값이 동일함을 알 수 있다.

반응형

+ Recent posts