단일 인수 (변환 지정자 없음)가있는 printf가 더 이상 사용되지 않는 이유는 무엇입니까?


102

내가 읽고있는 책 printf에서 단일 인수 (변환 지정자 없음)가 더 이상 사용되지 않는다고 기록되어 있습니다. 대체하는 것이 좋습니다

printf("Hello World!");

puts("Hello World!");

또는

printf("%s", "Hello World!");

누군가가 왜 printf("Hello World!");잘못된 것인지 말해 줄 수 있습니까 ? 책에 취약점이 있다고 기록되어 있습니다. 이 취약점은 무엇입니까?


34
참고 : printf("Hello World!")하지 와 동일 puts("Hello World!"). puts()추가합니다 '\n'. 대신 비교 printf("abc")fputs("abc", stdout)
chux - 분석 재개 모니카

5
그 책은 무엇입니까? printf예를 들어 getsC99에서 더 이상 사용되지 않는 것과 같은 방식으로 더 이상 사용되지 않는다고 생각하지 않으므로 질문을 더 정확하게 편집하는 것을 고려할 수 있습니다.
el.pescado

14
당신이 읽고있는 책이 그다지 좋지 않은 것 같습니다. 좋은 책은 단순히 "사용되지 않음"이라고 말해서는 안되며 (저자가 자신의 의견을 설명하기 위해 단어를 사용하지 않는 한 사실은 거짓입니다) 어떤 사용법을 설명해야합니다. "하지 말아야 할 일"의 예로 안전 / 유효한 코드를 보여주는 것이 아니라 실제로 유효하지 않고 위험합니다.
R .. GitHub의 STOP 돕기 ICE

8
책을 식별 할 수 있습니까?
Keith Thompson

7
책 제목, 저자 및 페이지 참조를 지정하십시오. 고마워.
Greenonline

답변:


122

printf("Hello World!"); IMHO는 취약하지 않지만 다음을 고려하십시오.

const char *str;
...
printf(str);

경우 str문자열을 포함에 포인트로 발생하는 %s형식 지정, 프로그램은 반면에 정의되지 않은 동작 (주로 충돌)를 전시 할 예정 puts(str)이기 때문에 단지 문자열을 표시합니다.

예:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

21
프로그램 충돌을 유발하는 것 외에도 형식 문자열로 가능한 많은 악용이 있습니다. 자세한 정보는 여기를 참조하십시오 : en.wikipedia.org/wiki/Unmanaged_format_string
e.dan

9
또 다른 이유는 puts아마도 더 빠를 것입니다.
edmz

38
@black : puts"아마도"더 빠르며 이것이 사람들이 추천하는 또 다른 이유 일 수 있지만 실제로는 더 빠르지 는 않습니다 . "Hello, world!"두 가지 방법으로 1,000,000 번 인쇄했습니다 . 함께 printf그것을 0.92 초 걸렸습니다. 함께 puts그것을 0.93 초 걸렸습니다. 효율성과 관련하여 걱정할 것이 있지만 printfvs. puts는 그중 하나가 아닙니다.
Steve Summit

10
@KonstantinWeitz :하지만 (a) gcc를 사용하지 않았고 (b) " 더 빠르다"라는 주장 이 puts 거짓 인지 는 중요하지 않습니다 . 여전히 거짓입니다.
Steve Summit

6
@KonstantinWeitz : 내가 증거를 제공 한 주장은 흑인 사용자가 주장하는 (반대) 주장이었습니다. 나는 프로그래머가 puts이런 이유로 전화 를하는 것에 대해 걱정해서는 안된다는 것을 분명히하려고합니다 . (하지만 당신은 그것에 대해 논쟁하기를 원한다면 : 당신은 어떤 현대 machne에 대한 현대의 컴파일러를 찾을 수 있다면 내가 놀랄 것 puts속도가 매우 빠르고보다 printf어떤 상황에서도를.)
스티브 정상 회의

75

printf("Hello world");

괜찮으며 보안 취약점이 없습니다.

문제는 다음과 같습니다.

printf(p);

여기서 p사용자에 의해 제어되는 입력에 대한 포인터이다. 그것은하는 경향이 형식 문자열 공격 : 변환 사양을 삽입 할 수 있습니다 사용자는, 예를 들어, 프로그램 제어 할 %x메모리 나 덤프 %n덮어 쓰기 메모리에 있습니다.

puts("Hello world")동작 printf("Hello world")printf("Hello world\n"). 컴파일러는 일반적으로 후자의 호출을 최적화하여 puts.


10
물론 printf(p,x)사용자가 p. 문제는 그래서 하지 를 사용하는 printf하나 개의 인수가 아니라 사용자가 제어하는 형식 문자열.
Hagen von Eitzen 2015

2
@HagenvonEitzen 기술적으로는 사실이지만 사용자가 제공 한 형식 문자열을 의도적으로 사용하는 사람은 거의 없습니다. 사람들이을 쓸 때 printf(p)그것은 그것이 형식 문자열이라는 것을 깨닫지 못하기 때문입니다. 그들은 단지 리터럴을 인쇄하고 있다고 생각합니다.
Barmar

33

다른 답변 외에도 printf("Hello world! I am 50% happy today")쉽게 버그를 만들 수 있으며 잠재적으로 모든 종류의 불쾌한 메모리 문제를 일으킬 수 있습니다 (UB!).

프로그래머가 축 어적 문자열 만 원할 때 절대적으로 명확하게 "요구"하는 것이 더 간단하고 쉽고 강력합니다 .

그리고 그것이 printf("%s", "Hello world! I am 50% happy today")당신을 얻는 것입니다. 완전히 완벽합니다.

(물론 스티브 printf("He has %d cherries\n", ncherries)는 절대적으로 같은 것이 아닙니다.이 경우 프로그래머는 "verbatim string"사고 방식이 아닙니다. 그녀는 "format string"사고 방식에 있습니다.)


2
이것은 논쟁의 가치가 없으며, 당신이 축 어적 대 형식 문자열 사고 방식에 대해 말하는 것을 이해합니다. 그러나 글쎄요, 모든 사람들이 그렇게 생각하는 것은 아닙니다. 이것이 하나의 크기에 맞는 규칙이 순위를 매길 수있는 한 가지 이유입니다. "never print constant strings with printf"라고 말하는 것은 "always write if(NULL == p).이 규칙은 일부 프로그래머에게 유용 할 수 있지만 전부는 아닙니다. 그리고 두 경우 모두 (일치하지 않는 printf형식과 Yoda 조건문) 현대 컴파일러는 오류에 대해 경고합니다. 그래서 인공 규칙도 덜 중요하다.
스티브 정상 회의

1
@Steve 무언가를 사용하는 것의 장점이 정확히 제로이지만 단점이 꽤 많다면 실제로 사용할 이유가 없습니다. 반면에 요다 조건은 DO 가 (없다 "p가 0 인 경우" "제로 쪽이"당신이 직관적으로 말할 것) 코드를 읽기 어렵게하는 단점이있다.
Voo

2
@Voo printf("%s", "hello")는보다 느리기 printf("hello")때문에 단점이 있습니다. IO는 이러한 간단한 형식보다 거의 항상 느리지 만 단점이 있기 때문에 작은 것입니다.
Yakk-Adam Nevraumont 2015

1
@Yakk 나는 그게 더 느릴 것 같지 않다
MM

gcc -Wall -W -Werror그러한 실수로 인한 나쁜 결과를 방지합니다.
chqrlie

17

여기에 취약성 부분 에 대한 정보 약간 추가 하겠습니다.

printf 문자열 형식 취약점으로 인해 취약한 것으로 알려져 있습니다. 귀하의 예에서 문자열이 하드 코딩 된 경우 무해합니다 (이와 같은 하드 코딩 문자열이 완전히 권장되지 않더라도). 그러나 매개 변수의 유형을 지정하는 것은 좋은 습관입니다. 이 예를 보자 :

누군가가 일반 문자열 대신에 형식 문자열 문자를 printf에 넣으면 (예를 들어 프로그램 stdin을 인쇄하려는 경우) printf는 스택에서 가능한 모든 것을 가져옵니다.

예를 들어 숨겨진 정보에 액세스하거나 인증을 우회하기 위해 스택을 탐색하도록 프로그램을 악용하는 데 매우 사용되었습니다.

예 (C) :

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

이 프로그램의 입력으로 넣으면 "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

이것은 printf 함수가 스택에서 5 개의 매개 변수를 검색하고 8 자리 패딩 된 16 진수로 표시하도록 지시합니다. 따라서 가능한 출력은 다음과 같습니다.

40012980 080628c4 bffff7a4 00000005 08059c04

더 완전한 설명과 다른 예는 이것을 참조하십시오 .


13

printf리터럴 형식 문자열로 호출 하는 것은 안전하고 효율적이며 printf사용자 제공 형식 문자열로 호출하는 것이 안전하지 않은 경우 자동으로 경고하는 도구가 있습니다 .

가장 심각한 공격 printf%n형식 지정자를 활용 합니다. 다른 모든 형식 지정자와 달리 예를 들어 %d%n실제로 형식 인수 중 하나에 제공된 메모리 주소에 값을 씁니다. 이는 공격자가 메모리를 덮어 쓸 수 있으므로 잠재적으로 프로그램을 제어 할 수 있음을 의미합니다. Wikipedia 는 더 자세한 정보를 제공합니다.

printf리터럴 형식 문자열로 호출 하면 공격자가 형식 문자열에 잠입 %n할 수 없으므로 안전합니다. 실제로 gcc는에 대한 호출을 printf에 대한 호출로 변경 puts하므로 별다른 차이가 없습니다 (실행하여 테스트 gcc -O3 -S).

printf사용자가 제공 한 형식 문자열을 사용하여 호출 하면 공격자가 잠재적 %n으로 형식 문자열에 침투하여 프로그램을 제어 할 수 있습니다. 컴파일러는 일반적으로 안전하지 않다고 경고합니다 -Wformat-security.을 참조하십시오 . 또한 printf사용자가 제공 한 형식 문자열을 사용해도를 안전하게 호출 할 수 있도록하는 고급 도구가 있으며 , 올바른 수와 유형의 인수를에 전달하는지 확인할 수도 있습니다 printf. 예를 들어 Java의 경우 Google의 Error ProneChecker Framework가 있습니다.


12

이것은 잘못된 조언입니다. 예, 인쇄 할 런타임 문자열이있는 경우

printf(str);

매우 위험하므로 항상

printf("%s", str);

대신 일반적 str으로 %기호 가 포함되어 있는지 여부를 알 수 없기 때문입니다 . 그러나 컴파일 타임 상수 문자열이 있으면 아무런 문제가 없습니다.

printf("Hello, world!\n");

(다른 것들 중에서, 그것은 말 그대로 Genesis의 C 프로그래밍 책에서 가져온 가장 고전적인 C 프로그램입니다. 따라서 그 사용법을 비난하는 사람은 다소 이단 적이며, 나는 다소 불쾌 할 것입니다!)


because printf's first argument is always a constant string그게 무슨 뜻인지 정확히 모르겠습니다.
Sebastian Mach

내가 말했듯 "He has %d cherries\n"이은 상수 문자열이며 이는 컴파일 타임 상수임을 의미합니다. 그러나 공정하게 말하면 저자의 조언은 " printf'의 첫 번째 인수 로 상수 문자열을 전달하지 마십시오"가 아니라 " '의 첫 번째 인수 %로 문자열을 전달하지 마십시오"였습니다 printf.
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical-최근 몇 년 동안 실제로 K & R을 읽지 않았습니다. 사용되지 않는 것이 아니라 요즘에는 나쁜 습관에 불과한 수많은 조언과 코딩 스타일이 있습니다.
Voo

@Voo : 음, 나쁜 연습으로 간주되는 모든 것이 실제로 나쁜 연습 이 아니라고 말합시다 . ( "절대 평범한 것을 사용하지 말라 int" 는 충고가
Steve Summit

1
@Steve 나는 당신이 그 소리를 어디서 들었는지 전혀 모르겠지만 그것은 확실히 우리가 이야기하고있는 나쁜 (나쁜?) 연습이 아닙니다. 나를 오해하지 마십시오. 그 당시 코드는 완벽하게 괜찮 았지만 k & r을 많이보고 싶지는 않지만 요즘에는 역사적인 기록으로보고 싶습니다. "그것은 K & R에있어는"단지 전부 좋은 품질의 지표 이러한 일이 아니다
Voo

9

의 다소 불쾌한 측면은 printf스트레이 메모리 읽기가 제한된 (및 허용 가능한) 해를 입힐 수있는 플랫폼에서도 서식 지정 문자 중 하나가 %n다음 인수를 쓰기 가능한 정수에 대한 포인터로 해석하도록하고 식별 된 변수에 저장 될 지금까지 출력 된 문자 수. 나는 그 기능을 직접 사용하지 않았고, 가끔은 내가 실제로 사용하는 기능만을 포함하도록 작성한 경량 printf 스타일 메소드를 사용하지만 (그 중 하나 또는 유사한 것을 포함하지 않음) 수신 된 표준 printf 함수 문자열을 공급합니다. 신뢰할 수없는 출처의 경우 임의 저장소를 읽을 수있는 능력 이상의 보안 취약성이 노출 될 수 있습니다.


8

아무도 언급하지 않았으므로 성능에 대한 메모를 추가하겠습니다.

정상적인 상황에서 컴파일러 최적화가 사용되지 않는다고 가정하면 (즉, printf()실제로 호출 printf()하지 않고 fputs()) printf(), 특히 긴 문자열의 경우 효율성이 떨어질 것으로 예상 합니다. printf()변환 지정자가 있는지 확인하기 위해 문자열을 구문 분석해야 하기 때문 입니다.

이를 확인하기 위해 몇 가지 테스트를 실행했습니다. 테스트는 gcc 4.8.4로 Ubuntu 14.04에서 수행됩니다. 내 컴퓨터는 Intel i5 CPU를 사용합니다. 테스트중인 프로그램은 다음과 같습니다.

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

둘 다 gcc -Wall -O0. 시간은를 사용하여 측정됩니다 time ./a.out > /dev/null. 다음은 일반적인 실행의 결과입니다 (5 번 실행했으며 모든 결과는 0.002 초 이내입니다).

에 대한 printf()변형 :

real    0m0.416s
user    0m0.384s
sys     0m0.033s

에 대한 fputs()변형 :

real    0m0.297s
user    0m0.265s
sys     0m0.032s

이 효과는 문자열 이 매우 긴 경우 증폭됩니다 .

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

를 들어 printf()변형 (세 번, 실제 플러스 / 마이너스 1.5 초를 실행) :

real    0m39.259s
user    0m34.445s
sys     0m4.839s

를 들어 fputs()변형 (세 번, 실제 플러스 / 마이너스 0.2 초를 실행) :

real    0m12.726s
user    0m8.152s
sys     0m4.581s

참고 : 어셈블리 GCC에 의해 생성 된 검사 후, 그 GCC는 최적화 실현 fputs()에 전화 fwrite()도 함께 전화를 -O0. ( printf()호출은 변경되지 않습니다.) 컴파일러 fwrite()가 컴파일 시간에 문자열 길이를 계산하기 때문에 이것이 내 테스트를 무효화할지 여부는 확실하지 않습니다 .


2
이 같은 테스트를 무효화하지 않습니다 fputs()종종 문자열 상수를 사용하고 최적화 기회 make.This 말에 당신이 원하는 지점의 일부입니다있는이 동적으로 생성 된 문자열로 테스트 실행을 추가 fputs()하고 fprintf()멋진 추가 데이터 포인트가 될 것입니다 .
Patrick Schlüter 2015

@ PatrickSchlüter 동적으로 생성 된 문자열로 테스트하는 것은이 질문의 목적에 맞지 않는 것 같습니다 ... OP는 인쇄 할 문자열 리터럴에만 관심이있는 것 같습니다.
user12205

1
그의 예제가 문자열 리터럴을 사용하더라도 명시 적으로 언급하지 않습니다. 사실,이 책의 조언에 대한 그의 혼란은 예제에서 문자열 리터럴을 사용한 결과라고 생각합니다. 문자열 리터럴을 사용하면 책의 조언은 다소 모호하며 동적 문자열을 사용하면 좋은 조언입니다.
Patrick Schlüter 2015

1
/dev/null일종의 장난감으로 만듭니다. 일반적으로 형식화 된 출력을 생성 할 때 목표는 출력을 버리지 않고 어딘가로가는 것입니다. "실제로 데이터를 버리지 않음"시간을 추가하면 어떻게 비교합니까?
Yakk-Adam Nevraumont 2015

7
printf("Hello World\n")

자동으로 동등한 것으로 컴파일됩니다.

puts("Hello World")

실행 파일을 diassembling하여 확인할 수 있습니다.

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

사용

char *variable;
... 
printf(variable)

보안 문제로 이어질 것이므로 printf를 그런 식으로 사용하지 마십시오!

그래서 당신의 책은 실제로 정확합니다. 하나의 변수와 함께 printf를 사용하는 것은 더 이상 사용되지 않지만 자동으로 puts가되기 때문에 여전히 printf ( "my string \ n")를 사용할 수 있습니다.


12
이 동작은 실제로 전적으로 컴파일러에 따라 다릅니다.
Jabberwocky

6
이것은 오해의 소지가 있습니다. 당신 A compiles to B은 말하지만 실제로는 당신이 의미 A and B compile to C합니다.
Sebastian Mach

6

gcc의 경우 printf()및 검사에 대한 특정 경고를 활성화 할 수 있습니다 scanf().

gcc 문서는 다음과 같이 설명합니다.

-Wformat에 포함되어 -Wall있습니다. , 옵션을 체크 형식의 일부 측면을 더 제어를 위해 -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, 및 -Wformat=2사용할 수 있지만에 포함되지 않습니다 -Wall.

-Wformat내에서 사용할 수있는 -Wall옵션은 도움이 이러한 경우를 찾을 수 있다는 몇 가지 특별한 경고를 사용하지 않습니다

  • -Wformat-nonliteral 형식 지정자로 문자열 litteral을 전달하지 않으면 경고합니다.
  • -Wformat-security위험한 구조를 포함 할 수있는 문자열을 전달하면 경고합니다. 의 하위 집합입니다 -Wformat-nonliteral.

활성화를 활성화하면 -Wformat-security코드베이스에있는 몇 가지 버그 (로깅 모듈, 오류 처리 모듈, xml 출력 모듈)가 모두 매개 변수에 % 문자로 호출되면 정의되지 않은 작업을 수행 할 수있는 일부 기능이 있음을 인정해야합니다. 우리 코드베이스는 이제 약 20 년이 지났으며 이러한 종류의 문제를 알고 있었지만 이러한 버그 중 코드베이스에 여전히 얼마나 많은 버그가 있는지 경고를 활성화했을 때 매우 놀랐습니다.)


1

부수적 인 우려 사항이있는 다른 잘 설명 된 답변 외에도 제공된 질문에 대해 정확하고 간결한 답변을 드리고자합니다.


printf단일 인수 (변환 지정자 없음)가 더 이상 사용되지 않는 이유는 무엇 입니까?

printf일반적으로 하나의 인자를 가진 함수 호출되어 있지 되지도 어떤 취약점이없는 경우에 당신은 항상 코드 겠지만 적절하게 사용.

C 상태 초보자부터 상태 전문가에 이르기까지 전 세계의 사용자는 printf이러한 방식을 사용하여 간단한 텍스트 구문을 콘솔에 출력합니다.

또한 누군가는이 유일한 인수가 문자열 리터럴인지 또는 문자열에 대한 포인터인지 구별해야합니다. 이는 유효하지만 일반적으로 사용되지 않습니다. 후자의 경우, 물론 포인터가 유효한 문자열을 가리 키도록 적절하게 설정 되지 않은 경우 불편한 출력 또는 모든 종류의 Undefined Behavior 가 발생할 수 있지만 형식 지정자가 다음을 제공하여 각 인수와 일치하지 않는 경우에도 이러한 일이 발생할 수 있습니다. 여러 인수.

물론, 하나의 유일한 인수로 제공된 문자열에 변환이 발생하지 않기 때문에 형식 또는 변환 지정자가있는 것도 옳고 적절하지 않습니다.

"Hello World!", 질문에서 제공 한 것처럼 해당 문자열 내부에 형식 지정자가없는 유일한 인수 와 같은 간단한 문자열 리터럴 을 제공합니다.

printf("Hello World!");

되어 있지 되지 또는 " 나쁜 관행 "전혀 없으며 어떤 취약성을 가지고있다.

사실, 많은 C 프로그래머들은 HelloWorld 프로그램과이 printf문장을 첫 번째 로 C 또는 프로그래밍 언어를 배우고 사용하기 시작했습니다 .

더 이상 사용되지 않는 경우에는 그렇지 않습니다.

내가 읽고있는 책 printf에서 단일 인수 (변환 지정자 없음)가 더 이상 사용되지 않는다고 기록되어 있습니다.

그럼 저는 책이나 저자 자체에 초점을 맞출 것입니다. 만약 저자가 정말로 그렇게하고 있다면, 잘못된 주장과 심지어 그 / 그녀가 그렇게하는 이유 를 명시 적으로 설명하지 않고 (그 주장이 그 책에서 실제로 문자 그대로 동일하다면) 나는 그것을 나쁜 책으로 간주 할 것 입니다. 좋은의 책은 그 반대로, 설명해야 하는 이유 방법이나 기능을 프로그램의 특정 종류를 피하기 위해.

위에서 말한 바에 따르면, printf하나의 인수 (문자열 리터럴)와 형식 지정자 없이 사용 하는 것은 어떤 경우에도 더 이상 사용되지 않거나 "나쁜 관행" 으로 간주되지 않습니다 .

저자에게 그가 의미하는 바가 무엇인지 물어봐야하며, 다음 판 또는 일반적으로 각인에 대한 관련 섹션을 명확히하거나 수정하도록하십시오.


당신은 추가 할 수 printf("Hello World!");있습니다 하지 에 해당 puts("Hello World!");추천의 저자에 대해 뭔가를 알려줍니다, 어쨌든.
chqrlie
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.