[C] 버퍼와 버퍼 오버플로우 공격(Buffer Overflow Attack)

김태희·2025년 2월 1일
0
post-thumbnail

버퍼와 버퍼 오버플로우 공격(Buffer Overflow Attack)

c를 깊게 배울수록 메모리를 생각하게되고 파이썬이나 자바가 얼마나 개발자 친화적인 언어인지 느낀곤 한다.

깊게배우고 문자열 등을 다루면서 버퍼를 고려안하고 코드를 짜니 내가 예상과는 다르게 동작하곤 했다.

코드를 짤때마다 헷갈려 정리하며 복습하고 나중에도 헷갈리면 보면서 복습하고자한다.

버퍼(Buffer)란?

버퍼(Buffer)는 데이터를 임시로 저장하는 메모리 공간이다.
특히 입출력(I/O) 속도를 최적화하기 위해 사용되며, CPU와 입출력 장치 간 속도 차이를 줄여준다.


1. 버퍼의 역할

(1) 입출력 속도 향상

  • 입출력 장치는 CPU보다 느리기 때문에, 데이터를 한번에 모아서 처리하는 방식(버퍼링)이 효율적이다.
  • 버퍼 없이 한 글자씩 처리하면 CPU가 계속 대기해야 한다 → 성능 저하

(2) 데이터 전송 최적화

  • 버퍼를 사용하면 여러 데이터를 한 번에 읽고/쓰기가 가능하다.
  • 네트워크, 파일 입출력, 키보드 입력 등에서 활용된다.

2. C 언어에서의 버퍼 활용

C 언어의 입출력 함수(printf, scanf, fgets, getchar, gets) 등에서 버퍼를 사용한다.

(1) 표준 입력(stdin)과 출력(stdout) 버퍼

  • printf()scanf()버퍼를 사용하여 출력/입력을 최적화한다.
  • 키보드 입력(stdin)은 엔터(Enter)를 눌러야 버퍼에서 프로그램으로 전달된다.

💡 예제 1: 버퍼의 지연 효과

#include <stdio.h>

int main() {
    printf("입력하세요: ");  // 출력 버퍼에 저장됨 (즉시 출력되지 않을 수 있음)
    while (1);  // 무한 루프 (출력 버퍼가 플러시되지 않아 텍스트가 안 보일 수도 있음)
    return 0;
}

해결 방법: fflush(stdout); 를 사용하여 버퍼를 강제 출력

printf("입력하세요: ");
fflush(stdout);  // 출력 버퍼 강제 비움

(2) 입력 버퍼(stdin)의 문제: 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 - 이 이슈에 대해 정말 쉽게 정리해놓으신 글


(3) 버퍼를 직접 조작하는 함수

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;
}

(4) 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)란?

버퍼 오버플로우(Buffer Overflow)는 버퍼 크기보다 더 많은 데이터를 저장하려고 할 때 발생하는 문제다.
프로그램이 의도치 않게 인접한 메모리 영역을 침범하여 데이터가 덮어씌워질 위험이 있다.


1. 버퍼 오버플로우가 발생하는 이유

(1) 입력 크기 제한을 고려하지 않음

  • gets() 같은 위험한 함수를 사용하여 입력 크기를 제한하지 않으면, 버퍼 크기를 초과하는 데이터가 입력될 수 있다.

(2) 메모리 관리 실수

  • strcpy(), sprintf()처럼 크기 검사를 하지 않는 함수 사용 시, 버퍼 크기를 초과하는 데이터를 복사할 위험이 크다.

(3) 스택과 힙의 구조적 특성

  • 스택이나 힙 영역에서 메모리가 연속적으로 배치되기 때문에, 특정 변수가 초과된 데이터를 받을 경우 다른 변수나 함수의 실행 흐름을 방해할 수 있다.

2. 버퍼 오버플로우 공격(Buffer Overflow Attack)이란?

버퍼 오버플로우 공격은 버퍼의 크기를 초과하는 데이터를 입력하여, 메모리 영역을 덮어씌우고 실행 흐름을 조작하는 해킹 기법이다.


1. 공격 방식

(1) 리턴 주소 덮어쓰기(Return Address Overwrite)

  • 스택에서 함수의 리턴 주소를 조작하여, 공격자가 원하는 코드가 실행되도록 만든다.
  • 쉘코드(shellcode)를 삽입하고 실행하는 방식이 대표적이다.

(2) 힙 오버플로우(Heap Overflow)

  • 동적 할당된 힙 영역을 초과하는 데이터를 삽입하여, 메모리 구조를 변조하거나 프로그램 흐름을 변경할 수 있다.

(3) SEH(Structured Exception Handler) 오버라이드

  • Windows 환경에서 예외 처리 핸들러(Structured Exception Handler)를 변조하여, 악성 코드 실행을 유도하는 방식이다.

(4) 포맷 스트링 공격(Format String Attack)

  • printf() 같은 함수에서 입력값을 잘못 처리하면, 공격자가 메모리 값을 읽거나 덮어씌울 수 있다.
  • %s, %x, %n 등의 서식 지정자를 악용하여, 리턴 주소를 조작하거나 메모리 데이터를 유출할 수 있다.

(5) 환경 변수 조작(Environment Variable Exploitation)

  • 환경 변수를 조작하여 셸코드를 실행하거나 프로그램의 동작을 변경할 수 있다.
  • LD_PRELOAD 같은 환경 변수를 이용한 공격이 대표적이다.

2. 버퍼 오버플로우 공격 예제 (리턴 주소 조작)

#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 - 위의 예제와 같은 원리로 더 깊게 설명되어있다.


3. 방어 기법

(1) 안전한 함수 사용

  • gets(), strcpy(), sprintf() 대신 fgets(), strncpy(), snprintf() 같은 안전한 함수 사용을 권장한다.

(2) 실행 방지 기법(DEP, ASLR)

  • DEP(Data Execution Prevention): 스택과 힙을 실행 불가능하게 설정하여, 공격자가 임의의 코드를 실행하지 못하도록 한다.
  • ASLR(Address Space Layout Randomization): 메모리 주소를 랜덤화하여, 공격자가 정확한 메모리 위치를 예측하지 못하게 만든다.

(3) 스택 보호(Stack Canaries)

  • 함수 호출 전에 스택에 특수한 값(Canary)을 저장하고, 함수 종료 전에 Canary 값이 변조되었는지 검사하여 공격을 탐지한다.

(4) 포맷 스트링 방어

  • printf(user_input)처럼 사용자 입력을 직접 포맷 문자열로 사용하지 않는다.
  • 항상 명시적으로 printf("%s", user_input); 형태로 지정하여 사용한다.

(5) 권한 제한 및 보안 강화

  • 실행 파일에 최소한의 권한을 부여하여, 공격자가 시스템을 완전히 장악하는 것을 방지한다.
  • 컴파일 시 -fstack-protector, -D_FORTIFY_SOURCE=2 같은 보안 옵션을 활성화한다.

버퍼 오버플로우가 발생하지 않도록 조심하라는 내용까지 알고있었는데 찾아보다보니 버퍼 오버플로우 공격으로 취약점이 될수있다는 점이 흥미로워 작성하게 되었다.

알아보니 이전에 다뤄봤던 sql injection과 입력값에 주입한다는 점이 비슷한것 같다.

지금까지의 내 지식으로는 사용자의 입력을 받는부분이 개발시에 가장 깊게 고려해야하는 취약점 중 하나인것 같다.

0개의 댓글

관련 채용 정보