시스템 해킹/dream hack

[dreamhack] Memory Corruption - C (I) 스택 버퍼 오버플로우

ruming 2021. 2. 8. 14:11

강의링크

 

1. 버퍼 오버플로우

버퍼 오버플로우란?

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

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

발생하는 위치에 따라 스택 버퍼 오버플로우, 힙 오버플로우로 나뉜다. 그 이유는 인접한 메모리를 오염시키는 취약점이기 때문에 어떤 메모리를 오염시킬 수 있는지에 따라 공격 방법이 달라지기 때문이다.

 

스택 버퍼 오버플로우

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

 

8 바이트의 버퍼 A와 8바이트 데이터 버퍼 B가 메모리에 선형적으로 할당되었다고 생각해보라. 버퍼 A에 16바이트의 데이터를 복사한다면 이 데이터의 뒷부분은 버퍼 A를 넘어 뒤에 있는 B에 쓰여진다. 이것을 버퍼 오버플로우가 발생했다고 하고, 이는 프로그램의 Undefined Behavior을 이끌어낸다. 만약 데이터 영역 B에 나중에 호출될 함수 포인터를 저장하고 있다면 이 값을 "AAAAAAAA"와 같은 데이터로 덮었을 때 Segmentation Fault를 발생시킬 것이다.

Undefined Behavior : 런타임중에 어떤 현상이 발생할 지 예측할 수 없다는 뜻

 

예상 공격

공격자는 어딘가에 기계어 코드를 삽입한 후 함수 포인터를 공격자의 코드의 주소로 덮어 코드를 실행할 수 있다.

 

예제 코드 (stack-1.c)

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

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

 

 

이처럼 프로그래머가 버퍼의 길이에 대한 가정을 올바르지 않게 할 때 취약점이 발생한다. 보통 길이 제한이 없는 API함수들을 사용하거나 버퍼의 크기보다 입력받는 데이터의 길이가 더 크게 될 때 자주 일어나는 실수다.

 

예제 코드 (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");
}

strncpy 함수를 통해 temp 버퍼를 복사할 때, 16바이트가 아닌 password 문자열의 길이만큼을 복사하기 때문에 argv[1]에 16바이트가 넘는 문자열을 전달하면 스택 버퍼 오버플로우가 발생한다. temp 버퍼 뒤에 auth값이 존재하기 때문에, 공격자가 auth값을 바꾼다면 0이 아닌 다른 값이 될 수 있다. 인증 여부와 상관없이 if(check_auth(argv[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---------}");
    }
}

main 함수는 24 바이트 크기의 버퍼를 할당한다. scanf함수를 통해 size 변수에 값을 입력받고 size만큼 buf에 데이터를 입력받는다. 고정된 크기의 버퍼보다 더 긴 데이터를 입력받아 스택 버퍼 오버플로우가 발생했다.

 

 

예제 코드 (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);
}

32바이트 buf를 초기화한 후 31바이트를 입력받고, sprintf함수를 통해 출력할 문자열을 저장한 뒤 출력한다. read 함수에서 받는 입력이 32바이트를 넘지 않더라도 sprintf함수를 통해 버퍼에 값을 쓸 때 "Your Input is: "문자열을 추가하기 때문에 buf에 31바이트를 꽉 채운다면 총 길이가 32바이트를 넘게 된다.

 

 

공격 벡터로부터 데이터를 입력받고 이를 버퍼에 저장하는 코드가 있다면 입력받은 데이터가 버퍼의 범위를 초과하지 않는지 항상 검사해야 한다. 입력받을 때 길이 제한이 없는 함수를 사용하는 것은 잠재적으로 취약하다. 입력받은 데이터가 버퍼에 저장되기까지의 흐름을 따라가 버퍼의 크기를 넘는 양을 저장할 수 있는지 가능성을 검토해야 한다.