profile image

L o a d i n g . . .

Buffer Overflow

C언어에서 버퍼란 지정된 크기의 메모리 공간이라는 뜻이다.

버퍼 오버플로우 취약점은 버퍼가 허용할 수 있는 양의 데이터보다 더 많은 값이 저장되어 버퍼가 넘치는 취약점을 뜻한다.

 

일반적으로 버퍼 오버플로우는 발생하는 위치에 따라 스택 버퍼 오버플로우, 힙 오버플로우와 같이 나눠서 부른다.

버퍼 오버플로우는 인접한 메모리를 오염시키는 취약점이기 때문에 어떤 메모리를 오염시킬 수 있는지에 따라 공격 방법이 달라지기 때문이다.

 

그중 스택 버퍼 오버플로우는 지역 변수가 할당되는 스택 메모리에서 오버플로우가 발생하는 경우이다.

 

 

스택 버퍼 오버플로우

// stack-1.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char buf[16];
    gets(buf);
    
    printf("%s", buf);
}

stack-1.c는 16 바이트 버퍼 buf를 스택에 할당한 후, gets 함수를 통해 사용자로부터 데이터를 입력받아 이를 그대로 출력하는 코드이다. gets함수는 사용자가 개행을 입력하기 전까지 입력했던 모든 내용을 첫 번째 인자로 전달된 버퍼에 저장하는 함수이다. 그런데 gets 함수에는 별도의 길이 제한이 없기 때문에 16 바이트가 넘는 데이터를 입력한다면 스택 버퍼 오버플로우가 발생한다.

 

 

ex1)

버퍼를 오버플로우시켜 ret 영역을 0x41414141로 만들려면?

ret 영역에 AAAA를 입력해준다.

 


 

 

// stack-2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {
    int auth = 0;
    char temp[16];
    
    strncpy(temp, password, strlen(password));
    
    if(!strcmp(temp, "SECRET_PASSWORD"))
        auth = 1;
   	return auth;
}
int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: ./stack-1 ADMIN_PASSWORD\n");
        exit(-1);
    }
    
    if (check_auth(argv[1]))
        printf("Hello Admin!\n");
    else
        printf("Access Denied!\n");
}

stack-2.c main 함수는 argv[1]을 check 함수의 인자로 전달한 후 그 리턴 값을 받아온다. 리턴 값이 0이 아니라면 "Hello Admin!"을, 0이라면 "Access Denied!"라는 문자열을 출력한다.

 

핵심이 되는 check 함수에서는 16 바이트 크기의 temp 버퍼에 입력받은 패스워드를 복사한 후 "SECRET_PASSWORD" 문자열과 비교한다. 문자열이 같다면 auth 변수를 1로 설정하고 auth를 리턴한다.

 

그런데 line 10에서 strncpy 함수를 통해 temp 버퍼에 복사할 때, temp의 크기인 16 바이트가 아닌 인자로 전달된 password 문자열의 길이만큼을 복사한다. 따라서 argv[1]에 16 바이트가 넘는 문자열을 전달한다면 길이 제한 없이 문자열이 복사되어 스택 버퍼 오버플로우가 발생하게 된다.

 

temp 버퍼 뒤에 auth 값이 존재하므로, 오버플로우가 발생해 공격자의 데이터가 auth 값을 바꾼다면 auth가 0이 아닌 다른 값이 될 수 있다. 이 경우 실제 인증 여부와는 상관없이 line 24의 if(check_auth(argv[1])) 문은 항상 참을 반환한다.

 

 

ex2)

버퍼 오버플로우를 통해 temp 뒤의 auth 값을 1로 바꿔 인증을 통과할 수 있다.

 


 

// stack-3.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
    char win[4];
    int size;
    char buf[24];
    
    scanf("%d", &size);
    read(0, buf, size);
    if (!strncmp(win, "ABCD", 4)){
        printf("Theori{-----------redacted---------}");
    }
}

stack-3.c main함수는 24 바이트 크기의 버퍼 buf를 할당한다. scanf 함수를 통해 size 변수에 값을 입력받고, size만큼 buf에 데이터를 입력받는다.

 

stack-1.c에서는 길이 검증이 없는 함수를 사용해 스택 버퍼 오버플로우가 발생했고, 이번에는 고정된 크기의 버퍼보다 더 긴 데이터를 입력받아 스택 버퍼 오버플로우가 발생한다.

 

 

ex3)

size를 32로 해 고정된 버퍼 길이보다 길게 입력받아 win을 조작할 수 있다.

 


 

// stack-4.c
#include <stdio.h>
int main(void) {
	char buf[32] = {0, };
	read(0, buf, 31);
	sprintf(buf, "Your Input is: %s\n", buf);
	puts(buf);
}

stack-4.c는 32바이트 크기 buf를 초기화한 후 데이터를 31바이트 입력받고, sprintf 함수를 통해 출력할 문자열을 저장한 뒤 출력하는 코드이다.

 

read 함수에서 받는 입력이 32바이트를 넘진 않지만, sprintf 함수를 통해 버퍼에 값을 쓸 때 "Your Input is: "문자열을 추가한다는 사실을 생각해야 한다. 만약 buf에 31바이트를 꽉 채운다면 "Your Input is: " 문자열이 앞에 붙어 총 길이가 32바이트를 넘게 된다.

 

ex4)

sprinf 함수를 통해 "Your Input is"가 buf에 추가될 경우 버퍼 길이를 넘을 수 있다.

 

 

지금까지 살펴본 바와 같이 버퍼 오버플로우는 프로그래머가 길이에 대한 검증을 정확히 수행하지 못해 발생한다. 만약 공격 벡터로부터 데이터를 입력받고 이를 버퍼에 저장하는 코드가 있다면 이를 유심히 살펴볼 필요가 있다. 데이터를 버퍼에 입력받을 때는 입력받은 데이터가 버퍼의 범위를 초과하지 않는지 항상 정확히 검사해야 한다.

 

버퍼 오버플로우는 스택에서만 발생하는 취약점이 아니다. 프로그래머가 동적으로 메모리를 관리할 수 있는 힙에서도 똑같이 발생할 수 있다. 이들은 단지 발생하는 메모리 영역의 차이만 있을 뿐이고 취약점이 발생하는 원인이 본질적으로 다르진 않다.

 

단, 힙 영역은 스택 영역과 사용 목적이 다르기 때문에, 스택 버퍼 오버플로우와는 다른 방법으로 익스플로잇해야 한다.

 


 

// heap-1.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *input = malloc(40);
    char *hello = malloc(40);
    
    memset(input, 0, 40);
    memset(hello, 0, 40);
    
    strcpy(hello, "HI!");
    read(0, input, 100);
    
    printf("Input: %s\n", input);
    printf("hello: %s\n", hello);
}

heap-1.c는 40바이트 크기의 힙 버퍼 input hello를 할당한 후, hello 버퍼에는 "HI!" 문자열을 복사하고 read 함수를 통해 input에 데이터를 입력받는 코드이다. 그러나 read 함수를 통해 입력받는 길이인 100바이트가 input 버퍼의 크기인 40바이트보다 크기 때문에 힙 오버플로우가 발생한다.

 

input 영역에서 버퍼 오버플로우가 발생해 hello의 메모리 영역까지 침범할 경우, line 16에서 hello 메모리를 출력할 때 "HI!" 문자열이 아니라 공격자에게 오염된 데이터가 출력된다.

 

 

Out-Of-Boundary

OOB(Out Of Boundary)는 버퍼의 길이 범위를 벗어나는 인덱스에 접근할 때 발생하는 취약점이다.

// oob-1.c
#include <stdio.h>
int main(void) {
    int win;
    int idx;
    int buf[10];
    
    printf("Which index? ");
    scanf("%d", &idx);
    printf("Value: ");
    scanf("%d", &buf[idx]);
    printf("idx: %d, value: %d\n", idx, buf[idx]);
    if(win == 31337){
        printf("Theori{-----------redacted---------}");
    }
}

oob1.c에서는 int형 배열 buf를 선언하고 idx 값을 입력받는다. 그 다음 buf[idx]에 정수를 입력받고 idx와 buf[idx] 값을 출력한다.

 

여기서 주의해야 할 점은 buf의 길이는 10이므로 buf의 인덱스로 사용될 수 있는 올바른 값은 0 이상 10 미만의 정수라는 사실이다. 그러나 코드에서는 line 10에서 입력받은 idx 값을 인덱스로 사용할 때 해당 값이 올바른 범위에 속해 있는지 검사하지 않는다. 

 

따라서 idx buf[idx]에 적당한 값을 줘서 win 변수를 31337로 만들어 줄 수 있는데, 바로 idx는 11로 그리고 11번째buf[idx]는 31337로 주면 된다.

 


 

// oob-2.c
#include <stdio.h>
int main(void) {
    int idx;
    int buf[10];
    int win;
    
    printf("Which index? ");
    scanf("%d", &idx);
    
    idx = idx % 10;
    printf("Value: ");
    scanf("%d", &buf[idx]);
    printf("idx: %d, value: %d\n", idx, buf[idx]);
    if(win == 31337){
        printf("Theori{-----------redacted---------}");
    }
}

oob-2.c oob-1.c와는 달리 line 12에 idx = idx % 10이라는 코드가 추가되었다. 이 코드로 OOB 취약점을 막을 수 있을까?

 

양의 정수를 10으로 나눈 나머지로 가능한 값은 0에서 9까지기 때문에 이는 얼핏 보면 안전해 보인다. 그러나 C언어에서는 피연산자가 음수라면 나머지 연산의 결과도 음수가 될 수 있다. 따라서 이 경우, buf의 인덱스로 쓸 수 있는 값의 범위는 -9 ~ 9이므로 나머지가 음수가 되게 한다면 OOB를 발생시킬 수 있다.

 

-> idx에 -1,-11,-21 등 일의 자리 수가 1로 끝나는 음수를 입력하고, buf 의 첫번째 값을 31337로 입력하면 된다.

 


 

//oob-3.c
#include <stdio.h>
int main(void) {
    int idx;
    int buf[10];
    int dummy[7];
    int win;
    printf("Which index? ");
    scanf("%d", &idx);
    
    if(idx < 0)
        idx = -idx;
    idx = idx % 10; // No more OOB!@!#!
    printf("Value: ");
    scanf("%d", &buf[idx]);
    printf("idx: %d, value: %d\n", idx, buf[idx]);
    if(win == 31337){
        printf("Theori{-----------redacted---------}");
    }
}

oob-3.c oob-2.c와는 다르게 idx가 음수일 경우 이를 양수로 바꿔주는 코드가 추가되었다. line 14에 들어가게 되는 idx 값은 양수가 되고, 10으로 나머지 연산을 했을 때 값의 범위는 0부터 9까지기 때문에 아무 문제가 없어 보인다.

 

그러나 C언어의 정수 표현에 대해 생각해보면, 뭔가 이상한 부분이 있다는 걸 알 수 있다.

  • idx = -pow(2, 31) (line 14) -> idx = -pow(2, 31) (line 15)

 

자세한 설명

더보기

C언어에서 int형으로 표현 가능한 정수의 범위는 -pow(2, 31) ~ pow(2, 31) - 1이다. int형은 32비트이기 때문에 총 pow(2, 32)개의 수를 표현할 수 있다. 그리고 int형은 0을 포함하기 때문에 표현할 수 있는 음의 정수의 갯수와 양의 정수의 갯수는 다르다.

 

int 형에서 -pow(2,31)은 표현 가능하지만 pow(2,31)은 표현 가능하지 않다. pow(2,31)은 표현 가능한 최대 정수보다 하나 더 크기 때문에 이는 -pow(2,31)과 같은 값이 된다.

 

oob-3.c를 다시 살펴보자. 만약 idx에 -pow(2, 31)을 넣었을 경우 line 14에서 절대값을 구하는 연산을 수행한 후에도 -2**31이 그대로 저장된다. 이는 buf 배열의 올바른 인덱스 범위를 벗어나기 때문에 OOB가 발생한다.

 

※ 이를 근본적으로 막기 위해서는 idx를 int형이 아닌 unsigned int형으로 선언하거나, 인덱스를 입력받은 이후에 if(idx < 0 || idx >= 10)과 같은 경계 검사 구문을 추가해야 한다.

 

-> %10 연산을 해서 -8 이 된다면 win 에 접근할 수 있다.

이때, 4byte int 의 최대값은 2147483647 (2진수로 0111 1111 1111 1111) 이고,
2147483648 은 여기다가 1 을 더해서 
2147483648 과 -2147483648 이 똑같다고 할 수 있다.

따라서 idx에 2147483648을 입력하면 -2147483648 % 10 = -8 이므로 win 변수에 접근할 수 있게된다.

 

 

Off-by-one

Off-by-one 취약점은 경계 검사에서 하나의 오차가 있을 때 발생하는 취약점이다. 이는 버퍼의 경계 계산 혹은 반복문의 횟수 계산 시 < 대신 <=을 쓰거나, 0부터 시작하는 인덱스를 고려하지 못할 때 발생하다.

// off-by-one-1.c
#include <stdio.h>
void copy_buf(char *buf, int sz) {
    char temp[16];
    
    for(i = 0; i <= sz; i++)
        temp[i] = buf[i];
}
int main(void) {
    char buf[16];
    
    read(0, buf, 16);
    copy_buf(buf, sizeof(buf));
}

 

off-by-one-1.c buf에 16바이트 문자열을 입력받은 후 buf sizeof(buf)의 값을 copy_buf 함수의 인자로 전달한다. copy_buf 함수에서는 임시 버퍼 temp를 할당하고 반복문을 통해 buf의 데이터를 복사한다. 그러나 반복문은 i가 0일 때부터 sz일 때까지 총 sz + 1번 반복하게 된다. 따라서 sz + 1만큼 데이터가 복사되고, off-by-one 취약점이 발생한다.

복사했습니다!