C언어 - 버퍼 오버플로우(Buffer Overflow) 취약점의 이해

1. Buffer Overflow

1.1. 보안 취약점

1.1.1. 발생 원인

  • Buffer Overflow는 버퍼의 크기를 초과하여 데이터를 기록하면서 인접한 메모리를 덮어쓰는 보안 취약점이다.
  • 버퍼의 경계를 넘어 데이터를 쓰면서 인접한 메모리를 덮어쓰게 된다.
Buffer Overflow

1.1.2. 영향 및 위험성

  • 프로그램이 비정상 종료(Crash)되거나 오류가 발생할 수 있다.
  • 중요한 데이터가 손상되어 예기치 않은 동작이 발생할 수 있다.
  • 공격자가 메모리를 조작하여 임의 코드 실행 또는 시스템 제어권을 탈취할 수 있다.

1.2. Stack Buffer Overflow

  • Stack Buffer Overflow는 Stack 영역에 할당된 버퍼의 크기를 초과하여 데이터를 쓰면서, 인접한 메모리 영역을 덮어쓰는 취약점이다.
  • 주요 원인은 입력 길이에 대한 검증 부족과 경계 검사를 수행하지 않는 취약한 함수 사용에 있다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// Demonstrate stack buffer overflow:
//   Stack layout: [buffer 16 bytes][cmd 16 bytes]
static void vulnerable(const char *input)
{
    char cmd[16]  = "date";  // default command to execute
    char buffer[16];         // overflow target (no bounds check)

    // VULNERABLE: strcpy does not check input length.
    // Input longer than 16 bytes overwrites cmd[].
    strcpy(buffer, input);

    // Execute whatever cmd[] contains (may be overwritten).
    system(cmd);
}

int main(int argc, char *argv[]) 
{
    char* input = "";

    if (argc >= 2) {
        input = argv[1];
    }

    vulnerable(input);
    return 0;
}
  • strcpy() 함수는 입력 데이터 길이를 검사하지 않기 때문에 버퍼 크기를 초과하는 데이터가 복사될 수 있다.
  • 이로 인해 스택의 인접 변수에 덮어써지며, 프로그램의 동작이 변경될 수 있다.
$ ./test1.out
Wed May 20 15:55:38 UTC 2026

$ ./test1.out "AAAAAAAAAAAAAAAAls -al"
total 80
drwxr-xr-x  2 root root  4096 May 20 15:55 .
drwxr-xr-x 13 root root  4096 May 20 15:46 ..
-rw-r--r--  1 root root   166 May 20 13:50 Makefile
-rw-r--r--  1 root root   853 May 20 15:55 test1.c
-rwxr-xr-x  1 root root 15960 May 20 15:55 test1.out

1.2.1. 정상 동작

input: ""

[Stack]
Low address
┌──────────────────────────────────────────────────────────────────────────────────┐
│ buffer[16]                                                                       │
│ [\0 ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ] │
├──────────────────────────────────────────────────────────────────────────────────┤
│ cmd[16]                                                                          │
│ [ d ][ a ][ t ][ e ][\0 ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ] │
└──────────────────────────────────────────────────────────────────────────────────┘
High address
=> system("date")

1.2.2. 버퍼 오버플로우 동작

input: "AAAAAAAAAAAAAAAAls -al"
       |<----- 16 ----->|overflows -->|

[Stack]
Low address
┌──────────────────────────────────────────────────────────────────────────────────┐
│ buffer[16]                                                                       │
│ [ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ][ A ] │
├──────────────────────────────────────────────────────────────────────────────────┤
│ cmd[16]                                                                          │
│ [ l ][ s ][   ][ - ][ a ][ l ][\0 ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ][ ? ] │
└──────────────────────────────────────────────────────────────────────────────────┘
High address
=> system("ls -al")

1.2.3. 안전하게 수정하는 방법

{
    ...
    // Copy at most 15 bytes (last 1 byte reserved for \0)
    strncpy(buffer, input, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // always null-terminate
    ...
}
  • 입력 값 길이를 제한하는 안전한 함수(strncpy 등)를 사용하여 버퍼 범위를 초과하지 않도록 해야 한다.
  • 또한, 길이를 제한하는 함수도 반드시 NULL 종료를 보장하지 않으므로, 명시적으로 '\0'을 설정해야 한다.

1.3. Heap Buffer Overflow

  • Buffer Overflow는 메모리 경계를 초과한 데이터 쓰기로 인해 발생하는 취약점이며, 이러한 현상은 Stack뿐만 아니라 Heap과 같은 동적 메모리 영역에서도 동일한 방식으로 발생한다.
  • 특히, Heap Overflow는 인접 데이터뿐만 아니라 메모리 할당 관리 시스템 정보를 손상시킬 수 있으며, 이는 프로그램 충돌뿐만 아니라 임의 코드 실행으로 이어질 수 있다.

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

// Heap layout (one malloc block for the whole struct):
//   [name: 8 bytes][role: 8 bytes]  <- contiguous inside the struct
typedef struct {
    char name[8];
    char role[8];
} Session;

static void check_access(const char *input) 
{
    Session *s = malloc(sizeof(Session));   // one heap allocation
    if (NULL == s ) {
           perror("malloc");
           exit(EXIT_FAILURE);
    }

    strcpy(s->role, "user");
    memcpy(s->name, input, strlen(input) + 1);

    printf("#'%s' access granted: '%s' privileges.\n", s->name, s->role);

    free(s);
}

int main(int argc, char *argv[]) 
{
    const char *input = (argc >= 2) ? argv[1] : "guest";

    check_access(input);

    return 0;
}
  • 입력 길이를 검증하지 않고 name[8]에 복사하기 때문에, 8바이트를 넘는 입력은 인접한 role[8]까지 덮어쓸 수 있다.
  • memcpy()는 복사할 크기를 직접 지정하지만, 입력 길이를 검증하지 않으면 버퍼 크기를 초과할 수 있다.
$ ./test2.out alice
#'alice' access granted: 'user' privileges.

$ ./test2.out operatoradmin
#'operatoradmin' access granted: 'admin' privileges.

1.3.1. 정상 동작

input: "alice"

[Heap]
┌───────────────────────────────────────────────────────────────────────────────────┐
│ name[8]                                  role[8]                                  │
│ [ a ][ l ][ i ][ c ][ e ][\0 ][ ? ][ ? ] [ u ][ s ][ e ][ r ][\0 ][ ? ][ ? ][ ? ] │
└───────────────────────────────────────────────────────────────────────────────────┘
=> role remains "user"

1.3.2. 버퍼 오버플로우 동작

input: "operatoradmin"
        |<- 8 ->|overflows -->|

[Heap]
┌───────────────────────────────────────────────────────────────────────────────────┐
│ name[8]                                  role[8]                                  │
│ [ o ][ p ][ e ][ r ][ a ][ t ][ o ][ r ] [ a ][ d ][ m ][ i ][ n ][\0 ][ ? ][ ? ] │
└───────────────────────────────────────────────────────────────────────────────────┘
=> role is overwritten to "admin"

1.3.3. 안전하게 수정하는 방법

{
    ...
    size_t n = strnlen(input, sizeof(s->name) - 1);
    memcpy(s->name, input, n);
    s->name[n] = '\0';
    ...
}
  • 입력값 검증과 길이 제한을 통해 버퍼 범위를 초과하지 않도록 해야 한다.
  • 또한, 경계 검사와 NULL 종료 보장을 통해 메모리 손상 및 취약점을 방지해야 한다.

1.4. 재현을 위한 컴파일 옵션

  • 버퍼 오버플로우(Buffer Overflow)는 메모리 경계를 초과하여 데이터를 쓰는 대표적인 보안 취약점으로, 최신 시스템에서는 다양한 보안 기법에 의해 기본적으로 차단되어 있다.
  • 따라서, 취약점 예제 소스를 실습 환경에서 재현하기 위해서는 일부 보안 기능을 의도적으로 비활성화해야 한다.
gcc -no-pie -fno-stack-protector -z execstack -o output input.c

1.4.1. ASLR (Address Space Layout Randomization)

  • 메모리 주소를 실행할 때마다 랜덤하게 배치한다.
  • 이 기능이 활성화되어 있으면 코드, 스택, 라이브러리의 주소가 매번 변경되므로 공격자가 정확한 주소를 예측하기 어렵다.
  • 이를 우회하기 위한 옵션이 -no-pie이다. 이 옵션은 PIE(Position Independent Executable)를 비활성화한다.

1.4.2. Stack Canary

  • 함수 반환 전에 스택이 변조되었는지 검사한다.
  • 기본적으로 스택 프레임에 Canary 값을 삽입하고, 함수 종료 시 이를 확인하여 값이 변경되면 프로그램을 강제 종료한다.
  • 이를 비활성화하는 옵션이 -fno-stack-protector이다. 이 옵션으로 버퍼 오버플로우로 반환 주소를 덮어써도 강제 종료되지 않고 정상 흐름으로 진행된다.

1.4.3. NX (Non-eXecutable) Stack

  • 스택 영역을 실행 불가능하게 설정한다.
  • 이를 해제하는 옵션이 -z execstack이다. 이 옵션은 스택 영역을 실행 가능(Executable) 상태로 변경한다.