Buffer Overflow 개념정리 2 - Buffer Overflow 대응 방안 및 보호 기법

Buffer Overflow 개념정리 2 - Buffer Overflow 대응 방안 및 보호 기법

in

현재까지 Buffer Overflow를 방지 및 감지 위해 다양한 기술이 적용되어 왔습니다.
여기서는 해당 기술들에 대해서 설명합니다.

안전한 프로그래밍 언어 선택

가장 효율적인 방법은 언어 레벨에서 자동 보호기법을 적용하는 것입니다.
그러나 이러한 방법은 Legacy 환경에서는 해당 방법을 적용할 수 없으며 기술적 혹은 비즈니스적 제약으로 인해 Legacy Programming Language를 사용할 수 밖에 없는 경우가 있을수 있습니다.

어셈블리 및 C/C++는 메모리 주소에대한 직접적인 액세스를 할 수 있어 Buffer Overflow에 취약한 프로그래밍 언어 입니다.

C는 특정 데이터 액세스 및 덮어쓰기에 대한 보호 기능을 제공하지 않습니다. 특히 버퍼내의 데이터가 할당된 메모리 경계 내에 있는지 확인하지 않습니다.

C++ 는 데이터를 안전한게 버퍼링하는 다양한 방법(at 멤버함수)을 표준 라이브러리로 제공합니다. 그러나 사용자가 해당 라이브러리를 활용하지 않으면 C 와 다를것이 없습니다. 그도 그럴것이 Buffer Overflow를 방지하는 라이브러리는 C에도 존재합니다.

반면, 직접 메모리 접근을 허용하지 않는 COBOL, Java, Python 등과 같은 언어는 Buffer Overflow에 상대적으로 안전합니다.

또한 C/C++ 이외의 많은 프로그래밍 언어들은 경계 검사를 제공하여 컴파일시 Buffer Overflow 발생을 미연에 방지 할 수 있습니다.

거의 모든 인터프리터 언어는 Buffer Overflow 발생 방지 기능을 제공하고 있습니다.

소프트웨어 엔지니어는 사용할 언어 및 컴파일러 를 결정할 때 안전 대 성능 비용의 장단점을 신중하게 고려하여 선택해야 합니다.

검증된 라이브러리 사용

위에서 말했듯 Buffer Overflow는 C/C++ 에서 주로 발생되는데, 그 이유는 Buffer 에 대한 낮은 수준의 데이터를 노출할수 있기 때문입니다.

gets, scanf, strcpy와 같은 경계값 검사를 수행하지 않는 라이브러리 함수를 사용하지 않는 것이 오랫동안 권장되어 왔습니다. Morris Worm 바이러스는 그당시 널리 사용되었던 Finger Network Service의 Buffer Overflow 취약점을 악용 했는데 그때 사용된 것이 gets 라이브러리를 호출하는 부분 이라고 합니다.

경계값 검사를 지원하거나 자동으로 수행하는 라이브러리를 사용하면 BUffer Overflow의 발생 가능성을 줄일 수 있습니다. 일반적으로 Buffer Overflow에 취약한 라이브러리의 데이터 유형은 String과 Array이기 때문에 라이브러리에서 범위를 지정하도록 하여 공격을 방지할 수 있습니다.

또한 검증된 라이브러리라도 제대로 사용하지 않으면 Buffer Overflow가 발생할 수 있습니다.

추가적으로, 2007년 9월 C 표준 위원회에서 준비한 기술 보고서 24731에 따라 C 라이브러리에서 문자열 및 I/O 함수를 기반으로 하는 함수들을 대상으로 매개변수로 Buffer 크기를 추가적으로 지정하도록 하였는데 이러한 방식의 효율성은 아직까지도 논쟁의 여지가 있습니다.

Memory Protection 기법 적용

함수가 반환될 때 Stack 영역이 변조되지 않았는지 확인하는 방식으로 Buffer Overflow를 감지하는 가장 일반적인 방법 입니다.

Memory Protection 기법은 소프트웨어 뿐만 아니라 하드 웨어에도 적용 됩니다.

Hardware Memory Protection

특정 소프트웨어가 자신에게 할당 받은 메모리 영역을 넘어 OS나 다른 소프트웨어에 영향을 끼칠 수 있기 때문입니다.

일반적으로 아래 두 Register를 이용하여 어떤 프로세스가 자신에게 할당된 메모리 영역을 벗어나는지 감지합니다.

  • Base Register: 어떤 프로세스에 할당된 메모리 영역의 시작주소를 가르킵니다.
  • Limit Register: 어떤 프로세스에 할당된 메모리 영역의 길이를 나타냅니다.

    만일 어떤 프로세스가 자신에게 할당된 메모리 영역을 벗어나 할당되지 않은 제한된 영역에 액세스하려고 한다면 OS에게 해당 소프트 웨어가 메모리의 제한된 영역에 액세스하려고 시도 했음을 알리고, “Segmentation Fault” 혹은 “Access Violation” 등과 같은 에러를 보냄과 동시에 해당 소프트웨어를 종료 시킵니다.

1. Stack Shild

함수 호출시 스택에 저장되는 Return Address를 “Global RET”이라는 특수 스택에 저장합니다. 함수종료 시 Global RET에 저장되어 있는 값과 스택의 RET 값을 비교해서 두 값이 다르면 프로그램을 종료 합니다.

2. Stack Guard

스택 영역 내의 Retrun Address와 Buffer에 위치한 변수 사이에 “Canary Word” 라는 Buffer Overflow를 모니터하기위한 특수한 값을 넣습니다.

만약, Buffer Overflow가 발생하면 Canary 값은 손상될 것이며, 이후 Canary 값의 검증에 실패하면 경고를 출력하고, 손상된 데이터를 무효화 합니다.

만약 공격자가 메모리에 저장된 Canary값을 Leak할 수 있다면 해당 값을 덮어 쓸때에 Canary값을 일치시켜 우회할 수 있습니다.

Canaries?

카나리아라는 조류는 독성 가스에 민감 하게 반응하는데, 탄광에서는 카나리아를 탄광 내 독성 가스에 대한 경고 시스템으로서 사용한 것에 Canaries 라고 명명 되었다고 합니다.

추가적으로, alternately known as cookies(깨진 쿠키)의 이미지를 불러 일으키기 위해서 지었다고도 합니다.

Canarys는 종류는 크게 세가지로 나뉩니다.

1) Terminator Canaries

Canary 값을 문자열의 끝을 나타내는 0x00(null), 0xff(EOF), 0x0d(CR), 0x0a(LF) 로 구성 합니다. 그러나 이는 알려진 값이기 때문에 공격자는 Retrun Address를 쓰기전에 Null문자를 덮어 우회할 수 있습니다.

2) Random Canaries

Canary 값을 랜덤하게 생성하여 공격자가 Canary 값을 알지 못하도록 합니다. 일반적인 방법으로 Canary의 값을 얻는 것은 불가능합니다.

Canary 값은 프로그램 초기화시 생성되어 전역 변수에 저장되며 해당 값은 매핑되지 않은 페이지에 저장되므로 메모리를 읽어 Canary 값을 얻으려고 한다면 “segmentation fault” 에러의 발생과 동시에 프로그램은 종료 됩니다.

만약 공격자가 canary 값의 주소를 알고 있거나 stack 영역에서 값을 읽는다면 canary 값을 얻을 수 있습니다.

3) Random XOR Canaries

제어 데이터의 전체 혹은 일부를 사용하여 XOR-scrambled 하여 canary 값을 생성합니다. 만약 제어 데이터가 변조된다면 canary와 XOR 했을때 1을 반환할 것입니다.

Random Canaries와 동일하게 Stack 영역에서 값을 읽어올수 있다는 취약점이 존재하는데, 단지 Canary 값을 읽어오는 방법이 조금 더 복잡해집니다.

공격자는 우회를 위한 Canary를 생성하기 위해서 원본 Canary 값 , XOR Encoding 알고리즘, 제어 데이터가 필요합니다.

3. SSP(Stack Smashing Protector) / Stack Canary

Stack Smashing Protector은 Stack Buffer Overflow를 방지하기 위한 보호기법으로 Stack Guard처럼 Stack 영역 내 Buffer와 SFP 사이에 Canary 값을 삽입하는 특징이 있습니다.

Canary값이 변조되면 Overflow로 판단하여 __stack_chk_fail() 함수를 호출, “stack smashing detected” 에러를 출력 후 프로그램을 강제 종료 시킵니다.

SSP의 기능은 아래와 같습니다.

    1. 로컬 변수 재배치
    1. 로컬 변수전에 포인터 배치
    1. canary 삽입

4. RELRO (RELocation Read-Only)

ELF 바이너리 또는 프로세스의 데이터 섹션에 특정 권한만을 부여해 데이터 섹션의 보안을 강화하여 메모리가 변조되는 것을 보호하는 기술입니다.

RELRO는 크게 NO RELRO, Partial RELRO, FULL RELRO 세가지가 있습니다.

1) NO RELRO

ELF 기본헤더, 코드영역을 제외한 거의 모든 부분에 Read , Write 권한을 줍니다. GOT Overwrite가 가능하여 취약합니다.

  • Lazy Binding
  • Write to GOT
$ gcc -o ./code ./code.c -z norelro

image compile

2) PARTIAL_RELRO

NO RELRO와 비슷하지만 .Dynamic 섹션에 쓰기 권한을 제거합니다. GOT Overwrite가 가능하여 취약합니다.

  • Lazy Binding
  • Write to GOT
  • RELRO in the Program Header
  • Section include in RELRO
    • INIT_ARRAY
    • FINI_ARRAY
$ gcc -o ./code ./code.c -Wl,-z,relro

image compile

3) FULL_RELRO

BSS 영역을 제외한 모든 부분의 Write 권한이 없어집니다. 당연히 GOT의 Write 권한 역시 상실하게 되며 GOT Overwrite 에 안전합니다. (NO RELRO 나 Partial RELRO가 아닌 FULL RELRO 일 경우엔 got에 쓰기 권한이 없어지므로, 실행 중간에 Dynamic Linker를 통해 got 영역에 해당 함수 주소를 쓸 수 가 없어지게 됩니다.)

  • Now Binding
  • RELRO in Program Header
  • Section include in RELRO
    • INIT_ARRAY
    • FINI_ARRAY
    • PLTGOT
$ gcc -o ./code code.c -Wl,-z,relro,-z,now

image compile

Binding?

  • Lazy Binding은 GOT(Global offset Table) 호출 시점에 해당 함수의 주소를 구하는 것
  • Now Binding은 프로그램 실행 시점에 해당 함수의 주소를 구하는 것이라고 알아두면 됩니다.

5. ASLR (Address Space Layout Randomization, 주소 공간 배치 난수화)

프로그램이 실행될 때 마다 Stack, Heap, Lib 등의 데이터 영역의 주소를 랜덤한 영역에 배치하여 공격에 필요한 Target address를 예측하기 어렵게 만듭니다.

예를들어 RTL(Return To Libc) 공격을 하기 위해서는 공유 라이브러리에서 사용되는 함수의 주소를 알아야 합니다.

만약 해당 함수의 주소값이 고정적이라면, 매우 쉽게 Exploit 코드를 작성 할 수 있습니다.

하지만 ASLR을 적용하여 매번 주소가 변경된다면 공격자는 libc 주소를 Leak하여 Fffset 연산을 거쳐 Base주소를 구하는 과정이 필요하므로 공격이 좀 더 어려워 집니다.

ASLR 설정 방법

ASLR 설정은 Linux Kernel 2.6부터 제공되며 Default 값은 ‘2’입니다.
sysctl -w kernel.randomize_va_space=<flag> 명령어로 설정하거나 “/proc/sys/kernel/randomize_va_space”를 직접 수정하여(echo 0 > /proc/sys/kernel/randomize_va_space) 설정할 수 있습니다.

재부팅시 기본값인 ‘2’로 재설정 되면 각 숫자는 아래와 같은 의미를 지닙니다.
(최근에 나오는 커널들은 1 이상의 숫자 지정시 2로 설정한 것과 동일하게 동작합니다.)

  • 0: ASLR 해제
  • 1: 랜덤 스택 & 랜덤 라이브러리 설정
  • 2: 랜덤 스택 & 랜덤 라이브러리 설정 & 랜덤 힙 설정
// aslr_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *global = "test";

int main(){
    char *heap = malloc(100);
    char *stack[] = {"test"};

    printf("[Heap]  address: %p\n", heap);
    printf("[Stack] address: %p\n", stack);
    printf("[libc]  address: %p\n",**(&stack + 3));
    printf("[.data] address: %p\n",global);
    return 0;
}

1) ASLR을 0(disabled)로 설정

고정적인 Stack, Heap, libc, .data의 주소를 갖습니다. image ASLR: 0

2) ASLR을 1(stack, library)로 설정

Stack, libc 의 주소가 랜덤하게 변경됩니다. image ASLR: 1

3) ASLR을 2(stack, heap, library) 로 설정

Stack, Heap, libc, .data의 주소가 랜덤하게 변경됩니다. image ASLR: 2

6. PIE(Position Independent Executable)

데이터 영역뿐만 아니라 코드 영역에도 ASLR이 적용 됩니다.

7. NX (Non-Executable, 실행 방지)

NX는 XD(eXecute Disabled) 혹은 DEP(Data Execution Prevention) 라고도 합니다.

메모리에 쓰기 권한과 실행 권한을 동시에 부여하지 않음으로 해당 프로그램의 공격을 어렵게 한다; 프로세스 명령어나 코드, 데이터의 저장을 위한 메모리 영역을 따로 분리하는 CPU의 기술입니다.

NX bit가 적용된 메모리 구역은 데이터 저장을 위해서만 사용되고, 프로세서 명령어가 실행되지 않도록 합니다.

자신의 CPU가 NX 기능을 지원하는지 확인하려면 아래 명령어를 입력하면 됩니다.

$ grep -m1 nx /proc/cpuinfo

image support nx

이전에는 NX/XD의 설정을 sysctl -w kernel.exec-shield=(0|1) 로 세팅했으나 최근에 나오는 Linux OS는 “kernel.exec-shield” key가 존재하지 않으며, 설정을 위해서는 서버를 재부팅하고 BIOS에서 수정해야 합니다.

또한 컴파일 하는 시점에서도 특정 옵션을 줘서 NX-bit 적용 여부를 설정할 수 있습니다. https://www.cyberciti.biz/faq/ubuntu-exec-shield-protection-nx-bit-protection-sysctl/

NX의 활성화 여부는 아래 명령어로 확인할 수 있습니다.

$ dmesg | grep '[NX|DX]*protection'

image check nx

NX/XD TEST

8. DEP(Data Execution Prevention, 데이터 읽기 방지)

Windows 운영체제에 포함된 시스템 수준의 메모리 보호기능으로 Windows XP 및 Server 2003부터 기본적으로 제공 됩니다.

악의적인 코드가 실행되는 것을 방지하기 위해 메모리를 추가로 확인하는 하드웨어 및 소프트웨어 기술이며 NX-bit와 동일한 개념을 갖습니다.

만약 메모리를 잘못 사용하는 프로그램이 탐지될 경우 프로그램을 닫고 사용자에게 알립니다.

DEP는 크게 두가지 모드로 나뉩니다.

1) 하드웨어 DEP

메모리에 명시적으로 실행 코드가 포함되어 있는 경우를 제외하고 프로세스의 모든 메모리 위치에서 실행할 수 없도록 표시합니다.

대부분의 최신 프로세스는 하드웨어 적용 DEP를 지원한다.

2) 소프트웨어 DEP

CPU가 하드웨어 DEP를 지원하지 않을 경우 사용한다.

예를 들어 공격자가 Heap, Stack 영역에 shellcode를 올려서 실행하려고 할 경우 DEP가 적용되었을 경우에는 해당 영역에 대한 실행권한이 없으므로 shellcode가 실행 되지 않으며 프로그램에서 해당 동작에 대한 예외처리 후 프로세스가 종료됩니다.

9. SEH(Structured Exception Handling, 구조적 예외처리) 메커니즘 적용

프로그램 실행시 발생하는 문제점 대부분을 예외라고 합니다.

Windows에서는 Termination Handler(종료 핸들러)__try __finally 그리고 Exception Handler(예외 핸들러)인 _try __exception을 기법을 제공합니다.

SEH 는 성능을 약간 저하시킨다는 단점이 존재합니다.

예외는 크게 하드웨어적 예외와 소프트웨어적 예외로 나뉘는데, 의미 그대로 하드웨어적 예외는 하드웨어에서 감지하고 알려주는 예외이며 소프트웨어적 예외는 소프트웨어에서 감지하는 예외입니다.

하드웨어 예외는 하드웨어가 디자인 될 당시에 결정되고 예외의 종류 추가는 불가능합니다. 그러나 소프트웨어는 소프트웨어가 디자인될 당시 결정되기에 예외의 종류 추가가 가능합니다. http://egloos.zum.com/sweeper/v/2815898

10. Shadow Stack

Control Flow Enforcement(제어 흐름 무결성)를 통해 ROP와 함께 Stack Return Address Overwrite을 근본적으로 막을 수 있는 기능을 탑재한 intel cpu의 보안기능 입니다.

기존 스택의 카피본을 공격자가 접근 할 수 없는 장소에 보관하여 에필로그 에서 호출 스택과 섀도우 스택 모두에서 반환 주소를 로드한 다음 이를 비교합니다.

만약 반환 주소의 레코드가 다르면 Overflow 공격으로 감지하여 프로그램을 종료 시킵니다.