c를 깊게 배울수록 메모리를 생각하게되고 파이썬이나 자바가 얼마나 개발자 친화적인 언어인지 느낀곤 한다.
깊게배우고 문자열 등을 다루면서 버퍼를 고려안하고 코드를 짜니 내가 예상과는 다르게 동작하곤 했다.
코드를 짤때마다 헷갈려 정리하며 복습하고 나중에도 헷갈리면 보면서 복습하고자한다.
버퍼(Buffer)는 데이터를 임시로 저장하는 메모리 공간이다.
특히 입출력(I/O) 속도를 최적화하기 위해 사용되며, CPU와 입출력 장치 간 속도 차이를 줄여준다.
(1) 입출력 속도 향상
(2) 데이터 전송 최적화
C 언어의 입출력 함수(printf
, scanf
, fgets
, getchar
, gets
) 등에서 버퍼를 사용한다.
printf()
와 scanf()
는 버퍼를 사용하여 출력/입력을 최적화한다.stdin
)은 엔터(Enter)를 눌러야 버퍼에서 프로그램으로 전달된다.💡 예제 1: 버퍼의 지연 효과
#include <stdio.h>
int main() {
printf("입력하세요: "); // 출력 버퍼에 저장됨 (즉시 출력되지 않을 수 있음)
while (1); // 무한 루프 (출력 버퍼가 플러시되지 않아 텍스트가 안 보일 수도 있음)
return 0;
}
해결 방법: fflush(stdout);
를 사용하여 버퍼를 강제 출력
printf("입력하세요: ");
fflush(stdout); // 출력 버퍼 강제 비움
scanf()
와 fgets()
scanf()
사용 시 버퍼에 남아 있는 개행 문자(\n
)가 문제를 일으킬 수 있음.💡 예제 2: scanf()
의 버퍼 문제
#include <stdio.h>
int main() {
int age;
char name[50];
printf("나이를 입력하세요: ");
scanf("%d", &age); // 개행문자(\n)가 버퍼에 남음
printf("이름을 입력하세요: ");
fgets(name, sizeof(name), stdin); // 이전의 \n 때문에 입력이 스킵됨
printf("나이: %d, 이름: %s\n", age, name);
return 0;
}
해결 방법: scanf()
후 getchar()
또는 fflush(stdin);
사용
scanf("%d", &age);
while (getchar() != '\n'); // 개행문자가 나올때까지 버퍼 쭉 비우기
https://deepcode.dev/7 - 이 이슈에 대해 정말 쉽게 정리해놓으신 글
C에서는 버퍼를 직접 조작하는 함수도 제공된다.
함수 | 설명 |
---|---|
fflush(FILE *stream) | 지정한 스트림(stdin , stdout , stderr )의 버퍼 비우기 |
setvbuf(FILE *stream, char *buf, int mode, size_t size) | 사용자 정의 버퍼 설정 |
setbuf(FILE *stream, char *buf) | 기본적인 버퍼 설정 |
💡 예제 3: setbuf()
로 버퍼 끄기
#include <stdio.h>
int main() {
setbuf(stdout, NULL); // 표준 출력 버퍼 끄기
printf("출력 버퍼 없이 즉시 출력됩니다.\n");
return 0;
}
fgets()
와 getchar()
를 사용한 안전한 입력fgets()
는 개행문자까지 읽고 버퍼를 비우기 때문에 안전하지만 개행문자를 직접 제거해줘야한다.💡 예제 4: 안전한 문자열 입력
#include <stdio.h>
int main() {
char buffer[50];
printf("문자열을 입력하세요: ");
fgets(buffer, sizeof(buffer), stdin); // 안전한 입력 방식
printf("입력된 문자열: %s", buffer);
return 0;
}
보완점: 개행 문자 제거
buffer[strcspn(buffer, "\n")] = '\0'; // 개행 문자를 꼭 제거해야한다
buffer[strlen(buffer)-1] = '\0';
버퍼 오버플로우(Buffer Overflow)는 버퍼 크기보다 더 많은 데이터를 저장하려고 할 때 발생하는 문제다.
프로그램이 의도치 않게 인접한 메모리 영역을 침범하여 데이터가 덮어씌워질 위험이 있다.
(1) 입력 크기 제한을 고려하지 않음
gets()
같은 위험한 함수를 사용하여 입력 크기를 제한하지 않으면, 버퍼 크기를 초과하는 데이터가 입력될 수 있다.(2) 메모리 관리 실수
strcpy()
, sprintf()
처럼 크기 검사를 하지 않는 함수 사용 시, 버퍼 크기를 초과하는 데이터를 복사할 위험이 크다.(3) 스택과 힙의 구조적 특성
버퍼 오버플로우 공격은 버퍼의 크기를 초과하는 데이터를 입력하여, 메모리 영역을 덮어씌우고 실행 흐름을 조작하는 해킹 기법이다.
(1) 리턴 주소 덮어쓰기(Return Address Overwrite)
(2) 힙 오버플로우(Heap Overflow)
(3) SEH(Structured Exception Handler) 오버라이드
(4) 포맷 스트링 공격(Format String Attack)
printf()
같은 함수에서 입력값을 잘못 처리하면, 공격자가 메모리 값을 읽거나 덮어씌울 수 있다.%s
, %x
, %n
등의 서식 지정자를 악용하여, 리턴 주소를 조작하거나 메모리 데이터를 유출할 수 있다.(5) 환경 변수 조작(Environment Variable Exploitation)
LD_PRELOAD
같은 환경 변수를 이용한 공격이 대표적이다.#include <stdio.h>
#include <string.h>
void secret_function() {
printf("You have successfully hacked the program!\n");
}
void vulnerable_function() {
char buffer[20];
printf("Enter input: ");
gets(buffer); // 버퍼 오버플로우 발생 가능
}
int main(void) {
vulnerable_function();
return 0;
}
공격자는 buffer를 초과하는 데이터를 입력하여 함수의 리턴 주소를 secret_function()의 주소로 변경할 수 있다.
이를 통해 허가되지 않은 코드 실행이 가능하다.
https://onecoin-life.com/31 - 위의 예제와 같은 원리로 더 깊게 설명되어있다.
(1) 안전한 함수 사용
gets()
, strcpy()
, sprintf()
대신 fgets()
, strncpy()
, snprintf()
같은 안전한 함수 사용을 권장한다.(2) 실행 방지 기법(DEP, ASLR)
(3) 스택 보호(Stack Canaries)
(4) 포맷 스트링 방어
printf(user_input)
처럼 사용자 입력을 직접 포맷 문자열로 사용하지 않는다.printf("%s", user_input);
형태로 지정하여 사용한다.(5) 권한 제한 및 보안 강화
-fstack-protector
, -D_FORTIFY_SOURCE=2
같은 보안 옵션을 활성화한다.버퍼 오버플로우가 발생하지 않도록 조심하라는 내용까지 알고있었는데 찾아보다보니 버퍼 오버플로우 공격으로 취약점이 될수있다는 점이 흥미로워 작성하게 되었다.
알아보니 이전에 다뤄봤던 sql injection과 입력값에 주입한다는 점이 비슷한것 같다.
지금까지의 내 지식으로는 사용자의 입력을 받는부분이 개발시에 가장 깊게 고려해야하는 취약점 중 하나인것 같다.